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,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fetch and parse pi-share (shittycodingagent.ai/buildwithpi.ai/buildwithpi.com) session exports.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node fetch-session.mjs <url-or-gist-id> [--header] [--entries] [--system] [--tools] [--human-summary] [--no-cache]
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* (no flag) Output full session data JSON
|
|
10
|
+
* --header Output just the session header
|
|
11
|
+
* --entries Output entries as JSON lines (one per line)
|
|
12
|
+
* --system Output the system prompt
|
|
13
|
+
* --tools Output tool definitions
|
|
14
|
+
* --human-summary Summarize what the human did in this session (uses haiku-4-5)
|
|
15
|
+
* --no-cache Bypass cache and fetch fresh
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
19
|
+
import { tmpdir } from 'os';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { spawnSync } from 'child_process';
|
|
22
|
+
|
|
23
|
+
const CACHE_DIR = join(tmpdir(), 'pi-share-cache');
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const input = args.find(a => !a.startsWith('--'));
|
|
27
|
+
const flags = new Set(args.filter(a => a.startsWith('--')));
|
|
28
|
+
|
|
29
|
+
if (!input) {
|
|
30
|
+
console.error('Usage: node fetch-session.mjs <url-or-gist-id> [--header|--entries|--system|--tools]');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Cache functions
|
|
35
|
+
function getCachePath(gistId) {
|
|
36
|
+
return join(CACHE_DIR, `${gistId}.json`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readCache(gistId) {
|
|
40
|
+
const path = getCachePath(gistId);
|
|
41
|
+
if (existsSync(path)) {
|
|
42
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeCache(gistId, data) {
|
|
48
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
49
|
+
writeFileSync(getCachePath(gistId), JSON.stringify(data));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extract gist ID from URL or use directly
|
|
53
|
+
function extractGistId(input) {
|
|
54
|
+
// Handle full URLs like https://shittycodingagent.ai/session/?<id>
|
|
55
|
+
const queryMatch = input.match(/[?&]([a-f0-9]{32})/i);
|
|
56
|
+
if (queryMatch) return queryMatch[1];
|
|
57
|
+
|
|
58
|
+
// Handle path-based URLs like https://buildwithpi.ai/session/<id>
|
|
59
|
+
const pathMatch = input.match(/\/session\/?([a-f0-9]{32})/i);
|
|
60
|
+
if (pathMatch) return pathMatch[1];
|
|
61
|
+
|
|
62
|
+
// Handle direct gist ID
|
|
63
|
+
if (/^[a-f0-9]{32}$/i.test(input)) return input;
|
|
64
|
+
|
|
65
|
+
// Handle gist.github.com URLs
|
|
66
|
+
const gistMatch = input.match(/gist\.github\.com\/[^/]+\/([a-f0-9]+)/i);
|
|
67
|
+
if (gistMatch) return gistMatch[1];
|
|
68
|
+
|
|
69
|
+
throw new Error(`Cannot extract gist ID from: ${input}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fetch session HTML from gist
|
|
73
|
+
async function fetchSessionHtml(gistId) {
|
|
74
|
+
const gistRes = await fetch(`https://api.github.com/gists/${gistId}`);
|
|
75
|
+
if (!gistRes.ok) {
|
|
76
|
+
if (gistRes.status === 404) throw new Error('Session not found (gist deleted or invalid ID)');
|
|
77
|
+
throw new Error(`GitHub API error: ${gistRes.status}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const gist = await gistRes.json();
|
|
81
|
+
const file = gist.files?.['session.html'];
|
|
82
|
+
if (!file) {
|
|
83
|
+
const available = Object.keys(gist.files || {}).join(', ') || 'none';
|
|
84
|
+
throw new Error(`No session.html in gist. Available: ${available}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fetch raw content if truncated
|
|
88
|
+
if (file.truncated && file.raw_url) {
|
|
89
|
+
const rawRes = await fetch(file.raw_url);
|
|
90
|
+
if (!rawRes.ok) throw new Error('Failed to fetch raw content');
|
|
91
|
+
return rawRes.text();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return file.content;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Extract base64 session data from HTML
|
|
98
|
+
function extractSessionData(html) {
|
|
99
|
+
// New format: <script id="session-data" type="application/json">BASE64</script>
|
|
100
|
+
const match = html.match(/<script[^>]*id="session-data"[^>]*>([^<]+)<\/script>/);
|
|
101
|
+
if (match) {
|
|
102
|
+
const base64 = match[1].trim();
|
|
103
|
+
const json = Buffer.from(base64, 'base64').toString('utf-8');
|
|
104
|
+
return JSON.parse(json);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new Error('No session data found in HTML. This may be an older export format without embedded data.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Truncate text to maxLen, adding ellipsis if truncated
|
|
111
|
+
function truncate(text, maxLen = 150) {
|
|
112
|
+
if (!text || text.length <= maxLen) return text;
|
|
113
|
+
return text.slice(0, maxLen) + '...';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Extract condensed session data for human summary
|
|
117
|
+
function extractForSummary(data) {
|
|
118
|
+
const turns = [];
|
|
119
|
+
let turnNumber = 0;
|
|
120
|
+
|
|
121
|
+
for (const entry of data.entries) {
|
|
122
|
+
if (entry.type !== 'message') continue;
|
|
123
|
+
|
|
124
|
+
const msg = entry.message;
|
|
125
|
+
if (!msg || !msg.role) continue;
|
|
126
|
+
|
|
127
|
+
if (msg.role === 'user') {
|
|
128
|
+
turnNumber++;
|
|
129
|
+
// Extract user text
|
|
130
|
+
const textParts = (msg.content || [])
|
|
131
|
+
.filter(c => c.type === 'text')
|
|
132
|
+
.map(c => c.text)
|
|
133
|
+
.join('\n');
|
|
134
|
+
|
|
135
|
+
if (textParts.trim()) {
|
|
136
|
+
turns.push({
|
|
137
|
+
turn: turnNumber,
|
|
138
|
+
role: 'human',
|
|
139
|
+
text: textParts
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} else if (msg.role === 'assistant') {
|
|
143
|
+
// Extract condensed assistant info: brief text + tool summary
|
|
144
|
+
const textParts = [];
|
|
145
|
+
const toolCalls = [];
|
|
146
|
+
|
|
147
|
+
for (const block of (msg.content || [])) {
|
|
148
|
+
if (block.type === 'text' && block.text) {
|
|
149
|
+
// Just first 200 chars of assistant text for context
|
|
150
|
+
textParts.push(truncate(block.text, 200));
|
|
151
|
+
} else if (block.type === 'toolCall') {
|
|
152
|
+
// Condense tool call: name + truncated key info
|
|
153
|
+
let summary = block.toolName;
|
|
154
|
+
if (block.args) {
|
|
155
|
+
if (block.args.path) {
|
|
156
|
+
summary += `: ${truncate(block.args.path, 100)}`;
|
|
157
|
+
} else if (block.args.command) {
|
|
158
|
+
summary += `: ${truncate(block.args.command, 100)}`;
|
|
159
|
+
} else {
|
|
160
|
+
// Generic args truncation
|
|
161
|
+
const argsStr = JSON.stringify(block.args);
|
|
162
|
+
summary += `: ${truncate(argsStr, 100)}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
toolCalls.push(summary);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (textParts.length || toolCalls.length) {
|
|
170
|
+
turns.push({
|
|
171
|
+
turn: turnNumber,
|
|
172
|
+
role: 'assistant',
|
|
173
|
+
text: textParts.length ? textParts[0] : null,
|
|
174
|
+
tools: toolCalls.length ? toolCalls : null
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
} else if (msg.role === 'toolResult') {
|
|
178
|
+
// Just note if there was an error
|
|
179
|
+
const hasError = (msg.content || []).some(c => c.isError);
|
|
180
|
+
if (hasError) {
|
|
181
|
+
turns.push({
|
|
182
|
+
turn: turnNumber,
|
|
183
|
+
role: 'tool_error',
|
|
184
|
+
text: 'Tool returned an error'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
sessionId: data.header?.id,
|
|
192
|
+
timestamp: data.header?.timestamp,
|
|
193
|
+
cwd: data.header?.cwd,
|
|
194
|
+
turns
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Format condensed data as text for the summarizer
|
|
199
|
+
function formatForSummary(condensed) {
|
|
200
|
+
const lines = [];
|
|
201
|
+
|
|
202
|
+
lines.push(`Session: ${condensed.sessionId || 'unknown'}`);
|
|
203
|
+
lines.push(`Time: ${condensed.timestamp || 'unknown'}`);
|
|
204
|
+
lines.push(`Directory: ${condensed.cwd || 'unknown'}`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push('=== Conversation ===');
|
|
207
|
+
lines.push('');
|
|
208
|
+
|
|
209
|
+
for (const turn of condensed.turns) {
|
|
210
|
+
if (turn.role === 'human') {
|
|
211
|
+
lines.push(`[Turn ${turn.turn}] HUMAN:`);
|
|
212
|
+
lines.push(turn.text);
|
|
213
|
+
lines.push('');
|
|
214
|
+
} else if (turn.role === 'assistant') {
|
|
215
|
+
lines.push(`[Turn ${turn.turn}] ASSISTANT (condensed):`);
|
|
216
|
+
if (turn.text) {
|
|
217
|
+
lines.push(` Response: ${turn.text}`);
|
|
218
|
+
}
|
|
219
|
+
if (turn.tools && turn.tools.length) {
|
|
220
|
+
lines.push(` Tools used: ${turn.tools.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
lines.push('');
|
|
223
|
+
} else if (turn.role === 'tool_error') {
|
|
224
|
+
lines.push(`[Turn ${turn.turn}] ⚠️ Tool error occurred`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Generate human summary using haiku via pi
|
|
233
|
+
async function generateHumanSummary(data) {
|
|
234
|
+
const condensed = extractForSummary(data);
|
|
235
|
+
const formatted = formatForSummary(condensed);
|
|
236
|
+
|
|
237
|
+
const prompt = `You are analyzing a coding agent session transcript. Your task is to summarize what the HUMAN did, not what the AI agent did.
|
|
238
|
+
|
|
239
|
+
Focus on:
|
|
240
|
+
1. What was the human's initial goal/request?
|
|
241
|
+
2. How many times did they have to re-prompt or steer the agent?
|
|
242
|
+
3. What kind of steering did they do? (corrections, clarifications, changes of direction, expressing frustration, etc.)
|
|
243
|
+
4. Did the human have to intervene when things went wrong?
|
|
244
|
+
5. How specific vs vague were their instructions?
|
|
245
|
+
|
|
246
|
+
Write a ~300 word summary in third person ("The user asked...", "They then had to clarify...").
|
|
247
|
+
Include a brief note about what domain/tools were involved for context, but keep focus on the human's actions and experience.
|
|
248
|
+
|
|
249
|
+
Here is the condensed session transcript:
|
|
250
|
+
|
|
251
|
+
${formatted}`;
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const result = spawnSync('pi', [
|
|
255
|
+
'--provider', 'anthropic',
|
|
256
|
+
'--model', 'claude-haiku-4-5',
|
|
257
|
+
'--no-tools',
|
|
258
|
+
'--no-session',
|
|
259
|
+
'-p',
|
|
260
|
+
prompt
|
|
261
|
+
], {
|
|
262
|
+
encoding: 'utf-8',
|
|
263
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
264
|
+
timeout: 60000
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (result.error) {
|
|
268
|
+
throw result.error;
|
|
269
|
+
}
|
|
270
|
+
if (result.status !== 0) {
|
|
271
|
+
throw new Error(result.stderr || 'pi command failed');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return result.stdout.trim();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
throw new Error(`Failed to generate summary: ${err.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Main
|
|
281
|
+
async function main() {
|
|
282
|
+
try {
|
|
283
|
+
const gistId = extractGistId(input);
|
|
284
|
+
|
|
285
|
+
// Check cache first (unless --no-cache)
|
|
286
|
+
let data = null;
|
|
287
|
+
if (!flags.has('--no-cache')) {
|
|
288
|
+
data = readCache(gistId);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!data) {
|
|
292
|
+
const html = await fetchSessionHtml(gistId);
|
|
293
|
+
data = extractSessionData(html);
|
|
294
|
+
writeCache(gistId, data);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (flags.has('--header')) {
|
|
298
|
+
console.log(JSON.stringify(data.header));
|
|
299
|
+
} else if (flags.has('--entries')) {
|
|
300
|
+
// Output as JSON lines - one entry per line
|
|
301
|
+
for (const entry of data.entries) {
|
|
302
|
+
console.log(JSON.stringify(entry));
|
|
303
|
+
}
|
|
304
|
+
} else if (flags.has('--system')) {
|
|
305
|
+
console.log(data.systemPrompt || '');
|
|
306
|
+
} else if (flags.has('--tools')) {
|
|
307
|
+
console.log(JSON.stringify(data.tools || []));
|
|
308
|
+
} else if (flags.has('--human-summary')) {
|
|
309
|
+
// Generate human-centric summary using haiku
|
|
310
|
+
const summary = await generateHumanSummary(data);
|
|
311
|
+
console.log(summary);
|
|
312
|
+
} else {
|
|
313
|
+
// Default: full session data
|
|
314
|
+
console.log(JSON.stringify(data));
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error(err.message);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
main();
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sentry
|
|
3
|
+
description: "Fetch and analyze Sentry issues, events, transactions, and logs. Helps agents debug errors, find root causes, and understand what happened at specific times."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sentry Skill
|
|
7
|
+
|
|
8
|
+
Access Sentry data via the API for debugging and investigation. Uses auth token from `~/.sentryclirc`.
|
|
9
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
| Task | Command |
|
|
13
|
+
|------|---------|
|
|
14
|
+
| Find errors on a date | `search-events.js --org X --start 2025-12-23T15:00:00 --level error` |
|
|
15
|
+
| List open issues | `list-issues.js --org X --status unresolved` |
|
|
16
|
+
| Get issue details | `fetch-issue.js <issue-id-or-url> --latest` |
|
|
17
|
+
| Get event details | `fetch-event.js <event-id> --org X --project Y` |
|
|
18
|
+
| Search logs | `search-logs.js --org X --project Y "level:error"` |
|
|
19
|
+
|
|
20
|
+
## Common Debugging Workflows
|
|
21
|
+
|
|
22
|
+
### "What went wrong at this time?"
|
|
23
|
+
|
|
24
|
+
Find events around a specific timestamp:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Find all events in a 2-hour window
|
|
28
|
+
./scripts/search-events.js --org myorg --project backend \
|
|
29
|
+
--start 2025-12-23T15:00:00 --end 2025-12-23T17:00:00
|
|
30
|
+
|
|
31
|
+
# Filter to just errors
|
|
32
|
+
./scripts/search-events.js --org myorg --start 2025-12-23T15:00:00 \
|
|
33
|
+
--level error
|
|
34
|
+
|
|
35
|
+
# Find a specific transaction type
|
|
36
|
+
./scripts/search-events.js --org myorg --start 2025-12-23T15:00:00 \
|
|
37
|
+
--transaction process-incoming-email
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### "What errors have occurred recently?"
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# List unresolved errors from last 24 hours
|
|
44
|
+
./scripts/list-issues.js --org myorg --status unresolved --level error --period 24h
|
|
45
|
+
|
|
46
|
+
# Find high-frequency issues
|
|
47
|
+
./scripts/list-issues.js --org myorg --query "times_seen:>50" --sort freq
|
|
48
|
+
|
|
49
|
+
# Issues affecting users
|
|
50
|
+
./scripts/list-issues.js --org myorg --query "is:unresolved has:user" --sort user
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### "Get details about a specific issue/event"
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Get issue with latest stack trace
|
|
57
|
+
./scripts/fetch-issue.js 5765604106 --latest
|
|
58
|
+
./scripts/fetch-issue.js https://sentry.io/organizations/myorg/issues/123/ --latest
|
|
59
|
+
./scripts/fetch-issue.js MYPROJ-123 --org myorg --latest
|
|
60
|
+
|
|
61
|
+
# Get specific event with all breadcrumbs
|
|
62
|
+
./scripts/fetch-event.js abc123def456 --org myorg --project backend --breadcrumbs
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### "Find events with a specific tag"
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Find by custom tag (e.g., thread_id, user_id)
|
|
69
|
+
./scripts/search-events.js --org myorg --tag thread_id:th_abc123
|
|
70
|
+
|
|
71
|
+
# Find by user email
|
|
72
|
+
./scripts/search-events.js --org myorg --query "user.email:*@example.com"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Fetch Issue
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
./scripts/fetch-issue.js <issue-id-or-url> [options]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Get details about a specific issue (grouped error).
|
|
84
|
+
|
|
85
|
+
**Accepts:**
|
|
86
|
+
- Issue ID: `5765604106`
|
|
87
|
+
- Issue URL: `https://sentry.io/organizations/sentry/issues/5765604106/`
|
|
88
|
+
- New URL format: `https://myorg.sentry.io/issues/5765604106/`
|
|
89
|
+
- Short ID: `JAVASCRIPT-ABC` (requires `--org` flag)
|
|
90
|
+
|
|
91
|
+
**Options:**
|
|
92
|
+
- `--latest` - Include the latest event with full stack trace
|
|
93
|
+
- `--org <org>` - Organization slug (for short IDs)
|
|
94
|
+
- `--json` - Output raw JSON
|
|
95
|
+
|
|
96
|
+
**Output includes:**
|
|
97
|
+
- Title, culprit, status, level
|
|
98
|
+
- First/last seen timestamps
|
|
99
|
+
- Event count and user impact
|
|
100
|
+
- Tags and environment info
|
|
101
|
+
- With `--latest`: stack trace, request details, breadcrumbs, runtime context
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Fetch Event
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
./scripts/fetch-event.js <event-id> --org <org> --project <project> [options]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Get full details of a specific event by its ID.
|
|
112
|
+
|
|
113
|
+
**Options:**
|
|
114
|
+
- `--org, -o <org>` - Organization slug (required)
|
|
115
|
+
- `--project, -p <project>` - Project slug (required)
|
|
116
|
+
- `--breadcrumbs, -b` - Show all breadcrumbs (default: last 30)
|
|
117
|
+
- `--spans` - Show span tree for transactions
|
|
118
|
+
- `--json` - Output raw JSON
|
|
119
|
+
|
|
120
|
+
**Output includes:**
|
|
121
|
+
- Timestamp, project, title, message
|
|
122
|
+
- All tags
|
|
123
|
+
- Context (runtime, browser, OS, trace info)
|
|
124
|
+
- Request details
|
|
125
|
+
- Exception with stack trace
|
|
126
|
+
- Breadcrumbs
|
|
127
|
+
- Spans (with `--spans`)
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Search Events
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
./scripts/search-events.js [options]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Search for events (transactions, errors) using Sentry Discover.
|
|
138
|
+
|
|
139
|
+
**Time Range Options:**
|
|
140
|
+
- `--period, -t <period>` - Relative time (24h, 7d, 14d)
|
|
141
|
+
- `--start <datetime>` - Start time (ISO 8601: 2025-12-23T15:00:00)
|
|
142
|
+
- `--end <datetime>` - End time (ISO 8601)
|
|
143
|
+
|
|
144
|
+
**Filter Options:**
|
|
145
|
+
- `--org, -o <org>` - Organization slug (required)
|
|
146
|
+
- `--project, -p <project>` - Project slug or ID
|
|
147
|
+
- `--query, -q <query>` - Discover search query
|
|
148
|
+
- `--transaction <name>` - Transaction name filter
|
|
149
|
+
- `--tag <key:value>` - Tag filter (repeatable)
|
|
150
|
+
- `--level <level>` - Level filter (error, warning, info)
|
|
151
|
+
- `--limit, -n <n>` - Max results (default: 25, max: 100)
|
|
152
|
+
- `--fields <fields>` - Comma-separated fields to include
|
|
153
|
+
|
|
154
|
+
**Query Syntax:**
|
|
155
|
+
```
|
|
156
|
+
transaction:process-* Wildcard transaction match
|
|
157
|
+
level:error Filter by level
|
|
158
|
+
user.email:foo@bar.com Filter by user
|
|
159
|
+
environment:production Filter by environment
|
|
160
|
+
has:stack.filename Has stack trace
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## List Issues
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
./scripts/list-issues.js [options]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
List and search issues (grouped errors) in a project.
|
|
172
|
+
|
|
173
|
+
**Options:**
|
|
174
|
+
- `--org, -o <org>` - Organization slug (required)
|
|
175
|
+
- `--project, -p <project>` - Project slug (repeatable)
|
|
176
|
+
- `--query, -q <query>` - Issue search query
|
|
177
|
+
- `--status <status>` - unresolved, resolved, ignored
|
|
178
|
+
- `--level <level>` - error, warning, info, fatal
|
|
179
|
+
- `--period, -t <period>` - Time period (default: 14d)
|
|
180
|
+
- `--limit, -n <n>` - Max results (default: 25)
|
|
181
|
+
- `--sort <sort>` - date, new, priority, freq, user
|
|
182
|
+
- `--json` - Output raw JSON
|
|
183
|
+
|
|
184
|
+
**Query Syntax:**
|
|
185
|
+
```
|
|
186
|
+
is:unresolved Status filter
|
|
187
|
+
is:assigned Has assignee
|
|
188
|
+
assigned:me Assigned to current user
|
|
189
|
+
level:error Level filter
|
|
190
|
+
firstSeen:+7d First seen > 7 days ago
|
|
191
|
+
lastSeen:-24h Last seen within 24h
|
|
192
|
+
times_seen:>100 Event count filter
|
|
193
|
+
has:user Has user context
|
|
194
|
+
error.handled:0 Unhandled errors only
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Search Logs
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
./scripts/search-logs.js [query|url] [options]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Search for logs in Sentry's Logs Explorer.
|
|
206
|
+
|
|
207
|
+
**Options:**
|
|
208
|
+
- `--org, -o <org>` - Organization slug (required unless URL provided)
|
|
209
|
+
- `--project, -p <project>` - Filter by project slug or ID
|
|
210
|
+
- `--period, -t <period>` - Time period (default: 24h)
|
|
211
|
+
- `--limit, -n <n>` - Max results (default: 100, max: 1000)
|
|
212
|
+
- `--json` - Output raw JSON
|
|
213
|
+
|
|
214
|
+
**Query Syntax:**
|
|
215
|
+
```
|
|
216
|
+
level:error Filter by level (trace, debug, info, warn, error, fatal)
|
|
217
|
+
message:*timeout* Search message text with wildcards
|
|
218
|
+
trace:abc123 Filter by trace ID
|
|
219
|
+
project:my-project Filter by project slug
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Accepts Sentry URLs:**
|
|
223
|
+
```bash
|
|
224
|
+
./scripts/search-logs.js "https://myorg.sentry.io/explore/logs/?project=123&statsPeriod=7d"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Tips for Debugging
|
|
230
|
+
|
|
231
|
+
1. **Start broad, then narrow down**: Use `search-events.js` with a time range first, then drill into specific events
|
|
232
|
+
|
|
233
|
+
2. **Use breadcrumbs**: The `--breadcrumbs` flag on `fetch-event.js` shows the full history of what happened before an error
|
|
234
|
+
|
|
235
|
+
3. **Look for patterns**: Use `list-issues.js --sort freq` to find frequently occurring problems
|
|
236
|
+
|
|
237
|
+
4. **Check related events**: If you find one event, look for others with the same transaction name or trace ID
|
|
238
|
+
|
|
239
|
+
5. **Tags are your friend**: Custom tags like `thread_id`, `user_id`, `request_id` help correlate events
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
export const SENTRY_API_BASE = "https://sentry.io/api/0";
|
|
6
|
+
|
|
7
|
+
// Cache for project slug -> ID resolution
|
|
8
|
+
const projectIdCache = new Map();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get auth token from ~/.sentryclirc
|
|
12
|
+
* @returns {string} The auth token
|
|
13
|
+
*/
|
|
14
|
+
export function getAuthToken() {
|
|
15
|
+
const rcPath = join(homedir(), ".sentryclirc");
|
|
16
|
+
if (!existsSync(rcPath)) {
|
|
17
|
+
console.error("Error: ~/.sentryclirc not found");
|
|
18
|
+
console.error("Run 'sentry-cli login' to authenticate");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const content = readFileSync(rcPath, "utf-8");
|
|
23
|
+
const match = content.match(/token\s*=\s*(.+)/);
|
|
24
|
+
if (!match) {
|
|
25
|
+
console.error("Error: No token found in ~/.sentryclirc");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
return match[1].trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fetch JSON from a Sentry API endpoint
|
|
33
|
+
* @param {string} url - The full URL to fetch
|
|
34
|
+
* @param {string} token - The auth token
|
|
35
|
+
* @returns {Promise<any>} The parsed JSON response
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchJson(url, token) {
|
|
38
|
+
const res = await fetch(url, {
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${token}`,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
throw new Error(`API error ${res.status}: ${text}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return res.json();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Format a timestamp for display
|
|
54
|
+
* @param {string|number} ts - Timestamp (ISO string or unix)
|
|
55
|
+
* @returns {string} Formatted timestamp
|
|
56
|
+
*/
|
|
57
|
+
export function formatTimestamp(ts) {
|
|
58
|
+
if (!ts) return "N/A";
|
|
59
|
+
try {
|
|
60
|
+
const date = new Date(ts);
|
|
61
|
+
if (isNaN(date.getTime())) return ts;
|
|
62
|
+
return date.toLocaleString();
|
|
63
|
+
} catch {
|
|
64
|
+
return ts;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a project slug to its numeric ID.
|
|
70
|
+
* If the input is already a numeric ID, returns it as-is.
|
|
71
|
+
* @param {string} org - Organization slug
|
|
72
|
+
* @param {string} project - Project slug or numeric ID
|
|
73
|
+
* @param {string} token - Auth token
|
|
74
|
+
* @returns {Promise<string>} The numeric project ID
|
|
75
|
+
*/
|
|
76
|
+
export async function resolveProjectId(org, project, token) {
|
|
77
|
+
// If already numeric, return as-is
|
|
78
|
+
if (/^\d+$/.test(project)) {
|
|
79
|
+
return project;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check cache
|
|
83
|
+
const cacheKey = `${org}/${project}`;
|
|
84
|
+
if (projectIdCache.has(cacheKey)) {
|
|
85
|
+
return projectIdCache.get(cacheKey);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fetch project details to get the ID
|
|
89
|
+
const url = `${SENTRY_API_BASE}/projects/${encodeURIComponent(org)}/${encodeURIComponent(project)}/`;
|
|
90
|
+
const data = await fetchJson(url, token);
|
|
91
|
+
|
|
92
|
+
if (!data || !data.id) {
|
|
93
|
+
throw new Error(`Project '${project}' not found in organization '${org}'`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const id = String(data.id);
|
|
97
|
+
projectIdCache.set(cacheKey, id);
|
|
98
|
+
return id;
|
|
99
|
+
}
|