@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.
- package/AGENTS.md +7 -7
- package/CHANGELOG.md +27 -0
- package/CLAUDE.md +8 -8
- package/analyse-app-logs/SKILL.md +3 -3
- package/bin/apollo-toolkit.ts +7 -0
- package/codex/codex-memory-manager/SKILL.md +2 -2
- package/codex/learn-skill-from-conversations/SKILL.md +3 -3
- package/dist/bin/apollo-toolkit.d.ts +2 -0
- package/dist/bin/apollo-toolkit.js +7 -0
- package/dist/lib/cli.d.ts +41 -0
- package/dist/lib/cli.js +655 -0
- package/dist/lib/installer.d.ts +59 -0
- package/dist/lib/installer.js +404 -0
- package/dist/lib/tool-runner.d.ts +19 -0
- package/dist/lib/tool-runner.js +536 -0
- package/dist/lib/tools/architecture.d.ts +2 -0
- package/dist/lib/tools/architecture.js +34 -0
- package/dist/lib/tools/create-specs.d.ts +2 -0
- package/dist/lib/tools/create-specs.js +175 -0
- package/dist/lib/tools/docs-to-voice.d.ts +2 -0
- package/dist/lib/tools/docs-to-voice.js +705 -0
- package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
- package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
- package/dist/lib/tools/extract-conversations.d.ts +2 -0
- package/dist/lib/tools/extract-conversations.js +105 -0
- package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
- package/dist/lib/tools/extract-pdf-text.js +92 -0
- package/dist/lib/tools/filter-logs.d.ts +2 -0
- package/dist/lib/tools/filter-logs.js +94 -0
- package/dist/lib/tools/find-github-issues.d.ts +2 -0
- package/dist/lib/tools/find-github-issues.js +176 -0
- package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
- package/dist/lib/tools/generate-storyboard-images.js +419 -0
- package/dist/lib/tools/log-cli-utils.d.ts +35 -0
- package/dist/lib/tools/log-cli-utils.js +233 -0
- package/dist/lib/tools/open-github-issue.d.ts +2 -0
- package/dist/lib/tools/open-github-issue.js +750 -0
- package/dist/lib/tools/read-github-issue.d.ts +2 -0
- package/dist/lib/tools/read-github-issue.js +134 -0
- package/dist/lib/tools/render-error-book.d.ts +2 -0
- package/dist/lib/tools/render-error-book.js +265 -0
- package/dist/lib/tools/render-katex.d.ts +2 -0
- package/dist/lib/tools/render-katex.js +294 -0
- package/dist/lib/tools/review-threads.d.ts +2 -0
- package/dist/lib/tools/review-threads.js +491 -0
- package/dist/lib/tools/search-logs.d.ts +2 -0
- package/dist/lib/tools/search-logs.js +164 -0
- package/dist/lib/tools/sync-memory-index.d.ts +2 -0
- package/dist/lib/tools/sync-memory-index.js +113 -0
- package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
- package/dist/lib/tools/validate-openai-agent-config.js +184 -0
- package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
- package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
- package/dist/lib/types.d.ts +82 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/updater.d.ts +34 -0
- package/dist/lib/updater.js +112 -0
- package/dist/lib/utils/format.d.ts +2 -0
- package/dist/lib/utils/format.js +6 -0
- package/dist/lib/utils/terminal.d.ts +12 -0
- package/dist/lib/utils/terminal.js +26 -0
- package/docs-to-voice/SKILL.md +0 -1
- package/generate-spec/SKILL.md +1 -1
- package/katex/SKILL.md +1 -2
- package/lib/cli.ts +780 -0
- package/lib/installer.ts +466 -0
- package/lib/tool-runner.ts +561 -0
- package/lib/tools/architecture.ts +34 -0
- package/lib/tools/create-specs.ts +204 -0
- package/lib/tools/docs-to-voice.ts +799 -0
- package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
- package/lib/tools/extract-conversations.ts +114 -0
- package/lib/tools/extract-pdf-text.ts +99 -0
- package/lib/tools/filter-logs.ts +118 -0
- package/lib/tools/find-github-issues.ts +211 -0
- package/lib/tools/generate-storyboard-images.ts +455 -0
- package/lib/tools/log-cli-utils.ts +262 -0
- package/lib/tools/open-github-issue.ts +930 -0
- package/lib/tools/read-github-issue.ts +179 -0
- package/lib/tools/render-error-book.ts +300 -0
- package/lib/tools/render-katex.ts +325 -0
- package/lib/tools/review-threads.ts +590 -0
- package/lib/tools/search-logs.ts +200 -0
- package/lib/tools/sync-memory-index.ts +114 -0
- package/lib/tools/validate-openai-agent-config.ts +209 -0
- package/lib/tools/validate-skill-frontmatter.ts +124 -0
- package/lib/types.ts +90 -0
- package/lib/updater.ts +165 -0
- package/lib/utils/format.ts +7 -0
- package/lib/utils/terminal.ts +22 -0
- package/open-github-issue/SKILL.md +2 -2
- package/optimise-skill/SKILL.md +1 -1
- package/package.json +13 -4
- package/resources/project-architecture/assets/architecture.css +764 -0
- package/resources/project-architecture/assets/viewer.client.js +144 -0
- package/resources/project-architecture/index.html +42 -0
- package/review-spec-related-changes/SKILL.md +1 -1
- package/solve-issues-found-during-review/SKILL.md +2 -1
- package/tsconfig.json +28 -0
- package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
- package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
- package/analyse-app-logs/scripts/search_logs.py +0 -137
- package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
- package/analyse-app-logs/tests/test_search_logs.py +0 -100
- package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
- package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
- package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
- package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
- package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
- package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
- package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
- package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
- package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
- package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
- package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
- package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
- package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/generate-spec/scripts/create-specs +0 -215
- package/generate-spec/tests/test_create_specs.py +0 -200
- package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
- package/init-project-html/scripts/architecture.js +0 -296
- package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
- package/katex/scripts/render_katex.py +0 -247
- package/katex/scripts/render_katex.sh +0 -11
- package/katex/tests/test_render_katex.py +0 -174
- package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
- package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
- package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
- package/open-github-issue/scripts/open_github_issue.py +0 -705
- package/open-github-issue/tests/test_open_github_issue.py +0 -381
- package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
- package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
- package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/find_issues.py +0 -148
- package/read-github-issue/scripts/read_issue.py +0 -108
- package/read-github-issue/tests/test_find_issues.py +0 -127
- package/read-github-issue/tests/test_read_issue.py +0 -109
- package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
- package/resolve-review-comments/scripts/review_threads.py +0 -425
- package/resolve-review-comments/tests/test_review_threads.py +0 -74
- package/scripts/validate_openai_agent_config.py +0 -209
- package/scripts/validate_skill_frontmatter.py +0 -131
- package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
- package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
- package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
- package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
- 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
|
+
}
|