@laitszkin/apollo-toolkit 3.13.2 → 3.14.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 (154) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +27 -0
  3. package/CLAUDE.md +8 -8
  4. package/analyse-app-logs/SKILL.md +3 -3
  5. package/bin/apollo-toolkit.ts +7 -0
  6. package/codex/codex-memory-manager/SKILL.md +2 -2
  7. package/codex/learn-skill-from-conversations/SKILL.md +3 -3
  8. package/dist/bin/apollo-toolkit.d.ts +2 -0
  9. package/dist/bin/apollo-toolkit.js +7 -0
  10. package/dist/lib/cli.d.ts +41 -0
  11. package/dist/lib/cli.js +655 -0
  12. package/dist/lib/installer.d.ts +59 -0
  13. package/dist/lib/installer.js +404 -0
  14. package/dist/lib/tool-runner.d.ts +19 -0
  15. package/dist/lib/tool-runner.js +536 -0
  16. package/dist/lib/tools/architecture.d.ts +2 -0
  17. package/dist/lib/tools/architecture.js +34 -0
  18. package/dist/lib/tools/create-specs.d.ts +2 -0
  19. package/dist/lib/tools/create-specs.js +175 -0
  20. package/dist/lib/tools/docs-to-voice.d.ts +2 -0
  21. package/dist/lib/tools/docs-to-voice.js +705 -0
  22. package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
  23. package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
  24. package/dist/lib/tools/extract-conversations.d.ts +2 -0
  25. package/dist/lib/tools/extract-conversations.js +105 -0
  26. package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
  27. package/dist/lib/tools/extract-pdf-text.js +92 -0
  28. package/dist/lib/tools/filter-logs.d.ts +2 -0
  29. package/dist/lib/tools/filter-logs.js +94 -0
  30. package/dist/lib/tools/find-github-issues.d.ts +2 -0
  31. package/dist/lib/tools/find-github-issues.js +176 -0
  32. package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
  33. package/dist/lib/tools/generate-storyboard-images.js +419 -0
  34. package/dist/lib/tools/log-cli-utils.d.ts +35 -0
  35. package/dist/lib/tools/log-cli-utils.js +233 -0
  36. package/dist/lib/tools/open-github-issue.d.ts +2 -0
  37. package/dist/lib/tools/open-github-issue.js +750 -0
  38. package/dist/lib/tools/read-github-issue.d.ts +2 -0
  39. package/dist/lib/tools/read-github-issue.js +134 -0
  40. package/dist/lib/tools/render-error-book.d.ts +2 -0
  41. package/dist/lib/tools/render-error-book.js +265 -0
  42. package/dist/lib/tools/render-katex.d.ts +2 -0
  43. package/dist/lib/tools/render-katex.js +294 -0
  44. package/dist/lib/tools/review-threads.d.ts +2 -0
  45. package/dist/lib/tools/review-threads.js +491 -0
  46. package/dist/lib/tools/search-logs.d.ts +2 -0
  47. package/dist/lib/tools/search-logs.js +164 -0
  48. package/dist/lib/tools/sync-memory-index.d.ts +2 -0
  49. package/dist/lib/tools/sync-memory-index.js +113 -0
  50. package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
  51. package/dist/lib/tools/validate-openai-agent-config.js +184 -0
  52. package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
  53. package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
  54. package/dist/lib/types.d.ts +82 -0
  55. package/dist/lib/types.js +2 -0
  56. package/dist/lib/updater.d.ts +34 -0
  57. package/dist/lib/updater.js +112 -0
  58. package/dist/lib/utils/format.d.ts +2 -0
  59. package/dist/lib/utils/format.js +6 -0
  60. package/dist/lib/utils/terminal.d.ts +12 -0
  61. package/dist/lib/utils/terminal.js +26 -0
  62. package/docs-to-voice/SKILL.md +0 -1
  63. package/generate-spec/SKILL.md +1 -1
  64. package/katex/SKILL.md +1 -2
  65. package/lib/cli.ts +780 -0
  66. package/lib/installer.ts +466 -0
  67. package/lib/tool-runner.ts +561 -0
  68. package/lib/tools/architecture.ts +34 -0
  69. package/lib/tools/create-specs.ts +204 -0
  70. package/lib/tools/docs-to-voice.ts +799 -0
  71. package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
  72. package/lib/tools/extract-conversations.ts +114 -0
  73. package/lib/tools/extract-pdf-text.ts +99 -0
  74. package/lib/tools/filter-logs.ts +118 -0
  75. package/lib/tools/find-github-issues.ts +211 -0
  76. package/lib/tools/generate-storyboard-images.ts +455 -0
  77. package/lib/tools/log-cli-utils.ts +262 -0
  78. package/lib/tools/open-github-issue.ts +930 -0
  79. package/lib/tools/read-github-issue.ts +179 -0
  80. package/lib/tools/render-error-book.ts +300 -0
  81. package/lib/tools/render-katex.ts +325 -0
  82. package/lib/tools/review-threads.ts +590 -0
  83. package/lib/tools/search-logs.ts +200 -0
  84. package/lib/tools/sync-memory-index.ts +114 -0
  85. package/lib/tools/validate-openai-agent-config.ts +209 -0
  86. package/lib/tools/validate-skill-frontmatter.ts +124 -0
  87. package/lib/types.ts +90 -0
  88. package/lib/updater.ts +165 -0
  89. package/lib/utils/format.ts +7 -0
  90. package/lib/utils/terminal.ts +22 -0
  91. package/open-github-issue/SKILL.md +2 -2
  92. package/optimise-skill/SKILL.md +1 -1
  93. package/package.json +13 -4
  94. package/resources/project-architecture/assets/architecture.css +764 -0
  95. package/resources/project-architecture/assets/viewer.client.js +144 -0
  96. package/resources/project-architecture/index.html +42 -0
  97. package/review-spec-related-changes/SKILL.md +1 -1
  98. package/solve-issues-found-during-review/SKILL.md +2 -1
  99. package/tsconfig.json +28 -0
  100. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  101. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  102. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  103. package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
  104. package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
  105. package/analyse-app-logs/scripts/search_logs.py +0 -137
  106. package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
  107. package/analyse-app-logs/tests/test_search_logs.py +0 -100
  108. package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
  109. package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
  110. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
  111. package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
  112. package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
  113. package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
  114. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
  115. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  116. package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
  117. package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
  118. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
  119. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
  120. package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
  121. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
  122. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
  123. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  124. package/generate-spec/scripts/create-specs +0 -215
  125. package/generate-spec/tests/test_create_specs.py +0 -200
  126. package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
  127. package/init-project-html/scripts/architecture.js +0 -296
  128. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  129. package/katex/scripts/render_katex.py +0 -247
  130. package/katex/scripts/render_katex.sh +0 -11
  131. package/katex/tests/test_render_katex.py +0 -174
  132. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
  133. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
  134. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  135. package/open-github-issue/scripts/open_github_issue.py +0 -705
  136. package/open-github-issue/tests/test_open_github_issue.py +0 -381
  137. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
  138. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
  139. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  140. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  141. package/read-github-issue/scripts/find_issues.py +0 -148
  142. package/read-github-issue/scripts/read_issue.py +0 -108
  143. package/read-github-issue/tests/test_find_issues.py +0 -127
  144. package/read-github-issue/tests/test_read_issue.py +0 -109
  145. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  146. package/resolve-review-comments/scripts/review_threads.py +0 -425
  147. package/resolve-review-comments/tests/test_review_threads.py +0 -74
  148. package/scripts/validate_openai_agent_config.py +0 -209
  149. package/scripts/validate_skill_frontmatter.py +0 -131
  150. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  151. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
  152. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
  153. package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
  154. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
@@ -0,0 +1,590 @@
1
+ // @ts-nocheck
2
+ import { execFile } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import type { ToolContext } from '../types';
5
+
6
+ const LIST_QUERY = `
7
+ query($owner: String!, $name: String!, $number: Int!, $after: String) {
8
+ repository(owner: $owner, name: $name) {
9
+ pullRequest(number: $number) {
10
+ reviewThreads(first: 100, after: $after) {
11
+ nodes {
12
+ id
13
+ isResolved
14
+ isOutdated
15
+ path
16
+ line
17
+ startLine
18
+ comments(first: 20) {
19
+ nodes {
20
+ id
21
+ url
22
+ body
23
+ author {
24
+ login
25
+ }
26
+ createdAt
27
+ path
28
+ line
29
+ outdated
30
+ }
31
+ }
32
+ }
33
+ pageInfo {
34
+ hasNextPage
35
+ endCursor
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ `;
42
+
43
+ const RESOLVE_MUTATION = `
44
+ mutation($threadId: ID!) {
45
+ resolveReviewThread(input: {threadId: $threadId}) {
46
+ thread {
47
+ id
48
+ isResolved
49
+ }
50
+ }
51
+ }
52
+ `;
53
+
54
+ interface ReviewThreadsArgs {
55
+ command: string;
56
+ repo: string | null;
57
+ pr: number | null;
58
+ state: string;
59
+ output: 'table' | 'json';
60
+ threadId: string[];
61
+ threadIdFile: string | null;
62
+ allUnresolved: boolean;
63
+ dryRun: boolean;
64
+ }
65
+
66
+ function parseArgs(argv: string[]): ReviewThreadsArgs {
67
+ const args: ReviewThreadsArgs = {
68
+ command: '',
69
+ repo: null,
70
+ pr: null,
71
+ state: 'unresolved',
72
+ output: 'table',
73
+ threadId: [],
74
+ threadIdFile: null,
75
+ allUnresolved: false,
76
+ dryRun: false,
77
+ };
78
+
79
+ // First argument is the subcommand (list/resolve)
80
+ let i = 0;
81
+ if (i < argv.length && !argv[i].startsWith('-')) {
82
+ args.command = argv[i++];
83
+ }
84
+
85
+ while (i < argv.length) {
86
+ const arg = argv[i];
87
+ switch (arg) {
88
+ case '--repo':
89
+ if (i + 1 < argv.length) args.repo = argv[++i];
90
+ break;
91
+ case '--pr':
92
+ if (i + 1 < argv.length) {
93
+ const n = parseInt(argv[++i], 10);
94
+ if (n > 0) args.pr = n;
95
+ }
96
+ break;
97
+ case '--state':
98
+ if (i + 1 < argv.length) {
99
+ const val = argv[++i];
100
+ if (['unresolved', 'resolved', 'all'].includes(val)) args.state = val;
101
+ }
102
+ break;
103
+ case '--output':
104
+ if (i + 1 < argv.length) {
105
+ const val = argv[++i];
106
+ if (val === 'table' || val === 'json') args.output = val;
107
+ }
108
+ break;
109
+ case '--thread-id':
110
+ if (i + 1 < argv.length) args.threadId.push(argv[++i]);
111
+ break;
112
+ case '--thread-id-file':
113
+ if (i + 1 < argv.length) args.threadIdFile = argv[++i];
114
+ break;
115
+ case '--all-unresolved':
116
+ args.allUnresolved = true;
117
+ break;
118
+ case '--dry-run':
119
+ args.dryRun = true;
120
+ break;
121
+ default:
122
+ break;
123
+ }
124
+ i++;
125
+ }
126
+
127
+ return args;
128
+ }
129
+
130
+ // ---- Utilities ----
131
+
132
+ interface CommandResult {
133
+ stdout: string;
134
+ stderr: string;
135
+ exitCode: number;
136
+ }
137
+
138
+ function runGh(cmdArgs: string[]): Promise<CommandResult> {
139
+ return new Promise((resolve) => {
140
+ execFile(
141
+ 'gh',
142
+ cmdArgs,
143
+ { maxBuffer: 10 * 1024 * 1024 },
144
+ (error, stdout, stderr) => {
145
+ if (error) {
146
+ resolve({
147
+ stdout: stdout || '',
148
+ stderr: stderr || '',
149
+ exitCode: (error as NodeJS.ErrnoException & { status?: number }).status ?? 1,
150
+ });
151
+ } else {
152
+ resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: 0 });
153
+ }
154
+ },
155
+ );
156
+ });
157
+ }
158
+
159
+ function runGhJson(cmdArgs: string[]): Promise<Record<string, unknown>> {
160
+ return runGh(cmdArgs).then((result) => {
161
+ if (result.exitCode !== 0) {
162
+ throw new Error(result.stderr.trim() || 'gh command failed');
163
+ }
164
+ try {
165
+ return JSON.parse(result.stdout);
166
+ } catch (exc) {
167
+ throw new Error('Failed to parse gh JSON output');
168
+ }
169
+ });
170
+ }
171
+
172
+ function parseOwnerRepo(repo: string): [string, string] {
173
+ const parts = repo.split('/');
174
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
175
+ throw new Error('repo must be in owner/name format');
176
+ }
177
+ return [parts[0], parts[1]];
178
+ }
179
+
180
+ async function resolveRepo(repo: string | null): Promise<string> {
181
+ if (repo) {
182
+ parseOwnerRepo(repo);
183
+ return repo;
184
+ }
185
+
186
+ const result = await runGh([
187
+ 'repo',
188
+ 'view',
189
+ '--json',
190
+ 'nameWithOwner',
191
+ '--jq',
192
+ '.nameWithOwner',
193
+ ]);
194
+ if (result.exitCode !== 0) {
195
+ throw new Error(result.stderr.trim() || 'Unable to resolve current repo');
196
+ }
197
+ return result.stdout.trim();
198
+ }
199
+
200
+ async function resolvePrNumber(repo: string, pr: number | null): Promise<number> {
201
+ if (pr !== null) return pr;
202
+
203
+ const result = await runGh([
204
+ 'pr',
205
+ 'view',
206
+ '--repo',
207
+ repo,
208
+ '--json',
209
+ 'number',
210
+ '--jq',
211
+ '.number',
212
+ ]);
213
+ if (result.exitCode !== 0) {
214
+ throw new Error(
215
+ 'Unable to infer PR number from current branch context',
216
+ );
217
+ }
218
+ return parseInt(result.stdout.trim(), 10);
219
+ }
220
+
221
+ function ghGraphql(
222
+ query: string,
223
+ variables: Record<string, unknown>,
224
+ ): Promise<Record<string, unknown>> {
225
+ const cmdArgs = ['api', 'graphql', '-f', `query=${query}`];
226
+ for (const [key, value] of Object.entries(variables)) {
227
+ cmdArgs.push('-F', `${key}=${JSON.stringify(value)}`);
228
+ }
229
+ return runGhJson(cmdArgs);
230
+ }
231
+
232
+ // ---- Thread fetching ----
233
+
234
+ async function fetchReviewThreads(
235
+ repo: string,
236
+ prNumber: number,
237
+ ): Promise<Array<Record<string, unknown>>> {
238
+ const [owner, name] = parseOwnerRepo(repo);
239
+ const threads: Array<Record<string, unknown>> = [];
240
+ let after: string | null = null;
241
+
242
+ while (true) {
243
+ const payload = await ghGraphql(LIST_QUERY, {
244
+ owner,
245
+ name,
246
+ number: prNumber,
247
+ after,
248
+ });
249
+
250
+ const pr = (payload.data as Record<string, unknown>)?.repository as Record<string, unknown> | undefined;
251
+ if (!pr) {
252
+ throw new Error(`PR #${prNumber} not found in ${repo}`);
253
+ }
254
+
255
+ const reviewThreads = pr.reviewThreads as Record<string, unknown>;
256
+ const nodes = (reviewThreads.nodes as Array<Record<string, unknown>>) || [];
257
+ threads.push(...nodes);
258
+
259
+ const pageInfo = reviewThreads.pageInfo as Record<string, unknown>;
260
+ if (!pageInfo.hasNextPage) break;
261
+ after = (pageInfo.endCursor as string) || null;
262
+ if (!after) break;
263
+ }
264
+
265
+ return threads;
266
+ }
267
+
268
+ function filterThreads(
269
+ threads: Array<Record<string, unknown>>,
270
+ state: string,
271
+ ): Array<Record<string, unknown>> {
272
+ if (state === 'all') return threads;
273
+ if (state === 'resolved') {
274
+ return threads.filter((item) => item.isResolved);
275
+ }
276
+ return threads.filter((item) => !item.isResolved);
277
+ }
278
+
279
+ function normalizeThread(
280
+ thread: Record<string, unknown>,
281
+ ): Record<string, unknown> {
282
+ const commentNodes = (thread.comments as Record<string, unknown>)?.nodes as
283
+ | Array<Record<string, unknown>>
284
+ | undefined;
285
+ const normalizedComments = (commentNodes || []).map((comment) => ({
286
+ id: comment.id,
287
+ url: comment.url,
288
+ author: ((comment.author as Record<string, unknown>)?.login as string) || null,
289
+ body: comment.body || '',
290
+ created_at: comment.createdAt,
291
+ path: comment.path,
292
+ line: comment.line,
293
+ outdated: comment.outdated,
294
+ }));
295
+
296
+ return {
297
+ thread_id: thread.id,
298
+ is_resolved: thread.isResolved,
299
+ is_outdated: thread.isOutdated,
300
+ path: thread.path,
301
+ line: thread.line,
302
+ start_line: thread.startLine,
303
+ comments: normalizedComments,
304
+ };
305
+ }
306
+
307
+ function truncate(text: string, width: number): string {
308
+ if (text.length <= width) return text;
309
+ if (width <= 3) return text.slice(0, width);
310
+ return text.slice(0, width - 3) + '...';
311
+ }
312
+
313
+ function previewBody(thread: Record<string, unknown>): string {
314
+ const comments = thread.comments as Array<Record<string, unknown>> | undefined;
315
+ if (!comments || comments.length === 0) return '-';
316
+ const body = (comments[0].body as string || '').replace(/\n/g, ' ').trim();
317
+ return truncate(body || '-', 72);
318
+ }
319
+
320
+ function renderLocation(thread: Record<string, unknown>): string {
321
+ const path = (thread.path as string) || '-';
322
+ const line = thread.line;
323
+ if (line == null) return path;
324
+ return `${path}:${line}`;
325
+ }
326
+
327
+ function printTable(
328
+ threads: Array<Record<string, unknown>>,
329
+ context: ToolContext,
330
+ ): void {
331
+ const { stdout } = context;
332
+ const widths = {
333
+ idx: 4,
334
+ thread: 12,
335
+ location: 36,
336
+ author: 18,
337
+ preview: 72,
338
+ };
339
+
340
+ const header =
341
+ `${'#'.padEnd(widths.idx)} ` +
342
+ `${'THREAD_ID'.padEnd(widths.thread)} ` +
343
+ `${'LOCATION'.padEnd(widths.location)} ` +
344
+ `${'AUTHOR'.padEnd(widths.author)} ` +
345
+ `${'COMMENT_PREVIEW'.padEnd(widths.preview)}`;
346
+ stdout.write(header + '\n');
347
+ stdout.write('-'.repeat(header.length) + '\n');
348
+
349
+ for (let idx = 0; idx < threads.length; idx++) {
350
+ const thread = threads[idx];
351
+ const comments = thread.comments as Array<Record<string, unknown>> | undefined;
352
+ const author = comments?.[0]?.author ?? '-';
353
+
354
+ const row =
355
+ `${String(idx + 1).padEnd(widths.idx)} ` +
356
+ `${truncate(String(thread.thread_id ?? '-'), widths.thread).padEnd(widths.thread)} ` +
357
+ `${truncate(renderLocation(thread), widths.location).padEnd(widths.location)} ` +
358
+ `${truncate(String(author ?? '-'), widths.author).padEnd(widths.author)} ` +
359
+ `${previewBody(thread).padEnd(widths.preview)}`;
360
+ stdout.write(row + '\n');
361
+ }
362
+ }
363
+
364
+ // ---- Thread ID loading ----
365
+
366
+ function loadThreadIds(filePath: string): string[] {
367
+ const raw = readFileSync(filePath, 'utf-8');
368
+ const payload = JSON.parse(raw);
369
+
370
+ let ids: unknown[];
371
+ if (Array.isArray(payload)) {
372
+ ids = payload;
373
+ } else if (typeof payload === 'object' && payload !== null) {
374
+ const p = payload as Record<string, unknown>;
375
+ if (Array.isArray(p.thread_ids)) {
376
+ ids = p.thread_ids;
377
+ } else if (Array.isArray(p.adopted_thread_ids)) {
378
+ ids = p.adopted_thread_ids;
379
+ } else if (Array.isArray(p.threads)) {
380
+ ids = (p.threads as Array<Record<string, unknown>>)
381
+ .filter((item) => typeof item === 'object' && item !== null)
382
+ .map((item) => item.thread_id)
383
+ .filter((id) => id !== undefined);
384
+ } else {
385
+ throw new Error(
386
+ 'JSON must include thread_ids, adopted_thread_ids, or threads',
387
+ );
388
+ }
389
+ } else {
390
+ throw new Error('Unsupported JSON payload for thread IDs');
391
+ }
392
+
393
+ const output = ids
394
+ .filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
395
+ .map((item) => item.trim());
396
+ return [...new Set(output)];
397
+ }
398
+
399
+ function collectThreadIds(
400
+ args: ReviewThreadsArgs,
401
+ unresolvedThreads: Array<Record<string, unknown>>,
402
+ ): string[] {
403
+ const ids: string[] = [];
404
+
405
+ if (args.allUnresolved) {
406
+ for (const item of unresolvedThreads) {
407
+ if (item.thread_id) {
408
+ ids.push(item.thread_id as string);
409
+ }
410
+ }
411
+ }
412
+
413
+ ids.push(...args.threadId);
414
+
415
+ if (args.threadIdFile) {
416
+ ids.push(...loadThreadIds(args.threadIdFile));
417
+ }
418
+
419
+ const normalized = ids.filter(Boolean);
420
+ return [...new Set(normalized)];
421
+ }
422
+
423
+ async function resolveThreads(
424
+ threadIds: string[],
425
+ dryRun: boolean,
426
+ ): Promise<{ resolved: string[]; failed: Array<Record<string, string>> }> {
427
+ const resolved: string[] = [];
428
+ const failed: Array<Record<string, string>> = [];
429
+
430
+ for (const threadId of threadIds) {
431
+ if (dryRun) {
432
+ resolved.push(threadId);
433
+ continue;
434
+ }
435
+
436
+ try {
437
+ const payload = await ghGraphql(RESOLVE_MUTATION, { threadId });
438
+ const thread = (
439
+ payload.data as Record<string, unknown>
440
+ )?.resolveReviewThread as Record<string, unknown> | undefined;
441
+ if (!thread?.thread) {
442
+ throw new Error('thread did not resolve');
443
+ }
444
+ const resolvedThread = thread.thread as Record<string, unknown>;
445
+ if (!resolvedThread.isResolved) {
446
+ throw new Error('thread did not resolve');
447
+ }
448
+ resolved.push(threadId);
449
+ } catch (exc) {
450
+ failed.push({ thread_id: threadId, error: (exc as Error).message });
451
+ }
452
+ }
453
+
454
+ return { resolved, failed };
455
+ }
456
+
457
+ // ---- Subcommands ----
458
+
459
+ async function cmdList(
460
+ args: ReviewThreadsArgs,
461
+ context: ToolContext,
462
+ ): Promise<number> {
463
+ const { stdout, stderr } = context;
464
+
465
+ let repo: string;
466
+ try {
467
+ repo = await resolveRepo(args.repo);
468
+ } catch (err) {
469
+ stderr.write(`Error: ${(err as Error).message}\n`);
470
+ return 1;
471
+ }
472
+
473
+ let prNumber: number;
474
+ try {
475
+ prNumber = await resolvePrNumber(repo, args.pr);
476
+ } catch (err) {
477
+ stderr.write(`Error: ${(err as Error).message}\n`);
478
+ return 1;
479
+ }
480
+
481
+ let threads: Array<Record<string, unknown>>;
482
+ try {
483
+ threads = await fetchReviewThreads(repo, prNumber);
484
+ } catch (err) {
485
+ stderr.write(`Error: ${(err as Error).message}\n`);
486
+ return 1;
487
+ }
488
+
489
+ const filtered = filterThreads(threads, args.state);
490
+ const normalized = filtered.map(normalizeThread);
491
+
492
+ const result = {
493
+ repo,
494
+ pr_number: prNumber,
495
+ state: args.state,
496
+ thread_count: normalized.length,
497
+ threads: normalized,
498
+ };
499
+
500
+ if (args.output === 'json') {
501
+ stdout.write(JSON.stringify(result, null, 2) + '\n');
502
+ } else {
503
+ stdout.write(`Repository: ${repo}\n`);
504
+ stdout.write(`PR: #${prNumber}\n`);
505
+ stdout.write(`Threads (${args.state}): ${normalized.length}\n`);
506
+ printTable(normalized, context);
507
+ }
508
+
509
+ return 0;
510
+ }
511
+
512
+ async function cmdResolve(
513
+ args: ReviewThreadsArgs,
514
+ context: ToolContext,
515
+ ): Promise<number> {
516
+ const { stdout, stderr } = context;
517
+
518
+ let repo: string;
519
+ try {
520
+ repo = await resolveRepo(args.repo);
521
+ } catch (err) {
522
+ stderr.write(`Error: ${(err as Error).message}\n`);
523
+ return 1;
524
+ }
525
+
526
+ let prNumber: number;
527
+ try {
528
+ prNumber = await resolvePrNumber(repo, args.pr);
529
+ } catch (err) {
530
+ stderr.write(`Error: ${(err as Error).message}\n`);
531
+ return 1;
532
+ }
533
+
534
+ let threads: Array<Record<string, unknown>>;
535
+ try {
536
+ threads = await fetchReviewThreads(repo, prNumber);
537
+ } catch (err) {
538
+ stderr.write(`Error: ${(err as Error).message}\n`);
539
+ return 1;
540
+ }
541
+
542
+ const unresolved = filterThreads(threads, 'unresolved').map(normalizeThread);
543
+ const threadIds = collectThreadIds(args, unresolved);
544
+
545
+ if (threadIds.length === 0) {
546
+ stderr.write(
547
+ 'Error: no thread IDs selected. Use --thread-id, --thread-id-file, or --all-unresolved.\n',
548
+ );
549
+ return 1;
550
+ }
551
+
552
+ const { resolved, failed } = await resolveThreads(threadIds, args.dryRun);
553
+
554
+ const summary = {
555
+ repo,
556
+ pr_number: prNumber,
557
+ requested: threadIds,
558
+ resolved,
559
+ failed,
560
+ dry_run: args.dryRun,
561
+ };
562
+ stdout.write(JSON.stringify(summary, null, 2) + '\n');
563
+
564
+ return failed.length > 0 ? 1 : 0;
565
+ }
566
+
567
+ // ---- Main handler ----
568
+
569
+ export async function reviewThreadsHandler(
570
+ argv: string[],
571
+ context: ToolContext,
572
+ ): Promise<number> {
573
+ const { stderr } = context;
574
+ const args = parseArgs(argv);
575
+
576
+ try {
577
+ switch (args.command) {
578
+ case 'list':
579
+ return await cmdList(args, context);
580
+ case 'resolve':
581
+ return await cmdResolve(args, context);
582
+ default:
583
+ stderr.write(`Unsupported command: ${args.command}\n`);
584
+ return 1;
585
+ }
586
+ } catch (err) {
587
+ stderr.write(`Error: ${(err as Error).message}\n`);
588
+ return 1;
589
+ }
590
+ }