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.
Files changed (77) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +95 -0
  3. package/TODO.md +11 -0
  4. package/commands/handoff.md +100 -0
  5. package/commands/make-release.md +75 -0
  6. package/commands/pickup.md +30 -0
  7. package/commands/update-changelog.md +78 -0
  8. package/package.json +22 -0
  9. package/pi-extensions/answer.ts +527 -0
  10. package/pi-extensions/codex-tuning.ts +632 -0
  11. package/pi-extensions/commit.ts +248 -0
  12. package/pi-extensions/cwd-history.ts +237 -0
  13. package/pi-extensions/issues.ts +548 -0
  14. package/pi-extensions/loop.ts +446 -0
  15. package/pi-extensions/qna.ts +167 -0
  16. package/pi-extensions/reveal.ts +689 -0
  17. package/pi-extensions/review.ts +807 -0
  18. package/pi-themes/armin.json +81 -0
  19. package/pi-themes/nightowl.json +82 -0
  20. package/skills/anachb/SKILL.md +183 -0
  21. package/skills/anachb/departures.sh +79 -0
  22. package/skills/anachb/disruptions.sh +53 -0
  23. package/skills/anachb/route.sh +87 -0
  24. package/skills/anachb/search.sh +43 -0
  25. package/skills/ghidra/SKILL.md +254 -0
  26. package/skills/ghidra/scripts/find-ghidra.sh +54 -0
  27. package/skills/ghidra/scripts/ghidra-analyze.sh +239 -0
  28. package/skills/ghidra/scripts/ghidra_scripts/ExportAll.java +278 -0
  29. package/skills/ghidra/scripts/ghidra_scripts/ExportCalls.java +148 -0
  30. package/skills/ghidra/scripts/ghidra_scripts/ExportDecompiled.java +84 -0
  31. package/skills/ghidra/scripts/ghidra_scripts/ExportFunctions.java +114 -0
  32. package/skills/ghidra/scripts/ghidra_scripts/ExportStrings.java +123 -0
  33. package/skills/ghidra/scripts/ghidra_scripts/ExportSymbols.java +135 -0
  34. package/skills/github/SKILL.md +47 -0
  35. package/skills/improve-skill/SKILL.md +155 -0
  36. package/skills/improve-skill/scripts/extract-session.js +349 -0
  37. package/skills/oebb-scotty/SKILL.md +429 -0
  38. package/skills/oebb-scotty/arrivals.sh +83 -0
  39. package/skills/oebb-scotty/departures.sh +83 -0
  40. package/skills/oebb-scotty/disruptions.sh +33 -0
  41. package/skills/oebb-scotty/search-station.sh +36 -0
  42. package/skills/oebb-scotty/trip.sh +119 -0
  43. package/skills/openscad/SKILL.md +232 -0
  44. package/skills/openscad/examples/parametric_box.scad +92 -0
  45. package/skills/openscad/examples/phone_stand.scad +95 -0
  46. package/skills/openscad/tools/common.sh +50 -0
  47. package/skills/openscad/tools/export-stl.sh +56 -0
  48. package/skills/openscad/tools/extract-params.sh +147 -0
  49. package/skills/openscad/tools/multi-preview.sh +68 -0
  50. package/skills/openscad/tools/preview.sh +74 -0
  51. package/skills/openscad/tools/render-with-params.sh +91 -0
  52. package/skills/openscad/tools/validate.sh +46 -0
  53. package/skills/pi-share/SKILL.md +105 -0
  54. package/skills/pi-share/fetch-session.mjs +322 -0
  55. package/skills/sentry/SKILL.md +239 -0
  56. package/skills/sentry/lib/auth.js +99 -0
  57. package/skills/sentry/scripts/fetch-event.js +329 -0
  58. package/skills/sentry/scripts/fetch-issue.js +356 -0
  59. package/skills/sentry/scripts/list-issues.js +239 -0
  60. package/skills/sentry/scripts/search-events.js +291 -0
  61. package/skills/sentry/scripts/search-logs.js +240 -0
  62. package/skills/tmux/SKILL.md +105 -0
  63. package/skills/tmux/scripts/find-sessions.sh +112 -0
  64. package/skills/tmux/scripts/wait-for-text.sh +83 -0
  65. package/skills/web-browser/SKILL.md +91 -0
  66. package/skills/web-browser/scripts/cdp.js +210 -0
  67. package/skills/web-browser/scripts/dismiss-cookies.js +373 -0
  68. package/skills/web-browser/scripts/eval.js +68 -0
  69. package/skills/web-browser/scripts/logs-tail.js +69 -0
  70. package/skills/web-browser/scripts/nav.js +65 -0
  71. package/skills/web-browser/scripts/net-summary.js +94 -0
  72. package/skills/web-browser/scripts/package-lock.json +33 -0
  73. package/skills/web-browser/scripts/package.json +6 -0
  74. package/skills/web-browser/scripts/pick.js +165 -0
  75. package/skills/web-browser/scripts/screenshot.js +52 -0
  76. package/skills/web-browser/scripts/start.js +80 -0
  77. 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();