snapfail 0.0.20 → 0.0.22

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/index.js CHANGED
@@ -95,7 +95,9 @@ import { readFileSync, writeFileSync, existsSync } from "fs";
95
95
  import { resolve } from "path";
96
96
  var DEFAULT_ENDPOINT = "https://app.snapfail.com";
97
97
  var ENV_FILE = ".env";
98
- function loadConfig(cwd = process.cwd()) {
98
+ function loadConfig(cwd = process.cwd(), pk) {
99
+ if (pk)
100
+ return { projectKey: pk, endpoint: DEFAULT_ENDPOINT };
99
101
  const fromEnv = process.env["SNAPFAIL_PROJECT_KEY"];
100
102
  if (fromEnv)
101
103
  return { projectKey: fromEnv, endpoint: DEFAULT_ENDPOINT };
@@ -127,6 +129,37 @@ ${line}
127
129
  }
128
130
  }
129
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 requireSession() {
156
+ const session = readSession();
157
+ if (!session) {
158
+ throw new Error('No active session. Run "snapfail init" to authenticate.');
159
+ }
160
+ return session;
161
+ }
162
+
130
163
  // src/api.ts
131
164
  async function apiFetch(url, options) {
132
165
  let res;
@@ -234,6 +267,68 @@ function formatIncidentList(data, total, status) {
234
267
  ${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
235
268
  return table + summary;
236
269
  }
270
+ function formatLLMContext(group, samples) {
271
+ const lines = [];
272
+ lines.push(`# Incident: ${group.title}`);
273
+ lines.push(`Group ID: ${group.id}`);
274
+ lines.push(`Fingerprint: ${group.fingerprint}`);
275
+ lines.push(`Occurrences: ${group.count} | First: ${new Date(group.firstSeen).toISOString()} | Last: ${new Date(group.lastSeen).toISOString()}`);
276
+ lines.push(`Environments: ${group.environments.join(", ")}`);
277
+ lines.push(`Status: ${group.status}`);
278
+ lines.push("");
279
+ lines.push("## Error");
280
+ lines.push(`Type: ${group.errorType}`);
281
+ lines.push(`Message: ${samples[0]?.errorMessage ?? group.title}`);
282
+ lines.push(`Normalized: ${samples[0]?.normalizedMessage ?? group.title}`);
283
+ if (samples[0] && samples[0].stackFrames.length > 0) {
284
+ lines.push("");
285
+ lines.push("## Stack");
286
+ for (const f of samples[0].stackFrames) {
287
+ lines.push(` ${f.fn ?? "(anonymous)"} ${f.file}:${f.line}${f.col != null ? `:${f.col}` : ""}`);
288
+ }
289
+ }
290
+ for (let i = 0;i < samples.length; i++) {
291
+ const s = samples[i];
292
+ lines.push("");
293
+ lines.push(`## Sample ${i + 1} / ${group.sampleIds.length}`);
294
+ lines.push(`Device: ${s.device.userAgent}`);
295
+ if (s.device.viewport) {
296
+ lines.push(`Viewport: ${s.device.viewport.width}x${s.device.viewport.height} Language: ${s.device.language ?? "unknown"}`);
297
+ }
298
+ lines.push(`URL: ${s.url}`);
299
+ if (s.route)
300
+ lines.push(`Route: ${s.route}`);
301
+ lines.push(`Environment: ${s.environmentMode}`);
302
+ if (s.consoleEntries.length > 0) {
303
+ lines.push("");
304
+ lines.push("### Console");
305
+ for (const e of s.consoleEntries) {
306
+ const args = e.args.map((a) => String(a)).join(" ");
307
+ lines.push(` [${e.level}] ${args}`);
308
+ }
309
+ }
310
+ if (s.networkEntries.length > 0) {
311
+ lines.push("");
312
+ lines.push("### Network");
313
+ for (const n of s.networkEntries) {
314
+ const status = n.status ? ` \u2192 ${n.status}` : n.error ? ` \u2192 ERROR` : "";
315
+ lines.push(` ${n.method} ${n.url}${status} (${n.durationMs}ms)`);
316
+ }
317
+ }
318
+ if (s.timeline.length > 0) {
319
+ lines.push("");
320
+ lines.push("### Timeline");
321
+ for (const e of s.timeline) {
322
+ lines.push(` +${e.t}ms ${e.kind}: ${e.summary}`);
323
+ }
324
+ }
325
+ if (i < samples.length - 1)
326
+ lines.push(`
327
+ ---`);
328
+ }
329
+ return lines.join(`
330
+ `);
331
+ }
237
332
  function formatIncidentDetail(group, sample) {
238
333
  const lines = [];
239
334
  lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
@@ -285,7 +380,8 @@ function formatIncidentDetail(group, sample) {
285
380
 
286
381
  // src/commands/incidents.ts
287
382
  async function runIncidents(opts) {
288
- const config = loadConfig();
383
+ requireSession();
384
+ const config = loadConfig(process.cwd(), opts.pk);
289
385
  const result = await fetchIncidents(config, {
290
386
  status: opts.status ?? "unresolved",
291
387
  limit: opts.limit,
@@ -301,7 +397,8 @@ async function runIncidents(opts) {
301
397
 
302
398
  // src/commands/incident.ts
303
399
  async function runIncident(opts) {
304
- const config = loadConfig();
400
+ requireSession();
401
+ const config = loadConfig(process.cwd(), opts.pk);
305
402
  if (opts.delete) {
306
403
  const ok = await deleteGroup(config, opts.id);
307
404
  if (!ok) {
@@ -1902,30 +1999,6 @@ Config file: \`.snapfail.json\` (gitignored)
1902
1999
  `;
1903
2000
  }
1904
2001
 
1905
- // src/session.ts
1906
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, rmSync } from "fs";
1907
- import { join } from "path";
1908
- import { homedir } from "os";
1909
- var SESSION_DIR = join(homedir(), ".snapfail");
1910
- var SESSION_FILE = join(SESSION_DIR, "auth.json");
1911
- function readSession() {
1912
- try {
1913
- if (!existsSync2(SESSION_FILE))
1914
- return null;
1915
- const raw = JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
1916
- if (!raw.token || !raw.endpoint)
1917
- return null;
1918
- return raw;
1919
- } catch {
1920
- return null;
1921
- }
1922
- }
1923
- function writeSession(session) {
1924
- mkdirSync(SESSION_DIR, { recursive: true });
1925
- writeFileSync2(SESSION_FILE, JSON.stringify(session, null, 2) + `
1926
- `, "utf-8");
1927
- }
1928
-
1929
2002
  // src/auth.ts
1930
2003
  import { execSync } from "child_process";
1931
2004
  function openBrowser(url) {
@@ -2295,69 +2368,28 @@ async function runInit(cwd = process.cwd()) {
2295
2368
  }
2296
2369
 
2297
2370
  // src/commands/explain.ts
2298
- async function fetchDiagnosis(config, id, force) {
2299
- const url = new URL(`${config.endpoint}/api/diagnose/${id}`);
2300
- if (force)
2301
- url.searchParams.set("force", "1");
2302
- let res;
2303
- try {
2304
- res = await fetch(url.toString(), {
2305
- method: "POST",
2306
- headers: { "X-Project-Key": config.projectKey, "Content-Type": "application/json" },
2307
- body: "{}"
2308
- });
2309
- } catch (err) {
2310
- throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
2311
- }
2312
- if (res.status === 401)
2313
- throw new Error("Unauthorized: check your project key.");
2314
- if (res.status === 404)
2315
- return null;
2316
- if (res.status === 422)
2317
- throw new Error("No samples available to diagnose this incident.");
2318
- if (!res.ok) {
2319
- const body = await res.text().catch(() => "");
2320
- throw new Error(`API error ${res.status}: ${body}`);
2321
- }
2322
- return res.json();
2323
- }
2324
- function confidenceColor(c3) {
2325
- if (c3 === "high")
2326
- return green(c3);
2327
- if (c3 === "medium")
2328
- return yellow(c3);
2329
- return red(c3);
2330
- }
2331
- function formatDiagnosis(d) {
2332
- const lines = [];
2333
- lines.push(bold("Root cause"));
2334
- lines.push(` ${d.rootCause}`);
2335
- lines.push("");
2336
- lines.push(bold("In plain language"));
2337
- lines.push(` ${d.plainSummary}`);
2338
- if (d.fixPrompt) {
2339
- lines.push("");
2340
- lines.push(bold("Fix prompt") + dim(" (paste into your AI assistant)"));
2341
- lines.push(` ${d.fixPrompt}`);
2342
- }
2343
- lines.push("");
2344
- lines.push(dim("Confidence:") + " " + confidenceColor(d.confidence) + dim(" \xB7 analyzed ") + d.analyzedSampleIds.length + dim(" samples \xB7 model: ") + dim(d.model));
2345
- return lines.join(`
2346
- `);
2347
- }
2348
2371
  async function runExplain(opts) {
2349
- const config = loadConfig();
2350
- const diagnosis = await fetchDiagnosis(config, opts.id, opts.force ?? false);
2351
- if (!diagnosis) {
2372
+ requireSession();
2373
+ const config = loadConfig(process.cwd(), opts.pk);
2374
+ const result = await fetchIncident(config, opts.id);
2375
+ if (!result) {
2352
2376
  console.error(`Incident ${opts.id} not found.`);
2353
2377
  process.exit(1);
2354
2378
  }
2379
+ const { group, sample } = result;
2380
+ const samples = [sample];
2381
+ for (let i2 = 1;i2 < Math.min(group.sampleIds.length, 3); i2++) {
2382
+ const s = await fetchSample(config, opts.id, i2);
2383
+ if (s)
2384
+ samples.push(s);
2385
+ }
2355
2386
  if (opts.json) {
2356
- process.stdout.write(JSON.stringify(diagnosis, null, 2) + `
2387
+ process.stdout.write(JSON.stringify({ group, samples }, null, 2) + `
2357
2388
  `);
2358
2389
  return;
2359
2390
  }
2360
- console.log(formatDiagnosis(diagnosis));
2391
+ process.stdout.write(formatLLMContext(group, samples) + `
2392
+ `);
2361
2393
  }
2362
2394
 
2363
2395
  // src/index.ts
@@ -2384,6 +2416,7 @@ var VERSION = "0.0.18";
2384
2416
  async function main() {
2385
2417
  const { command, args, flags } = parseArgs(process.argv);
2386
2418
  const json = flags["json"] === true;
2419
+ const pk = typeof flags["pk"] === "string" ? flags["pk"] : undefined;
2387
2420
  if (command === "--version" || command === "-v" || flags["version"] === true) {
2388
2421
  console.log(VERSION);
2389
2422
  return;
@@ -2396,6 +2429,7 @@ async function main() {
2396
2429
  if (command === "incidents") {
2397
2430
  await runIncidents({
2398
2431
  json,
2432
+ pk,
2399
2433
  status: typeof flags["status"] === "string" ? flags["status"] : undefined,
2400
2434
  limit: typeof flags["limit"] === "string" ? parseInt(flags["limit"]) : undefined,
2401
2435
  offset: typeof flags["offset"] === "string" ? parseInt(flags["offset"]) : undefined
@@ -2410,7 +2444,7 @@ async function main() {
2410
2444
  }
2411
2445
  const sampleFlag = flags["sample"];
2412
2446
  const sample = typeof sampleFlag === "string" ? parseInt(sampleFlag) : undefined;
2413
- await runIncident({ id, sample, json, delete: flags["delete"] === true });
2447
+ await runIncident({ id, sample, json, pk, delete: flags["delete"] === true });
2414
2448
  return;
2415
2449
  }
2416
2450
  if (command === "explain") {
@@ -2419,7 +2453,7 @@ async function main() {
2419
2453
  console.error("Usage: snapfail explain <id> [--force] [--json]");
2420
2454
  process.exit(1);
2421
2455
  }
2422
- await runExplain({ id, force: flags["force"] === true, json });
2456
+ await runExplain({ id, json, pk });
2423
2457
  return;
2424
2458
  }
2425
2459
  console.error(`Unknown command: ${command}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapfail",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "type": "module",
5
5
  "description": "CLI for snapfail — project setup, incident inspection and AI diagnostics",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ const VERSION = "0.0.18";
35
35
  async function main(): Promise<void> {
36
36
  const { command, args, flags } = parseArgs(process.argv);
37
37
  const json = flags["json"] === true;
38
+ const pk = typeof flags["pk"] === "string" ? flags["pk"] : undefined;
38
39
 
39
40
  if (command === "--version" || command === "-v" || flags["version"] === true) {
40
41
  console.log(VERSION);
@@ -50,6 +51,7 @@ async function main(): Promise<void> {
50
51
  if (command === "incidents") {
51
52
  await runIncidents({
52
53
  json,
54
+ pk,
53
55
  status: typeof flags["status"] === "string" ? flags["status"] : undefined,
54
56
  limit: typeof flags["limit"] === "string" ? parseInt(flags["limit"]) : undefined,
55
57
  offset: typeof flags["offset"] === "string" ? parseInt(flags["offset"]) : undefined,
@@ -66,7 +68,7 @@ async function main(): Promise<void> {
66
68
  const sampleFlag = flags["sample"];
67
69
  const sample =
68
70
  typeof sampleFlag === "string" ? parseInt(sampleFlag) : undefined;
69
- await runIncident({ id, sample, json, delete: flags["delete"] === true });
71
+ await runIncident({ id, sample, json, pk, delete: flags["delete"] === true });
70
72
  return;
71
73
  }
72
74
 
@@ -76,7 +78,7 @@ async function main(): Promise<void> {
76
78
  console.error("Usage: snapfail explain <id> [--force] [--json]");
77
79
  process.exit(1);
78
80
  }
79
- await runExplain({ id, force: flags["force"] === true, json });
81
+ await runExplain({ id, json, pk });
80
82
  return;
81
83
  }
82
84