tau-coding-agent 0.1.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 +21 -0
- package/README.md +42 -0
- package/extensions/answer.ts +601 -0
- package/extensions/branch-term.ts +405 -0
- package/extensions/btw.ts +444 -0
- package/extensions/ghostty.ts +301 -0
- package/extensions/git-diff-stats.ts +277 -0
- package/extensions/git-pr-status.ts +286 -0
- package/extensions/insights.ts +2367 -0
- package/extensions/interlude.ts +144 -0
- package/extensions/loop.ts +529 -0
- package/extensions/memory.ts +1889 -0
- package/extensions/notify.ts +161 -0
- package/extensions/openai-fast.ts +227 -0
- package/extensions/openai-verbosity.ts +223 -0
- package/extensions/review.ts +4347 -0
- package/extensions/sandbox/index.ts +2578 -0
- package/extensions/usage/anthropic.ts +198 -0
- package/extensions/usage/github-copilot.ts +204 -0
- package/extensions/usage/google-gemini-cli.ts +232 -0
- package/extensions/usage/index.ts +2310 -0
- package/extensions/usage/minimax.ts +208 -0
- package/extensions/usage/openai-codex.ts +180 -0
- package/extensions/usage/openrouter.ts +168 -0
- package/extensions/usage/providers.ts +26 -0
- package/extensions/usage/shared.ts +156 -0
- package/extensions/usage/types.ts +53 -0
- package/extensions/usage/zai.ts +186 -0
- package/extensions/websearch/README.md +68 -0
- package/extensions/websearch/browser/chromium.ts +330 -0
- package/extensions/websearch/browser/discovery.ts +75 -0
- package/extensions/websearch/browser/firefox.ts +150 -0
- package/extensions/websearch/browser/sqlite.ts +38 -0
- package/extensions/websearch/config.ts +76 -0
- package/extensions/websearch/index.ts +312 -0
- package/extensions/websearch/normalize.ts +155 -0
- package/extensions/websearch/providers/anthropic.pi.ts +139 -0
- package/extensions/websearch/providers/gemini.browser.ts +185 -0
- package/extensions/websearch/providers/gemini.pi.ts +108 -0
- package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
- package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
- package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
- package/extensions/websearch/providers/pi-model.shared.ts +86 -0
- package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
- package/extensions/websearch/providers/shared.ts +76 -0
- package/extensions/websearch/types.ts +52 -0
- package/extensions/worktree.ts +2223 -0
- package/package.json +72 -0
- package/skills/browser-tools/SKILL.md +253 -0
- package/skills/browser-tools/scripts/browser-content.js +100 -0
- package/skills/browser-tools/scripts/browser-cookies.js +33 -0
- package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
- package/skills/browser-tools/scripts/browser-eval.js +75 -0
- package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
- package/skills/browser-tools/scripts/browser-nav.js +28 -0
- package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
- package/skills/browser-tools/scripts/browser-pick.js +133 -0
- package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
- package/skills/browser-tools/scripts/browser-start.js +281 -0
- package/skills/browser-tools/scripts/browser-watch.js +321 -0
- package/skills/browser-tools/scripts/utils.js +60 -0
- package/skills/git-clean-history/SKILL.md +45 -0
- package/skills/git-commit/SKILL.md +55 -0
- package/skills/oracle/SKILL.md +64 -0
- package/skills/oracle/scripts/oracle +249 -0
- package/skills/oracle/scripts/oracle-bundle +227 -0
- package/skills/sentry/SKILL.md +198 -0
- package/skills/sentry/lib/auth.js +99 -0
- package/skills/sentry/scripts/fetch-event.js +325 -0
- package/skills/sentry/scripts/fetch-issue.js +368 -0
- package/skills/sentry/scripts/list-issues.js +245 -0
- package/skills/sentry/scripts/search-events.js +291 -0
- package/skills/sentry/scripts/search-logs.js +234 -0
- package/skills/update-changelog/SKILL.md +135 -0
- package/skills/web-design/SKILL.md +117 -0
|
@@ -0,0 +1,368 @@
|
|
|
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(/sentry\.io\/organizations\/([^/]+)\/issues\/(\d+)/);
|
|
8
|
+
if (urlMatch) {
|
|
9
|
+
return { org: urlMatch[1], issueId: urlMatch[2] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// New URL format: https://ORG.sentry.io/issues/5765604106/
|
|
13
|
+
const newUrlMatch = input.match(/([^/.]+)\.sentry\.io\/issues\/(\d+)/);
|
|
14
|
+
if (newUrlMatch) {
|
|
15
|
+
return { org: newUrlMatch[1], issueId: newUrlMatch[2] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Numeric issue ID
|
|
19
|
+
if (/^\d+$/.test(input)) {
|
|
20
|
+
return { issueId: input };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Short ID like JAVASCRIPT-ABC
|
|
24
|
+
if (/^[A-Z]+-[A-Z0-9]+$/i.test(input)) {
|
|
25
|
+
return { shortId: input };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { issueId: input };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatStacktrace(frames, { maxFrames = 20, showContext = true } = {}) {
|
|
32
|
+
if (!frames || frames.length === 0) return " (no frames)";
|
|
33
|
+
|
|
34
|
+
const reversed = frames.slice().reverse();
|
|
35
|
+
const appFrames = reversed.filter((f) => f.inApp !== false);
|
|
36
|
+
const framesToShow = appFrames.length > 0 ? appFrames : reversed;
|
|
37
|
+
|
|
38
|
+
return framesToShow
|
|
39
|
+
.slice(0, maxFrames)
|
|
40
|
+
.map((f, i) => {
|
|
41
|
+
const file = f.filename || f.absPath || f.module || "unknown";
|
|
42
|
+
const func = f.function || "(anonymous)";
|
|
43
|
+
const line = f.lineNo || f.lineno;
|
|
44
|
+
const col = f.colNo || f.colno;
|
|
45
|
+
const loc = line ? `:${line}${col ? `:${col}` : ""}` : "";
|
|
46
|
+
|
|
47
|
+
let out = ` ${i + 1}. ${file}${loc}\n → ${func}`;
|
|
48
|
+
|
|
49
|
+
if (showContext && f.context_line) {
|
|
50
|
+
out += `\n | ${f.context_line.trim()}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Show pre/post context if available
|
|
54
|
+
if (showContext && f.preContext && f.preContext.length > 0) {
|
|
55
|
+
const pre = f.preContext
|
|
56
|
+
.slice(-2)
|
|
57
|
+
.map((l) => ` . ${l.trim()}`)
|
|
58
|
+
.join("\n");
|
|
59
|
+
const post =
|
|
60
|
+
f.postContext
|
|
61
|
+
?.slice(0, 2)
|
|
62
|
+
.map((l) => ` . ${l.trim()}`)
|
|
63
|
+
.join("\n") || "";
|
|
64
|
+
if (pre || post) {
|
|
65
|
+
out = ` ${i + 1}. ${file}${loc}\n → ${func}`;
|
|
66
|
+
if (pre) out += `\n${pre}`;
|
|
67
|
+
out += `\n > ${f.context_line.trim()}`;
|
|
68
|
+
if (post) out += `\n${post}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return out;
|
|
73
|
+
})
|
|
74
|
+
.join("\n\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatException(exc) {
|
|
78
|
+
let out = `**${exc.type || "Error"}:** ${exc.value || "(no message)"}\n`;
|
|
79
|
+
|
|
80
|
+
if (exc.module) {
|
|
81
|
+
out += `Module: ${exc.module}\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (exc.stacktrace?.frames) {
|
|
85
|
+
out += "\n" + formatStacktrace(exc.stacktrace.frames);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatIssue(issue) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
|
|
94
|
+
lines.push(`# ${issue.title}`);
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push(`**Project:** ${issue.project?.slug || "unknown"}`);
|
|
97
|
+
lines.push(`**Short ID:** ${issue.shortId}`);
|
|
98
|
+
lines.push(`**Status:** ${issue.status}`);
|
|
99
|
+
lines.push(`**Level:** ${issue.level}`);
|
|
100
|
+
if (issue.culprit) {
|
|
101
|
+
lines.push(`**Culprit:** ${issue.culprit}`);
|
|
102
|
+
}
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push(`**First Seen:** ${formatTimestamp(issue.firstSeen)}`);
|
|
105
|
+
lines.push(`**Last Seen:** ${formatTimestamp(issue.lastSeen)}`);
|
|
106
|
+
lines.push(`**Events:** ${issue.count || 0}`);
|
|
107
|
+
lines.push(`**Users Affected:** ${issue.userCount || 0}`);
|
|
108
|
+
|
|
109
|
+
if (issue.tags && issue.tags.length > 0) {
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push("## Tags");
|
|
112
|
+
for (const tag of issue.tags.slice(0, 10)) {
|
|
113
|
+
const topValue = tag.topValues?.[0];
|
|
114
|
+
if (topValue) {
|
|
115
|
+
lines.push(`- **${tag.key}:** ${topValue.value} (${topValue.count})`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (issue.metadata) {
|
|
121
|
+
const m = issue.metadata;
|
|
122
|
+
if (m.type || m.value) {
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push("## Exception");
|
|
125
|
+
lines.push(`**Type:** ${m.type || "unknown"}`);
|
|
126
|
+
lines.push(`**Value:** ${m.value || "unknown"}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatEvent(event) {
|
|
134
|
+
const lines = [];
|
|
135
|
+
|
|
136
|
+
lines.push(`## Latest Event`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push(`**Event ID:** ${event.eventID}`);
|
|
139
|
+
lines.push(`**Timestamp:** ${formatTimestamp(event.dateCreated)}`);
|
|
140
|
+
|
|
141
|
+
// Show relevant tags (filter out noisy ones)
|
|
142
|
+
if (event.tags && event.tags.length > 0) {
|
|
143
|
+
const importantTags = [
|
|
144
|
+
"environment",
|
|
145
|
+
"release",
|
|
146
|
+
"server_name",
|
|
147
|
+
"transaction",
|
|
148
|
+
"url",
|
|
149
|
+
"browser",
|
|
150
|
+
"os",
|
|
151
|
+
"runtime",
|
|
152
|
+
];
|
|
153
|
+
const filteredTags = event.tags.filter(
|
|
154
|
+
(t) => importantTags.includes(t.key) || t.key.startsWith("sentry:"),
|
|
155
|
+
);
|
|
156
|
+
if (filteredTags.length > 0) {
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push("### Tags");
|
|
159
|
+
for (const tag of filteredTags) {
|
|
160
|
+
lines.push(`- **${tag.key}:** ${tag.value}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (event.entries) {
|
|
166
|
+
// Show request info first if available
|
|
167
|
+
for (const entry of event.entries) {
|
|
168
|
+
if (entry.type === "request" && entry.data) {
|
|
169
|
+
const req = entry.data;
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push("### Request");
|
|
172
|
+
if (req.method && req.url) {
|
|
173
|
+
lines.push(`**${req.method}** ${req.url}`);
|
|
174
|
+
}
|
|
175
|
+
if (req.headers && req.headers.length > 0) {
|
|
176
|
+
const importantHeaders = ["User-Agent", "Content-Type", "Accept", "Host", "Referer"];
|
|
177
|
+
const headers = req.headers.filter(([k]) => importantHeaders.includes(k));
|
|
178
|
+
if (headers.length > 0) {
|
|
179
|
+
lines.push("");
|
|
180
|
+
for (const [k, v] of headers) {
|
|
181
|
+
lines.push(` ${k}: ${v}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (req.data) {
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push("**Body:**");
|
|
188
|
+
const body = typeof req.data === "string" ? req.data : JSON.stringify(req.data, null, 2);
|
|
189
|
+
lines.push("```");
|
|
190
|
+
lines.push(body.slice(0, 1000) + (body.length > 1000 ? "..." : ""));
|
|
191
|
+
lines.push("```");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Show exceptions with stack traces
|
|
197
|
+
for (const entry of event.entries) {
|
|
198
|
+
if (entry.type === "exception" && entry.data?.values) {
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push("### Exception");
|
|
201
|
+
for (const exc of entry.data.values) {
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push(formatException(exc));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (entry.type === "message" && entry.data?.formatted) {
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push("### Message");
|
|
210
|
+
lines.push(entry.data.formatted);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Show breadcrumbs last
|
|
215
|
+
for (const entry of event.entries) {
|
|
216
|
+
if (entry.type === "breadcrumbs" && entry.data?.values) {
|
|
217
|
+
const crumbs = entry.data.values.slice(-15);
|
|
218
|
+
if (crumbs.length > 0) {
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push("### Recent Breadcrumbs");
|
|
221
|
+
for (const c of crumbs) {
|
|
222
|
+
let ts = "??:??:??";
|
|
223
|
+
if (c.timestamp) {
|
|
224
|
+
try {
|
|
225
|
+
// Handle both unix timestamps and ISO strings
|
|
226
|
+
const date =
|
|
227
|
+
typeof c.timestamp === "number"
|
|
228
|
+
? new Date(c.timestamp * 1000)
|
|
229
|
+
: new Date(c.timestamp);
|
|
230
|
+
if (!isNaN(date.getTime())) {
|
|
231
|
+
ts = date.toISOString().slice(11, 19);
|
|
232
|
+
}
|
|
233
|
+
} catch {}
|
|
234
|
+
}
|
|
235
|
+
const cat = c.category || c.type || "?";
|
|
236
|
+
const level = c.level && c.level !== "info" ? `[${c.level}] ` : "";
|
|
237
|
+
let msg = c.message || "";
|
|
238
|
+
if (!msg && c.data) {
|
|
239
|
+
if (c.data.url) msg = c.data.url;
|
|
240
|
+
else if (c.data.method) msg = `${c.data.method} ${c.data.url || ""}`;
|
|
241
|
+
else msg = JSON.stringify(c.data);
|
|
242
|
+
}
|
|
243
|
+
lines.push(` [${ts}] ${level}${cat}: ${msg}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Show contexts (runtime, browser, os, device, etc.)
|
|
251
|
+
if (event.contexts) {
|
|
252
|
+
const ctx = event.contexts;
|
|
253
|
+
const contextLines = [];
|
|
254
|
+
|
|
255
|
+
if (ctx.runtime) {
|
|
256
|
+
contextLines.push(`- **Runtime:** ${ctx.runtime.name || "?"} ${ctx.runtime.version || ""}`);
|
|
257
|
+
}
|
|
258
|
+
if (ctx.browser) {
|
|
259
|
+
contextLines.push(`- **Browser:** ${ctx.browser.name || "?"} ${ctx.browser.version || ""}`);
|
|
260
|
+
}
|
|
261
|
+
if (ctx.os) {
|
|
262
|
+
contextLines.push(`- **OS:** ${ctx.os.name || "?"} ${ctx.os.version || ""}`);
|
|
263
|
+
}
|
|
264
|
+
if (ctx.device && ctx.device.family) {
|
|
265
|
+
contextLines.push(`- **Device:** ${ctx.device.family}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (contextLines.length > 0) {
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push("### Context");
|
|
271
|
+
lines.push(...contextLines);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Fallback to old context field
|
|
276
|
+
if (!event.contexts && event.context) {
|
|
277
|
+
const ctx = event.context;
|
|
278
|
+
if (ctx.browser || ctx.os || ctx.device) {
|
|
279
|
+
lines.push("");
|
|
280
|
+
lines.push("### Context");
|
|
281
|
+
if (ctx.browser)
|
|
282
|
+
lines.push(`- **Browser:** ${ctx.browser.name} ${ctx.browser.version || ""}`);
|
|
283
|
+
if (ctx.os) lines.push(`- **OS:** ${ctx.os.name} ${ctx.os.version || ""}`);
|
|
284
|
+
if (ctx.device) lines.push(`- **Device:** ${ctx.device.family || "unknown"}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function main() {
|
|
292
|
+
const args = process.argv.slice(2);
|
|
293
|
+
|
|
294
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
295
|
+
console.log("Usage: fetch-issue.js <issue-id-or-url> [options]");
|
|
296
|
+
console.log("");
|
|
297
|
+
console.log("Options:");
|
|
298
|
+
console.log(" --latest Fetch the latest event with full stack trace");
|
|
299
|
+
console.log(" --org <org> Organization slug (for short IDs like PROJECT-123)");
|
|
300
|
+
console.log(" --json Output raw JSON instead of formatted text");
|
|
301
|
+
console.log("");
|
|
302
|
+
console.log("Examples:");
|
|
303
|
+
console.log(" fetch-issue.js 5765604106");
|
|
304
|
+
console.log(" fetch-issue.js https://sentry.io/organizations/sentry/issues/123/");
|
|
305
|
+
console.log(" fetch-issue.js MYPROJ-ABC --org myorg");
|
|
306
|
+
console.log(" fetch-issue.js 5765604106 --latest");
|
|
307
|
+
process.exit(0);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const input = args[0];
|
|
311
|
+
const wantLatest = args.includes("--latest");
|
|
312
|
+
const wantJson = args.includes("--json");
|
|
313
|
+
const orgIndex = args.indexOf("--org");
|
|
314
|
+
const cliOrg = orgIndex !== -1 ? args[orgIndex + 1] : null;
|
|
315
|
+
|
|
316
|
+
const token = getAuthToken();
|
|
317
|
+
const parsed = parseIssueInput(input);
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
let issue;
|
|
321
|
+
|
|
322
|
+
if (parsed.shortId) {
|
|
323
|
+
const org = cliOrg || parsed.org;
|
|
324
|
+
if (!org) {
|
|
325
|
+
console.error("Error: Short ID requires --org flag");
|
|
326
|
+
console.error("Example: fetch-issue.js MYPROJ-123 --org myorg");
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
// Use the shortids endpoint to resolve the short ID
|
|
330
|
+
const shortIdUrl = `${SENTRY_API_BASE}/organizations/${org}/shortids/${encodeURIComponent(parsed.shortId)}/`;
|
|
331
|
+
const result = await fetchJson(shortIdUrl, token);
|
|
332
|
+
if (!result || !result.group) {
|
|
333
|
+
console.error(`Error: Issue ${parsed.shortId} not found`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
issue = result.group;
|
|
337
|
+
} else {
|
|
338
|
+
const issueUrl = `${SENTRY_API_BASE}/issues/${parsed.issueId}/`;
|
|
339
|
+
issue = await fetchJson(issueUrl, token);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (wantJson && !wantLatest) {
|
|
343
|
+
console.log(JSON.stringify(issue, null, 2));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let output = formatIssue(issue);
|
|
348
|
+
|
|
349
|
+
if (wantLatest) {
|
|
350
|
+
const eventUrl = `${SENTRY_API_BASE}/issues/${issue.id}/events/latest/`;
|
|
351
|
+
const event = await fetchJson(eventUrl, token);
|
|
352
|
+
|
|
353
|
+
if (wantJson) {
|
|
354
|
+
console.log(JSON.stringify({ issue, event }, null, 2));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
output += "\n\n" + formatEvent(event);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log(output);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.error("Error:", err.message);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
main();
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SENTRY_API_BASE,
|
|
5
|
+
getAuthToken,
|
|
6
|
+
fetchJson,
|
|
7
|
+
formatTimestamp,
|
|
8
|
+
resolveProjectId,
|
|
9
|
+
} from "../lib/auth.js";
|
|
10
|
+
|
|
11
|
+
const HELP = `Usage: list-issues.js [options]
|
|
12
|
+
|
|
13
|
+
List and search issues in Sentry.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--org, -o <org> Organization slug (required)
|
|
17
|
+
--project, -p <project> Project slug (can be repeated for multiple projects)
|
|
18
|
+
--query, -q <query> Search query (Sentry issue search syntax)
|
|
19
|
+
--status <status> Filter by status (unresolved, resolved, ignored)
|
|
20
|
+
--level <level> Filter by level (error, warning, info, fatal)
|
|
21
|
+
--period, -t <period> Time period (default: 14d, e.g., 24h, 7d, 14d)
|
|
22
|
+
--limit, -n <n> Max results (default: 25)
|
|
23
|
+
--sort <sort> Sort order (date, new, priority, freq, user)
|
|
24
|
+
--json Output raw JSON
|
|
25
|
+
-h, --help Show this help
|
|
26
|
+
|
|
27
|
+
Query Syntax:
|
|
28
|
+
is:unresolved Unresolved issues only
|
|
29
|
+
is:resolved Resolved issues only
|
|
30
|
+
is:ignored Ignored issues only
|
|
31
|
+
is:assigned Assigned to someone
|
|
32
|
+
is:unassigned Not assigned
|
|
33
|
+
assigned:me Assigned to current user
|
|
34
|
+
level:error Error level only
|
|
35
|
+
firstSeen:+7d First seen more than 7 days ago
|
|
36
|
+
lastSeen:-24h Last seen within 24 hours
|
|
37
|
+
event.timestamp:>=2025-12-23 Events since date
|
|
38
|
+
times_seen:>100 Seen more than 100 times
|
|
39
|
+
user.email:*@example.com User email pattern
|
|
40
|
+
has:user Has user context
|
|
41
|
+
error.handled:0 Unhandled errors
|
|
42
|
+
|
|
43
|
+
Sort Options:
|
|
44
|
+
date - Last seen (default)
|
|
45
|
+
new - First seen (newest first)
|
|
46
|
+
priority - Priority score
|
|
47
|
+
freq - Frequency (events per time)
|
|
48
|
+
user - Users affected
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
# List unresolved errors
|
|
52
|
+
list-issues.js --org myorg --project backend --status unresolved
|
|
53
|
+
|
|
54
|
+
# Find recent high-priority issues
|
|
55
|
+
list-issues.js --org myorg --query "is:unresolved lastSeen:-24h" --sort priority
|
|
56
|
+
|
|
57
|
+
# Search for specific error type
|
|
58
|
+
list-issues.js --org myorg --query "AI_NoOutputGeneratedError"
|
|
59
|
+
|
|
60
|
+
# Find issues first seen in a date range
|
|
61
|
+
list-issues.js --org myorg --query "firstSeen:>=2025-12-23 firstSeen:<=2025-12-24"
|
|
62
|
+
|
|
63
|
+
# Find issues with many events
|
|
64
|
+
list-issues.js --org myorg --query "times_seen:>50" --sort freq
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
function parseArgs(args) {
|
|
68
|
+
const options = {
|
|
69
|
+
org: null,
|
|
70
|
+
projects: [],
|
|
71
|
+
query: null,
|
|
72
|
+
status: null,
|
|
73
|
+
level: null,
|
|
74
|
+
period: "14d",
|
|
75
|
+
limit: 25,
|
|
76
|
+
sort: null,
|
|
77
|
+
json: false,
|
|
78
|
+
help: false,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < args.length; i++) {
|
|
82
|
+
const arg = args[i];
|
|
83
|
+
|
|
84
|
+
switch (arg) {
|
|
85
|
+
case "--help":
|
|
86
|
+
case "-h":
|
|
87
|
+
options.help = true;
|
|
88
|
+
break;
|
|
89
|
+
case "--json":
|
|
90
|
+
options.json = true;
|
|
91
|
+
break;
|
|
92
|
+
case "--org":
|
|
93
|
+
case "-o":
|
|
94
|
+
options.org = args[++i];
|
|
95
|
+
break;
|
|
96
|
+
case "--project":
|
|
97
|
+
case "-p":
|
|
98
|
+
options.projects.push(args[++i]);
|
|
99
|
+
break;
|
|
100
|
+
case "--query":
|
|
101
|
+
case "-q":
|
|
102
|
+
options.query = args[++i];
|
|
103
|
+
break;
|
|
104
|
+
case "--status":
|
|
105
|
+
options.status = args[++i];
|
|
106
|
+
break;
|
|
107
|
+
case "--level":
|
|
108
|
+
options.level = args[++i];
|
|
109
|
+
break;
|
|
110
|
+
case "--period":
|
|
111
|
+
case "-t":
|
|
112
|
+
options.period = args[++i];
|
|
113
|
+
break;
|
|
114
|
+
case "--limit":
|
|
115
|
+
case "-n":
|
|
116
|
+
options.limit = parseInt(args[++i], 10);
|
|
117
|
+
break;
|
|
118
|
+
case "--sort":
|
|
119
|
+
options.sort = args[++i];
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return options;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function formatIssue(issue) {
|
|
128
|
+
const lines = [];
|
|
129
|
+
|
|
130
|
+
const id = issue.shortId || issue.id;
|
|
131
|
+
const title = issue.title || "(no title)";
|
|
132
|
+
const level = issue.level || "?";
|
|
133
|
+
const status = issue.status || "?";
|
|
134
|
+
const count = issue.count || 0;
|
|
135
|
+
const userCount = issue.userCount || 0;
|
|
136
|
+
const firstSeen = formatTimestamp(issue.firstSeen);
|
|
137
|
+
const lastSeen = formatTimestamp(issue.lastSeen);
|
|
138
|
+
const project = issue.project?.slug || "?";
|
|
139
|
+
const culprit = issue.culprit || "";
|
|
140
|
+
const permalink = issue.permalink || "";
|
|
141
|
+
|
|
142
|
+
lines.push(`[${id}] ${title}`);
|
|
143
|
+
lines.push(` level: ${level} | status: ${status} | project: ${project}`);
|
|
144
|
+
lines.push(` events: ${count} | users: ${userCount}`);
|
|
145
|
+
lines.push(` first: ${firstSeen} | last: ${lastSeen}`);
|
|
146
|
+
|
|
147
|
+
if (culprit) {
|
|
148
|
+
lines.push(` culprit: ${culprit}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (permalink) {
|
|
152
|
+
lines.push(` url: ${permalink}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatOutput(issues) {
|
|
159
|
+
if (!issues || issues.length === 0) {
|
|
160
|
+
return "No issues found matching your query.";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const lines = [];
|
|
164
|
+
lines.push(`Found ${issues.length} issues:\n`);
|
|
165
|
+
|
|
166
|
+
for (const issue of issues) {
|
|
167
|
+
lines.push(formatIssue(issue));
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return lines.join("\n").trimEnd();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function main() {
|
|
175
|
+
const args = process.argv.slice(2);
|
|
176
|
+
const options = parseArgs(args);
|
|
177
|
+
|
|
178
|
+
if (options.help) {
|
|
179
|
+
console.log(HELP);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!options.org) {
|
|
184
|
+
console.error("Error: --org is required");
|
|
185
|
+
console.error("Run with --help for usage information");
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const token = getAuthToken();
|
|
190
|
+
|
|
191
|
+
// Build query parameters
|
|
192
|
+
const params = new URLSearchParams();
|
|
193
|
+
|
|
194
|
+
if (options.period) {
|
|
195
|
+
params.set("statsPeriod", options.period);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
params.set("limit", Math.min(options.limit, 100).toString());
|
|
199
|
+
|
|
200
|
+
// Build search query
|
|
201
|
+
const queryParts = [];
|
|
202
|
+
|
|
203
|
+
if (options.query) {
|
|
204
|
+
queryParts.push(options.query);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (options.status) {
|
|
208
|
+
queryParts.push(`is:${options.status}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (options.level) {
|
|
212
|
+
queryParts.push(`level:${options.level}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (queryParts.length > 0) {
|
|
216
|
+
params.set("query", queryParts.join(" "));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (options.sort) {
|
|
220
|
+
params.set("sort", options.sort);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Build URL - always use org endpoint with resolved project IDs
|
|
224
|
+
// This handles both slugs and numeric IDs uniformly
|
|
225
|
+
for (const project of options.projects) {
|
|
226
|
+
const projectId = await resolveProjectId(options.org, project, token);
|
|
227
|
+
params.append("project", projectId);
|
|
228
|
+
}
|
|
229
|
+
const url = `${SENTRY_API_BASE}/organizations/${encodeURIComponent(options.org)}/issues/?${params.toString()}`;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const data = await fetchJson(url, token);
|
|
233
|
+
|
|
234
|
+
if (options.json) {
|
|
235
|
+
console.log(JSON.stringify(data, null, 2));
|
|
236
|
+
} else {
|
|
237
|
+
console.log(formatOutput(data));
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error("Error:", err.message);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
main();
|