snapfail 0.0.25 → 0.0.26

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 (2) hide show
  1. package/dist/index.js +730 -415
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -34,8 +34,8 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
34
34
 
35
35
  // ../../node_modules/.bun/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
36
36
  var require_src = __commonJS((exports, module) => {
37
- var ESC3 = "\x1B";
38
- var CSI2 = `${ESC3}[`;
37
+ var ESC2 = "\x1B";
38
+ var CSI2 = `${ESC2}[`;
39
39
  var beep = "\x07";
40
40
  var cursor = {
41
41
  to(x, y) {
@@ -64,8 +64,8 @@ var require_src = __commonJS((exports, module) => {
64
64
  left: `${CSI2}G`,
65
65
  hide: `${CSI2}?25l`,
66
66
  show: `${CSI2}?25h`,
67
- save: `${ESC3}7`,
68
- restore: `${ESC3}8`
67
+ save: `${ESC2}7`,
68
+ restore: `${ESC2}8`
69
69
  };
70
70
  var scroll = {
71
71
  up: (count = 1) => `${CSI2}S`.repeat(count),
@@ -93,368 +93,6 @@ var require_src = __commonJS((exports, module) => {
93
93
  // src/config.ts
94
94
  import { readFileSync, writeFileSync, existsSync } from "fs";
95
95
  import { resolve } from "path";
96
- var DEFAULT_ENDPOINT = "https://app.snapfail.com";
97
- var ENV_FILE = ".env";
98
- function loadConfig(cwd = process.cwd(), pk) {
99
- if (pk)
100
- return { projectKey: pk, endpoint: DEFAULT_ENDPOINT };
101
- const fromEnv = process.env["SNAPFAIL_PROJECT_KEY"];
102
- if (fromEnv)
103
- return { projectKey: fromEnv, endpoint: DEFAULT_ENDPOINT };
104
- const envPath = resolve(cwd, ENV_FILE);
105
- if (existsSync(envPath)) {
106
- const match = readFileSync(envPath, "utf-8").match(/^SNAPFAIL_PROJECT_KEY=(.+)$/m);
107
- if (match)
108
- return { projectKey: match[1].trim(), endpoint: DEFAULT_ENDPOINT };
109
- }
110
- throw new Error('No project key found. Run "snapfail init" or set SNAPFAIL_PROJECT_KEY.');
111
- }
112
- function writeProjectKey(projectKey, cwd = process.cwd(), keyName = "SNAPFAIL_PROJECT_KEY") {
113
- const envPath = resolve(cwd, ENV_FILE);
114
- const line = `${keyName}=${projectKey}`;
115
- const pattern = new RegExp(`^${keyName}=.*`, "m");
116
- if (existsSync(envPath)) {
117
- let content = readFileSync(envPath, "utf-8");
118
- if (pattern.test(content)) {
119
- content = content.replace(pattern, line);
120
- } else {
121
- content = content.trimEnd() + `
122
- ${line}
123
- `;
124
- }
125
- writeFileSync(envPath, content, "utf-8");
126
- } else {
127
- writeFileSync(envPath, `${line}
128
- `, "utf-8");
129
- }
130
- }
131
-
132
- // src/session.ts
133
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, rmSync } from "fs";
134
- import { join } from "path";
135
- import { homedir } from "os";
136
- var SESSION_DIR = join(homedir(), ".snapfail");
137
- var SESSION_FILE = join(SESSION_DIR, "auth.json");
138
- function readSession() {
139
- try {
140
- if (!existsSync2(SESSION_FILE))
141
- return null;
142
- const raw = JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
143
- if (!raw.token || !raw.endpoint)
144
- return null;
145
- return raw;
146
- } catch {
147
- return null;
148
- }
149
- }
150
- function writeSession(session) {
151
- mkdirSync(SESSION_DIR, { recursive: true });
152
- writeFileSync2(SESSION_FILE, JSON.stringify(session, null, 2) + `
153
- `, "utf-8");
154
- }
155
- function clearSession() {
156
- try {
157
- if (existsSync2(SESSION_FILE))
158
- rmSync(SESSION_FILE);
159
- } catch {}
160
- }
161
- function requireSession() {
162
- const session = readSession();
163
- if (!session) {
164
- throw new Error('No active session. Run "snapfail init" to authenticate.');
165
- }
166
- return session;
167
- }
168
-
169
- // src/api.ts
170
- async function apiFetch(url, options) {
171
- let res;
172
- try {
173
- res = await fetch(url, options);
174
- } catch (err) {
175
- throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
176
- }
177
- if (res.status === 401)
178
- throw new Error("Unauthorized: check your project key.");
179
- if (res.status === 404)
180
- return null;
181
- if (!res.ok) {
182
- const body = await res.text().catch(() => "");
183
- throw new Error(`API error ${res.status}: ${body}`);
184
- }
185
- return res.json();
186
- }
187
- async function fetchIncidents(config, opts = {}) {
188
- const url = new URL(`${config.endpoint}/api/incidents`);
189
- url.searchParams.set("projectKey", config.projectKey);
190
- if (opts.status)
191
- url.searchParams.set("status", opts.status);
192
- if (opts.limit != null)
193
- url.searchParams.set("limit", String(opts.limit));
194
- if (opts.offset != null)
195
- url.searchParams.set("offset", String(opts.offset));
196
- const data = await apiFetch(url.toString());
197
- return data;
198
- }
199
- async function fetchIncident(config, id) {
200
- const url = `${config.endpoint}/api/incidents/${id}`;
201
- const data = await apiFetch(url, {
202
- headers: { "X-Project-Key": config.projectKey }
203
- });
204
- return data;
205
- }
206
- async function deleteGroup(config, id) {
207
- const url = `${config.endpoint}/api/incidents/${id}`;
208
- const data = await apiFetch(url, {
209
- method: "DELETE",
210
- headers: { "X-Project-Key": config.projectKey }
211
- });
212
- return data !== null;
213
- }
214
- async function fetchSample(config, groupId, sampleIndex) {
215
- const url = `${config.endpoint}/api/incidents/${groupId}/samples/${sampleIndex}`;
216
- const data = await apiFetch(url, {
217
- headers: { "X-Project-Key": config.projectKey }
218
- });
219
- return data;
220
- }
221
-
222
- // src/format.ts
223
- var ESC = "\x1B[";
224
- var bold = (s) => `${ESC}1m${s}${ESC}0m`;
225
- var dim = (s) => `${ESC}2m${s}${ESC}0m`;
226
- var red = (s) => `${ESC}31m${s}${ESC}0m`;
227
- var green = (s) => `${ESC}32m${s}${ESC}0m`;
228
- var yellow = (s) => `${ESC}33m${s}${ESC}0m`;
229
- var cyan = (s) => `${ESC}36m${s}${ESC}0m`;
230
- function truncate(s, n) {
231
- if (s.length <= n)
232
- return s;
233
- return s.slice(0, n - 1) + "\u2026";
234
- }
235
- function relativeTime(ts) {
236
- const diff = Date.now() - ts;
237
- const s = Math.floor(diff / 1000);
238
- if (s < 60)
239
- return `${s}s ago`;
240
- const m = Math.floor(s / 60);
241
- if (m < 60)
242
- return `${m}m ago`;
243
- const h = Math.floor(m / 60);
244
- if (h < 24)
245
- return `${h}h ago`;
246
- const d = Math.floor(h / 24);
247
- return `${d}d ago`;
248
- }
249
- function formatTable(rows, headers) {
250
- const allRows = [headers, ...rows];
251
- const widths = headers.map((_, i) => Math.max(...allRows.map((r) => (r[i] ?? "").length)));
252
- const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
253
- const header = " " + headers.map((h, i) => bold(pad(h, widths[i]))).join(" ");
254
- const divider = " " + widths.map((w) => dim("\u2500".repeat(w))).join(" ");
255
- const body = rows.map((r) => " " + r.map((cell, i) => pad(cell, widths[i])).join(" ")).join(`
256
- `);
257
- return [header, divider, body].join(`
258
- `);
259
- }
260
- function formatIncidentList(data, total, status) {
261
- if (data.length === 0) {
262
- return dim(`No ${status ?? "unresolved"} incidents found.`);
263
- }
264
- const rows = data.map((g) => [
265
- cyan(truncate(g.id, 12)),
266
- truncate(g.title, 38),
267
- String(g.count),
268
- g.environments.join(","),
269
- relativeTime(g.lastSeen)
270
- ]);
271
- const table = formatTable(rows, ["ID", "TITLE", "COUNT", "ENV", "LAST SEEN"]);
272
- const summary = `
273
- ${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
274
- return table + summary;
275
- }
276
- function formatLLMContext(group, samples) {
277
- const lines = [];
278
- lines.push(`# Incident: ${group.title}`);
279
- lines.push(`Group ID: ${group.id}`);
280
- lines.push(`Fingerprint: ${group.fingerprint}`);
281
- lines.push(`Occurrences: ${group.count} | First: ${new Date(group.firstSeen).toISOString()} | Last: ${new Date(group.lastSeen).toISOString()}`);
282
- lines.push(`Environments: ${group.environments.join(", ")}`);
283
- lines.push(`Status: ${group.status}`);
284
- lines.push("");
285
- lines.push("## Error");
286
- lines.push(`Type: ${group.errorType}`);
287
- lines.push(`Message: ${samples[0]?.errorMessage ?? group.title}`);
288
- lines.push(`Normalized: ${samples[0]?.normalizedMessage ?? group.title}`);
289
- if (samples[0] && samples[0].stackFrames.length > 0) {
290
- lines.push("");
291
- lines.push("## Stack");
292
- for (const f of samples[0].stackFrames) {
293
- lines.push(` ${f.fn ?? "(anonymous)"} ${f.file}:${f.line}${f.col != null ? `:${f.col}` : ""}`);
294
- }
295
- }
296
- for (let i = 0;i < samples.length; i++) {
297
- const s = samples[i];
298
- lines.push("");
299
- lines.push(`## Sample ${i + 1} / ${group.sampleIds.length}`);
300
- lines.push(`Device: ${s.device.userAgent}`);
301
- if (s.device.viewport) {
302
- lines.push(`Viewport: ${s.device.viewport.width}x${s.device.viewport.height} Language: ${s.device.language ?? "unknown"}`);
303
- }
304
- lines.push(`URL: ${s.url}`);
305
- if (s.route)
306
- lines.push(`Route: ${s.route}`);
307
- lines.push(`Environment: ${s.environmentMode}`);
308
- if (s.consoleEntries.length > 0) {
309
- lines.push("");
310
- lines.push("### Console");
311
- for (const e of s.consoleEntries) {
312
- const args = e.args.map((a) => String(a)).join(" ");
313
- lines.push(` [${e.level}] ${args}`);
314
- }
315
- }
316
- if (s.networkEntries.length > 0) {
317
- lines.push("");
318
- lines.push("### Network");
319
- for (const n of s.networkEntries) {
320
- const status = n.status ? ` \u2192 ${n.status}` : n.error ? ` \u2192 ERROR` : "";
321
- lines.push(` ${n.method} ${n.url}${status} (${n.durationMs}ms)`);
322
- }
323
- }
324
- if (s.timeline.length > 0) {
325
- lines.push("");
326
- lines.push("### Timeline");
327
- for (const e of s.timeline) {
328
- lines.push(` +${e.t}ms ${e.kind}: ${e.summary}`);
329
- }
330
- }
331
- if (i < samples.length - 1)
332
- lines.push(`
333
- ---`);
334
- }
335
- return lines.join(`
336
- `);
337
- }
338
- function formatIncidentDetail(group, sample) {
339
- const lines = [];
340
- lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
341
- lines.push(`${dim("Status:")} ${group.status} ${dim("\xB7")} ${group.count} occurrences ${dim("\xB7")} ${group.environments.join(", ")}`);
342
- lines.push(`${dim("First seen:")} ${new Date(group.firstSeen).toISOString().slice(0, 10)} ${dim("\xB7")} ${dim("Last seen:")} ${relativeTime(group.lastSeen)}`);
343
- if (sample.stackFrames.length > 0) {
344
- lines.push("");
345
- lines.push(bold("Stack"));
346
- for (const f of sample.stackFrames.slice(0, 6)) {
347
- const fn = f.fn ? `${cyan(f.fn)} ` : "";
348
- lines.push(` ${fn}${dim(f.file + ":" + f.line)}`);
349
- if (f.codeSnippet) {
350
- for (const snippetLine of f.codeSnippet.split(`
351
- `)) {
352
- const isError = snippetLine.startsWith("\u25BA");
353
- lines.push(isError ? ` ${bold(snippetLine)}` : ` ${dim(snippetLine)}`);
354
- }
355
- }
356
- }
357
- }
358
- if (sample.consoleEntries.length > 0) {
359
- lines.push("");
360
- lines.push(bold("Console"));
361
- for (const e of sample.consoleEntries.slice(-5)) {
362
- const level = e.level === "error" ? red(`[${e.level}]`) : yellow(`[${e.level}]`);
363
- const args = e.args.map((a) => truncate(String(a), 60)).join(" ");
364
- lines.push(` ${level} ${args}`);
365
- }
366
- }
367
- if (sample.networkEntries.length > 0) {
368
- lines.push("");
369
- lines.push(bold("Network"));
370
- for (const n of sample.networkEntries.slice(-5)) {
371
- const status = n.status ? n.status >= 400 ? red(String(n.status)) : green(String(n.status)) : dim("???");
372
- lines.push(` ${dim(n.method)} ${truncate(n.url, 50)} ${dim("\u2192")} ${status} ${dim(`(${n.durationMs}ms)`)}`);
373
- }
374
- }
375
- if (sample.timeline.length > 0) {
376
- lines.push("");
377
- lines.push(bold("Timeline"));
378
- for (const e of sample.timeline.slice(0, 10)) {
379
- const t = String(e.t).padStart(5);
380
- lines.push(` ${dim(t + "ms")} ${e.summary}`);
381
- }
382
- }
383
- return lines.join(`
384
- `);
385
- }
386
-
387
- // src/commands/incidents.ts
388
- async function runIncidents(opts) {
389
- requireSession();
390
- const config = loadConfig(process.cwd(), opts.pk);
391
- const result = await fetchIncidents(config, {
392
- status: opts.status ?? "unresolved",
393
- limit: opts.limit,
394
- offset: opts.offset
395
- });
396
- if (opts.json) {
397
- process.stdout.write(JSON.stringify(result, null, 2) + `
398
- `);
399
- return;
400
- }
401
- console.log(formatIncidentList(result.data, result.total, opts.status));
402
- }
403
-
404
- // src/commands/incident.ts
405
- async function runIncident(opts) {
406
- requireSession();
407
- const config = loadConfig(process.cwd(), opts.pk);
408
- if (opts.delete) {
409
- const ok = await deleteGroup(config, opts.id);
410
- if (!ok) {
411
- console.error(`Incident group ${opts.id} not found.`);
412
- process.exit(1);
413
- }
414
- if (opts.json) {
415
- process.stdout.write(JSON.stringify({ ok: true, deleted: opts.id }) + `
416
- `);
417
- } else {
418
- console.log(`Deleted incident group ${opts.id} and all related data.`);
419
- }
420
- return;
421
- }
422
- if (opts.sample != null) {
423
- const s = await fetchSample(config, opts.id, opts.sample);
424
- if (!s) {
425
- console.error(`Sample ${opts.sample} not found for incident ${opts.id}`);
426
- process.exit(1);
427
- }
428
- if (opts.json) {
429
- process.stdout.write(JSON.stringify(s, null, 2) + `
430
- `);
431
- return;
432
- }
433
- const detail = await fetchIncident(config, opts.id);
434
- if (!detail) {
435
- console.error(`Incident ${opts.id} not found.`);
436
- process.exit(1);
437
- }
438
- console.log(formatIncidentDetail(detail.group, s));
439
- return;
440
- }
441
- const result = await fetchIncident(config, opts.id);
442
- if (!result) {
443
- console.error(`Incident ${opts.id} not found.`);
444
- process.exit(1);
445
- }
446
- if (opts.json) {
447
- process.stdout.write(JSON.stringify(result, null, 2) + `
448
- `);
449
- return;
450
- }
451
- console.log(formatIncidentDetail(result.group, result.sample));
452
- }
453
-
454
- // src/commands/init.ts
455
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
456
- import { resolve as resolve2, join as join2 } from "path";
457
- import { execSync as execSync2 } from "child_process";
458
96
 
459
97
  // ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
460
98
  import { styleText } from "util";
@@ -592,7 +230,7 @@ var fastStringWidth = (input, options = {}) => {
592
230
  var dist_default2 = fastStringWidth;
593
231
 
594
232
  // ../../node_modules/.bun/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js
595
- var ESC2 = "\x1B";
233
+ var ESC = "\x1B";
596
234
  var CSI = "\x9B";
597
235
  var END_CODE = 39;
598
236
  var ANSI_ESCAPE_BELL = "\x07";
@@ -626,8 +264,8 @@ var getClosingCode = (openingCode) => {
626
264
  return 0;
627
265
  return;
628
266
  };
629
- var wrapAnsiCode = (code) => `${ESC2}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
630
- var wrapAnsiHyperlink = (url) => `${ESC2}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
267
+ var wrapAnsiCode = (code) => `${ESC}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
268
+ var wrapAnsiHyperlink = (url) => `${ESC}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
631
269
  var wrapWord = (rows, word, columns) => {
632
270
  const characters = word[Symbol.iterator]();
633
271
  let isInsideEscape = false;
@@ -646,7 +284,7 @@ var wrapWord = (rows, word, columns) => {
646
284
  rows.push(character);
647
285
  visible = 0;
648
286
  }
649
- if (character === ESC2 || character === CSI) {
287
+ if (character === ESC || character === CSI) {
650
288
  isInsideEscape = true;
651
289
  isInsideLinkEscape = word.startsWith(ANSI_ESCAPE_LINK, rawCharacterIndex + 1);
652
290
  }
@@ -765,7 +403,7 @@ var exec = (string, columns, options = {}) => {
765
403
  } else {
766
404
  inSurrogate = false;
767
405
  }
768
- if (character === ESC2 || character === CSI) {
406
+ if (character === ESC || character === CSI) {
769
407
  GROUP_REGEX.lastIndex = i + 1;
770
408
  const groupsResult = GROUP_REGEX.exec(preString);
771
409
  const groups = groupsResult?.groups;
@@ -1901,47 +1539,511 @@ ${l2}
1901
1539
  }
1902
1540
  }
1903
1541
  }
1904
- }).prompt();
1905
- };
1906
- var i = `${styleText2("gray", S_BAR)} `;
1907
- var text = (t2) => new n({
1908
- validate: t2.validate,
1909
- placeholder: t2.placeholder,
1910
- defaultValue: t2.defaultValue,
1911
- initialValue: t2.initialValue,
1912
- output: t2.output,
1913
- signal: t2.signal,
1914
- input: t2.input,
1915
- render() {
1916
- const i2 = t2?.withGuide ?? settings.withGuide, s = `${`${i2 ? `${styleText2("gray", S_BAR)}
1917
- ` : ""}${symbol(this.state)} `}${t2.message}
1918
- `, c3 = t2.placeholder ? styleText2("inverse", t2.placeholder[0]) + styleText2("dim", t2.placeholder.slice(1)) : styleText2(["inverse", "hidden"], "_"), o2 = this.userInput ? this.userInputWithCursor : c3, a2 = this.value ?? "";
1919
- switch (this.state) {
1920
- case "error": {
1921
- const n2 = this.error ? ` ${styleText2("yellow", this.error)}` : "", r2 = i2 ? `${styleText2("yellow", S_BAR)} ` : "", d = i2 ? styleText2("yellow", S_BAR_END) : "";
1922
- return `${s.trim()}
1923
- ${r2}${o2}
1924
- ${d}${n2}
1925
- `;
1542
+ }).prompt();
1543
+ };
1544
+ var i = `${styleText2("gray", S_BAR)} `;
1545
+ var text = (t2) => new n({
1546
+ validate: t2.validate,
1547
+ placeholder: t2.placeholder,
1548
+ defaultValue: t2.defaultValue,
1549
+ initialValue: t2.initialValue,
1550
+ output: t2.output,
1551
+ signal: t2.signal,
1552
+ input: t2.input,
1553
+ render() {
1554
+ const i2 = t2?.withGuide ?? settings.withGuide, s = `${`${i2 ? `${styleText2("gray", S_BAR)}
1555
+ ` : ""}${symbol(this.state)} `}${t2.message}
1556
+ `, c3 = t2.placeholder ? styleText2("inverse", t2.placeholder[0]) + styleText2("dim", t2.placeholder.slice(1)) : styleText2(["inverse", "hidden"], "_"), o2 = this.userInput ? this.userInputWithCursor : c3, a2 = this.value ?? "";
1557
+ switch (this.state) {
1558
+ case "error": {
1559
+ const n2 = this.error ? ` ${styleText2("yellow", this.error)}` : "", r2 = i2 ? `${styleText2("yellow", S_BAR)} ` : "", d = i2 ? styleText2("yellow", S_BAR_END) : "";
1560
+ return `${s.trim()}
1561
+ ${r2}${o2}
1562
+ ${d}${n2}
1563
+ `;
1564
+ }
1565
+ case "submit": {
1566
+ const n2 = a2 ? ` ${styleText2("dim", a2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
1567
+ return `${s}${r2}${n2}`;
1568
+ }
1569
+ case "cancel": {
1570
+ const n2 = a2 ? ` ${styleText2(["strikethrough", "dim"], a2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
1571
+ return `${s}${r2}${n2}${a2.trim() ? `
1572
+ ${r2}` : ""}`;
1573
+ }
1574
+ default: {
1575
+ const n2 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", r2 = i2 ? styleText2("cyan", S_BAR_END) : "";
1576
+ return `${s}${n2}${o2}
1577
+ ${r2}
1578
+ `;
1579
+ }
1580
+ }
1581
+ }
1582
+ }).prompt();
1583
+
1584
+ // src/config.ts
1585
+ var DEFAULT_ENDPOINT = "https://app.snapfail.com";
1586
+ var ENV_FILE = ".env";
1587
+ function tryLoadProjectKey(cwd = process.cwd(), pk) {
1588
+ if (pk)
1589
+ return { projectKey: pk, endpoint: DEFAULT_ENDPOINT };
1590
+ const fromEnv = process.env["SNAPFAIL_PROJECT_KEY"];
1591
+ if (fromEnv)
1592
+ return { projectKey: fromEnv, endpoint: DEFAULT_ENDPOINT };
1593
+ const envPath = resolve(cwd, ENV_FILE);
1594
+ if (existsSync(envPath)) {
1595
+ const match = readFileSync(envPath, "utf-8").match(/^SNAPFAIL_PROJECT_KEY=(.+)$/m);
1596
+ if (match)
1597
+ return { projectKey: match[1].trim(), endpoint: DEFAULT_ENDPOINT };
1598
+ }
1599
+ return null;
1600
+ }
1601
+ function loadConfig(cwd = process.cwd(), pk) {
1602
+ const key = tryLoadProjectKey(cwd, pk);
1603
+ if (key)
1604
+ return key;
1605
+ throw new Error('No project key found. Run "snapfail init" or set SNAPFAIL_PROJECT_KEY.');
1606
+ }
1607
+ async function resolveProjectKey(pk, interactive, sessionToken) {
1608
+ const config = tryLoadProjectKey(process.cwd(), pk);
1609
+ if (config)
1610
+ return config.projectKey;
1611
+ if (!interactive || !sessionToken) {
1612
+ throw new Error('No project key found. Run "snapfail init" or pass --pk.');
1613
+ }
1614
+ const res = await fetch(`${DEFAULT_ENDPOINT}/api/cli/projects`, {
1615
+ headers: { Authorization: `Bearer ${sessionToken}` }
1616
+ });
1617
+ if (!res.ok)
1618
+ throw new Error(`Failed to fetch projects (${res.status})`);
1619
+ const { projects } = await res.json();
1620
+ if (projects.length === 0) {
1621
+ throw new Error('No projects found. Run "snapfail init" to create one.');
1622
+ }
1623
+ if (projects.length === 1) {
1624
+ console.log(`Using project: ${projects[0].name}`);
1625
+ return projects[0].key;
1626
+ }
1627
+ const pick = await select({
1628
+ message: "Select a project",
1629
+ options: projects.map((proj) => ({ value: proj.key, label: proj.name, hint: proj.key }))
1630
+ });
1631
+ if (isCancel(pick)) {
1632
+ cancel("Cancelled.");
1633
+ process.exit(0);
1634
+ }
1635
+ return pick;
1636
+ }
1637
+ function writeProjectKey(projectKey, cwd = process.cwd(), keyName = "SNAPFAIL_PROJECT_KEY") {
1638
+ const envPath = resolve(cwd, ENV_FILE);
1639
+ const line = `${keyName}=${projectKey}`;
1640
+ const pattern = new RegExp(`^${keyName}=.*`, "m");
1641
+ if (existsSync(envPath)) {
1642
+ let content = readFileSync(envPath, "utf-8");
1643
+ if (pattern.test(content)) {
1644
+ content = content.replace(pattern, line);
1645
+ } else {
1646
+ content = content.trimEnd() + `
1647
+ ${line}
1648
+ `;
1649
+ }
1650
+ writeFileSync(envPath, content, "utf-8");
1651
+ } else {
1652
+ writeFileSync(envPath, `${line}
1653
+ `, "utf-8");
1654
+ }
1655
+ }
1656
+
1657
+ // src/session.ts
1658
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, rmSync } from "fs";
1659
+ import { join } from "path";
1660
+ import { homedir } from "os";
1661
+ var SESSION_DIR = join(homedir(), ".snapfail");
1662
+ var SESSION_FILE = join(SESSION_DIR, "auth.json");
1663
+ function readSession() {
1664
+ try {
1665
+ if (!existsSync2(SESSION_FILE))
1666
+ return null;
1667
+ const raw = JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
1668
+ if (!raw.token || !raw.endpoint)
1669
+ return null;
1670
+ return raw;
1671
+ } catch {
1672
+ return null;
1673
+ }
1674
+ }
1675
+ function writeSession(session) {
1676
+ mkdirSync(SESSION_DIR, { recursive: true });
1677
+ writeFileSync2(SESSION_FILE, JSON.stringify(session, null, 2) + `
1678
+ `, "utf-8");
1679
+ }
1680
+ function clearSession() {
1681
+ try {
1682
+ if (existsSync2(SESSION_FILE))
1683
+ rmSync(SESSION_FILE);
1684
+ } catch {}
1685
+ }
1686
+ function requireSession() {
1687
+ const session = readSession();
1688
+ if (!session) {
1689
+ throw new Error('No active session. Run "snapfail init" to authenticate.');
1690
+ }
1691
+ return session;
1692
+ }
1693
+
1694
+ // src/api.ts
1695
+ async function apiFetch(url, options) {
1696
+ let res;
1697
+ try {
1698
+ res = await fetch(url, options);
1699
+ } catch (err) {
1700
+ throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
1701
+ }
1702
+ if (res.status === 401)
1703
+ throw new Error("Unauthorized: check your project key.");
1704
+ if (res.status === 404)
1705
+ return null;
1706
+ if (!res.ok) {
1707
+ const body = await res.text().catch(() => "");
1708
+ throw new Error(`API error ${res.status}: ${body}`);
1709
+ }
1710
+ return res.json();
1711
+ }
1712
+ async function fetchIncidents(config, opts = {}) {
1713
+ const url = new URL(`${config.endpoint}/api/incidents`);
1714
+ url.searchParams.set("projectKey", config.projectKey);
1715
+ if (opts.status)
1716
+ url.searchParams.set("status", opts.status);
1717
+ if (opts.limit != null)
1718
+ url.searchParams.set("limit", String(opts.limit));
1719
+ if (opts.offset != null)
1720
+ url.searchParams.set("offset", String(opts.offset));
1721
+ const data = await apiFetch(url.toString());
1722
+ return data;
1723
+ }
1724
+ async function fetchIncident(config, id) {
1725
+ const url = `${config.endpoint}/api/incidents/${id}`;
1726
+ const data = await apiFetch(url, {
1727
+ headers: { "X-Project-Key": config.projectKey }
1728
+ });
1729
+ return data;
1730
+ }
1731
+ async function deleteGroup(config, id) {
1732
+ const url = `${config.endpoint}/api/incidents/${id}`;
1733
+ const data = await apiFetch(url, {
1734
+ method: "DELETE",
1735
+ headers: { "X-Project-Key": config.projectKey }
1736
+ });
1737
+ return data !== null;
1738
+ }
1739
+ async function updateIncidentStatus(config, id, status) {
1740
+ const url = `${config.endpoint}/api/incidents/${id}`;
1741
+ const data = await apiFetch(url, {
1742
+ method: "PATCH",
1743
+ headers: { "X-Project-Key": config.projectKey, "Content-Type": "application/json" },
1744
+ body: JSON.stringify({ status })
1745
+ });
1746
+ return data !== null;
1747
+ }
1748
+ async function fetchSample(config, groupId, sampleIndex) {
1749
+ const url = `${config.endpoint}/api/incidents/${groupId}/samples/${sampleIndex}`;
1750
+ const data = await apiFetch(url, {
1751
+ headers: { "X-Project-Key": config.projectKey }
1752
+ });
1753
+ return data;
1754
+ }
1755
+
1756
+ // src/format.ts
1757
+ var ESC2 = "\x1B[";
1758
+ var bold = (s) => `${ESC2}1m${s}${ESC2}0m`;
1759
+ var dim = (s) => `${ESC2}2m${s}${ESC2}0m`;
1760
+ var red = (s) => `${ESC2}31m${s}${ESC2}0m`;
1761
+ var green = (s) => `${ESC2}32m${s}${ESC2}0m`;
1762
+ var yellow = (s) => `${ESC2}33m${s}${ESC2}0m`;
1763
+ var cyan = (s) => `${ESC2}36m${s}${ESC2}0m`;
1764
+ function truncate(s, n2) {
1765
+ if (s.length <= n2)
1766
+ return s;
1767
+ return s.slice(0, n2 - 1) + "\u2026";
1768
+ }
1769
+ function relativeTime(ts) {
1770
+ const diff = Date.now() - ts;
1771
+ const s = Math.floor(diff / 1000);
1772
+ if (s < 60)
1773
+ return `${s}s ago`;
1774
+ const m2 = Math.floor(s / 60);
1775
+ if (m2 < 60)
1776
+ return `${m2}m ago`;
1777
+ const h2 = Math.floor(m2 / 60);
1778
+ if (h2 < 24)
1779
+ return `${h2}h ago`;
1780
+ const d = Math.floor(h2 / 24);
1781
+ return `${d}d ago`;
1782
+ }
1783
+ function formatTable(rows, headers) {
1784
+ const allRows = [headers, ...rows];
1785
+ const widths = headers.map((_2, i2) => Math.max(...allRows.map((r2) => (r2[i2] ?? "").length)));
1786
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
1787
+ const header = " " + headers.map((h2, i2) => bold(pad(h2, widths[i2]))).join(" ");
1788
+ const divider = " " + widths.map((w) => dim("\u2500".repeat(w))).join(" ");
1789
+ const body = rows.map((r2) => " " + r2.map((cell, i2) => pad(cell, widths[i2])).join(" ")).join(`
1790
+ `);
1791
+ return [header, divider, body].join(`
1792
+ `);
1793
+ }
1794
+ function formatIncidentList(data, total, status) {
1795
+ if (data.length === 0) {
1796
+ return dim(`No ${status ?? "unresolved"} incidents found.`);
1797
+ }
1798
+ const rows = data.map((g2) => [
1799
+ cyan(truncate(g2.id, 12)),
1800
+ truncate(g2.title, 38),
1801
+ String(g2.count),
1802
+ g2.environments.join(","),
1803
+ relativeTime(g2.lastSeen)
1804
+ ]);
1805
+ const table = formatTable(rows, ["ID", "TITLE", "COUNT", "ENV", "LAST SEEN"]);
1806
+ const summary = `
1807
+ ${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
1808
+ return table + summary;
1809
+ }
1810
+ function formatLLMContext(group, samples) {
1811
+ const lines = [];
1812
+ lines.push(`# Incident: ${group.title}`);
1813
+ lines.push(`Group ID: ${group.id}`);
1814
+ lines.push(`Fingerprint: ${group.fingerprint}`);
1815
+ lines.push(`Occurrences: ${group.count} | First: ${new Date(group.firstSeen).toISOString()} | Last: ${new Date(group.lastSeen).toISOString()}`);
1816
+ lines.push(`Environments: ${group.environments.join(", ")}`);
1817
+ lines.push(`Status: ${group.status}`);
1818
+ lines.push("");
1819
+ lines.push("## Error");
1820
+ lines.push(`Type: ${group.errorType}`);
1821
+ lines.push(`Message: ${samples[0]?.errorMessage ?? group.title}`);
1822
+ lines.push(`Normalized: ${samples[0]?.normalizedMessage ?? group.title}`);
1823
+ if (samples[0] && samples[0].stackFrames.length > 0) {
1824
+ lines.push("");
1825
+ lines.push("## Stack");
1826
+ for (const f2 of samples[0].stackFrames) {
1827
+ lines.push(` ${f2.fn ?? "(anonymous)"} ${f2.file}:${f2.line}${f2.col != null ? `:${f2.col}` : ""}`);
1828
+ }
1829
+ }
1830
+ for (let i2 = 0;i2 < samples.length; i2++) {
1831
+ const s = samples[i2];
1832
+ lines.push("");
1833
+ lines.push(`## Sample ${i2 + 1} / ${group.sampleIds.length}`);
1834
+ lines.push(`Device: ${s.device.userAgent}`);
1835
+ if (s.device.viewport) {
1836
+ lines.push(`Viewport: ${s.device.viewport.width}x${s.device.viewport.height} Language: ${s.device.language ?? "unknown"}`);
1837
+ }
1838
+ lines.push(`URL: ${s.url}`);
1839
+ if (s.route)
1840
+ lines.push(`Route: ${s.route}`);
1841
+ lines.push(`Environment: ${s.environmentMode}`);
1842
+ if (s.consoleEntries.length > 0) {
1843
+ lines.push("");
1844
+ lines.push("### Console");
1845
+ for (const e of s.consoleEntries) {
1846
+ const args = e.args.map((a2) => String(a2)).join(" ");
1847
+ lines.push(` [${e.level}] ${args}`);
1926
1848
  }
1927
- case "submit": {
1928
- const n2 = a2 ? ` ${styleText2("dim", a2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
1929
- return `${s}${r2}${n2}`;
1849
+ }
1850
+ if (s.networkEntries.length > 0) {
1851
+ lines.push("");
1852
+ lines.push("### Network");
1853
+ for (const n2 of s.networkEntries) {
1854
+ const status = n2.status ? ` \u2192 ${n2.status}` : n2.error ? ` \u2192 ERROR` : "";
1855
+ lines.push(` ${n2.method} ${n2.url}${status} (${n2.durationMs}ms)`);
1930
1856
  }
1931
- case "cancel": {
1932
- const n2 = a2 ? ` ${styleText2(["strikethrough", "dim"], a2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
1933
- return `${s}${r2}${n2}${a2.trim() ? `
1934
- ${r2}` : ""}`;
1857
+ }
1858
+ if (s.timeline.length > 0) {
1859
+ lines.push("");
1860
+ lines.push("### Timeline");
1861
+ for (const e of s.timeline) {
1862
+ lines.push(` +${e.t}ms ${e.kind}: ${e.summary}`);
1935
1863
  }
1936
- default: {
1937
- const n2 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", r2 = i2 ? styleText2("cyan", S_BAR_END) : "";
1938
- return `${s}${n2}${o2}
1939
- ${r2}
1940
- `;
1864
+ }
1865
+ if (i2 < samples.length - 1)
1866
+ lines.push(`
1867
+ ---`);
1868
+ }
1869
+ return lines.join(`
1870
+ `);
1871
+ }
1872
+ function formatIncidentDetail(group, sample) {
1873
+ const lines = [];
1874
+ lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
1875
+ lines.push(`${dim("Status:")} ${group.status} ${dim("\xB7")} ${group.count} occurrences ${dim("\xB7")} ${group.environments.join(", ")}`);
1876
+ lines.push(`${dim("First seen:")} ${new Date(group.firstSeen).toISOString().slice(0, 10)} ${dim("\xB7")} ${dim("Last seen:")} ${relativeTime(group.lastSeen)}`);
1877
+ if (sample.stackFrames.length > 0) {
1878
+ lines.push("");
1879
+ lines.push(bold("Stack"));
1880
+ for (const f2 of sample.stackFrames.slice(0, 6)) {
1881
+ const fn = f2.fn ? `${cyan(f2.fn)} ` : "";
1882
+ lines.push(` ${fn}${dim(f2.file + ":" + f2.line)}`);
1883
+ if (f2.codeSnippet) {
1884
+ for (const snippetLine of f2.codeSnippet.split(`
1885
+ `)) {
1886
+ const isError = snippetLine.startsWith("\u25BA");
1887
+ lines.push(isError ? ` ${bold(snippetLine)}` : ` ${dim(snippetLine)}`);
1888
+ }
1941
1889
  }
1942
1890
  }
1943
1891
  }
1944
- }).prompt();
1892
+ if (sample.consoleEntries.length > 0) {
1893
+ lines.push("");
1894
+ lines.push(bold("Console"));
1895
+ for (const e of sample.consoleEntries.slice(-5)) {
1896
+ const level = e.level === "error" ? red(`[${e.level}]`) : yellow(`[${e.level}]`);
1897
+ const args = e.args.map((a2) => truncate(String(a2), 60)).join(" ");
1898
+ lines.push(` ${level} ${args}`);
1899
+ }
1900
+ }
1901
+ if (sample.networkEntries.length > 0) {
1902
+ lines.push("");
1903
+ lines.push(bold("Network"));
1904
+ for (const n2 of sample.networkEntries.slice(-5)) {
1905
+ const status = n2.status ? n2.status >= 400 ? red(String(n2.status)) : green(String(n2.status)) : dim("???");
1906
+ lines.push(` ${dim(n2.method)} ${truncate(n2.url, 50)} ${dim("\u2192")} ${status} ${dim(`(${n2.durationMs}ms)`)}`);
1907
+ }
1908
+ }
1909
+ if (sample.timeline.length > 0) {
1910
+ lines.push("");
1911
+ lines.push(bold("Timeline"));
1912
+ for (const e of sample.timeline.slice(0, 10)) {
1913
+ const t2 = String(e.t).padStart(5);
1914
+ lines.push(` ${dim(t2 + "ms")} ${e.summary}`);
1915
+ }
1916
+ }
1917
+ return lines.join(`
1918
+ `);
1919
+ }
1920
+
1921
+ // src/commands/incidents.ts
1922
+ var DEFAULT_LIMIT = 5;
1923
+ async function runIncidents(opts) {
1924
+ const session = requireSession();
1925
+ let projectKey;
1926
+ try {
1927
+ projectKey = await resolveProjectKey(opts.pk, !opts.json, session.token);
1928
+ } catch (err) {
1929
+ if (!opts.json) {
1930
+ console.error(err.message);
1931
+ console.error('Run "snapfail init" to set up a project.');
1932
+ } else {
1933
+ process.stdout.write(JSON.stringify({ error: err.message }) + `
1934
+ `);
1935
+ }
1936
+ process.exit(1);
1937
+ }
1938
+ const config = loadConfig(process.cwd(), projectKey);
1939
+ const limit = opts.limit ?? DEFAULT_LIMIT;
1940
+ const result = await fetchIncidents(config, {
1941
+ status: opts.status ?? "unresolved",
1942
+ limit,
1943
+ offset: opts.offset
1944
+ });
1945
+ if (opts.json) {
1946
+ process.stdout.write(JSON.stringify(result, null, 2) + `
1947
+ `);
1948
+ return;
1949
+ }
1950
+ const statusLabel = opts.status ?? "unresolved";
1951
+ const offset = opts.offset ?? 0;
1952
+ const showing = result.data.length;
1953
+ const total = result.total;
1954
+ console.log(`
1955
+ Incidents (${statusLabel}) \u2014 showing ${offset + 1}\u2013${offset + showing} of ${total} groups`);
1956
+ if (total > offset + showing) {
1957
+ console.log(` Use --limit ${limit * 2} or --offset ${offset + showing} to see more.
1958
+ `);
1959
+ } else {
1960
+ console.log("");
1961
+ }
1962
+ console.log(formatIncidentList(result.data, result.total, opts.status));
1963
+ }
1964
+
1965
+ // src/commands/incident.ts
1966
+ async function runIncident(opts) {
1967
+ const session = requireSession();
1968
+ let projectKey;
1969
+ try {
1970
+ projectKey = await resolveProjectKey(opts.pk, !opts.json, session.token);
1971
+ } catch (err) {
1972
+ if (!opts.json) {
1973
+ console.error(err.message);
1974
+ } else {
1975
+ process.stdout.write(JSON.stringify({ error: err.message }) + `
1976
+ `);
1977
+ }
1978
+ process.exit(1);
1979
+ }
1980
+ const config = loadConfig(process.cwd(), projectKey);
1981
+ if (opts.delete) {
1982
+ const ok = await deleteGroup(config, opts.id);
1983
+ if (!ok) {
1984
+ console.error(`Incident group ${opts.id} not found.`);
1985
+ process.exit(1);
1986
+ }
1987
+ if (opts.json) {
1988
+ process.stdout.write(JSON.stringify({ ok: true, deleted: opts.id }) + `
1989
+ `);
1990
+ } else {
1991
+ console.log(`Deleted incident group ${opts.id} and all related data.`);
1992
+ }
1993
+ return;
1994
+ }
1995
+ const statusAction = opts.resolve ? "resolved" : opts.ignore ? "ignored" : opts.reopen ? "unresolved" : null;
1996
+ if (statusAction) {
1997
+ const ok = await updateIncidentStatus(config, opts.id, statusAction);
1998
+ if (!ok) {
1999
+ console.error(`Incident ${opts.id} not found.`);
2000
+ process.exit(1);
2001
+ }
2002
+ if (opts.json) {
2003
+ process.stdout.write(JSON.stringify({ ok: true, id: opts.id, status: statusAction }) + `
2004
+ `);
2005
+ } else {
2006
+ const label = statusAction === "resolved" ? "resolved" : statusAction === "ignored" ? "ignored" : "reopened";
2007
+ console.log(`Incident ${opts.id} marked as ${label}.`);
2008
+ }
2009
+ return;
2010
+ }
2011
+ if (opts.sample != null) {
2012
+ const s = await fetchSample(config, opts.id, opts.sample);
2013
+ if (!s) {
2014
+ console.error(`Sample ${opts.sample} not found for incident ${opts.id}`);
2015
+ process.exit(1);
2016
+ }
2017
+ if (opts.json) {
2018
+ process.stdout.write(JSON.stringify(s, null, 2) + `
2019
+ `);
2020
+ return;
2021
+ }
2022
+ const detail = await fetchIncident(config, opts.id);
2023
+ if (!detail) {
2024
+ console.error(`Incident ${opts.id} not found.`);
2025
+ process.exit(1);
2026
+ }
2027
+ console.log(formatIncidentDetail(detail.group, s));
2028
+ return;
2029
+ }
2030
+ const result = await fetchIncident(config, opts.id);
2031
+ if (!result) {
2032
+ console.error(`Incident ${opts.id} not found.`);
2033
+ process.exit(1);
2034
+ }
2035
+ if (opts.json) {
2036
+ process.stdout.write(JSON.stringify(result, null, 2) + `
2037
+ `);
2038
+ return;
2039
+ }
2040
+ console.log(formatIncidentDetail(result.group, result.sample));
2041
+ }
2042
+
2043
+ // src/commands/init.ts
2044
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
2045
+ import { resolve as resolve2, join as join2 } from "path";
2046
+ import { execSync as execSync2 } from "child_process";
1945
2047
 
1946
2048
  // src/skill.ts
1947
2049
  var SKILL_BODY = `
@@ -2059,8 +2161,8 @@ async function waitForCliToken(endpoint) {
2059
2161
  resolved = true;
2060
2162
  const session = { token, email, name: name ?? email, endpoint };
2061
2163
  writeSession(session);
2062
- setTimeout(() => server.stop(), 200);
2063
2164
  resolve2(session);
2165
+ setTimeout(() => server.stop(true), 50);
2064
2166
  return new Response(`<!doctype html><html><head><meta charset="utf-8"><title>snapfail</title>
2065
2167
  <style>
2066
2168
  body{background:#0a0a0a;color:#f5f5f5;font-family:system-ui,sans-serif;
@@ -2390,8 +2492,20 @@ async function runInit(cwd = process.cwd()) {
2390
2492
 
2391
2493
  // src/commands/explain.ts
2392
2494
  async function runExplain(opts) {
2393
- requireSession();
2394
- const config = loadConfig(process.cwd(), opts.pk);
2495
+ const session = requireSession();
2496
+ let projectKey;
2497
+ try {
2498
+ projectKey = await resolveProjectKey(opts.pk, !opts.json, session.token);
2499
+ } catch (err) {
2500
+ if (!opts.json) {
2501
+ console.error(err.message);
2502
+ } else {
2503
+ process.stdout.write(JSON.stringify({ error: err.message }) + `
2504
+ `);
2505
+ }
2506
+ process.exit(1);
2507
+ }
2508
+ const config = loadConfig(process.cwd(), projectKey);
2395
2509
  const result = await fetchIncident(config, opts.id);
2396
2510
  if (!result) {
2397
2511
  console.error(`Incident ${opts.id} not found.`);
@@ -2436,10 +2550,192 @@ async function runLogin() {
2436
2550
  console.log(`
2437
2551
  Logged in as ${session.email}`);
2438
2552
  }
2553
+
2554
+ // src/help.ts
2555
+ var GLOBAL_HELP = `
2556
+ snapfail \u2014 browser error capture for AI-assisted debugging
2557
+
2558
+ USAGE
2559
+ snapfail <command> [options]
2560
+
2561
+ COMMANDS
2562
+ init Set up snapfail in a project (detects framework, authenticates, writes .env)
2563
+ login Authenticate with your snapfail account
2564
+ skill Install the snapfail skill globally for Claude Code (~/.claude/skills/)
2565
+ incidents List incident groups for your project
2566
+ incident View or update a single incident group
2567
+ explain Output raw evidence context for LLM analysis
2568
+ help Show this help, or help for a specific command
2569
+
2570
+ OPTIONS
2571
+ --version Print the CLI version
2572
+ --help Show help for the current command
2573
+
2574
+ Run \`snapfail help <command>\` for detailed usage of any command.
2575
+ `.trimStart();
2576
+ var COMMAND_HELP = {
2577
+ init: `
2578
+ snapfail init \u2014 Set up snapfail in the current project
2579
+
2580
+ USAGE
2581
+ snapfail init
2582
+
2583
+ DESCRIPTION
2584
+ Authenticates your account, selects or creates a project, writes the
2585
+ project key to .env, injects the browser SDK into your framework config,
2586
+ and generates .claude/skills/snapfail/SKILL.md for AI agents.
2587
+
2588
+ Supported frameworks: astro, vite, next
2589
+ Run once per project. Idempotent \u2014 safe to run again.
2590
+
2591
+ EXIT CODES
2592
+ 0 Success
2593
+ 1 Cancelled or error
2594
+ `.trimStart(),
2595
+ login: `
2596
+ snapfail login \u2014 Authenticate with your snapfail account
2597
+
2598
+ USAGE
2599
+ snapfail login
2600
+
2601
+ DESCRIPTION
2602
+ Opens a browser window for OAuth authentication. Saves the session to
2603
+ ~/.snapfail/auth.json. Clears any existing session for the default
2604
+ endpoint first.
2605
+
2606
+ EXIT CODES
2607
+ 0 Authenticated successfully
2608
+ 1 Authentication timed out or failed
2609
+ `.trimStart(),
2610
+ skill: `
2611
+ snapfail skill \u2014 Install the snapfail skill globally for Claude Code
2612
+
2613
+ USAGE
2614
+ snapfail skill
2615
+
2616
+ DESCRIPTION
2617
+ Writes the snapfail skill definition to ~/.claude/skills/snapfail/SKILL.md.
2618
+ Claude Code picks this up automatically \u2014 no project configuration needed.
2619
+
2620
+ Use this when you want snapfail available in all your Claude Code sessions,
2621
+ not just projects where you ran \`snapfail init\`.
2622
+
2623
+ EXIT CODES
2624
+ 0 Skill installed or updated
2625
+ `.trimStart(),
2626
+ incidents: `
2627
+ snapfail incidents \u2014 List incident groups for your project
2628
+
2629
+ USAGE
2630
+ snapfail incidents [options]
2631
+
2632
+ OPTIONS
2633
+ --pk <key> Project key (overrides .env / SNAPFAIL_PROJECT_KEY)
2634
+ --status <s> Filter by status: unresolved (default), resolved, ignored, all
2635
+ --limit <n> Number of results to show (default: 5)
2636
+ --offset <n> Pagination offset (default: 0)
2637
+ --json Output raw JSON for LLM consumption
2638
+
2639
+ DESCRIPTION
2640
+ Lists grouped incident summaries. Each group aggregates all occurrences of
2641
+ the same error fingerprint. Use \`snapfail incident <id>\` to drill in.
2642
+
2643
+ When running interactively (no --json), prompts for project selection if
2644
+ no project key is configured.
2645
+
2646
+ EXAMPLES
2647
+ snapfail incidents
2648
+ snapfail incidents --limit 20 --status resolved
2649
+ snapfail incidents --json --pk proj_abc123
2650
+
2651
+ EXIT CODES
2652
+ 0 Success
2653
+ 1 Auth or API error
2654
+ `.trimStart(),
2655
+ incident: `
2656
+ snapfail incident \u2014 View or update a single incident group
2657
+
2658
+ USAGE
2659
+ snapfail incident <id> [options]
2660
+
2661
+ OPTIONS
2662
+ --pk <key> Project key (overrides .env / SNAPFAIL_PROJECT_KEY)
2663
+ --sample <n> Show the Nth raw sample (0-based index)
2664
+ --resolve Mark the incident as resolved
2665
+ --ignore Mark the incident as ignored
2666
+ --reopen Reopen a resolved or ignored incident (sets to unresolved)
2667
+ --delete Permanently delete the group and all its samples
2668
+ --json Output raw JSON for LLM consumption
2669
+
2670
+ DESCRIPTION
2671
+ Without status flags: displays the incident group summary and a
2672
+ representative timeline sample.
2673
+
2674
+ With --sample <n>: shows the full raw event data for that specific sample,
2675
+ useful for comparing across different occurrences.
2676
+
2677
+ Status flags (--resolve, --ignore, --reopen) update the group on the server
2678
+ and print confirmation. Use --json to get machine-readable output.
2679
+
2680
+ EXAMPLES
2681
+ snapfail incident abc123
2682
+ snapfail incident abc123 --sample 2
2683
+ snapfail incident abc123 --resolve
2684
+ snapfail incident abc123 --json --pk proj_abc123
2685
+
2686
+ EXIT CODES
2687
+ 0 Success
2688
+ 1 Not found or API error
2689
+ `.trimStart(),
2690
+ explain: `
2691
+ snapfail explain \u2014 Output raw evidence context for LLM analysis
2692
+
2693
+ USAGE
2694
+ snapfail explain <id> [options]
2695
+
2696
+ OPTIONS
2697
+ --pk <key> Project key (overrides .env / SNAPFAIL_PROJECT_KEY)
2698
+ --json Output raw JSON ({ group, samples }) instead of formatted markdown
2699
+
2700
+ DESCRIPTION
2701
+ Fetches the incident group and up to 3 representative samples, then outputs
2702
+ structured evidence (error, stack, console, network, timeline) formatted for
2703
+ an LLM to reason about root cause.
2704
+
2705
+ This command does NOT pre-diagnose. Pipe the output to a frontier model
2706
+ (Claude, GPT-4, etc.) and ask it to identify the root cause.
2707
+
2708
+ The --json flag outputs the raw data objects, useful when you want to
2709
+ process the evidence programmatically.
2710
+
2711
+ EXAMPLES
2712
+ snapfail explain abc123
2713
+ snapfail explain abc123 --json | claude --print "What caused this error?"
2714
+
2715
+ EXIT CODES
2716
+ 0 Success
2717
+ 1 Not found or API error
2718
+ `.trimStart()
2719
+ };
2720
+ function printHelp(command) {
2721
+ if (!command) {
2722
+ process.stdout.write(GLOBAL_HELP);
2723
+ return;
2724
+ }
2725
+ const text2 = COMMAND_HELP[command];
2726
+ if (text2) {
2727
+ process.stdout.write(text2);
2728
+ } else {
2729
+ process.stdout.write(`No help found for command: ${command}
2730
+
2731
+ `);
2732
+ process.stdout.write(GLOBAL_HELP);
2733
+ }
2734
+ }
2439
2735
  // package.json
2440
2736
  var package_default = {
2441
2737
  name: "snapfail",
2442
- version: "0.0.25",
2738
+ version: "0.0.26",
2443
2739
  type: "module",
2444
2740
  description: "CLI for snapfail \u2014 project setup, incident inspection and AI diagnostics",
2445
2741
  license: "MIT",
@@ -2483,10 +2779,19 @@ async function main() {
2483
2779
  const { command, args, flags } = parseArgs(process.argv);
2484
2780
  const json = flags["json"] === true;
2485
2781
  const pk = typeof flags["pk"] === "string" ? flags["pk"] : undefined;
2782
+ const helpFlag = flags["help"] === true || flags["h"] === true;
2486
2783
  if (command === "--version" || command === "-v" || flags["version"] === true) {
2487
2784
  console.log(VERSION);
2488
2785
  return;
2489
2786
  }
2787
+ if (command === "help") {
2788
+ printHelp(args[0]);
2789
+ return;
2790
+ }
2791
+ if (helpFlag) {
2792
+ printHelp(command);
2793
+ return;
2794
+ }
2490
2795
  try {
2491
2796
  if (command === "init") {
2492
2797
  await runInit();
@@ -2513,25 +2818,35 @@ async function main() {
2513
2818
  if (command === "incident") {
2514
2819
  const id = args[0];
2515
2820
  if (!id) {
2516
- console.error("Usage: snapfail incident <id> [--sample <n>] [--json]");
2821
+ printHelp("incident");
2517
2822
  process.exit(1);
2518
2823
  }
2519
2824
  const sampleFlag = flags["sample"];
2520
2825
  const sample = typeof sampleFlag === "string" ? parseInt(sampleFlag) : undefined;
2521
- await runIncident({ id, sample, json, pk, delete: flags["delete"] === true });
2826
+ await runIncident({
2827
+ id,
2828
+ sample,
2829
+ json,
2830
+ pk,
2831
+ delete: flags["delete"] === true,
2832
+ resolve: flags["resolve"] === true,
2833
+ ignore: flags["ignore"] === true,
2834
+ reopen: flags["reopen"] === true
2835
+ });
2522
2836
  return;
2523
2837
  }
2524
2838
  if (command === "explain") {
2525
2839
  const id = args[0];
2526
2840
  if (!id) {
2527
- console.error("Usage: snapfail explain <id> [--force] [--json]");
2841
+ printHelp("explain");
2528
2842
  process.exit(1);
2529
2843
  }
2530
2844
  await runExplain({ id, json, pk });
2531
2845
  return;
2532
2846
  }
2533
- console.error(`Unknown command: ${command}`);
2534
- console.error(`snapfail v${VERSION} \u2014 Usage: snapfail [init|login|incidents|incident|explain|skill] [--version]`);
2847
+ console.error(`Unknown command: ${command}
2848
+ `);
2849
+ printHelp();
2535
2850
  process.exit(1);
2536
2851
  } catch (err) {
2537
2852
  const message = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapfail",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "type": "module",
5
5
  "description": "CLI for snapfail — project setup, incident inspection and AI diagnostics",
6
6
  "license": "MIT",