snapfail 0.0.21 → 0.0.23

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
@@ -267,6 +267,68 @@ function formatIncidentList(data, total, status) {
267
267
  ${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
268
268
  return table + summary;
269
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
+ }
270
332
  function formatIncidentDetail(group, sample) {
271
333
  const lines = [];
272
334
  lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
@@ -1876,9 +1938,7 @@ ${r2}
1876
1938
  }).prompt();
1877
1939
 
1878
1940
  // src/skill.ts
1879
- function generateSkillMd(config, framework) {
1880
- return `# snapfail
1881
-
1941
+ var SKILL_BODY = `
1882
1942
  snapfail captures browser error context (stack, console, network, replay timeline)
1883
1943
  and stores it so you can diagnose root causes. Evidence is structured for LLM reasoning.
1884
1944
 
@@ -1890,14 +1950,18 @@ When the user reports an error or unexpected behavior in their app, run:
1890
1950
  snapfail incidents --json
1891
1951
  \`\`\`
1892
1952
 
1893
- Then drill into a specific incident:
1953
+ Then get full structured context for a specific incident:
1894
1954
 
1895
1955
  \`\`\`
1896
- snapfail incident <id> --json
1956
+ snapfail explain <id>
1897
1957
  \`\`\`
1898
1958
 
1899
- **Always use \`--json\` when querying from the LLM** \u2014 it returns structured data
1900
- without ANSI color codes.
1959
+ \`snapfail explain\` outputs all captured evidence (stack, console, network, timeline, device,
1960
+ multiple samples) formatted as structured text. **Read it and reason about the root cause
1961
+ yourself** \u2014 do not delegate this to another model.
1962
+
1963
+ **Always use \`--json\` when querying from the LLM** for \`incidents\` and \`incident\` \u2014
1964
+ it returns structured data without ANSI color codes.
1901
1965
 
1902
1966
  ## Commands
1903
1967
 
@@ -1906,34 +1970,47 @@ snapfail incidents [--status=unresolved|resolved|ignored] [--json]
1906
1970
  List incident groups. Default status: unresolved.
1907
1971
 
1908
1972
  snapfail incident <id> [--sample <n>] [--json]
1909
- Show incident detail: group summary + most recent sample (stack, console, network, timeline).
1910
- Use --sample <n> (0-indexed) to compare different occurrences of the same error.
1973
+ Group summary + one sample (stack, console, network, timeline).
1974
+ Use --sample <n> (0-indexed) to compare different occurrences.
1911
1975
 
1912
1976
  snapfail explain <id>
1913
- AI diagnosis: root cause, plain summary, fix prompt. (requires @snapfail/ai)
1977
+ Outputs structured evidence for up to 3 samples. Read and diagnose directly.
1914
1978
  \`\`\`
1915
1979
 
1980
+ All commands accept \`--pk <project_key>\` to override the project key from \`.env\`.
1981
+
1916
1982
  ## Interpreting an incident
1917
1983
 
1918
- Key fields from \`snapfail incident <id> --json\`:
1984
+ Key fields:
1919
1985
 
1920
1986
  - \`group.title\` \u2014 normalized error message (dynamic values replaced with [uuid], [id], etc.)
1921
- - \`group.count\` \u2014 total occurrences
1922
- - \`group.environments\` \u2014 where it happened: "dev" | "prod"
1987
+ - \`group.count\` \u2014 total occurrences (\u2260 number of groups)
1988
+ - \`group.environments\` \u2014 "dev" | "prod"
1923
1989
  - \`group.severity\` \u2014 "critical" | "error" | "warning"
1924
1990
  - \`sample.stackFrames\` \u2014 call stack at the time of the error
1925
- - \`sample.consoleEntries\` \u2014 last ~15s of console output before the error
1926
- - \`sample.networkEntries\` \u2014 last ~15s of network requests (scrubbed)
1927
- - \`sample.timeline\` \u2014 derived user interaction sequence (clicks, navigation, mutations)
1991
+ - \`sample.consoleEntries\` \u2014 console output before the error
1992
+ - \`sample.networkEntries\` \u2014 network requests (auth headers scrubbed)
1993
+ - \`sample.timeline\` \u2014 user interaction sequence (clicks, navigation, mutations)
1994
+ - \`sample.device.userAgent\` \u2014 browser/OS \u2014 critical for mobile/in-app browser bugs
1928
1995
  - \`sample.url\` / \`sample.route\` \u2014 where the error occurred
1929
-
1996
+ `;
1997
+ function generateSkillMd(config, framework) {
1998
+ return `# snapfail
1999
+ ${SKILL_BODY}
1930
2000
  ## Config
1931
2001
 
1932
2002
  Project key: \`${config.projectKey}\`
1933
2003
  Endpoint: \`${config.endpoint}\`
1934
- ${framework ? `Framework: ${framework}
1935
- ` : ""}
1936
- Config file: \`.snapfail.json\` (gitignored)
2004
+ ${framework ? `Framework: \`${framework}\`
2005
+ ` : ""}`;
2006
+ }
2007
+ function generateGlobalSkillMd() {
2008
+ return `# snapfail
2009
+ ${SKILL_BODY}
2010
+ ## Project key
2011
+
2012
+ The project key is read automatically from \`SNAPFAIL_PROJECT_KEY\` in \`.env\` or the shell.
2013
+ Pass \`--pk <key>\` to override when querying a specific project.
1937
2014
  `;
1938
2015
  }
1939
2016
 
@@ -2254,10 +2331,10 @@ async function runInit(cwd = process.cwd()) {
2254
2331
  writeProjectKey(selectedKey, cwd, "NEXT_PUBLIC_SNAPFAIL_PROJECT_KEY");
2255
2332
  addToGitignore(cwd, ".env");
2256
2333
  log.success("SNAPFAIL_PROJECT_KEY written to .env");
2257
- const skillDir = join2(cwd, ".agents", "skills", "snapfail");
2334
+ const skillDir = join2(cwd, ".claude", "skills", "snapfail");
2258
2335
  mkdirSync2(skillDir, { recursive: true });
2259
2336
  writeFileSync3(join2(skillDir, "SKILL.md"), generateSkillMd({ projectKey: selectedKey, endpoint: DEFAULT_ENDPOINT }, framework), "utf-8");
2260
- log.success("SKILL.md created at .agents/skills/snapfail/SKILL.md");
2337
+ log.success("SKILL.md created at .claude/skills/snapfail/SKILL.md");
2261
2338
  const pm = detectPackageManager(cwd);
2262
2339
  if (framework === "astro") {
2263
2340
  const cfg = findConfigFile(cwd, ["astro.config.ts", "astro.config.mjs", "astro.config.js"]);
@@ -2306,70 +2383,41 @@ async function runInit(cwd = process.cwd()) {
2306
2383
  }
2307
2384
 
2308
2385
  // src/commands/explain.ts
2309
- async function fetchDiagnosis(config, id, force) {
2310
- const url = new URL(`${config.endpoint}/api/diagnose/${id}`);
2311
- if (force)
2312
- url.searchParams.set("force", "1");
2313
- let res;
2314
- try {
2315
- res = await fetch(url.toString(), {
2316
- method: "POST",
2317
- headers: { "X-Project-Key": config.projectKey, "Content-Type": "application/json" },
2318
- body: "{}"
2319
- });
2320
- } catch (err) {
2321
- throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
2322
- }
2323
- if (res.status === 401)
2324
- throw new Error("Unauthorized: check your project key.");
2325
- if (res.status === 404)
2326
- return null;
2327
- if (res.status === 422)
2328
- throw new Error("No samples available to diagnose this incident.");
2329
- if (!res.ok) {
2330
- const body = await res.text().catch(() => "");
2331
- throw new Error(`API error ${res.status}: ${body}`);
2332
- }
2333
- return res.json();
2334
- }
2335
- function confidenceColor(c3) {
2336
- if (c3 === "high")
2337
- return green(c3);
2338
- if (c3 === "medium")
2339
- return yellow(c3);
2340
- return red(c3);
2341
- }
2342
- function formatDiagnosis(d) {
2343
- const lines = [];
2344
- lines.push(bold("Root cause"));
2345
- lines.push(` ${d.rootCause}`);
2346
- lines.push("");
2347
- lines.push(bold("In plain language"));
2348
- lines.push(` ${d.plainSummary}`);
2349
- if (d.fixPrompt) {
2350
- lines.push("");
2351
- lines.push(bold("Fix prompt") + dim(" (paste into your AI assistant)"));
2352
- lines.push(` ${d.fixPrompt}`);
2353
- }
2354
- lines.push("");
2355
- lines.push(dim("Confidence:") + " " + confidenceColor(d.confidence) + dim(" \xB7 analyzed ") + d.analyzedSampleIds.length + dim(" samples \xB7 model: ") + dim(d.model));
2356
- return lines.join(`
2357
- `);
2358
- }
2359
2386
  async function runExplain(opts) {
2360
2387
  requireSession();
2361
2388
  const config = loadConfig(process.cwd(), opts.pk);
2362
- const diagnosis = await fetchDiagnosis(config, opts.id, opts.force ?? false);
2363
- if (!diagnosis) {
2389
+ const result = await fetchIncident(config, opts.id);
2390
+ if (!result) {
2364
2391
  console.error(`Incident ${opts.id} not found.`);
2365
2392
  process.exit(1);
2366
2393
  }
2394
+ const { group, sample } = result;
2395
+ const samples = [sample];
2396
+ for (let i2 = 1;i2 < Math.min(group.sampleIds.length, 3); i2++) {
2397
+ const s = await fetchSample(config, opts.id, i2);
2398
+ if (s)
2399
+ samples.push(s);
2400
+ }
2367
2401
  if (opts.json) {
2368
- process.stdout.write(JSON.stringify(diagnosis, null, 2) + `
2402
+ process.stdout.write(JSON.stringify({ group, samples }, null, 2) + `
2369
2403
  `);
2370
2404
  return;
2371
2405
  }
2372
- console.log(formatDiagnosis(diagnosis));
2406
+ process.stdout.write(formatLLMContext(group, samples) + `
2407
+ `);
2408
+ }
2409
+
2410
+ // src/commands/skill.ts
2411
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2412
+ import { join as join3 } from "path";
2413
+ import { homedir as homedir2 } from "os";
2414
+ var SKILL_DIR = join3(homedir2(), ".claude", "skills", "snapfail");
2415
+ var SKILL_FILE = join3(SKILL_DIR, "SKILL.md");
2416
+ async function runSkill() {
2417
+ mkdirSync3(SKILL_DIR, { recursive: true });
2418
+ const existed = existsSync4(SKILL_FILE);
2419
+ writeFileSync4(SKILL_FILE, generateGlobalSkillMd(), "utf-8");
2420
+ console.log(existed ? `Updated ${SKILL_FILE}` : `Created ${SKILL_FILE}`);
2373
2421
  }
2374
2422
 
2375
2423
  // src/index.ts
@@ -2406,6 +2454,10 @@ async function main() {
2406
2454
  await runInit();
2407
2455
  return;
2408
2456
  }
2457
+ if (command === "skill") {
2458
+ await runSkill();
2459
+ return;
2460
+ }
2409
2461
  if (command === "incidents") {
2410
2462
  await runIncidents({
2411
2463
  json,
@@ -2433,11 +2485,11 @@ async function main() {
2433
2485
  console.error("Usage: snapfail explain <id> [--force] [--json]");
2434
2486
  process.exit(1);
2435
2487
  }
2436
- await runExplain({ id, force: flags["force"] === true, json, pk });
2488
+ await runExplain({ id, json, pk });
2437
2489
  return;
2438
2490
  }
2439
2491
  console.error(`Unknown command: ${command}`);
2440
- console.error(`snapfail v${VERSION} \u2014 Usage: snapfail [incidents|incident|init|explain] [--version]`);
2492
+ console.error(`snapfail v${VERSION} \u2014 Usage: snapfail [incidents|incident|init|explain|skill] [--version]`);
2441
2493
  process.exit(1);
2442
2494
  } catch (err) {
2443
2495
  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.21",
3
+ "version": "0.0.23",
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
@@ -3,6 +3,7 @@ import { runIncidents } from "./commands/incidents.ts";
3
3
  import { runIncident } from "./commands/incident.ts";
4
4
  import { runInit } from "./commands/init.ts";
5
5
  import { runExplain } from "./commands/explain.ts";
6
+ import { runSkill } from "./commands/skill.ts";
6
7
 
7
8
  function parseArgs(argv: string[]): {
8
9
  command: string;
@@ -48,6 +49,11 @@ async function main(): Promise<void> {
48
49
  return;
49
50
  }
50
51
 
52
+ if (command === "skill") {
53
+ await runSkill();
54
+ return;
55
+ }
56
+
51
57
  if (command === "incidents") {
52
58
  await runIncidents({
53
59
  json,
@@ -78,12 +84,12 @@ async function main(): Promise<void> {
78
84
  console.error("Usage: snapfail explain <id> [--force] [--json]");
79
85
  process.exit(1);
80
86
  }
81
- await runExplain({ id, force: flags["force"] === true, json, pk });
87
+ await runExplain({ id, json, pk });
82
88
  return;
83
89
  }
84
90
 
85
91
  console.error(`Unknown command: ${command}`);
86
- console.error(`snapfail v${VERSION} — Usage: snapfail [incidents|incident|init|explain] [--version]`);
92
+ console.error(`snapfail v${VERSION} — Usage: snapfail [incidents|incident|init|explain|skill] [--version]`);
87
93
  process.exit(1);
88
94
  } catch (err) {
89
95
  const message = err instanceof Error ? err.message : String(err);