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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/extensions/answer.ts +601 -0
  4. package/extensions/branch-term.ts +405 -0
  5. package/extensions/btw.ts +444 -0
  6. package/extensions/ghostty.ts +301 -0
  7. package/extensions/git-diff-stats.ts +277 -0
  8. package/extensions/git-pr-status.ts +286 -0
  9. package/extensions/insights.ts +2367 -0
  10. package/extensions/interlude.ts +144 -0
  11. package/extensions/loop.ts +529 -0
  12. package/extensions/memory.ts +1889 -0
  13. package/extensions/notify.ts +161 -0
  14. package/extensions/openai-fast.ts +227 -0
  15. package/extensions/openai-verbosity.ts +223 -0
  16. package/extensions/review.ts +4347 -0
  17. package/extensions/sandbox/index.ts +2578 -0
  18. package/extensions/usage/anthropic.ts +198 -0
  19. package/extensions/usage/github-copilot.ts +204 -0
  20. package/extensions/usage/google-gemini-cli.ts +232 -0
  21. package/extensions/usage/index.ts +2310 -0
  22. package/extensions/usage/minimax.ts +208 -0
  23. package/extensions/usage/openai-codex.ts +180 -0
  24. package/extensions/usage/openrouter.ts +168 -0
  25. package/extensions/usage/providers.ts +26 -0
  26. package/extensions/usage/shared.ts +156 -0
  27. package/extensions/usage/types.ts +53 -0
  28. package/extensions/usage/zai.ts +186 -0
  29. package/extensions/websearch/README.md +68 -0
  30. package/extensions/websearch/browser/chromium.ts +330 -0
  31. package/extensions/websearch/browser/discovery.ts +75 -0
  32. package/extensions/websearch/browser/firefox.ts +150 -0
  33. package/extensions/websearch/browser/sqlite.ts +38 -0
  34. package/extensions/websearch/config.ts +76 -0
  35. package/extensions/websearch/index.ts +312 -0
  36. package/extensions/websearch/normalize.ts +155 -0
  37. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  38. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  39. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  40. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  41. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  42. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  43. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  44. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  45. package/extensions/websearch/providers/shared.ts +76 -0
  46. package/extensions/websearch/types.ts +52 -0
  47. package/extensions/worktree.ts +2223 -0
  48. package/package.json +72 -0
  49. package/skills/browser-tools/SKILL.md +253 -0
  50. package/skills/browser-tools/scripts/browser-content.js +100 -0
  51. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  52. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  53. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  54. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  55. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  56. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  57. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  58. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  59. package/skills/browser-tools/scripts/browser-start.js +281 -0
  60. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  61. package/skills/browser-tools/scripts/utils.js +60 -0
  62. package/skills/git-clean-history/SKILL.md +45 -0
  63. package/skills/git-commit/SKILL.md +55 -0
  64. package/skills/oracle/SKILL.md +64 -0
  65. package/skills/oracle/scripts/oracle +249 -0
  66. package/skills/oracle/scripts/oracle-bundle +227 -0
  67. package/skills/sentry/SKILL.md +198 -0
  68. package/skills/sentry/lib/auth.js +99 -0
  69. package/skills/sentry/scripts/fetch-event.js +325 -0
  70. package/skills/sentry/scripts/fetch-issue.js +368 -0
  71. package/skills/sentry/scripts/list-issues.js +245 -0
  72. package/skills/sentry/scripts/search-events.js +291 -0
  73. package/skills/sentry/scripts/search-logs.js +234 -0
  74. package/skills/update-changelog/SKILL.md +135 -0
  75. 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();