mitsupi 1.0.0
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/LICENSE +201 -0
- package/README.md +95 -0
- package/TODO.md +11 -0
- package/commands/handoff.md +100 -0
- package/commands/make-release.md +75 -0
- package/commands/pickup.md +30 -0
- package/commands/update-changelog.md +78 -0
- package/package.json +22 -0
- package/pi-extensions/answer.ts +527 -0
- package/pi-extensions/codex-tuning.ts +632 -0
- package/pi-extensions/commit.ts +248 -0
- package/pi-extensions/cwd-history.ts +237 -0
- package/pi-extensions/issues.ts +548 -0
- package/pi-extensions/loop.ts +446 -0
- package/pi-extensions/qna.ts +167 -0
- package/pi-extensions/reveal.ts +689 -0
- package/pi-extensions/review.ts +807 -0
- package/pi-themes/armin.json +81 -0
- package/pi-themes/nightowl.json +82 -0
- package/skills/anachb/SKILL.md +183 -0
- package/skills/anachb/departures.sh +79 -0
- package/skills/anachb/disruptions.sh +53 -0
- package/skills/anachb/route.sh +87 -0
- package/skills/anachb/search.sh +43 -0
- package/skills/ghidra/SKILL.md +254 -0
- package/skills/ghidra/scripts/find-ghidra.sh +54 -0
- package/skills/ghidra/scripts/ghidra-analyze.sh +239 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportAll.java +278 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportCalls.java +148 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportDecompiled.java +84 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportFunctions.java +114 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportStrings.java +123 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportSymbols.java +135 -0
- package/skills/github/SKILL.md +47 -0
- package/skills/improve-skill/SKILL.md +155 -0
- package/skills/improve-skill/scripts/extract-session.js +349 -0
- package/skills/oebb-scotty/SKILL.md +429 -0
- package/skills/oebb-scotty/arrivals.sh +83 -0
- package/skills/oebb-scotty/departures.sh +83 -0
- package/skills/oebb-scotty/disruptions.sh +33 -0
- package/skills/oebb-scotty/search-station.sh +36 -0
- package/skills/oebb-scotty/trip.sh +119 -0
- package/skills/openscad/SKILL.md +232 -0
- package/skills/openscad/examples/parametric_box.scad +92 -0
- package/skills/openscad/examples/phone_stand.scad +95 -0
- package/skills/openscad/tools/common.sh +50 -0
- package/skills/openscad/tools/export-stl.sh +56 -0
- package/skills/openscad/tools/extract-params.sh +147 -0
- package/skills/openscad/tools/multi-preview.sh +68 -0
- package/skills/openscad/tools/preview.sh +74 -0
- package/skills/openscad/tools/render-with-params.sh +91 -0
- package/skills/openscad/tools/validate.sh +46 -0
- package/skills/pi-share/SKILL.md +105 -0
- package/skills/pi-share/fetch-session.mjs +322 -0
- package/skills/sentry/SKILL.md +239 -0
- package/skills/sentry/lib/auth.js +99 -0
- package/skills/sentry/scripts/fetch-event.js +329 -0
- package/skills/sentry/scripts/fetch-issue.js +356 -0
- package/skills/sentry/scripts/list-issues.js +239 -0
- package/skills/sentry/scripts/search-events.js +291 -0
- package/skills/sentry/scripts/search-logs.js +240 -0
- package/skills/tmux/SKILL.md +105 -0
- package/skills/tmux/scripts/find-sessions.sh +112 -0
- package/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/skills/web-browser/SKILL.md +91 -0
- package/skills/web-browser/scripts/cdp.js +210 -0
- package/skills/web-browser/scripts/dismiss-cookies.js +373 -0
- package/skills/web-browser/scripts/eval.js +68 -0
- package/skills/web-browser/scripts/logs-tail.js +69 -0
- package/skills/web-browser/scripts/nav.js +65 -0
- package/skills/web-browser/scripts/net-summary.js +94 -0
- package/skills/web-browser/scripts/package-lock.json +33 -0
- package/skills/web-browser/scripts/package.json +6 -0
- package/skills/web-browser/scripts/pick.js +165 -0
- package/skills/web-browser/scripts/screenshot.js +52 -0
- package/skills/web-browser/scripts/start.js +80 -0
- package/skills/web-browser/scripts/watch.js +266 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { SENTRY_API_BASE, getAuthToken, fetchJson, formatTimestamp } from "../lib/auth.js";
|
|
4
|
+
|
|
5
|
+
const HELP = `Usage: fetch-event.js <event-id> [options]
|
|
6
|
+
|
|
7
|
+
Fetch a specific event by ID with full details.
|
|
8
|
+
|
|
9
|
+
Arguments:
|
|
10
|
+
event-id The event ID to fetch
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--org, -o <org> Organization slug (required)
|
|
14
|
+
--project, -p <project> Project slug (required)
|
|
15
|
+
--json Output raw JSON
|
|
16
|
+
--breadcrumbs, -b Show all breadcrumbs (default: last 30)
|
|
17
|
+
--spans Show span tree (for transactions)
|
|
18
|
+
-h, --help Show this help
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
# Fetch an event
|
|
22
|
+
fetch-event.js 571076d9728248739cecac2c9e96a24c --org myorg --project backend
|
|
23
|
+
|
|
24
|
+
# Get full breadcrumb history
|
|
25
|
+
fetch-event.js abc123 --org myorg --project backend --breadcrumbs
|
|
26
|
+
|
|
27
|
+
# Show spans for a transaction
|
|
28
|
+
fetch-event.js abc123 --org myorg --project backend --spans
|
|
29
|
+
|
|
30
|
+
# Get raw JSON for further analysis
|
|
31
|
+
fetch-event.js abc123 --org myorg --project backend --json
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
function parseArgs(args) {
|
|
35
|
+
const options = {
|
|
36
|
+
eventId: null,
|
|
37
|
+
org: null,
|
|
38
|
+
project: null,
|
|
39
|
+
json: false,
|
|
40
|
+
allBreadcrumbs: false,
|
|
41
|
+
showSpans: false,
|
|
42
|
+
help: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
const arg = args[i];
|
|
47
|
+
|
|
48
|
+
switch (arg) {
|
|
49
|
+
case "--help":
|
|
50
|
+
case "-h":
|
|
51
|
+
options.help = true;
|
|
52
|
+
break;
|
|
53
|
+
case "--json":
|
|
54
|
+
options.json = true;
|
|
55
|
+
break;
|
|
56
|
+
case "--org":
|
|
57
|
+
case "-o":
|
|
58
|
+
options.org = args[++i];
|
|
59
|
+
break;
|
|
60
|
+
case "--project":
|
|
61
|
+
case "-p":
|
|
62
|
+
options.project = args[++i];
|
|
63
|
+
break;
|
|
64
|
+
case "--breadcrumbs":
|
|
65
|
+
case "-b":
|
|
66
|
+
options.allBreadcrumbs = true;
|
|
67
|
+
break;
|
|
68
|
+
case "--spans":
|
|
69
|
+
options.showSpans = true;
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
if (!arg.startsWith("-") && !options.eventId) {
|
|
73
|
+
options.eventId = arg;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return options;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatStacktrace(frames, { maxFrames = 20, showContext = true } = {}) {
|
|
82
|
+
if (!frames || frames.length === 0) return " (no frames)";
|
|
83
|
+
|
|
84
|
+
const reversed = frames.slice().reverse();
|
|
85
|
+
const appFrames = reversed.filter((f) => f.inApp !== false);
|
|
86
|
+
const framesToShow = appFrames.length > 0 ? appFrames : reversed;
|
|
87
|
+
|
|
88
|
+
return framesToShow
|
|
89
|
+
.slice(0, maxFrames)
|
|
90
|
+
.map((f, i) => {
|
|
91
|
+
const file = f.filename || f.absPath || f.module || "unknown";
|
|
92
|
+
const func = f.function || "(anonymous)";
|
|
93
|
+
const line = f.lineNo || f.lineno;
|
|
94
|
+
const col = f.colNo || f.colno;
|
|
95
|
+
const loc = line ? `:${line}${col ? `:${col}` : ""}` : "";
|
|
96
|
+
|
|
97
|
+
let out = ` ${i + 1}. ${file}${loc}\n → ${func}`;
|
|
98
|
+
|
|
99
|
+
if (showContext && f.context_line) {
|
|
100
|
+
out += `\n | ${f.context_line.trim()}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return out;
|
|
104
|
+
})
|
|
105
|
+
.join("\n\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatBreadcrumb(crumb) {
|
|
109
|
+
let ts = "??:??:??";
|
|
110
|
+
if (crumb.timestamp) {
|
|
111
|
+
try {
|
|
112
|
+
const date =
|
|
113
|
+
typeof crumb.timestamp === "number"
|
|
114
|
+
? new Date(crumb.timestamp * 1000)
|
|
115
|
+
: new Date(crumb.timestamp);
|
|
116
|
+
if (!isNaN(date.getTime())) {
|
|
117
|
+
ts = date.toISOString().slice(11, 19);
|
|
118
|
+
}
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cat = crumb.category || crumb.type || "?";
|
|
123
|
+
const level = crumb.level && crumb.level !== "info" ? `[${crumb.level}] ` : "";
|
|
124
|
+
let msg = crumb.message || "";
|
|
125
|
+
|
|
126
|
+
if (!msg && crumb.data) {
|
|
127
|
+
if (crumb.data.url) msg = crumb.data.url;
|
|
128
|
+
else if (crumb.data.method) msg = `${crumb.data.method} ${crumb.data.url || ""}`;
|
|
129
|
+
else if (typeof crumb.data === "object") msg = JSON.stringify(crumb.data);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return ` [${ts}] ${level}${cat}: ${msg}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatSpan(span, indent = 0) {
|
|
136
|
+
const prefix = " ".repeat(indent);
|
|
137
|
+
const op = span.op || "?";
|
|
138
|
+
const desc = span.description || "(no description)";
|
|
139
|
+
const status = span.status || "?";
|
|
140
|
+
const duration = span.exclusive_time ? `${span.exclusive_time.toFixed(1)}ms` : "?";
|
|
141
|
+
|
|
142
|
+
return `${prefix}[${op}] ${desc}\n${prefix} status: ${status} | duration: ${duration}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function formatEvent(event, options) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
|
|
148
|
+
lines.push(`# Event: ${event.eventID || event.id}`);
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push(`**Timestamp:** ${formatTimestamp(event.dateCreated || event.timestamp)}`);
|
|
151
|
+
lines.push(`**Project:** ${event.projectSlug || event.projectID || "?"}`);
|
|
152
|
+
|
|
153
|
+
if (event.title) {
|
|
154
|
+
lines.push(`**Title:** ${event.title}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (event.message) {
|
|
158
|
+
lines.push(`**Message:** ${event.message}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Tags
|
|
162
|
+
if (event.tags && event.tags.length > 0) {
|
|
163
|
+
lines.push("");
|
|
164
|
+
lines.push("## Tags");
|
|
165
|
+
for (const tag of event.tags) {
|
|
166
|
+
lines.push(`- **${tag.key}:** ${tag.value}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Contexts
|
|
171
|
+
if (event.contexts) {
|
|
172
|
+
const ctx = event.contexts;
|
|
173
|
+
const contextLines = [];
|
|
174
|
+
|
|
175
|
+
if (ctx.runtime) {
|
|
176
|
+
contextLines.push(
|
|
177
|
+
`- **Runtime:** ${ctx.runtime.name || "?"} ${ctx.runtime.version || ""}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (ctx.browser) {
|
|
181
|
+
contextLines.push(
|
|
182
|
+
`- **Browser:** ${ctx.browser.name || "?"} ${ctx.browser.version || ""}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (ctx.os) {
|
|
186
|
+
contextLines.push(`- **OS:** ${ctx.os.name || "?"} ${ctx.os.version || ""}`);
|
|
187
|
+
}
|
|
188
|
+
if (ctx.device && ctx.device.family) {
|
|
189
|
+
contextLines.push(`- **Device:** ${ctx.device.family}`);
|
|
190
|
+
}
|
|
191
|
+
if (ctx.trace) {
|
|
192
|
+
contextLines.push(`- **Trace ID:** ${ctx.trace.trace_id || "?"}`);
|
|
193
|
+
contextLines.push(`- **Span ID:** ${ctx.trace.span_id || "?"}`);
|
|
194
|
+
if (ctx.trace.op) {
|
|
195
|
+
contextLines.push(`- **Operation:** ${ctx.trace.op}`);
|
|
196
|
+
}
|
|
197
|
+
if (ctx.trace.status) {
|
|
198
|
+
contextLines.push(`- **Status:** ${ctx.trace.status}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (contextLines.length > 0) {
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push("## Context");
|
|
205
|
+
lines.push(...contextLines);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Process entries
|
|
210
|
+
if (event.entries) {
|
|
211
|
+
// Request
|
|
212
|
+
for (const entry of event.entries) {
|
|
213
|
+
if (entry.type === "request" && entry.data) {
|
|
214
|
+
const req = entry.data;
|
|
215
|
+
lines.push("");
|
|
216
|
+
lines.push("## Request");
|
|
217
|
+
if (req.method && req.url) {
|
|
218
|
+
lines.push(`**${req.method}** ${req.url}`);
|
|
219
|
+
}
|
|
220
|
+
if (req.headers && req.headers.length > 0) {
|
|
221
|
+
const importantHeaders = ["User-Agent", "Content-Type", "Host"];
|
|
222
|
+
const headers = req.headers.filter(([k]) => importantHeaders.includes(k));
|
|
223
|
+
if (headers.length > 0) {
|
|
224
|
+
for (const [k, v] of headers) {
|
|
225
|
+
lines.push(` ${k}: ${v}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Exceptions
|
|
233
|
+
for (const entry of event.entries) {
|
|
234
|
+
if (entry.type === "exception" && entry.data?.values) {
|
|
235
|
+
lines.push("");
|
|
236
|
+
lines.push("## Exception");
|
|
237
|
+
for (const exc of entry.data.values) {
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push(`**${exc.type || "Error"}:** ${exc.value || "(no message)"}`);
|
|
240
|
+
if (exc.stacktrace?.frames) {
|
|
241
|
+
lines.push("");
|
|
242
|
+
lines.push(formatStacktrace(exc.stacktrace.frames));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Breadcrumbs
|
|
249
|
+
for (const entry of event.entries) {
|
|
250
|
+
if (entry.type === "breadcrumbs" && entry.data?.values) {
|
|
251
|
+
const crumbs = options.allBreadcrumbs
|
|
252
|
+
? entry.data.values
|
|
253
|
+
: entry.data.values.slice(-30);
|
|
254
|
+
|
|
255
|
+
if (crumbs.length > 0) {
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push(`## Breadcrumbs (${crumbs.length}${options.allBreadcrumbs ? "" : " most recent"})`);
|
|
258
|
+
for (const c of crumbs) {
|
|
259
|
+
lines.push(formatBreadcrumb(c));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Spans (for transactions)
|
|
266
|
+
if (options.showSpans) {
|
|
267
|
+
for (const entry of event.entries) {
|
|
268
|
+
if (entry.type === "spans" && entry.data) {
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push("## Spans");
|
|
271
|
+
const spans = Array.isArray(entry.data) ? entry.data : [entry.data];
|
|
272
|
+
for (const span of spans.slice(0, 50)) {
|
|
273
|
+
lines.push(formatSpan(span));
|
|
274
|
+
lines.push("");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function main() {
|
|
285
|
+
const args = process.argv.slice(2);
|
|
286
|
+
const options = parseArgs(args);
|
|
287
|
+
|
|
288
|
+
if (options.help) {
|
|
289
|
+
console.log(HELP);
|
|
290
|
+
process.exit(0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!options.eventId) {
|
|
294
|
+
console.error("Error: event-id is required");
|
|
295
|
+
console.error("Run with --help for usage information");
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!options.org) {
|
|
300
|
+
console.error("Error: --org is required");
|
|
301
|
+
console.error("Run with --help for usage information");
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!options.project) {
|
|
306
|
+
console.error("Error: --project is required");
|
|
307
|
+
console.error("Run with --help for usage information");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const token = getAuthToken();
|
|
312
|
+
|
|
313
|
+
const url = `${SENTRY_API_BASE}/projects/${encodeURIComponent(options.org)}/${encodeURIComponent(options.project)}/events/${encodeURIComponent(options.eventId)}/`;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const event = await fetchJson(url, token);
|
|
317
|
+
|
|
318
|
+
if (options.json) {
|
|
319
|
+
console.log(JSON.stringify(event, null, 2));
|
|
320
|
+
} else {
|
|
321
|
+
console.log(formatEvent(event, options));
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error("Error:", err.message);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
main();
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { SENTRY_API_BASE, getAuthToken, fetchJson, formatTimestamp } from "../lib/auth.js";
|
|
4
|
+
|
|
5
|
+
function parseIssueInput(input) {
|
|
6
|
+
// Full URL: https://sentry.io/organizations/sentry/issues/5765604106/
|
|
7
|
+
const urlMatch = input.match(
|
|
8
|
+
/sentry\.io\/organizations\/([^/]+)\/issues\/(\d+)/
|
|
9
|
+
);
|
|
10
|
+
if (urlMatch) {
|
|
11
|
+
return { org: urlMatch[1], issueId: urlMatch[2] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// New URL format: https://ORG.sentry.io/issues/5765604106/
|
|
15
|
+
const newUrlMatch = input.match(
|
|
16
|
+
/([^/.]+)\.sentry\.io\/issues\/(\d+)/
|
|
17
|
+
);
|
|
18
|
+
if (newUrlMatch) {
|
|
19
|
+
return { org: newUrlMatch[1], issueId: newUrlMatch[2] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Numeric issue ID
|
|
23
|
+
if (/^\d+$/.test(input)) {
|
|
24
|
+
return { issueId: input };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Short ID like JAVASCRIPT-ABC
|
|
28
|
+
if (/^[A-Z]+-[A-Z0-9]+$/i.test(input)) {
|
|
29
|
+
return { shortId: input };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { issueId: input };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
function formatStacktrace(frames, { maxFrames = 20, showContext = true } = {}) {
|
|
37
|
+
if (!frames || frames.length === 0) return " (no frames)";
|
|
38
|
+
|
|
39
|
+
const reversed = frames.slice().reverse();
|
|
40
|
+
const appFrames = reversed.filter(f => f.inApp !== false);
|
|
41
|
+
const framesToShow = appFrames.length > 0 ? appFrames : reversed;
|
|
42
|
+
|
|
43
|
+
return framesToShow
|
|
44
|
+
.slice(0, maxFrames)
|
|
45
|
+
.map((f, i) => {
|
|
46
|
+
const file = f.filename || f.absPath || f.module || "unknown";
|
|
47
|
+
const func = f.function || "(anonymous)";
|
|
48
|
+
const line = f.lineNo || f.lineno;
|
|
49
|
+
const col = f.colNo || f.colno;
|
|
50
|
+
const loc = line ? `:${line}${col ? `:${col}` : ""}` : "";
|
|
51
|
+
|
|
52
|
+
let out = ` ${i + 1}. ${file}${loc}\n → ${func}`;
|
|
53
|
+
|
|
54
|
+
if (showContext && f.context_line) {
|
|
55
|
+
out += `\n | ${f.context_line.trim()}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Show pre/post context if available
|
|
59
|
+
if (showContext && f.preContext && f.preContext.length > 0) {
|
|
60
|
+
const pre = f.preContext.slice(-2).map(l => ` . ${l.trim()}`).join("\n");
|
|
61
|
+
const post = f.postContext?.slice(0, 2).map(l => ` . ${l.trim()}`).join("\n") || "";
|
|
62
|
+
if (pre || post) {
|
|
63
|
+
out = ` ${i + 1}. ${file}${loc}\n → ${func}`;
|
|
64
|
+
if (pre) out += `\n${pre}`;
|
|
65
|
+
out += `\n > ${f.context_line.trim()}`;
|
|
66
|
+
if (post) out += `\n${post}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return out;
|
|
71
|
+
})
|
|
72
|
+
.join("\n\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatException(exc) {
|
|
76
|
+
let out = `**${exc.type || "Error"}:** ${exc.value || "(no message)"}\n`;
|
|
77
|
+
|
|
78
|
+
if (exc.module) {
|
|
79
|
+
out += `Module: ${exc.module}\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (exc.stacktrace?.frames) {
|
|
83
|
+
out += "\n" + formatStacktrace(exc.stacktrace.frames);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatIssue(issue) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
|
|
92
|
+
lines.push(`# ${issue.title}`);
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push(`**Project:** ${issue.project?.slug || "unknown"}`);
|
|
95
|
+
lines.push(`**Short ID:** ${issue.shortId}`);
|
|
96
|
+
lines.push(`**Status:** ${issue.status}`);
|
|
97
|
+
lines.push(`**Level:** ${issue.level}`);
|
|
98
|
+
if (issue.culprit) {
|
|
99
|
+
lines.push(`**Culprit:** ${issue.culprit}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push(`**First Seen:** ${formatTimestamp(issue.firstSeen)}`);
|
|
103
|
+
lines.push(`**Last Seen:** ${formatTimestamp(issue.lastSeen)}`);
|
|
104
|
+
lines.push(`**Events:** ${issue.count || 0}`);
|
|
105
|
+
lines.push(`**Users Affected:** ${issue.userCount || 0}`);
|
|
106
|
+
|
|
107
|
+
if (issue.tags && issue.tags.length > 0) {
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push("## Tags");
|
|
110
|
+
for (const tag of issue.tags.slice(0, 10)) {
|
|
111
|
+
const topValue = tag.topValues?.[0];
|
|
112
|
+
if (topValue) {
|
|
113
|
+
lines.push(`- **${tag.key}:** ${topValue.value} (${topValue.count})`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (issue.metadata) {
|
|
119
|
+
const m = issue.metadata;
|
|
120
|
+
if (m.type || m.value) {
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push("## Exception");
|
|
123
|
+
lines.push(`**Type:** ${m.type || "unknown"}`);
|
|
124
|
+
lines.push(`**Value:** ${m.value || "unknown"}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatEvent(event) {
|
|
132
|
+
const lines = [];
|
|
133
|
+
|
|
134
|
+
lines.push(`## Latest Event`);
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push(`**Event ID:** ${event.eventID}`);
|
|
137
|
+
lines.push(`**Timestamp:** ${formatTimestamp(event.dateCreated)}`);
|
|
138
|
+
|
|
139
|
+
// Show relevant tags (filter out noisy ones)
|
|
140
|
+
if (event.tags && event.tags.length > 0) {
|
|
141
|
+
const importantTags = ["environment", "release", "server_name", "transaction", "url", "browser", "os", "runtime"];
|
|
142
|
+
const filteredTags = event.tags.filter(t =>
|
|
143
|
+
importantTags.includes(t.key) || t.key.startsWith("sentry:")
|
|
144
|
+
);
|
|
145
|
+
if (filteredTags.length > 0) {
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push("### Tags");
|
|
148
|
+
for (const tag of filteredTags) {
|
|
149
|
+
lines.push(`- **${tag.key}:** ${tag.value}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (event.entries) {
|
|
155
|
+
// Show request info first if available
|
|
156
|
+
for (const entry of event.entries) {
|
|
157
|
+
if (entry.type === "request" && entry.data) {
|
|
158
|
+
const req = entry.data;
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push("### Request");
|
|
161
|
+
if (req.method && req.url) {
|
|
162
|
+
lines.push(`**${req.method}** ${req.url}`);
|
|
163
|
+
}
|
|
164
|
+
if (req.headers && req.headers.length > 0) {
|
|
165
|
+
const importantHeaders = ["User-Agent", "Content-Type", "Accept", "Host", "Referer"];
|
|
166
|
+
const headers = req.headers.filter(([k]) => importantHeaders.includes(k));
|
|
167
|
+
if (headers.length > 0) {
|
|
168
|
+
lines.push("");
|
|
169
|
+
for (const [k, v] of headers) {
|
|
170
|
+
lines.push(` ${k}: ${v}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (req.data) {
|
|
175
|
+
lines.push("");
|
|
176
|
+
lines.push("**Body:**");
|
|
177
|
+
const body = typeof req.data === "string" ? req.data : JSON.stringify(req.data, null, 2);
|
|
178
|
+
lines.push("```");
|
|
179
|
+
lines.push(body.slice(0, 1000) + (body.length > 1000 ? "..." : ""));
|
|
180
|
+
lines.push("```");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Show exceptions with stack traces
|
|
186
|
+
for (const entry of event.entries) {
|
|
187
|
+
if (entry.type === "exception" && entry.data?.values) {
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push("### Exception");
|
|
190
|
+
for (const exc of entry.data.values) {
|
|
191
|
+
lines.push("");
|
|
192
|
+
lines.push(formatException(exc));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (entry.type === "message" && entry.data?.formatted) {
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push("### Message");
|
|
199
|
+
lines.push(entry.data.formatted);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Show breadcrumbs last
|
|
204
|
+
for (const entry of event.entries) {
|
|
205
|
+
if (entry.type === "breadcrumbs" && entry.data?.values) {
|
|
206
|
+
const crumbs = entry.data.values.slice(-15);
|
|
207
|
+
if (crumbs.length > 0) {
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push("### Recent Breadcrumbs");
|
|
210
|
+
for (const c of crumbs) {
|
|
211
|
+
let ts = "??:??:??";
|
|
212
|
+
if (c.timestamp) {
|
|
213
|
+
try {
|
|
214
|
+
// Handle both unix timestamps and ISO strings
|
|
215
|
+
const date = typeof c.timestamp === "number"
|
|
216
|
+
? new Date(c.timestamp * 1000)
|
|
217
|
+
: new Date(c.timestamp);
|
|
218
|
+
if (!isNaN(date.getTime())) {
|
|
219
|
+
ts = date.toISOString().slice(11, 19);
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
const cat = c.category || c.type || "?";
|
|
224
|
+
const level = c.level && c.level !== "info" ? `[${c.level}] ` : "";
|
|
225
|
+
let msg = c.message || "";
|
|
226
|
+
if (!msg && c.data) {
|
|
227
|
+
if (c.data.url) msg = c.data.url;
|
|
228
|
+
else if (c.data.method) msg = `${c.data.method} ${c.data.url || ""}`;
|
|
229
|
+
else msg = JSON.stringify(c.data);
|
|
230
|
+
}
|
|
231
|
+
lines.push(` [${ts}] ${level}${cat}: ${msg}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Show contexts (runtime, browser, os, device, etc.)
|
|
239
|
+
if (event.contexts) {
|
|
240
|
+
const ctx = event.contexts;
|
|
241
|
+
const contextLines = [];
|
|
242
|
+
|
|
243
|
+
if (ctx.runtime) {
|
|
244
|
+
contextLines.push(`- **Runtime:** ${ctx.runtime.name || "?"} ${ctx.runtime.version || ""}`);
|
|
245
|
+
}
|
|
246
|
+
if (ctx.browser) {
|
|
247
|
+
contextLines.push(`- **Browser:** ${ctx.browser.name || "?"} ${ctx.browser.version || ""}`);
|
|
248
|
+
}
|
|
249
|
+
if (ctx.os) {
|
|
250
|
+
contextLines.push(`- **OS:** ${ctx.os.name || "?"} ${ctx.os.version || ""}`);
|
|
251
|
+
}
|
|
252
|
+
if (ctx.device && ctx.device.family) {
|
|
253
|
+
contextLines.push(`- **Device:** ${ctx.device.family}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (contextLines.length > 0) {
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push("### Context");
|
|
259
|
+
lines.push(...contextLines);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Fallback to old context field
|
|
264
|
+
if (!event.contexts && event.context) {
|
|
265
|
+
const ctx = event.context;
|
|
266
|
+
if (ctx.browser || ctx.os || ctx.device) {
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push("### Context");
|
|
269
|
+
if (ctx.browser)
|
|
270
|
+
lines.push(`- **Browser:** ${ctx.browser.name} ${ctx.browser.version || ""}`);
|
|
271
|
+
if (ctx.os) lines.push(`- **OS:** ${ctx.os.name} ${ctx.os.version || ""}`);
|
|
272
|
+
if (ctx.device) lines.push(`- **Device:** ${ctx.device.family || "unknown"}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function main() {
|
|
280
|
+
const args = process.argv.slice(2);
|
|
281
|
+
|
|
282
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
283
|
+
console.log("Usage: fetch-issue.js <issue-id-or-url> [options]");
|
|
284
|
+
console.log("");
|
|
285
|
+
console.log("Options:");
|
|
286
|
+
console.log(" --latest Fetch the latest event with full stack trace");
|
|
287
|
+
console.log(" --org <org> Organization slug (for short IDs like PROJECT-123)");
|
|
288
|
+
console.log(" --json Output raw JSON instead of formatted text");
|
|
289
|
+
console.log("");
|
|
290
|
+
console.log("Examples:");
|
|
291
|
+
console.log(" fetch-issue.js 5765604106");
|
|
292
|
+
console.log(" fetch-issue.js https://sentry.io/organizations/sentry/issues/123/");
|
|
293
|
+
console.log(" fetch-issue.js MYPROJ-ABC --org myorg");
|
|
294
|
+
console.log(" fetch-issue.js 5765604106 --latest");
|
|
295
|
+
process.exit(0);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const input = args[0];
|
|
299
|
+
const wantLatest = args.includes("--latest");
|
|
300
|
+
const wantJson = args.includes("--json");
|
|
301
|
+
const orgIndex = args.indexOf("--org");
|
|
302
|
+
const cliOrg = orgIndex !== -1 ? args[orgIndex + 1] : null;
|
|
303
|
+
|
|
304
|
+
const token = getAuthToken();
|
|
305
|
+
const parsed = parseIssueInput(input);
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
let issue;
|
|
309
|
+
|
|
310
|
+
if (parsed.shortId) {
|
|
311
|
+
const org = cliOrg || parsed.org;
|
|
312
|
+
if (!org) {
|
|
313
|
+
console.error("Error: Short ID requires --org flag");
|
|
314
|
+
console.error("Example: fetch-issue.js MYPROJ-123 --org myorg");
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
// Use the shortids endpoint to resolve the short ID
|
|
318
|
+
const shortIdUrl = `${SENTRY_API_BASE}/organizations/${org}/shortids/${encodeURIComponent(parsed.shortId)}/`;
|
|
319
|
+
const result = await fetchJson(shortIdUrl, token);
|
|
320
|
+
if (!result || !result.group) {
|
|
321
|
+
console.error(`Error: Issue ${parsed.shortId} not found`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
issue = result.group;
|
|
325
|
+
} else {
|
|
326
|
+
const issueUrl = `${SENTRY_API_BASE}/issues/${parsed.issueId}/`;
|
|
327
|
+
issue = await fetchJson(issueUrl, token);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (wantJson && !wantLatest) {
|
|
331
|
+
console.log(JSON.stringify(issue, null, 2));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let output = formatIssue(issue);
|
|
336
|
+
|
|
337
|
+
if (wantLatest) {
|
|
338
|
+
const eventUrl = `${SENTRY_API_BASE}/issues/${issue.id}/events/latest/`;
|
|
339
|
+
const event = await fetchJson(eventUrl, token);
|
|
340
|
+
|
|
341
|
+
if (wantJson) {
|
|
342
|
+
console.log(JSON.stringify({ issue, event }, null, 2));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
output += "\n\n" + formatEvent(event);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
console.log(output);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.error("Error:", err.message);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
main();
|