slapify 0.0.13 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +342 -258
  2. package/dist/ai/interpreter.d.ts +13 -0
  3. package/dist/ai/interpreter.d.ts.map +1 -1
  4. package/dist/ai/interpreter.js +43 -5
  5. package/dist/ai/interpreter.js.map +1 -1
  6. package/dist/cli.js +500 -152
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/perf/audit.d.ts +215 -0
  13. package/dist/perf/audit.d.ts.map +1 -0
  14. package/dist/perf/audit.js +635 -0
  15. package/dist/perf/audit.js.map +1 -0
  16. package/dist/report/generator.d.ts +1 -0
  17. package/dist/report/generator.d.ts.map +1 -1
  18. package/dist/report/generator.js +92 -0
  19. package/dist/report/generator.js.map +1 -1
  20. package/dist/runner/index.d.ts +14 -1
  21. package/dist/runner/index.d.ts.map +1 -1
  22. package/dist/runner/index.js +195 -13
  23. package/dist/runner/index.js.map +1 -1
  24. package/dist/task/index.d.ts +5 -0
  25. package/dist/task/index.d.ts.map +1 -0
  26. package/dist/task/index.js +4 -0
  27. package/dist/task/index.js.map +1 -0
  28. package/dist/task/report.d.ts +9 -0
  29. package/dist/task/report.d.ts.map +1 -0
  30. package/dist/task/report.js +740 -0
  31. package/dist/task/report.js.map +1 -0
  32. package/dist/task/runner.d.ts +3 -0
  33. package/dist/task/runner.d.ts.map +1 -0
  34. package/dist/task/runner.js +1362 -0
  35. package/dist/task/runner.js.map +1 -0
  36. package/dist/task/session.d.ts +18 -0
  37. package/dist/task/session.d.ts.map +1 -0
  38. package/dist/task/session.js +153 -0
  39. package/dist/task/session.js.map +1 -0
  40. package/dist/task/tools.d.ts +253 -0
  41. package/dist/task/tools.d.ts.map +1 -0
  42. package/dist/task/tools.js +258 -0
  43. package/dist/task/tools.js.map +1 -0
  44. package/dist/task/types.d.ts +153 -0
  45. package/dist/task/types.d.ts.map +1 -0
  46. package/dist/task/types.js +2 -0
  47. package/dist/task/types.js.map +1 -0
  48. package/dist/types.d.ts +2 -0
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +20 -13
@@ -0,0 +1,740 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ function statusColor(status) {
4
+ switch (status) {
5
+ case "completed":
6
+ return "#22c55e";
7
+ case "failed":
8
+ return "#ef4444";
9
+ case "scheduled":
10
+ return "#3b82f6";
11
+ case "sleeping":
12
+ return "#a78bfa";
13
+ default:
14
+ return "#f59e0b";
15
+ }
16
+ }
17
+ function statusIcon(status) {
18
+ switch (status) {
19
+ case "completed":
20
+ return "✅";
21
+ case "failed":
22
+ return "❌";
23
+ case "scheduled":
24
+ return "⏰";
25
+ case "sleeping":
26
+ return "😴";
27
+ default:
28
+ return "⟳";
29
+ }
30
+ }
31
+ function toolIcon(toolName) {
32
+ const icons = {
33
+ navigate: "🌐",
34
+ get_page_state: "📄",
35
+ click: "🖱️",
36
+ type: "⌨️",
37
+ press: "⌨️",
38
+ scroll: "↕️",
39
+ wait: "⏳",
40
+ screenshot: "📸",
41
+ reload: "🔄",
42
+ go_back: "⬅️",
43
+ list_credential_profiles: "🔑",
44
+ inject_credentials: "💉",
45
+ fill_login_form: "🔐",
46
+ save_credentials: "💾",
47
+ remember: "🧠",
48
+ recall: "🔍",
49
+ list_memories: "📚",
50
+ schedule: "⏰",
51
+ sleep_until: "😴",
52
+ done: "✅",
53
+ fetch_url: "⚡",
54
+ status_update: "📢",
55
+ ask_user: "🙋",
56
+ };
57
+ return icons[toolName] || "🔧";
58
+ }
59
+ function perfScoreColor(score) {
60
+ if (score >= 90)
61
+ return "#22c55e";
62
+ if (score >= 50)
63
+ return "#f59e0b";
64
+ return "#ef4444";
65
+ }
66
+ function perfScoreLabel(score) {
67
+ if (score >= 90)
68
+ return "Good";
69
+ if (score >= 50)
70
+ return "Needs Improvement";
71
+ return "Poor";
72
+ }
73
+ function vitalRating(name, value) {
74
+ const thresholds = {
75
+ fcp: [1800, 3000],
76
+ lcp: [2500, 4000],
77
+ cls: [0.1, 0.25],
78
+ ttfb: [800, 1800],
79
+ tbt: [200, 600],
80
+ };
81
+ const t = thresholds[name];
82
+ if (!t)
83
+ return "#94a3b8";
84
+ if (value <= t[0])
85
+ return "#22c55e";
86
+ if (value <= t[1])
87
+ return "#f59e0b";
88
+ return "#ef4444";
89
+ }
90
+ function fmt(bytes) {
91
+ if (bytes >= 1_048_576)
92
+ return `${(bytes / 1_048_576).toFixed(1)} MB`;
93
+ if (bytes >= 1024)
94
+ return `${(bytes / 1024).toFixed(0)} KB`;
95
+ return `${bytes} B`;
96
+ }
97
+ function renderNetworkSection(net) {
98
+ if (!net)
99
+ return "";
100
+ const th = `style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:5px 10px"`;
101
+ const td = (color = "#e2e8f0") => `style="color:${color};padding:5px 10px;font-size:0.8rem"`;
102
+ // ── Summary pills ─────────────────────────────────────────────────────────
103
+ const pill = (label, value, color = "#94a3b8") => `<div style="background:#1e293b;border-radius:8px;padding:10px 14px;min-width:100px">
104
+ <div style="font-size:0.68rem;color:#64748b">${label}</div>
105
+ <div style="font-size:1rem;font-weight:700;color:${color};margin-top:2px">${value}</div>
106
+ </div>`;
107
+ const pills = [
108
+ pill("Requests", String(net.totalRequests)),
109
+ pill("Total Size", fmt(net.totalBytes), net.totalBytes > 2_000_000 ? "#f87171" : "#e2e8f0"),
110
+ pill("JavaScript", fmt(net.jsBytes), net.jsBytes > 500_000 ? "#f59e0b" : "#e2e8f0"),
111
+ pill("CSS", fmt(net.cssBytes)),
112
+ pill("Images", fmt(net.imageBytes)),
113
+ pill("Long Tasks", String(net.longTasks.length), net.longTasks.length > 3 ? "#f59e0b" : "#e2e8f0"),
114
+ pill("Blocking JS", `${net.totalBlockingMs}ms`, net.totalBlockingMs > 300
115
+ ? "#f87171"
116
+ : net.totalBlockingMs > 100
117
+ ? "#f59e0b"
118
+ : "#22c55e"),
119
+ ...(net.memoryMB != null ? [pill("JS Heap", `${net.memoryMB} MB`)] : []),
120
+ ].join("");
121
+ // ── Heaviest resources ────────────────────────────────────────────────────
122
+ const heavyRows = net.heaviestResources
123
+ .filter((r) => r.size > 0)
124
+ .map((r) => `<tr>
125
+ <td ${td()}>${escHtml(r.url.split("/").slice(-3).join("/"))}</td>
126
+ <td ${td("#94a3b8")}>${r.type}</td>
127
+ <td ${td(r.size > 500_000
128
+ ? "#f87171"
129
+ : r.size > 100_000
130
+ ? "#f59e0b"
131
+ : "#86efac")}>${fmt(r.size)}</td>
132
+ <td ${td("#64748b")}>${r.duration}ms</td>
133
+ ${r.renderBlocking
134
+ ? `<td style="color:#f87171;font-size:0.75rem;padding:5px 10px">⚠ blocking</td>`
135
+ : `<td></td>`}
136
+ </tr>`)
137
+ .join("");
138
+ const heavyTable = heavyRows
139
+ ? `<div style="margin-top:16px">
140
+ <h4 style="font-size:0.8rem;font-weight:600;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em">Heaviest Resources</h4>
141
+ <table style="width:100%;border-collapse:collapse">
142
+ <thead><tr>
143
+ <th ${th}>URL</th><th ${th}>Type</th><th ${th}>Size</th><th ${th}>Load</th><th ${th}></th>
144
+ </tr></thead>
145
+ <tbody>${heavyRows}</tbody>
146
+ </table>
147
+ </div>`
148
+ : "";
149
+ // ── API calls ─────────────────────────────────────────────────────────────
150
+ const apiRows = net.apiCalls
151
+ .slice(0, 20)
152
+ .map((r) => {
153
+ const isSlow = r.duration >= 500;
154
+ const statusColor = r.failed ? "#f87171" : "#86efac";
155
+ return `<tr>
156
+ <td ${td("#7dd3fc")}>${escHtml(r.method)}</td>
157
+ <td ${td()}>${escHtml(r.url.length > 80 ? "…" + r.url.slice(-80) : r.url)}</td>
158
+ <td ${td(statusColor)}>${r.status || "err"}</td>
159
+ <td ${td(isSlow ? "#f87171" : r.duration > 200 ? "#f59e0b" : "#86efac")}>${r.duration}ms${isSlow ? " ⚠" : ""}</td>
160
+ </tr>`;
161
+ })
162
+ .join("");
163
+ const apiTable = apiRows
164
+ ? `<div style="margin-top:16px">
165
+ <h4 style="font-size:0.8rem;font-weight:600;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em">
166
+ API Calls <span style="color:#64748b;font-weight:400;font-size:0.72rem">${net.failedApiCalls.length > 0
167
+ ? `· ${net.failedApiCalls.length} failed`
168
+ : ""}${net.slowApiCalls.length > 0 ? ` · ${net.slowApiCalls.length} slow` : ""}</span>
169
+ </h4>
170
+ <table style="width:100%;border-collapse:collapse">
171
+ <thead><tr>
172
+ <th ${th}>Method</th><th ${th}>URL</th><th ${th}>Status</th><th ${th}>Time</th>
173
+ </tr></thead>
174
+ <tbody>${apiRows}</tbody>
175
+ </table>
176
+ </div>`
177
+ : "";
178
+ // ── Long tasks ────────────────────────────────────────────────────────────
179
+ const longTaskRows = net.longTasks
180
+ .sort((a, b) => b.duration - a.duration)
181
+ .slice(0, 8)
182
+ .map((t) => `<tr>
183
+ <td ${td(t.duration > 200 ? "#f87171" : "#f59e0b")}>${t.duration}ms</td>
184
+ <td ${td("#64748b")}>at ${t.startTime}ms</td>
185
+ </tr>`)
186
+ .join("");
187
+ const longTaskTable = longTaskRows && net.longTasks.length > 0
188
+ ? `<div style="margin-top:16px">
189
+ <h4 style="font-size:0.8rem;font-weight:600;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em">
190
+ Long Tasks <span style="color:#64748b;font-weight:400;font-size:0.72rem">(JS blocking main thread &gt;50ms)</span>
191
+ </h4>
192
+ <table style="width:100%;border-collapse:collapse">
193
+ <thead><tr><th ${th}>Duration</th><th ${th}>When</th></tr></thead>
194
+ <tbody>${longTaskRows}</tbody>
195
+ </table>
196
+ </div>`
197
+ : "";
198
+ return `
199
+ <div style="border-top:1px solid #1e293b;margin-top:24px;padding-top:20px">
200
+ <h3 style="font-size:0.9rem;font-weight:600;color:#e2e8f0;margin-bottom:14px">🌐 Network & Runtime</h3>
201
+ <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:4px">${pills}</div>
202
+ ${heavyTable}
203
+ ${apiTable}
204
+ ${longTaskTable}
205
+ </div>`;
206
+ }
207
+ function renderPerfSection(perf) {
208
+ // Support both old .lighthouse and new .scores field names
209
+ const scores = perf.scores ?? perf.lighthouse ?? null;
210
+ const gauges = scores
211
+ ? ["performance", "accessibility", "bestPractices", "seo"]
212
+ .map((key) => {
213
+ const score = scores[key] ?? 0;
214
+ const label = key === "bestPractices"
215
+ ? "Best Practices"
216
+ : key.charAt(0).toUpperCase() + key.slice(1);
217
+ const color = perfScoreColor(score);
218
+ const circumference = 2 * Math.PI * 28;
219
+ const dash = (score / 100) * circumference;
220
+ return `<div style="text-align:center;min-width:100px">
221
+ <svg width="72" height="72" viewBox="0 0 72 72">
222
+ <circle cx="36" cy="36" r="28" fill="none" stroke="#1e293b" stroke-width="8"/>
223
+ <circle cx="36" cy="36" r="28" fill="none" stroke="${color}" stroke-width="8"
224
+ stroke-dasharray="${dash.toFixed(1)} ${circumference.toFixed(1)}"
225
+ stroke-linecap="round" transform="rotate(-90 36 36)"/>
226
+ <text x="36" y="41" text-anchor="middle" fill="${color}" font-size="16" font-weight="700">${score}</text>
227
+ </svg>
228
+ <div style="font-size:0.72rem;color:#94a3b8;margin-top:4px">${label}</div>
229
+ <div style="font-size:0.68rem;color:${color}">${perfScoreLabel(score)}</div>
230
+ </div>`;
231
+ })
232
+ .join("")
233
+ : "";
234
+ const vitalsRows = Object.entries(perf.vitals)
235
+ .filter(([, v]) => v != null)
236
+ .map(([key, value]) => {
237
+ const display = key === "cls" ? value.toFixed(4) : `${value}ms`;
238
+ const color = vitalRating(key, value);
239
+ const labels = {
240
+ fcp: "First Contentful Paint",
241
+ lcp: "Largest Contentful Paint",
242
+ cls: "Cumulative Layout Shift",
243
+ ttfb: "Time to First Byte",
244
+ domContentLoaded: "DOM Content Loaded",
245
+ loadComplete: "Load Complete",
246
+ };
247
+ return `<tr>
248
+ <td style="color:#94a3b8;padding:6px 12px;font-size:0.8rem">${labels[key] || key}</td>
249
+ <td style="color:${color};padding:6px 12px;font-size:0.8rem;font-weight:600">${display}</td>
250
+ </tr>`;
251
+ })
252
+ .join("");
253
+ const labMetrics = scores
254
+ ? [
255
+ ["FCP", scores.fcp, "ms"],
256
+ ["LCP", scores.lcp, "ms"],
257
+ ["CLS", scores.cls?.toFixed(4), ""],
258
+ ["TBT", scores.tbt, "ms"],
259
+ ["Speed Index", scores.speedIndex, "ms"],
260
+ ["TTI", scores.tti, "ms"],
261
+ ]
262
+ .filter(([, v]) => v != null)
263
+ .map(([label, value, unit]) => `<div style="background:#1e293b;border-radius:8px;padding:12px 16px;min-width:120px">
264
+ <div style="font-size:0.72rem;color:#64748b">${label}</div>
265
+ <div style="font-size:1.1rem;font-weight:700;color:#e2e8f0;margin-top:2px">${value}${unit}</div>
266
+ </div>`)
267
+ .join("")
268
+ : "";
269
+ // Framework badge (strip outer parens: "(Next.js)" → "Next.js")
270
+ const reactVer = perf.react?.version;
271
+ const frameworkName = reactVer?.startsWith("(")
272
+ ? reactVer.slice(1, -1)
273
+ : reactVer;
274
+ const frameworkBadge = frameworkName
275
+ ? ` <span style="color:#64748b;font-weight:400;font-size:0.8rem">${escHtml(frameworkName)}</span>`
276
+ : "";
277
+ // Passive render issues table
278
+ const renderIssuesHtml = perf.react?.detected && (perf.react.issues?.length ?? 0) > 0
279
+ ? `<table style="width:100%;border-collapse:collapse;margin-top:8px">
280
+ <thead><tr>
281
+ <th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Component</th>
282
+ <th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Renders</th>
283
+ ${perf.react.issues[0]?.avgMs != null
284
+ ? '<th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Avg Time</th>'
285
+ : ""}
286
+ </tr></thead>
287
+ <tbody>
288
+ ${perf.react.issues
289
+ .map((i) => `<tr>
290
+ <td style="color:#fcd34d;padding:6px 12px;font-size:0.8rem;font-family:monospace">${escHtml(i.component)}</td>
291
+ <td style="color:#f87171;padding:6px 12px;font-size:0.8rem;font-weight:600">${i.renderCount}</td>
292
+ ${i.avgMs != null
293
+ ? `<td style="color:#94a3b8;padding:6px 12px;font-size:0.8rem">${i.avgMs}ms</td>`
294
+ : ""}
295
+ </tr>`)
296
+ .join("")}
297
+ </tbody>
298
+ </table>`
299
+ : "";
300
+ // Interaction test results table
301
+ const interactionTests = perf.react?.interactionTests ?? [];
302
+ const interactionHtml = interactionTests.length > 0
303
+ ? `<div style="margin-top:16px">
304
+ <h4 style="font-size:0.8rem;font-weight:600;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em">Interaction Tests</h4>
305
+ <table style="width:100%;border-collapse:collapse">
306
+ <thead><tr>
307
+ <th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Action</th>
308
+ <th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">DOM Changes</th>
309
+ <th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Status</th>
310
+ </tr></thead>
311
+ <tbody>
312
+ ${interactionTests
313
+ .map((t) => `<tr>
314
+ <td style="color:#e2e8f0;padding:6px 12px;font-size:0.8rem">${escHtml(t.action)}</td>
315
+ <td style="color:${t.flagged ? "#f87171" : "#86efac"};padding:6px 12px;font-size:0.8rem;font-weight:600">${t.mutations}</td>
316
+ <td style="padding:6px 12px;font-size:0.8rem">${t.flagged
317
+ ? '<span style="color:#f87171">⚠ High activity</span>'
318
+ : '<span style="color:#86efac">✓ Normal</span>'}</td>
319
+ </tr>`)
320
+ .join("")}
321
+ </tbody>
322
+ </table>
323
+ </div>`
324
+ : "";
325
+ const frameworkSection = perf.react?.detected
326
+ ? `<div style="margin-top:24px">
327
+ <h3 style="font-size:0.9rem;font-weight:600;color:#e2e8f0;margin-bottom:12px">⚛️ Framework Analysis${frameworkBadge}</h3>
328
+ ${renderIssuesHtml
329
+ ? renderIssuesHtml
330
+ : `<p style="color:#22c55e;font-size:0.85rem">✅ No passive re-render issues detected</p>`}
331
+ ${interactionHtml}
332
+ </div>`
333
+ : perf.react?.detected === false
334
+ ? `<p style="color:#64748b;font-size:0.85rem;margin-top:16px">ℹ️ No component framework detected on this page</p>`
335
+ : "";
336
+ return `
337
+ <div style="background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:24px;margin-bottom:32px">
338
+ <h2 style="font-size:1.1rem;font-weight:600;color:#e2e8f0;margin-bottom:20px">⚡ Performance
339
+ <span style="font-size:0.75rem;font-weight:400;color:#64748b;margin-left:8px">${escHtml(perf.url)}</span>
340
+ </h2>
341
+
342
+ ${gauges
343
+ ? `<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:24px">${gauges}</div>`
344
+ : ""}
345
+
346
+ ${labMetrics
347
+ ? `<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:24px">${labMetrics}</div>`
348
+ : ""}
349
+
350
+ ${vitalsRows
351
+ ? `<div style="margin-bottom:16px">
352
+ <h3 style="font-size:0.85rem;font-weight:600;color:#94a3b8;margin-bottom:8px">Real User Metrics</h3>
353
+ <table style="border-collapse:collapse"><tbody>${vitalsRows}</tbody></table>
354
+ </div>`
355
+ : ""}
356
+
357
+ ${frameworkSection}
358
+
359
+ ${renderNetworkSection(perf.network)}
360
+
361
+ ${perf.lighthouseReportPath
362
+ ? `<p style="margin-top:16px;font-size:0.78rem;color:#64748b">Full report: <a href="${escHtml(perf.lighthouseReportPath)}" style="color:#7dd3fc">${escHtml(perf.lighthouseReportPath)}</a></p>`
363
+ : ""}
364
+ </div>`;
365
+ }
366
+ /**
367
+ * Renders either a single audit section or, when multiple pages were audited,
368
+ * a URL tab bar + the selected page's full detail panel.
369
+ */
370
+ function renderAllPerfSections(session) {
371
+ const audits = session.perfAudits ?? (session.perfAudit ? [session.perfAudit] : []);
372
+ if (audits.length === 0)
373
+ return "";
374
+ if (audits.length === 1)
375
+ return renderPerfSection(audits[0]);
376
+ const pageLabel = (a) => {
377
+ try {
378
+ return new URL(a.url).pathname || "/";
379
+ }
380
+ catch {
381
+ return a.url;
382
+ }
383
+ };
384
+ const tabs = audits
385
+ .map((a, i) => `
386
+ <button
387
+ id="slp-tab-${i}"
388
+ onclick="slpSwitch(${i})"
389
+ title="${escHtml(a.url)}"
390
+ style="
391
+ cursor:pointer;background:none;border:none;outline:none;
392
+ padding:9px 18px;font-size:0.82rem;font-weight:500;white-space:nowrap;
393
+ border-bottom:2px solid ${i === 0 ? "#7c3aed" : "transparent"};
394
+ color:${i === 0 ? "#e2e8f0" : "#64748b"};
395
+ transition:color .15s,border-color .15s;
396
+ "
397
+ >${escHtml(pageLabel(a))}</button>`)
398
+ .join("");
399
+ const panels = audits
400
+ .map((a, i) => `<div id="slp-panel-${i}" style="display:${i === 0 ? "block" : "none"}">${renderPerfSection(a)}</div>`)
401
+ .join("");
402
+ const script = `
403
+ <script>
404
+ function slpSwitch(idx) {
405
+ var n = ${audits.length};
406
+ for (var i = 0; i < n; i++) {
407
+ var p = document.getElementById('slp-panel-' + i);
408
+ var t = document.getElementById('slp-tab-' + i);
409
+ var active = i === idx;
410
+ if (p) p.style.display = active ? 'block' : 'none';
411
+ if (t) {
412
+ t.style.borderBottomColor = active ? '#7c3aed' : 'transparent';
413
+ t.style.color = active ? '#e2e8f0' : '#64748b';
414
+ }
415
+ }
416
+ }
417
+ <\/script>`;
418
+ return `
419
+ <div>
420
+ <div style="display:flex;flex-wrap:wrap;border-bottom:1px solid #1e293b;margin-bottom:20px;overflow-x:auto">
421
+ ${tabs}
422
+ </div>
423
+ ${panels}
424
+ </div>
425
+ ${script}`;
426
+ }
427
+ function escHtml(s) {
428
+ return s
429
+ .replace(/&/g, "&amp;")
430
+ .replace(/</g, "&lt;")
431
+ .replace(/>/g, "&gt;")
432
+ .replace(/"/g, "&quot;");
433
+ }
434
+ function formatArgs(args) {
435
+ const entries = Object.entries(args);
436
+ if (entries.length === 0)
437
+ return "";
438
+ return entries
439
+ .map(([k, v]) => {
440
+ const val = typeof v === "string" ? v : JSON.stringify(v);
441
+ return `<span class="arg-key">${escHtml(k)}</span><span class="arg-eq">=</span><span class="arg-val">${escHtml(val.slice(0, 120))}${val.length > 120 ? "…" : ""}</span>`;
442
+ })
443
+ .join(" &nbsp; ");
444
+ }
445
+ function formatResult(result) {
446
+ const s = typeof result === "string" ? result : JSON.stringify(result);
447
+ return escHtml(s.slice(0, 300)) + (s.length > 300 ? "…" : "");
448
+ }
449
+ export function generateTaskReportHtml(report) {
450
+ const { session, events } = report;
451
+ const durationMs = new Date(session.updatedAt).getTime() -
452
+ new Date(session.createdAt).getTime();
453
+ const durationStr = durationMs < 60000
454
+ ? `${Math.round(durationMs / 1000)}s`
455
+ : `${Math.floor(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`;
456
+ const timelineRows = events
457
+ .filter((e) => [
458
+ "tool_call",
459
+ "tool_error",
460
+ "memory_update",
461
+ "scheduled",
462
+ "sleeping_until",
463
+ "session_end",
464
+ "llm_response",
465
+ ].includes(e.type))
466
+ .map((event) => {
467
+ const ts = new Date(event.ts).toLocaleTimeString();
468
+ if (event.type === "tool_call") {
469
+ const icon = toolIcon(event.toolName);
470
+ const isDone = event.toolName === "done";
471
+ return `
472
+ <tr class="${isDone ? "row-done" : "row-tool"}">
473
+ <td class="cell-time">${ts}</td>
474
+ <td class="cell-icon">${icon}</td>
475
+ <td class="cell-tool"><span class="tag-tool">${escHtml(event.toolName)}</span></td>
476
+ <td class="cell-args">${formatArgs(event.args)}</td>
477
+ <td class="cell-result result-ok">${formatResult(event.result)}</td>
478
+ </tr>`;
479
+ }
480
+ if (event.type === "tool_error") {
481
+ const icon = toolIcon(event.toolName);
482
+ return `
483
+ <tr class="row-error">
484
+ <td class="cell-time">${ts}</td>
485
+ <td class="cell-icon">${icon}</td>
486
+ <td class="cell-tool"><span class="tag-tool tag-error">${escHtml(event.toolName)}</span></td>
487
+ <td class="cell-args">${formatArgs(event.args)}</td>
488
+ <td class="cell-result result-error">${escHtml(event.error)}</td>
489
+ </tr>`;
490
+ }
491
+ if (event.type === "memory_update") {
492
+ return `
493
+ <tr class="row-memory">
494
+ <td class="cell-time">${ts}</td>
495
+ <td class="cell-icon">🧠</td>
496
+ <td class="cell-tool"><span class="tag-memory">remember</span></td>
497
+ <td class="cell-args"><span class="arg-key">${escHtml(event.key)}</span> = <span class="arg-val">${escHtml(event.value.slice(0, 120))}</span></td>
498
+ <td class="cell-result result-ok">stored</td>
499
+ </tr>`;
500
+ }
501
+ if (event.type === "scheduled") {
502
+ return `
503
+ <tr class="row-scheduled">
504
+ <td class="cell-time">${ts}</td>
505
+ <td class="cell-icon">⏰</td>
506
+ <td class="cell-tool"><span class="tag-scheduled">schedule</span></td>
507
+ <td class="cell-args"><span class="arg-key">cron</span>=<span class="arg-val">${escHtml(event.cron)}</span></td>
508
+ <td class="cell-result result-ok">${escHtml(event.task)}</td>
509
+ </tr>`;
510
+ }
511
+ if (event.type === "sleeping_until") {
512
+ return `
513
+ <tr class="row-sleeping">
514
+ <td class="cell-time">${ts}</td>
515
+ <td class="cell-icon">😴</td>
516
+ <td class="cell-tool"><span class="tag-sleeping">sleep</span></td>
517
+ <td class="cell-args"></td>
518
+ <td class="cell-result result-ok">until ${escHtml(new Date(event.until).toLocaleString())}</td>
519
+ </tr>`;
520
+ }
521
+ if (event.type === "llm_response" && event.text) {
522
+ const preview = event.text.slice(0, 300);
523
+ return `
524
+ <tr class="row-think">
525
+ <td class="cell-time">${ts}</td>
526
+ <td class="cell-icon">💬</td>
527
+ <td class="cell-tool"><span class="tag-think">thought</span></td>
528
+ <td class="cell-args" colspan="2"><span class="markdown-inline" data-md="${escHtml(preview)}${event.text.length > 300 ? "…" : ""}"></span></td>
529
+ </tr>`;
530
+ }
531
+ return "";
532
+ })
533
+ .join("\n");
534
+ const memoryRows = Object.entries(session.memory)
535
+ .map(([k, v]) => `
536
+ <tr>
537
+ <td class="mem-key">${escHtml(k)}</td>
538
+ <td class="mem-val">${escHtml(v)}</td>
539
+ </tr>`)
540
+ .join("\n");
541
+ const scheduledRows = session.scheduledJobs
542
+ .map((j) => `
543
+ <tr>
544
+ <td>${escHtml(j.cron)}</td>
545
+ <td>${escHtml(j.taskDescription)}</td>
546
+ <td>${j.lastRun ? new Date(j.lastRun).toLocaleString() : "—"}</td>
547
+ </tr>`)
548
+ .join("\n");
549
+ // Summary is stored as raw markdown text — rendered by marked.js in the browser
550
+ const summaryHtml = session.finalSummary
551
+ ? `<div class="summary-box markdown-body" data-md="${escHtml(session.finalSummary)}"></div>`
552
+ : "";
553
+ return `<!DOCTYPE html>
554
+ <html lang="en">
555
+ <head>
556
+ <meta charset="UTF-8"/>
557
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
558
+ <title>Task Report — ${escHtml(session.goal.slice(0, 60))}</title>
559
+ <script src="https://cdn.tailwindcss.com"></script>
560
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
561
+ <style>
562
+ body { font-family: 'Inter', system-ui, sans-serif; background: #0f172a; color: #e2e8f0; }
563
+ .summary-box { background: #1e293b; border-left: 4px solid #22c55e; padding: 1rem 1.5rem; border-radius: 0.5rem; font-size: 0.95rem; line-height: 1.7; }
564
+ /* Markdown prose styles inside summary and thought cells */
565
+ .markdown-body h1,.markdown-body h2,.markdown-body h3 { font-weight: 700; margin: 0.75em 0 0.35em; color: #f1f5f9; }
566
+ .markdown-body h1 { font-size: 1.25rem; }
567
+ .markdown-body h2 { font-size: 1.05rem; }
568
+ .markdown-body h3 { font-size: 0.95rem; }
569
+ .markdown-body p { margin: 0.4em 0; }
570
+ .markdown-body ul,.markdown-body ol { padding-left: 1.4em; margin: 0.4em 0; }
571
+ .markdown-body li { margin: 0.15em 0; }
572
+ .markdown-body strong { color: #f8fafc; font-weight: 600; }
573
+ .markdown-body em { color: #cbd5e1; }
574
+ .markdown-body code { background: #0f172a; color: #7dd3fc; padding: 1px 5px; border-radius: 4px; font-size: 0.85em; font-family: monospace; }
575
+ .markdown-body pre { background: #0f172a; border-radius: 6px; padding: 0.75rem 1rem; overflow-x: auto; margin: 0.5em 0; }
576
+ .markdown-body pre code { background: none; padding: 0; }
577
+ .markdown-body hr { border-color: #334155; margin: 0.75em 0; }
578
+ .markdown-body blockquote { border-left: 3px solid #475569; padding-left: 0.75rem; color: #94a3b8; margin: 0.4em 0; }
579
+ .markdown-body table { border-collapse: collapse; width: 100%; margin: 0.5em 0; font-size: 0.85rem; }
580
+ .markdown-body th { background: #1e293b; color: #94a3b8; padding: 6px 12px; text-align: left; font-weight: 600; border: 1px solid #334155; }
581
+ .markdown-body td { padding: 5px 12px; border: 1px solid #1e293b; color: #e2e8f0; }
582
+ .markdown-body tr:nth-child(even) td { background: #0f1b2d; }
583
+ /* Inline thought text (smaller, muted) */
584
+ .markdown-inline p { margin: 0; display: inline; }
585
+ .markdown-inline { color: #94a3b8; font-size: 0.82rem; }
586
+ table { border-collapse: collapse; width: 100%; }
587
+ th { background: #1e293b; color: #94a3b8; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.6rem 0.75rem; text-align: left; }
588
+ td { padding: 0.45rem 0.75rem; font-size: 0.82rem; border-bottom: 1px solid #1e293b; vertical-align: top; }
589
+ tr:hover td { background: #1e293b55; }
590
+ .cell-time { color: #64748b; font-family: monospace; white-space: nowrap; width: 70px; }
591
+ .cell-icon { font-size: 1rem; text-align: center; width: 30px; }
592
+ .cell-tool { white-space: nowrap; }
593
+ .cell-args { color: #94a3b8; font-size: 0.78rem; }
594
+ .arg-key { color: #7dd3fc; }
595
+ .arg-eq { color: #64748b; }
596
+ .arg-val { color: #fcd34d; }
597
+ .tag-tool { background: #1e40af33; color: #93c5fd; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
598
+ .tag-error { background: #7f1d1d44; color: #fca5a5; }
599
+ .tag-memory { background: #4c1d9544; color: #c4b5fd; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
600
+ .tag-scheduled { background: #164e6344; color: #67e8f9; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
601
+ .tag-sleeping { background: #3b0764; color: #d8b4fe; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
602
+ .tag-think { background: #422006; color: #fde68a; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
603
+ .result-ok { color: #86efac; }
604
+ .result-error { color: #fca5a5; }
605
+ .row-done td { background: #14532d22; }
606
+ .row-error td { background: #7f1d1d22; }
607
+ .row-memory td { background: #1a0533; }
608
+ .row-scheduled td { background: #0c4a6e22; }
609
+ .row-sleeping td { background: #150a33; }
610
+ .row-think td { background: #1c1a00; }
611
+ .mem-key { color: #7dd3fc; font-family: monospace; width: 220px; }
612
+ .mem-val { color: #fde68a; font-family: monospace; white-space: pre-wrap; word-break: break-all; }
613
+ .stat-card { background: #1e293b; border-radius: 0.75rem; padding: 1.25rem 1.5rem; }
614
+ .stat-num { font-size: 2rem; font-weight: 700; line-height: 1; }
615
+ .stat-label { font-size: 0.75rem; color: #64748b; margin-top: 0.25rem; }
616
+ </style>
617
+ </head>
618
+ <body class="min-h-screen">
619
+ <div class="max-w-6xl mx-auto px-4 py-10">
620
+ <div class="mb-8">
621
+ <div class="flex items-center gap-3 mb-1">
622
+ <span class="text-2xl">🤖</span>
623
+ <h1 class="text-2xl font-bold text-white">Task Report</h1>
624
+ <span style="color:${statusColor(session.status)}" class="ml-2 text-lg">${statusIcon(session.status)} ${session.status}</span>
625
+ </div>
626
+ <p class="text-slate-400 text-sm mt-1 font-mono">${escHtml(session.id)}</p>
627
+ <p class="text-slate-200 text-base mt-3 font-medium">"${escHtml(session.goal)}"</p>
628
+ </div>
629
+
630
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
631
+ <div class="stat-card">
632
+ <div class="stat-num text-blue-400">${session.iteration}</div>
633
+ <div class="stat-label">Iterations</div>
634
+ </div>
635
+ <div class="stat-card">
636
+ <div class="stat-num text-green-400">${durationStr}</div>
637
+ <div class="stat-label">Duration</div>
638
+ </div>
639
+ <div class="stat-card">
640
+ <div class="stat-num text-purple-400">${Object.keys(session.memory).length}</div>
641
+ <div class="stat-label">Memory Items</div>
642
+ </div>
643
+ <div class="stat-card">
644
+ <div class="stat-num text-cyan-400">${session.scheduledJobs.length}</div>
645
+ <div class="stat-label">Scheduled Jobs</div>
646
+ </div>
647
+ </div>
648
+
649
+ ${summaryHtml
650
+ ? `
651
+ <div class="mb-8">
652
+ <h2 class="text-lg font-semibold text-white mb-3">📋 Summary</h2>
653
+ ${summaryHtml}
654
+ </div>`
655
+ : ""}
656
+
657
+ ${renderAllPerfSections(session)}
658
+
659
+ ${memoryRows
660
+ ? `
661
+ <div class="mb-8">
662
+ <h2 class="text-lg font-semibold text-white mb-3">🧠 Memory at Completion</h2>
663
+ <div class="rounded-lg overflow-hidden border border-slate-700">
664
+ <table>
665
+ <thead><tr><th>Key</th><th>Value</th></tr></thead>
666
+ <tbody>${memoryRows}</tbody>
667
+ </table>
668
+ </div>
669
+ </div>`
670
+ : ""}
671
+
672
+ ${scheduledRows
673
+ ? `
674
+ <div class="mb-8">
675
+ <h2 class="text-lg font-semibold text-white mb-3">⏰ Scheduled Jobs</h2>
676
+ <div class="rounded-lg overflow-hidden border border-slate-700">
677
+ <table>
678
+ <thead><tr><th>Cron</th><th>Task</th><th>Last Run</th></tr></thead>
679
+ <tbody>${scheduledRows}</tbody>
680
+ </table>
681
+ </div>
682
+ </div>`
683
+ : ""}
684
+
685
+ <div class="mb-8">
686
+ <h2 class="text-lg font-semibold text-white mb-3">📜 Action Timeline</h2>
687
+ <div class="rounded-lg overflow-hidden border border-slate-700">
688
+ <table>
689
+ <thead>
690
+ <tr><th>Time</th><th></th><th>Action</th><th>Arguments</th><th>Result</th></tr>
691
+ </thead>
692
+ <tbody>${timelineRows}</tbody>
693
+ </table>
694
+ </div>
695
+ </div>
696
+
697
+ <p class="text-center text-slate-600 text-xs mt-8">
698
+ Generated by Slapify Task Agent &bull; ${new Date(report.generatedAt).toLocaleString()}
699
+ &bull; Session: ${escHtml(session.id)}
700
+ </p>
701
+ </div>
702
+
703
+ <script>
704
+ // Render all markdown blocks once marked.js is loaded
705
+ (function renderMarkdown() {
706
+ if (typeof marked === 'undefined') {
707
+ // Retry after CDN script loads
708
+ window.addEventListener('load', renderMarkdown);
709
+ return;
710
+ }
711
+ marked.setOptions({ breaks: true, gfm: true });
712
+
713
+ // Summary box — full markdown prose
714
+ document.querySelectorAll('.markdown-body[data-md]').forEach(function(el) {
715
+ el.innerHTML = marked.parse(el.getAttribute('data-md') || '');
716
+ });
717
+
718
+ // Inline thought previews — strip to plain text then render inline
719
+ document.querySelectorAll('.markdown-inline[data-md]').forEach(function(el) {
720
+ el.innerHTML = marked.parseInline(el.getAttribute('data-md') || '');
721
+ });
722
+ })();
723
+ </script>
724
+ </body>
725
+ </html>`;
726
+ }
727
+ export function saveTaskReport(session, events, outputDir) {
728
+ const dir = outputDir || path.join(process.cwd(), "slapify-task-reports");
729
+ if (!fs.existsSync(dir))
730
+ fs.mkdirSync(dir, { recursive: true });
731
+ const reportPath = path.join(dir, `${session.id}.html`);
732
+ const html = generateTaskReportHtml({
733
+ session,
734
+ events,
735
+ generatedAt: new Date().toISOString(),
736
+ });
737
+ fs.writeFileSync(reportPath, html);
738
+ return reportPath;
739
+ }
740
+ //# sourceMappingURL=report.js.map