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 +130 -78
- package/package.json +1 -1
- package/src/index.ts +8 -2
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
|
-
|
|
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
|
|
1953
|
+
Then get full structured context for a specific incident:
|
|
1894
1954
|
|
|
1895
1955
|
\`\`\`
|
|
1896
|
-
snapfail
|
|
1956
|
+
snapfail explain <id>
|
|
1897
1957
|
\`\`\`
|
|
1898
1958
|
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
-
|
|
1910
|
-
Use --sample <n> (0-indexed) to compare different occurrences
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1926
|
-
- \`sample.networkEntries\` \u2014
|
|
1927
|
-
- \`sample.timeline\` \u2014
|
|
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:
|
|
1935
|
-
` : ""}
|
|
1936
|
-
|
|
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, ".
|
|
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 .
|
|
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
|
|
2363
|
-
if (!
|
|
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(
|
|
2402
|
+
process.stdout.write(JSON.stringify({ group, samples }, null, 2) + `
|
|
2369
2403
|
`);
|
|
2370
2404
|
return;
|
|
2371
2405
|
}
|
|
2372
|
-
|
|
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,
|
|
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
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,
|
|
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);
|