techunter 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 +197 -0
- package/dist/index.js +2865 -0
- package/dist/mcp.js +2195 -0
- package/package.json +65 -0
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,2195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/lib/github.ts
|
|
13
|
+
var github_exports = {};
|
|
14
|
+
__export(github_exports, {
|
|
15
|
+
acceptTask: () => acceptTask,
|
|
16
|
+
claimTask: () => claimTask,
|
|
17
|
+
closeTask: () => closeTask,
|
|
18
|
+
createPR: () => createPR,
|
|
19
|
+
createTask: () => createTask,
|
|
20
|
+
ensureLabels: () => ensureLabels,
|
|
21
|
+
formatGuideAsMarkdown: () => formatGuideAsMarkdown,
|
|
22
|
+
getAuthenticatedUser: () => getAuthenticatedUser,
|
|
23
|
+
getBaseBranch: () => getBaseBranch,
|
|
24
|
+
getDefaultBranch: () => getDefaultBranch,
|
|
25
|
+
getTask: () => getTask,
|
|
26
|
+
listComments: () => listComments,
|
|
27
|
+
listMyTasks: () => listMyTasks,
|
|
28
|
+
listTasks: () => listTasks,
|
|
29
|
+
listTasksForReview: () => listTasksForReview,
|
|
30
|
+
markInReview: () => markInReview,
|
|
31
|
+
postComment: () => postComment,
|
|
32
|
+
postGuideComment: () => postGuideComment,
|
|
33
|
+
rejectTask: () => rejectTask
|
|
34
|
+
});
|
|
35
|
+
import { Octokit } from "@octokit/rest";
|
|
36
|
+
function createOctokit(token) {
|
|
37
|
+
return new Octokit({
|
|
38
|
+
auth: token,
|
|
39
|
+
log: { debug: () => {
|
|
40
|
+
}, info: () => {
|
|
41
|
+
}, warn: () => {
|
|
42
|
+
}, error: () => {
|
|
43
|
+
} }
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function parseIssue(issue) {
|
|
47
|
+
return {
|
|
48
|
+
number: issue.number,
|
|
49
|
+
title: issue.title,
|
|
50
|
+
body: issue.body ?? null,
|
|
51
|
+
state: issue.state,
|
|
52
|
+
assignee: issue.assignee?.login ?? null,
|
|
53
|
+
labels: (issue.labels ?? []).map(
|
|
54
|
+
(l) => typeof l === "string" ? l : l.name ?? ""
|
|
55
|
+
),
|
|
56
|
+
htmlUrl: issue.html_url
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function listTasks(config) {
|
|
60
|
+
const octokit = createOctokit(config.githubToken);
|
|
61
|
+
const { owner, repo } = config.github;
|
|
62
|
+
const [available, claimed, inReview, changesNeeded] = await Promise.all([
|
|
63
|
+
octokit.issues.listForRepo({
|
|
64
|
+
owner,
|
|
65
|
+
repo,
|
|
66
|
+
labels: LABEL_AVAILABLE,
|
|
67
|
+
state: "open",
|
|
68
|
+
per_page: 50
|
|
69
|
+
}),
|
|
70
|
+
octokit.issues.listForRepo({
|
|
71
|
+
owner,
|
|
72
|
+
repo,
|
|
73
|
+
labels: LABEL_CLAIMED,
|
|
74
|
+
state: "open",
|
|
75
|
+
per_page: 50
|
|
76
|
+
}),
|
|
77
|
+
octokit.issues.listForRepo({
|
|
78
|
+
owner,
|
|
79
|
+
repo,
|
|
80
|
+
labels: LABEL_IN_REVIEW,
|
|
81
|
+
state: "open",
|
|
82
|
+
per_page: 50
|
|
83
|
+
}),
|
|
84
|
+
octokit.issues.listForRepo({
|
|
85
|
+
owner,
|
|
86
|
+
repo,
|
|
87
|
+
labels: LABEL_CHANGES_NEEDED,
|
|
88
|
+
state: "open",
|
|
89
|
+
per_page: 50
|
|
90
|
+
})
|
|
91
|
+
]);
|
|
92
|
+
const allIssues = [...available.data, ...claimed.data, ...inReview.data, ...changesNeeded.data];
|
|
93
|
+
const seen = /* @__PURE__ */ new Set();
|
|
94
|
+
const unique = [];
|
|
95
|
+
for (const issue of allIssues) {
|
|
96
|
+
if (!seen.has(issue.number)) {
|
|
97
|
+
seen.add(issue.number);
|
|
98
|
+
unique.push(parseIssue(issue));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return unique.sort((a, b) => a.number - b.number);
|
|
102
|
+
}
|
|
103
|
+
async function getTask(config, number) {
|
|
104
|
+
const octokit = createOctokit(config.githubToken);
|
|
105
|
+
const { owner, repo } = config.github;
|
|
106
|
+
const { data } = await octokit.issues.get({ owner, repo, issue_number: number });
|
|
107
|
+
return parseIssue(data);
|
|
108
|
+
}
|
|
109
|
+
async function createTask(config, title, body) {
|
|
110
|
+
const octokit = createOctokit(config.githubToken);
|
|
111
|
+
const { owner, repo } = config.github;
|
|
112
|
+
await ensureLabels(config);
|
|
113
|
+
const { data } = await octokit.issues.create({
|
|
114
|
+
owner,
|
|
115
|
+
repo,
|
|
116
|
+
title,
|
|
117
|
+
body,
|
|
118
|
+
labels: [LABEL_AVAILABLE]
|
|
119
|
+
});
|
|
120
|
+
return parseIssue(data);
|
|
121
|
+
}
|
|
122
|
+
async function claimTask(config, number, username) {
|
|
123
|
+
const octokit = createOctokit(config.githubToken);
|
|
124
|
+
const { owner, repo } = config.github;
|
|
125
|
+
await octokit.issues.update({
|
|
126
|
+
owner,
|
|
127
|
+
repo,
|
|
128
|
+
issue_number: number,
|
|
129
|
+
assignees: [username]
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
await octokit.issues.removeLabel({
|
|
133
|
+
owner,
|
|
134
|
+
repo,
|
|
135
|
+
issue_number: number,
|
|
136
|
+
name: LABEL_AVAILABLE
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
await octokit.issues.addLabels({
|
|
141
|
+
owner,
|
|
142
|
+
repo,
|
|
143
|
+
issue_number: number,
|
|
144
|
+
labels: [LABEL_CLAIMED]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function formatGuideAsMarkdown(guide, issueNumber) {
|
|
148
|
+
const lines = [
|
|
149
|
+
`## Task Guide \u2014 #${issueNumber}`,
|
|
150
|
+
"",
|
|
151
|
+
`> ${guide.summary}`,
|
|
152
|
+
""
|
|
153
|
+
];
|
|
154
|
+
if (guide.acceptanceCriteria.length > 0) {
|
|
155
|
+
lines.push("### Must Deliver");
|
|
156
|
+
for (const item of guide.acceptanceCriteria) lines.push(`- [ ] ${item}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
}
|
|
159
|
+
if (guide.filesToModify.length > 0) {
|
|
160
|
+
lines.push("### Files");
|
|
161
|
+
for (const file of guide.filesToModify) lines.push(`- \`${file}\``);
|
|
162
|
+
lines.push("");
|
|
163
|
+
}
|
|
164
|
+
if (guide.suggestedSteps.length > 0) {
|
|
165
|
+
lines.push("<details><summary>Implementation steps</summary>");
|
|
166
|
+
lines.push("");
|
|
167
|
+
guide.suggestedSteps.forEach((step, i) => lines.push(`${i + 1}. ${step}`));
|
|
168
|
+
lines.push("</details>");
|
|
169
|
+
lines.push("");
|
|
170
|
+
}
|
|
171
|
+
if (guide.optionalImprovements.length > 0) {
|
|
172
|
+
lines.push("### Optional Improvements");
|
|
173
|
+
for (const item of guide.optionalImprovements) lines.push(`- ${item}`);
|
|
174
|
+
lines.push("");
|
|
175
|
+
}
|
|
176
|
+
lines.push("---");
|
|
177
|
+
lines.push("*Generated by Techunter*");
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
async function postComment(config, number, body) {
|
|
181
|
+
const octokit = createOctokit(config.githubToken);
|
|
182
|
+
const { owner, repo } = config.github;
|
|
183
|
+
await octokit.issues.createComment({ owner, repo, issue_number: number, body });
|
|
184
|
+
}
|
|
185
|
+
async function postGuideComment(config, number, guide) {
|
|
186
|
+
const octokit = createOctokit(config.githubToken);
|
|
187
|
+
const { owner, repo } = config.github;
|
|
188
|
+
const body = formatGuideAsMarkdown(guide, number);
|
|
189
|
+
await octokit.issues.createComment({
|
|
190
|
+
owner,
|
|
191
|
+
repo,
|
|
192
|
+
issue_number: number,
|
|
193
|
+
body
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function createPR(config, title, body, branch, base) {
|
|
197
|
+
const octokit = createOctokit(config.githubToken);
|
|
198
|
+
const { owner, repo } = config.github;
|
|
199
|
+
const { data } = await octokit.pulls.create({
|
|
200
|
+
owner,
|
|
201
|
+
repo,
|
|
202
|
+
title,
|
|
203
|
+
body,
|
|
204
|
+
head: branch,
|
|
205
|
+
base
|
|
206
|
+
});
|
|
207
|
+
return data.html_url;
|
|
208
|
+
}
|
|
209
|
+
async function markInReview(config, number) {
|
|
210
|
+
const octokit = createOctokit(config.githubToken);
|
|
211
|
+
const { owner, repo } = config.github;
|
|
212
|
+
try {
|
|
213
|
+
await octokit.issues.removeLabel({
|
|
214
|
+
owner,
|
|
215
|
+
repo,
|
|
216
|
+
issue_number: number,
|
|
217
|
+
name: LABEL_CLAIMED
|
|
218
|
+
});
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
await octokit.issues.addLabels({
|
|
222
|
+
owner,
|
|
223
|
+
repo,
|
|
224
|
+
issue_number: number,
|
|
225
|
+
labels: [LABEL_IN_REVIEW]
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async function closeTask(config, number) {
|
|
229
|
+
const octokit = createOctokit(config.githubToken);
|
|
230
|
+
const { owner, repo } = config.github;
|
|
231
|
+
const { data: issue } = await octokit.issues.get({ owner, repo, issue_number: number });
|
|
232
|
+
const techunterLabels = issue.labels.map((l) => l.name ?? "").filter((l) => [LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED].includes(l));
|
|
233
|
+
await octokit.issues.update({ owner, repo, issue_number: number, state: "closed" });
|
|
234
|
+
for (const label of techunterLabels) {
|
|
235
|
+
await octokit.issues.removeLabel({ owner, repo, issue_number: number, name: label });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function listComments(config, number, limit = 5) {
|
|
239
|
+
const octokit = createOctokit(config.githubToken);
|
|
240
|
+
const { owner, repo } = config.github;
|
|
241
|
+
const { data } = await octokit.issues.listComments({
|
|
242
|
+
owner,
|
|
243
|
+
repo,
|
|
244
|
+
issue_number: number,
|
|
245
|
+
per_page: 100
|
|
246
|
+
});
|
|
247
|
+
return data.slice(-limit).map((c) => ({
|
|
248
|
+
id: c.id,
|
|
249
|
+
author: c.user?.login ?? "unknown",
|
|
250
|
+
body: c.body ?? "",
|
|
251
|
+
createdAt: c.created_at
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
async function getAuthenticatedUser(config) {
|
|
255
|
+
const octokit = createOctokit(config.githubToken);
|
|
256
|
+
const { data } = await octokit.users.getAuthenticated();
|
|
257
|
+
return data.login;
|
|
258
|
+
}
|
|
259
|
+
async function listMyTasks(config, username) {
|
|
260
|
+
const octokit = createOctokit(config.githubToken);
|
|
261
|
+
const { owner, repo } = config.github;
|
|
262
|
+
const { data } = await octokit.issues.listForRepo({
|
|
263
|
+
owner,
|
|
264
|
+
repo,
|
|
265
|
+
assignee: username,
|
|
266
|
+
state: "open",
|
|
267
|
+
per_page: 50
|
|
268
|
+
});
|
|
269
|
+
return data.filter(
|
|
270
|
+
(issue) => issue.labels.some(
|
|
271
|
+
(l) => l.name === LABEL_CLAIMED || l.name === LABEL_IN_REVIEW || l.name === LABEL_CHANGES_NEEDED
|
|
272
|
+
)
|
|
273
|
+
).map(parseIssue);
|
|
274
|
+
}
|
|
275
|
+
async function listTasksForReview(config, username) {
|
|
276
|
+
const octokit = createOctokit(config.githubToken);
|
|
277
|
+
const { owner, repo } = config.github;
|
|
278
|
+
const { data } = await octokit.issues.listForRepo({
|
|
279
|
+
owner,
|
|
280
|
+
repo,
|
|
281
|
+
creator: username,
|
|
282
|
+
labels: LABEL_IN_REVIEW,
|
|
283
|
+
state: "open",
|
|
284
|
+
per_page: 50
|
|
285
|
+
});
|
|
286
|
+
return data.map(parseIssue).sort((a, b) => a.number - b.number);
|
|
287
|
+
}
|
|
288
|
+
async function rejectTask(config, number) {
|
|
289
|
+
const octokit = createOctokit(config.githubToken);
|
|
290
|
+
const { owner, repo } = config.github;
|
|
291
|
+
try {
|
|
292
|
+
await octokit.issues.removeLabel({
|
|
293
|
+
owner,
|
|
294
|
+
repo,
|
|
295
|
+
issue_number: number,
|
|
296
|
+
name: LABEL_IN_REVIEW
|
|
297
|
+
});
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
await octokit.issues.addLabels({
|
|
301
|
+
owner,
|
|
302
|
+
repo,
|
|
303
|
+
issue_number: number,
|
|
304
|
+
labels: [LABEL_CHANGES_NEEDED]
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async function ensureLabels(config) {
|
|
308
|
+
const octokit = createOctokit(config.githubToken);
|
|
309
|
+
const { owner, repo } = config.github;
|
|
310
|
+
const { data: existing } = await octokit.issues.listLabelsForRepo({ owner, repo, per_page: 100 });
|
|
311
|
+
const existingNames = new Set(existing.map((l) => l.name));
|
|
312
|
+
await Promise.all(
|
|
313
|
+
LABELS.filter((label) => !existingNames.has(label.name)).map(
|
|
314
|
+
(label) => octokit.issues.createLabel({ owner, repo, name: label.name, color: label.color, description: label.description }).catch(() => {
|
|
315
|
+
})
|
|
316
|
+
)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
async function getDefaultBranch(config) {
|
|
320
|
+
const octokit = createOctokit(config.githubToken);
|
|
321
|
+
const { owner, repo } = config.github;
|
|
322
|
+
const { data } = await octokit.repos.get({ owner, repo });
|
|
323
|
+
return data.default_branch;
|
|
324
|
+
}
|
|
325
|
+
function getBaseBranch(config) {
|
|
326
|
+
if (config.github.baseBranch) return Promise.resolve(config.github.baseBranch);
|
|
327
|
+
return getDefaultBranch(config);
|
|
328
|
+
}
|
|
329
|
+
async function acceptTask(config, issueNumber) {
|
|
330
|
+
const octokit = createOctokit(config.githubToken);
|
|
331
|
+
const { owner, repo } = config.github;
|
|
332
|
+
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
333
|
+
const pr = prs.find((p) => p.head.ref.startsWith(`task-${issueNumber}-`));
|
|
334
|
+
if (!pr) throw new Error(`No open PR found for task #${issueNumber} (expected branch starting with task-${issueNumber}-)`);
|
|
335
|
+
const { data: merge } = await octokit.pulls.merge({
|
|
336
|
+
owner,
|
|
337
|
+
repo,
|
|
338
|
+
pull_number: pr.number,
|
|
339
|
+
merge_method: "merge"
|
|
340
|
+
});
|
|
341
|
+
await closeTask(config, issueNumber);
|
|
342
|
+
return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "" };
|
|
343
|
+
}
|
|
344
|
+
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS;
|
|
345
|
+
var init_github = __esm({
|
|
346
|
+
"src/lib/github.ts"() {
|
|
347
|
+
"use strict";
|
|
348
|
+
LABEL_AVAILABLE = "techunter:available";
|
|
349
|
+
LABEL_CLAIMED = "techunter:claimed";
|
|
350
|
+
LABEL_IN_REVIEW = "techunter:in-review";
|
|
351
|
+
LABEL_CHANGES_NEEDED = "techunter:changes-needed";
|
|
352
|
+
LABELS = [
|
|
353
|
+
{ name: LABEL_AVAILABLE, color: "0e8a16", description: "Task available to claim" },
|
|
354
|
+
{ name: LABEL_CLAIMED, color: "e4a000", description: "Task claimed by a developer" },
|
|
355
|
+
{ name: LABEL_IN_REVIEW, color: "0075ca", description: "Task submitted for review" },
|
|
356
|
+
{ name: LABEL_CHANGES_NEEDED, color: "e11d48", description: "Task needs changes" }
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// src/mcp.ts
|
|
362
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
363
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
364
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
365
|
+
|
|
366
|
+
// src/tools/pick/index.ts
|
|
367
|
+
var pick_exports = {};
|
|
368
|
+
__export(pick_exports, {
|
|
369
|
+
definition: () => definition3,
|
|
370
|
+
execute: () => execute3,
|
|
371
|
+
run: () => run3,
|
|
372
|
+
terminal: () => terminal3
|
|
373
|
+
});
|
|
374
|
+
init_github();
|
|
375
|
+
import chalk6 from "chalk";
|
|
376
|
+
import ora3 from "ora";
|
|
377
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
378
|
+
|
|
379
|
+
// src/lib/git.ts
|
|
380
|
+
import chalk from "chalk";
|
|
381
|
+
import { simpleGit } from "simple-git";
|
|
382
|
+
var git = simpleGit();
|
|
383
|
+
async function getCurrentBranch() {
|
|
384
|
+
const summary = await git.branch();
|
|
385
|
+
return summary.current;
|
|
386
|
+
}
|
|
387
|
+
async function createAndSwitchBranch(name) {
|
|
388
|
+
await git.checkoutLocalBranch(name);
|
|
389
|
+
}
|
|
390
|
+
async function pushBranch(name) {
|
|
391
|
+
await git.push("origin", name, ["--set-upstream"]);
|
|
392
|
+
}
|
|
393
|
+
function makeBranchName(issueNumber, username) {
|
|
394
|
+
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
395
|
+
return `task-${issueNumber}-${slug}`;
|
|
396
|
+
}
|
|
397
|
+
async function findMergeBase(configuredBase) {
|
|
398
|
+
const candidates = configuredBase ? [`origin/${configuredBase}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
|
|
399
|
+
const unique = [...new Set(candidates)];
|
|
400
|
+
for (const base of unique) {
|
|
401
|
+
try {
|
|
402
|
+
const result = await git.raw(["merge-base", "HEAD", base]);
|
|
403
|
+
return result.trim();
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
async function getDiff(baseBranch) {
|
|
410
|
+
const status = await git.status();
|
|
411
|
+
const parts = [];
|
|
412
|
+
const fileLines = [
|
|
413
|
+
...status.modified.map((f) => ` M ${f}`),
|
|
414
|
+
...status.created.map((f) => ` A ${f}`),
|
|
415
|
+
...status.deleted.map((f) => ` D ${f}`),
|
|
416
|
+
...status.renamed.map((f) => ` R ${f.from} \u2192 ${f.to}`),
|
|
417
|
+
...status.not_added.map((f) => ` ? ${f}`)
|
|
418
|
+
];
|
|
419
|
+
if (fileLines.length > 0) {
|
|
420
|
+
parts.push("## Uncommitted changes\n" + fileLines.join("\n"));
|
|
421
|
+
const uncommitted = await git.diff(["HEAD"]);
|
|
422
|
+
if (uncommitted) {
|
|
423
|
+
const capped = uncommitted.length > 8e3 ? uncommitted.slice(0, 8e3) + "\n... (truncated)" : uncommitted;
|
|
424
|
+
parts.push("## Uncommitted diff\n```diff\n" + capped + "\n```");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const mergeBase = await findMergeBase(baseBranch);
|
|
428
|
+
if (mergeBase) {
|
|
429
|
+
const log = await git.log({ from: mergeBase, to: "HEAD" });
|
|
430
|
+
if (log.total > 0) {
|
|
431
|
+
const logLines = log.all.map((c) => ` ${c.hash.slice(0, 7)} ${c.message}`);
|
|
432
|
+
parts.push(`## Branch commits (${log.total} total)
|
|
433
|
+
` + logLines.join("\n"));
|
|
434
|
+
const branchDiff = await git.diff([mergeBase, "HEAD"]);
|
|
435
|
+
if (branchDiff) {
|
|
436
|
+
const capped = branchDiff.length > 12e3 ? branchDiff.slice(0, 12e3) + "\n... (truncated)" : branchDiff;
|
|
437
|
+
parts.push("## Full branch diff vs main\n```diff\n" + capped + "\n```");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return parts.length > 0 ? parts.join("\n\n") : "No changes detected.";
|
|
442
|
+
}
|
|
443
|
+
async function stageAllAndCommit(message) {
|
|
444
|
+
const status = await git.status();
|
|
445
|
+
if (!status.isClean()) {
|
|
446
|
+
await git.add(".");
|
|
447
|
+
await git.commit(message);
|
|
448
|
+
} else {
|
|
449
|
+
console.log(chalk.dim(" Working tree clean \u2014 no new commit created, pushing existing commits."));
|
|
450
|
+
}
|
|
451
|
+
const branch = (await git.branch()).current;
|
|
452
|
+
await git.push("origin", branch, ["--set-upstream"]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/lib/markdown.ts
|
|
456
|
+
import { marked } from "marked";
|
|
457
|
+
import { markedTerminal } from "marked-terminal";
|
|
458
|
+
marked.use(markedTerminal({ showSectionPrefix: false }));
|
|
459
|
+
function renderMarkdown(text) {
|
|
460
|
+
return marked(text);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/lib/display.ts
|
|
464
|
+
init_github();
|
|
465
|
+
import chalk2 from "chalk";
|
|
466
|
+
var LABEL_AVAILABLE2 = "techunter:available";
|
|
467
|
+
var LABEL_CLAIMED2 = "techunter:claimed";
|
|
468
|
+
var LABEL_IN_REVIEW2 = "techunter:in-review";
|
|
469
|
+
var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
|
|
470
|
+
function getStatus(issue) {
|
|
471
|
+
if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
|
|
472
|
+
if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
|
|
473
|
+
if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
|
|
474
|
+
if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
|
|
475
|
+
return "unknown";
|
|
476
|
+
}
|
|
477
|
+
function colorStatus(status) {
|
|
478
|
+
const padded = status.padEnd(14);
|
|
479
|
+
switch (status) {
|
|
480
|
+
case "available":
|
|
481
|
+
return chalk2.green(padded);
|
|
482
|
+
case "claimed":
|
|
483
|
+
return chalk2.yellow(padded);
|
|
484
|
+
case "in-review":
|
|
485
|
+
return chalk2.blue(padded);
|
|
486
|
+
case "changes-needed":
|
|
487
|
+
return chalk2.red(padded);
|
|
488
|
+
default:
|
|
489
|
+
return padded;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function printTaskDetail(issue) {
|
|
493
|
+
const divider = chalk2.dim("\u2500".repeat(70));
|
|
494
|
+
console.log("\n" + divider);
|
|
495
|
+
console.log(
|
|
496
|
+
chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014")
|
|
497
|
+
);
|
|
498
|
+
console.log(chalk2.bold("\n " + issue.title));
|
|
499
|
+
if (issue.body) {
|
|
500
|
+
console.log("");
|
|
501
|
+
console.log(renderMarkdown(issue.body));
|
|
502
|
+
}
|
|
503
|
+
console.log("\n " + chalk2.dim(issue.htmlUrl));
|
|
504
|
+
console.log(divider + "\n");
|
|
505
|
+
}
|
|
506
|
+
async function printTaskList(config) {
|
|
507
|
+
try {
|
|
508
|
+
const tasks = await listTasks(config);
|
|
509
|
+
const divider = chalk2.dim("\u2500".repeat(70));
|
|
510
|
+
console.log("");
|
|
511
|
+
console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
|
|
512
|
+
console.log(divider);
|
|
513
|
+
if (tasks.length === 0) {
|
|
514
|
+
console.log(chalk2.dim(" (no tasks)"));
|
|
515
|
+
} else {
|
|
516
|
+
for (const t of tasks) {
|
|
517
|
+
const num = `#${t.number}`.padEnd(5);
|
|
518
|
+
const status = colorStatus(getStatus(t));
|
|
519
|
+
const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
|
|
520
|
+
const title = t.title.length > 36 ? t.title.slice(0, 33) + "..." : t.title;
|
|
521
|
+
console.log(` ${num}${status}${assignee}${title}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
console.log(divider);
|
|
525
|
+
return tasks;
|
|
526
|
+
} catch (err) {
|
|
527
|
+
console.log(chalk2.yellow(`(Could not load tasks: ${err.message})`));
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/lib/launch.ts
|
|
533
|
+
import { spawn } from "child_process";
|
|
534
|
+
import chalk3 from "chalk";
|
|
535
|
+
function buildClaudePrompt(issue, branch) {
|
|
536
|
+
const lines = [
|
|
537
|
+
`You are working on task #${issue.number}: ${issue.title}`,
|
|
538
|
+
`Branch: ${branch}`,
|
|
539
|
+
""
|
|
540
|
+
];
|
|
541
|
+
if (issue.body) lines.push(issue.body.trim(), "");
|
|
542
|
+
lines.push(
|
|
543
|
+
"Implement the task. A detailed guide has been posted as a comment on the GitHub issue.",
|
|
544
|
+
"When done, return to tch and run /submit to review and deliver."
|
|
545
|
+
);
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
|
548
|
+
async function launchClaudeCode(issue, branch) {
|
|
549
|
+
const prompt = buildClaudePrompt(issue, branch);
|
|
550
|
+
console.log(chalk3.dim("\n Launching Claude Code\u2026\n"));
|
|
551
|
+
await new Promise((resolve) => {
|
|
552
|
+
const child = spawn("claude", [prompt], { stdio: "inherit", shell: true });
|
|
553
|
+
child.on("close", () => resolve());
|
|
554
|
+
child.on("error", () => {
|
|
555
|
+
console.log(
|
|
556
|
+
chalk3.yellow(
|
|
557
|
+
" Could not launch claude. Make sure Claude Code is installed:\n npm install -g @anthropic-ai/claude-code"
|
|
558
|
+
)
|
|
559
|
+
);
|
|
560
|
+
resolve();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/tools/submit/index.ts
|
|
566
|
+
var submit_exports = {};
|
|
567
|
+
__export(submit_exports, {
|
|
568
|
+
definition: () => definition,
|
|
569
|
+
execute: () => execute,
|
|
570
|
+
run: () => run,
|
|
571
|
+
terminal: () => terminal
|
|
572
|
+
});
|
|
573
|
+
init_github();
|
|
574
|
+
import chalk5 from "chalk";
|
|
575
|
+
import ora from "ora";
|
|
576
|
+
import { select, input as promptInput } from "@inquirer/prompts";
|
|
577
|
+
|
|
578
|
+
// src/lib/client.ts
|
|
579
|
+
import OpenAI from "openai";
|
|
580
|
+
var DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
|
|
581
|
+
var DEFAULT_MODEL = "z-ai/glm-5";
|
|
582
|
+
function createClient(config) {
|
|
583
|
+
return new OpenAI({
|
|
584
|
+
baseURL: config.aiBaseUrl ?? DEFAULT_BASE_URL,
|
|
585
|
+
apiKey: config.aiApiKey
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function getModel(config) {
|
|
589
|
+
return config.aiModel ?? DEFAULT_MODEL;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/lib/agent-ui.ts
|
|
593
|
+
import chalk4 from "chalk";
|
|
594
|
+
function formatInput(input) {
|
|
595
|
+
return Object.entries(input).map(([k, v]) => {
|
|
596
|
+
if (typeof v === "number") return `${k}=${v}`;
|
|
597
|
+
if (typeof v === "string") {
|
|
598
|
+
if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
|
|
599
|
+
return `${k}="${v}"`;
|
|
600
|
+
}
|
|
601
|
+
return `${k}=${JSON.stringify(v)}`;
|
|
602
|
+
}).join(" ");
|
|
603
|
+
}
|
|
604
|
+
function summarize(result) {
|
|
605
|
+
const first = result.split("\n").find((l) => l.trim()) ?? result;
|
|
606
|
+
return first.length > 100 ? first.slice(0, 97) + "..." : first;
|
|
607
|
+
}
|
|
608
|
+
function printToolCall(name, input) {
|
|
609
|
+
const params = formatInput(input);
|
|
610
|
+
console.log(` ${chalk4.cyan("\u2192")} ${chalk4.bold(name)}${params ? " " + chalk4.dim(params) : ""}`);
|
|
611
|
+
}
|
|
612
|
+
function printToolResult(result) {
|
|
613
|
+
const ok = !result.startsWith("Error:");
|
|
614
|
+
const icon = ok ? chalk4.green("\u2713") : chalk4.red("\u2717");
|
|
615
|
+
console.log(` ${icon} ${chalk4.dim(summarize(result))}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/lib/sub-agent.ts
|
|
619
|
+
async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
|
|
620
|
+
const client = createClient(config);
|
|
621
|
+
const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
|
|
622
|
+
const tools2 = selected.map((m) => m.definition);
|
|
623
|
+
const messages = [
|
|
624
|
+
{ role: "system", content: systemPrompt },
|
|
625
|
+
{ role: "user", content: userMessage }
|
|
626
|
+
];
|
|
627
|
+
const MAX_ITERATIONS = 20;
|
|
628
|
+
let iterations = 0;
|
|
629
|
+
for (; ; ) {
|
|
630
|
+
if (++iterations > MAX_ITERATIONS) {
|
|
631
|
+
throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
632
|
+
}
|
|
633
|
+
const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
|
|
634
|
+
const choice = res.choices[0];
|
|
635
|
+
messages.push({
|
|
636
|
+
role: "assistant",
|
|
637
|
+
content: choice.message.content ?? null,
|
|
638
|
+
...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
|
|
639
|
+
});
|
|
640
|
+
if (choice.finish_reason === "stop") {
|
|
641
|
+
return choice.message.content ?? "";
|
|
642
|
+
}
|
|
643
|
+
if (choice.finish_reason === "tool_calls") {
|
|
644
|
+
for (const tc of choice.message.tool_calls ?? []) {
|
|
645
|
+
let input;
|
|
646
|
+
try {
|
|
647
|
+
input = JSON.parse(tc.function.arguments);
|
|
648
|
+
} catch {
|
|
649
|
+
input = {};
|
|
650
|
+
}
|
|
651
|
+
printToolCall(tc.function.name, input);
|
|
652
|
+
const mod = selected.find((m) => m.definition.function.name === tc.function.name);
|
|
653
|
+
const result = mod ? await mod.execute(input, config) : `Unknown tool: ${tc.function.name}`;
|
|
654
|
+
printToolResult(result);
|
|
655
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/tools/submit/prompts.ts
|
|
662
|
+
var REVIEWER_SYSTEM_PROMPT = "You are a concise code reviewer. Use run_command to run tests/lint if needed, and read_file to inspect specific files. Then output your review: for each acceptance criterion mark \u2705 met or \u274C not met with a one-line reason. End with an overall verdict line: Ready to submit / Not ready. Reply in the same language as the task.";
|
|
663
|
+
|
|
664
|
+
// src/tools/submit/reviewer.ts
|
|
665
|
+
async function reviewChanges(config, issueNumber, issue, diff) {
|
|
666
|
+
return runSubAgentLoop(
|
|
667
|
+
config,
|
|
668
|
+
REVIEWER_SYSTEM_PROMPT,
|
|
669
|
+
`Task #${issueNumber}: ${issue.title}
|
|
670
|
+
|
|
671
|
+
Acceptance Criteria:
|
|
672
|
+
${issue.body ?? "(none)"}
|
|
673
|
+
|
|
674
|
+
Diff:
|
|
675
|
+
${diff || "(no changes)"}`,
|
|
676
|
+
["run_command", "read_file", "get_diff"]
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/tools/submit/index.ts
|
|
681
|
+
var definition = {
|
|
682
|
+
type: "function",
|
|
683
|
+
function: {
|
|
684
|
+
name: "submit",
|
|
685
|
+
description: "Submit the current task: reviews changes against acceptance criteria, then commits, creates a PR, and marks the issue as in-review. Equivalent to /submit.",
|
|
686
|
+
parameters: {
|
|
687
|
+
type: "object",
|
|
688
|
+
properties: {
|
|
689
|
+
commit_message: { type: "string", description: 'Commit message (optional \u2014 defaults to "complete: {task title}").' }
|
|
690
|
+
},
|
|
691
|
+
required: []
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
async function run(config) {
|
|
696
|
+
const branch = await getCurrentBranch();
|
|
697
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
698
|
+
if (!match) {
|
|
699
|
+
return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
|
|
700
|
+
}
|
|
701
|
+
const issueNumber = parseInt(match[1], 10);
|
|
702
|
+
let spinner = ora("Loading task and diff\u2026").start();
|
|
703
|
+
const [issue, defaultBranch, diff] = await Promise.all([
|
|
704
|
+
getTask(config, issueNumber),
|
|
705
|
+
getBaseBranch(config),
|
|
706
|
+
getDiff(config.github.baseBranch)
|
|
707
|
+
]);
|
|
708
|
+
spinner.stop();
|
|
709
|
+
const reviewSpinner = ora("Reviewing changes\u2026").start();
|
|
710
|
+
let review = "";
|
|
711
|
+
try {
|
|
712
|
+
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
review = `(Review failed: ${err.message})`;
|
|
715
|
+
}
|
|
716
|
+
reviewSpinner.stop();
|
|
717
|
+
const divider = chalk5.dim("\u2500".repeat(70));
|
|
718
|
+
console.log("\n" + divider);
|
|
719
|
+
console.log(chalk5.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
|
|
720
|
+
console.log(divider);
|
|
721
|
+
console.log(renderMarkdown(review));
|
|
722
|
+
console.log(divider + "\n");
|
|
723
|
+
let shouldProceed;
|
|
724
|
+
try {
|
|
725
|
+
shouldProceed = await select({
|
|
726
|
+
message: `Submit task #${issueNumber}?`,
|
|
727
|
+
choices: [
|
|
728
|
+
{ name: "Yes, submit", value: true },
|
|
729
|
+
{ name: "No, not ready yet", value: false }
|
|
730
|
+
]
|
|
731
|
+
});
|
|
732
|
+
} catch {
|
|
733
|
+
return "Submit cancelled.";
|
|
734
|
+
}
|
|
735
|
+
if (!shouldProceed) return "Submit cancelled by user.";
|
|
736
|
+
let commitMessage;
|
|
737
|
+
try {
|
|
738
|
+
commitMessage = await promptInput({
|
|
739
|
+
message: "Commit message:",
|
|
740
|
+
default: `complete: ${issue.title}`
|
|
741
|
+
});
|
|
742
|
+
} catch {
|
|
743
|
+
return "Submit cancelled.";
|
|
744
|
+
}
|
|
745
|
+
if (!commitMessage.trim()) return "Submit cancelled.";
|
|
746
|
+
spinner = ora("Committing and pushing\u2026").start();
|
|
747
|
+
try {
|
|
748
|
+
await stageAllAndCommit(commitMessage.trim());
|
|
749
|
+
spinner.stop();
|
|
750
|
+
} catch (err) {
|
|
751
|
+
spinner.stop();
|
|
752
|
+
return `Commit failed: ${err.message}`;
|
|
753
|
+
}
|
|
754
|
+
spinner = ora("Creating pull request\u2026").start();
|
|
755
|
+
let prUrl;
|
|
756
|
+
try {
|
|
757
|
+
prUrl = await createPR(
|
|
758
|
+
config,
|
|
759
|
+
issue.title,
|
|
760
|
+
`Closes #${issueNumber}
|
|
761
|
+
|
|
762
|
+
${issue.body ?? ""}`.trim(),
|
|
763
|
+
branch,
|
|
764
|
+
defaultBranch
|
|
765
|
+
);
|
|
766
|
+
spinner.stop();
|
|
767
|
+
} catch (err) {
|
|
768
|
+
spinner.stop();
|
|
769
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
770
|
+
}
|
|
771
|
+
spinner = ora("Marking as in-review\u2026").start();
|
|
772
|
+
try {
|
|
773
|
+
await markInReview(config, issueNumber);
|
|
774
|
+
spinner.stop();
|
|
775
|
+
} catch (err) {
|
|
776
|
+
spinner.stop();
|
|
777
|
+
return `PR created (${prUrl}) but failed to update label: ${err.message}`;
|
|
778
|
+
}
|
|
779
|
+
return `Task #${issueNumber} submitted.
|
|
780
|
+
Commit: "${commitMessage.trim()}"
|
|
781
|
+
PR: ${prUrl}`;
|
|
782
|
+
}
|
|
783
|
+
async function execute(input, config) {
|
|
784
|
+
const branch = await getCurrentBranch();
|
|
785
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
786
|
+
if (!match) return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
|
|
787
|
+
const issueNumber = parseInt(match[1], 10);
|
|
788
|
+
const [issue, defaultBranch, diff] = await Promise.all([
|
|
789
|
+
getTask(config, issueNumber),
|
|
790
|
+
getBaseBranch(config),
|
|
791
|
+
getDiff(config.github.baseBranch)
|
|
792
|
+
]);
|
|
793
|
+
let review = "";
|
|
794
|
+
try {
|
|
795
|
+
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
796
|
+
} catch (err) {
|
|
797
|
+
review = `(Review failed: ${err.message})`;
|
|
798
|
+
}
|
|
799
|
+
const commitMessage = input["commit_message"]?.trim() || `complete: ${issue.title}`;
|
|
800
|
+
try {
|
|
801
|
+
await stageAllAndCommit(commitMessage);
|
|
802
|
+
} catch (err) {
|
|
803
|
+
return `Commit failed: ${err.message}`;
|
|
804
|
+
}
|
|
805
|
+
let prUrl;
|
|
806
|
+
try {
|
|
807
|
+
prUrl = await createPR(
|
|
808
|
+
config,
|
|
809
|
+
issue.title,
|
|
810
|
+
`Closes #${issueNumber}
|
|
811
|
+
|
|
812
|
+
${issue.body ?? ""}`.trim(),
|
|
813
|
+
branch,
|
|
814
|
+
defaultBranch
|
|
815
|
+
);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
await markInReview(config, issueNumber);
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
return `Task #${issueNumber} submitted.
|
|
824
|
+
Review:
|
|
825
|
+
${review}
|
|
826
|
+
Commit: "${commitMessage}"
|
|
827
|
+
PR: ${prUrl}`;
|
|
828
|
+
}
|
|
829
|
+
var terminal = true;
|
|
830
|
+
|
|
831
|
+
// src/tools/close/index.ts
|
|
832
|
+
var close_exports = {};
|
|
833
|
+
__export(close_exports, {
|
|
834
|
+
definition: () => definition2,
|
|
835
|
+
execute: () => execute2,
|
|
836
|
+
run: () => run2,
|
|
837
|
+
terminal: () => terminal2
|
|
838
|
+
});
|
|
839
|
+
init_github();
|
|
840
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
841
|
+
import ora2 from "ora";
|
|
842
|
+
var definition2 = {
|
|
843
|
+
type: "function",
|
|
844
|
+
function: {
|
|
845
|
+
name: "close",
|
|
846
|
+
description: "Close a task (GitHub Issue). Equivalent to /close.",
|
|
847
|
+
parameters: {
|
|
848
|
+
type: "object",
|
|
849
|
+
properties: {
|
|
850
|
+
issue_number: { type: "number", description: "Issue number to close." }
|
|
851
|
+
},
|
|
852
|
+
required: ["issue_number"]
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
async function run2(config, opts = {}) {
|
|
857
|
+
let issueNumber = opts.issue_number;
|
|
858
|
+
if (!issueNumber) {
|
|
859
|
+
let tasks;
|
|
860
|
+
try {
|
|
861
|
+
tasks = await listTasks(config);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
return `Error loading tasks: ${err.message}`;
|
|
864
|
+
}
|
|
865
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
866
|
+
try {
|
|
867
|
+
issueNumber = await select2({
|
|
868
|
+
message: "Select task to close:",
|
|
869
|
+
choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
|
|
870
|
+
});
|
|
871
|
+
} catch {
|
|
872
|
+
return "Cancelled.";
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
let confirmed;
|
|
876
|
+
try {
|
|
877
|
+
confirmed = await select2({
|
|
878
|
+
message: `Close task #${issueNumber}?`,
|
|
879
|
+
choices: [
|
|
880
|
+
{ name: "Yes, close it", value: true },
|
|
881
|
+
{ name: "No, cancel", value: false }
|
|
882
|
+
]
|
|
883
|
+
});
|
|
884
|
+
} catch {
|
|
885
|
+
return "Cancelled.";
|
|
886
|
+
}
|
|
887
|
+
if (!confirmed) return "Cancelled.";
|
|
888
|
+
const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
|
|
889
|
+
try {
|
|
890
|
+
await closeTask(config, issueNumber);
|
|
891
|
+
spinner.stop();
|
|
892
|
+
return `Task #${issueNumber} closed.`;
|
|
893
|
+
} catch (err) {
|
|
894
|
+
spinner.stop();
|
|
895
|
+
return `Error: ${err.message}`;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function execute2(input, config) {
|
|
899
|
+
const issueNumber = input["issue_number"];
|
|
900
|
+
const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
|
|
901
|
+
try {
|
|
902
|
+
await closeTask(config, issueNumber);
|
|
903
|
+
spinner.stop();
|
|
904
|
+
return `Task #${issueNumber} closed.`;
|
|
905
|
+
} catch (err) {
|
|
906
|
+
spinner.stop();
|
|
907
|
+
return `Error: ${err.message}`;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
var terminal2 = true;
|
|
911
|
+
|
|
912
|
+
// src/tools/pick/index.ts
|
|
913
|
+
var definition3 = {
|
|
914
|
+
type: "function",
|
|
915
|
+
function: {
|
|
916
|
+
name: "pick",
|
|
917
|
+
description: "Browse the task list and act on a specific task (claim, submit, close, or view). Equivalent to /pick. Use when the user wants to explore or take action on a task.",
|
|
918
|
+
parameters: {
|
|
919
|
+
type: "object",
|
|
920
|
+
properties: {
|
|
921
|
+
issue_number: { type: "number", description: "Issue number to act on." },
|
|
922
|
+
action: {
|
|
923
|
+
type: "string",
|
|
924
|
+
enum: ["claim", "view"],
|
|
925
|
+
description: '"claim" to assign yourself and create a branch; "view" to return task details.'
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
required: ["issue_number", "action"]
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
async function run3(config, preselected) {
|
|
933
|
+
let chosenNumber;
|
|
934
|
+
if (preselected !== void 0) {
|
|
935
|
+
chosenNumber = preselected;
|
|
936
|
+
} else {
|
|
937
|
+
let tasks;
|
|
938
|
+
try {
|
|
939
|
+
tasks = await listTasks(config);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
return `Error: ${err.message}`;
|
|
942
|
+
}
|
|
943
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
944
|
+
try {
|
|
945
|
+
chosenNumber = await select3({
|
|
946
|
+
message: "Select a task:",
|
|
947
|
+
choices: tasks.map((t) => ({
|
|
948
|
+
name: `#${String(t.number).padEnd(4)} ${colorStatus(getStatus(t))} ${t.title}`,
|
|
949
|
+
value: t.number
|
|
950
|
+
}))
|
|
951
|
+
});
|
|
952
|
+
} catch {
|
|
953
|
+
return "Cancelled.";
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
let issue;
|
|
957
|
+
try {
|
|
958
|
+
issue = await getTask(config, chosenNumber);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
return `Error loading task: ${err.message}`;
|
|
961
|
+
}
|
|
962
|
+
printTaskDetail(issue);
|
|
963
|
+
const status = getStatus(issue);
|
|
964
|
+
if (status === "changes-needed") {
|
|
965
|
+
try {
|
|
966
|
+
const comments = await listComments(config, issue.number, 1);
|
|
967
|
+
if (comments.length > 0) {
|
|
968
|
+
const c = comments[0];
|
|
969
|
+
const divider = chalk6.dim("\u2500".repeat(70));
|
|
970
|
+
console.log(
|
|
971
|
+
chalk6.red.bold(" Latest rejection feedback") + chalk6.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
|
|
972
|
+
);
|
|
973
|
+
console.log(divider);
|
|
974
|
+
console.log(renderMarkdown(c.body));
|
|
975
|
+
console.log(divider + "\n");
|
|
976
|
+
}
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const actions = [];
|
|
981
|
+
if (status === "available") actions.push({ name: "Claim this task", value: "claim" });
|
|
982
|
+
if (status === "claimed" || status === "changes-needed") actions.push({ name: "Submit this task", value: "submit" });
|
|
983
|
+
actions.push({ name: "Close this task", value: "close" });
|
|
984
|
+
actions.push({ name: "Nothing, just viewing", value: "none" });
|
|
985
|
+
let action;
|
|
986
|
+
try {
|
|
987
|
+
action = await select3({ message: "Action:", choices: actions });
|
|
988
|
+
} catch {
|
|
989
|
+
return "Cancelled.";
|
|
990
|
+
}
|
|
991
|
+
if (action === "none") return `Viewed task #${issue.number}.`;
|
|
992
|
+
if (action === "claim") {
|
|
993
|
+
try {
|
|
994
|
+
const { getAuthenticatedUser: getAuthenticatedUser2, listMyTasks: listMyTasks2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
995
|
+
const me = await getAuthenticatedUser2(config);
|
|
996
|
+
const myTasks = await listMyTasks2(config, me);
|
|
997
|
+
const activeTask = myTasks.find((t) => {
|
|
998
|
+
const labels = t.labels;
|
|
999
|
+
return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
|
|
1000
|
+
});
|
|
1001
|
+
if (activeTask) {
|
|
1002
|
+
return `You already have an active task: #${activeTask.number} "${activeTask.title}"
|
|
1003
|
+
Finish or submit it before claiming a new one.`;
|
|
1004
|
+
}
|
|
1005
|
+
let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
|
|
1006
|
+
await claimTask(config, issue.number, me);
|
|
1007
|
+
spinner.stop();
|
|
1008
|
+
const branch = makeBranchName(issue.number, me);
|
|
1009
|
+
spinner = ora3(`Creating branch ${branch}\u2026`).start();
|
|
1010
|
+
try {
|
|
1011
|
+
await createAndSwitchBranch(branch);
|
|
1012
|
+
spinner.stop();
|
|
1013
|
+
} catch {
|
|
1014
|
+
spinner.warn(`Could not create branch ${branch}`);
|
|
1015
|
+
}
|
|
1016
|
+
spinner = ora3("Pushing branch\u2026").start();
|
|
1017
|
+
try {
|
|
1018
|
+
await pushBranch(branch);
|
|
1019
|
+
spinner.stop();
|
|
1020
|
+
} catch {
|
|
1021
|
+
spinner.warn("Could not push branch");
|
|
1022
|
+
}
|
|
1023
|
+
console.log(chalk6.green(`
|
|
1024
|
+
Claimed! Branch: ${branch}
|
|
1025
|
+
`));
|
|
1026
|
+
let openClaude;
|
|
1027
|
+
try {
|
|
1028
|
+
openClaude = await select3({
|
|
1029
|
+
message: "Open Claude Code for this task?",
|
|
1030
|
+
choices: [
|
|
1031
|
+
{ name: "Yes, start coding now", value: true },
|
|
1032
|
+
{ name: "No, return to tch", value: false }
|
|
1033
|
+
]
|
|
1034
|
+
});
|
|
1035
|
+
} catch {
|
|
1036
|
+
openClaude = false;
|
|
1037
|
+
}
|
|
1038
|
+
if (openClaude) await launchClaudeCode(issue, branch);
|
|
1039
|
+
return `Task #${issue.number} claimed. Branch: ${branch}`;
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
return `Error claiming task: ${err.message}`;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (action === "submit") return run(config);
|
|
1045
|
+
if (action === "close") return run2(config, { issue_number: issue.number });
|
|
1046
|
+
return "Cancelled.";
|
|
1047
|
+
}
|
|
1048
|
+
async function execute3(input, config) {
|
|
1049
|
+
const issueNumber = input["issue_number"];
|
|
1050
|
+
const action = input["action"];
|
|
1051
|
+
let issue;
|
|
1052
|
+
try {
|
|
1053
|
+
issue = await getTask(config, issueNumber);
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
return `Error loading task: ${err.message}`;
|
|
1056
|
+
}
|
|
1057
|
+
if (action === "view") {
|
|
1058
|
+
const status = getStatus(issue);
|
|
1059
|
+
const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
|
|
1060
|
+
return [`#${issue.number} [${status}] ${assignee} ${issue.title}`, issue.body ?? ""].join("\n\n");
|
|
1061
|
+
}
|
|
1062
|
+
if (action === "claim") {
|
|
1063
|
+
const { getAuthenticatedUser: getAuthenticatedUser2, listMyTasks: listMyTasks2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1064
|
+
const me = await getAuthenticatedUser2(config);
|
|
1065
|
+
const myTasks = await listMyTasks2(config, me);
|
|
1066
|
+
const activeTask = myTasks.find((t) => {
|
|
1067
|
+
return t.labels.includes("techunter:claimed") || t.labels.includes("techunter:changes-needed");
|
|
1068
|
+
});
|
|
1069
|
+
if (activeTask) {
|
|
1070
|
+
return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
await claimTask(config, issueNumber, me);
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
return `Error claiming task: ${err.message}`;
|
|
1076
|
+
}
|
|
1077
|
+
const branch = makeBranchName(issueNumber, me);
|
|
1078
|
+
try {
|
|
1079
|
+
await createAndSwitchBranch(branch);
|
|
1080
|
+
} catch {
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
await pushBranch(branch);
|
|
1084
|
+
} catch {
|
|
1085
|
+
}
|
|
1086
|
+
return `Task #${issueNumber} claimed. Branch: ${branch}`;
|
|
1087
|
+
}
|
|
1088
|
+
return `Unknown action: ${action}`;
|
|
1089
|
+
}
|
|
1090
|
+
var terminal3 = true;
|
|
1091
|
+
|
|
1092
|
+
// src/tools/new-task/index.ts
|
|
1093
|
+
var new_task_exports = {};
|
|
1094
|
+
__export(new_task_exports, {
|
|
1095
|
+
definition: () => definition4,
|
|
1096
|
+
execute: () => execute4,
|
|
1097
|
+
run: () => run4,
|
|
1098
|
+
terminal: () => terminal4
|
|
1099
|
+
});
|
|
1100
|
+
init_github();
|
|
1101
|
+
import { select as select4, input as promptInput2 } from "@inquirer/prompts";
|
|
1102
|
+
import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
|
|
1103
|
+
import { spawn as spawn2 } from "child_process";
|
|
1104
|
+
import { tmpdir } from "os";
|
|
1105
|
+
import path from "path";
|
|
1106
|
+
import ora4 from "ora";
|
|
1107
|
+
import chalk7 from "chalk";
|
|
1108
|
+
import open from "open";
|
|
1109
|
+
|
|
1110
|
+
// src/tools/new-task/prompts.ts
|
|
1111
|
+
var GUIDE_FORMAT = `
|
|
1112
|
+
## Guide format
|
|
1113
|
+
Write the guide in the same language as the task title. Use plain markdown, no code blocks. Be concise. Include exactly these sections:
|
|
1114
|
+
|
|
1115
|
+
### \u4EFB\u52A1\u63CF\u8FF0
|
|
1116
|
+
Describe what needs to be done and why. Cover the background, the problem being solved, and the expected outcome. Be as detailed as needed to make the task clear.
|
|
1117
|
+
|
|
1118
|
+
### \u6D89\u53CA\u6587\u4EF6
|
|
1119
|
+
List each file path with CREATE/MODIFY, and one sentence describing what changes.
|
|
1120
|
+
|
|
1121
|
+
### \u8F93\u5165 / \u8F93\u51FA
|
|
1122
|
+
What the feature/fix receives as input and what it produces or affects.
|
|
1123
|
+
|
|
1124
|
+
### \u9A8C\u6536\u6807\u51C6
|
|
1125
|
+
Checkbox list of testable conditions. Keep it short \u2014 3 to 6 items max.
|
|
1126
|
+
`.trim();
|
|
1127
|
+
|
|
1128
|
+
// src/tools/new-task/guide-generator.ts
|
|
1129
|
+
async function generateGuide(config, title, revise) {
|
|
1130
|
+
const userMessage = revise ? `Revise the following implementation guide for task: "${title}"
|
|
1131
|
+
|
|
1132
|
+
User feedback: ${revise.feedback}
|
|
1133
|
+
|
|
1134
|
+
Previous guide:
|
|
1135
|
+
${revise.previousGuide}` : `Write an implementation guide for this task: "${title}"`;
|
|
1136
|
+
return runSubAgentLoop(
|
|
1137
|
+
config,
|
|
1138
|
+
"You are a senior engineer writing a brief task guide for a developer. Use scan_project and read_file to identify which files are relevant. Do NOT include code snippets or implementation details. When you have enough context, write the guide.\n\n" + GUIDE_FORMAT,
|
|
1139
|
+
userMessage,
|
|
1140
|
+
["scan_project", "read_file", "run_command", "ask_user"]
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/tools/new-task/index.ts
|
|
1145
|
+
async function openInEditor(content) {
|
|
1146
|
+
const dir = await mkdtemp(path.join(tmpdir(), "tch-guide-"));
|
|
1147
|
+
const file = path.join(dir, "guide.md");
|
|
1148
|
+
try {
|
|
1149
|
+
await writeFile(file, content, "utf-8");
|
|
1150
|
+
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? (process.platform === "win32" ? "notepad" : "vi");
|
|
1151
|
+
await new Promise((resolve, reject) => {
|
|
1152
|
+
const child = spawn2(editor, [file], { stdio: "inherit", shell: true });
|
|
1153
|
+
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Editor exited with code ${code}`)));
|
|
1154
|
+
child.on("error", reject);
|
|
1155
|
+
});
|
|
1156
|
+
return await readFile(file, "utf-8");
|
|
1157
|
+
} finally {
|
|
1158
|
+
await rm(dir, { recursive: true, force: true });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
var definition4 = {
|
|
1162
|
+
type: "function",
|
|
1163
|
+
function: {
|
|
1164
|
+
name: "new_task",
|
|
1165
|
+
description: "Create a new task (GitHub Issue): scans the project, generates a full implementation guide, then creates the issue. Equivalent to /new.",
|
|
1166
|
+
parameters: {
|
|
1167
|
+
type: "object",
|
|
1168
|
+
properties: {
|
|
1169
|
+
title: { type: "string", description: "Task title." },
|
|
1170
|
+
feedback: { type: "string", description: "Optional feedback to revise the generated guide before creating the issue." }
|
|
1171
|
+
},
|
|
1172
|
+
required: ["title"]
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
async function run4(config, opts = {}) {
|
|
1177
|
+
let title = opts.title?.trim();
|
|
1178
|
+
if (!title) {
|
|
1179
|
+
try {
|
|
1180
|
+
title = (await promptInput2({ message: "Task title:" })).trim();
|
|
1181
|
+
} catch {
|
|
1182
|
+
return "Cancelled.";
|
|
1183
|
+
}
|
|
1184
|
+
if (!title) return "Cancelled.";
|
|
1185
|
+
}
|
|
1186
|
+
const spinner = ora4("Scanning project and generating guide\u2026").start();
|
|
1187
|
+
let guide;
|
|
1188
|
+
try {
|
|
1189
|
+
guide = await generateGuide(config, title);
|
|
1190
|
+
spinner.stop();
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
spinner.stop();
|
|
1193
|
+
return `Error generating guide: ${err.message}`;
|
|
1194
|
+
}
|
|
1195
|
+
const divider = chalk7.dim("\u2500".repeat(70));
|
|
1196
|
+
for (; ; ) {
|
|
1197
|
+
console.log("\n" + divider);
|
|
1198
|
+
console.log(chalk7.bold(" Generated guide preview"));
|
|
1199
|
+
console.log(divider);
|
|
1200
|
+
console.log(renderMarkdown(guide));
|
|
1201
|
+
console.log(divider + "\n");
|
|
1202
|
+
let action;
|
|
1203
|
+
try {
|
|
1204
|
+
action = await select4({
|
|
1205
|
+
message: "Create this task?",
|
|
1206
|
+
choices: [
|
|
1207
|
+
{ name: "Yes, create task", value: "create" },
|
|
1208
|
+
{ name: "Edit in editor", value: "edit" },
|
|
1209
|
+
{ name: "Let AI revise", value: "ai" },
|
|
1210
|
+
{ name: "Cancel", value: "cancel" }
|
|
1211
|
+
]
|
|
1212
|
+
});
|
|
1213
|
+
} catch {
|
|
1214
|
+
return "Cancelled.";
|
|
1215
|
+
}
|
|
1216
|
+
if (action === "cancel") return "Cancelled.";
|
|
1217
|
+
if (action === "create") break;
|
|
1218
|
+
if (action === "edit") {
|
|
1219
|
+
try {
|
|
1220
|
+
guide = await openInEditor(guide);
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
console.log(chalk7.yellow(` Editor error: ${err.message}`));
|
|
1223
|
+
}
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
let feedback;
|
|
1227
|
+
try {
|
|
1228
|
+
feedback = (await promptInput2({ message: "What should be changed?" })).trim();
|
|
1229
|
+
} catch {
|
|
1230
|
+
return "Cancelled.";
|
|
1231
|
+
}
|
|
1232
|
+
if (!feedback) continue;
|
|
1233
|
+
const reviseSpinner = ora4("Revising guide\u2026").start();
|
|
1234
|
+
try {
|
|
1235
|
+
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1236
|
+
reviseSpinner.stop();
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
reviseSpinner.stop();
|
|
1239
|
+
console.log(chalk7.yellow(` Revision error: ${err.message}`));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const createSpinner = ora4(`Creating "${title}"\u2026`).start();
|
|
1243
|
+
let htmlUrl;
|
|
1244
|
+
let issueNumber;
|
|
1245
|
+
let issueTitle;
|
|
1246
|
+
try {
|
|
1247
|
+
const issue = await createTask(config, title, guide);
|
|
1248
|
+
createSpinner.stop();
|
|
1249
|
+
htmlUrl = issue.htmlUrl;
|
|
1250
|
+
issueNumber = issue.number;
|
|
1251
|
+
issueTitle = issue.title;
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
createSpinner.stop();
|
|
1254
|
+
return `Error: ${err.message}`;
|
|
1255
|
+
}
|
|
1256
|
+
console.log(chalk7.green(`
|
|
1257
|
+
Created #${issueNumber} "${issueTitle}"
|
|
1258
|
+
${chalk7.dim(htmlUrl)}
|
|
1259
|
+
`));
|
|
1260
|
+
try {
|
|
1261
|
+
const openBrowser = await select4({
|
|
1262
|
+
message: "Open issue in browser?",
|
|
1263
|
+
choices: [
|
|
1264
|
+
{ name: "Yes", value: true },
|
|
1265
|
+
{ name: "No", value: false }
|
|
1266
|
+
]
|
|
1267
|
+
});
|
|
1268
|
+
if (openBrowser) await open(htmlUrl);
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1271
|
+
return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
|
|
1272
|
+
}
|
|
1273
|
+
async function execute4(input, config) {
|
|
1274
|
+
const title = input["title"].trim();
|
|
1275
|
+
const feedback = input["feedback"];
|
|
1276
|
+
let guide = await generateGuide(config, title);
|
|
1277
|
+
if (feedback) {
|
|
1278
|
+
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1279
|
+
}
|
|
1280
|
+
try {
|
|
1281
|
+
const issue = await createTask(config, title, guide);
|
|
1282
|
+
return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
|
|
1283
|
+
|
|
1284
|
+
Guide:
|
|
1285
|
+
${guide}`;
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
return `Error: ${err.message}`;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
var terminal4 = true;
|
|
1291
|
+
|
|
1292
|
+
// src/tools/my-status/index.ts
|
|
1293
|
+
var my_status_exports = {};
|
|
1294
|
+
__export(my_status_exports, {
|
|
1295
|
+
definition: () => definition5,
|
|
1296
|
+
execute: () => execute5,
|
|
1297
|
+
run: () => run5,
|
|
1298
|
+
terminal: () => terminal5
|
|
1299
|
+
});
|
|
1300
|
+
init_github();
|
|
1301
|
+
import ora5 from "ora";
|
|
1302
|
+
var definition5 = {
|
|
1303
|
+
type: "function",
|
|
1304
|
+
function: {
|
|
1305
|
+
name: "my_status",
|
|
1306
|
+
description: "Show all tasks currently assigned to the authenticated GitHub user. Equivalent to /status.",
|
|
1307
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
async function run5(config) {
|
|
1311
|
+
const spinner = ora5("Fetching your tasks\u2026").start();
|
|
1312
|
+
try {
|
|
1313
|
+
const me = await getAuthenticatedUser(config);
|
|
1314
|
+
const tasks = await listMyTasks(config, me);
|
|
1315
|
+
spinner.stop();
|
|
1316
|
+
if (tasks.length === 0) return `No tasks assigned to @${me}.`;
|
|
1317
|
+
const lines = tasks.map((t) => ` #${t.number} [${getStatus(t)}] ${t.title}`);
|
|
1318
|
+
return `Tasks assigned to @${me}:
|
|
1319
|
+
${lines.join("\n")}`;
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
spinner.stop();
|
|
1322
|
+
return `Error: ${err.message}`;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
var execute5 = (_input, config) => run5(config);
|
|
1326
|
+
var terminal5 = true;
|
|
1327
|
+
|
|
1328
|
+
// src/tools/review/index.ts
|
|
1329
|
+
var review_exports = {};
|
|
1330
|
+
__export(review_exports, {
|
|
1331
|
+
definition: () => definition6,
|
|
1332
|
+
execute: () => execute6,
|
|
1333
|
+
run: () => run6,
|
|
1334
|
+
terminal: () => terminal6
|
|
1335
|
+
});
|
|
1336
|
+
init_github();
|
|
1337
|
+
import ora6 from "ora";
|
|
1338
|
+
var definition6 = {
|
|
1339
|
+
type: "function",
|
|
1340
|
+
function: {
|
|
1341
|
+
name: "review",
|
|
1342
|
+
description: "List tasks waiting for your review (submitted by others, created by you). Equivalent to /review.",
|
|
1343
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
async function run6(config) {
|
|
1347
|
+
const spinner = ora6("Loading tasks for review\u2026").start();
|
|
1348
|
+
try {
|
|
1349
|
+
const me = await getAuthenticatedUser(config);
|
|
1350
|
+
const tasks = await listTasksForReview(config, me);
|
|
1351
|
+
spinner.stop();
|
|
1352
|
+
if (tasks.length === 0) return `No tasks pending review for @${me}.`;
|
|
1353
|
+
const lines = tasks.map((t) => ` #${t.number} [in-review] @${t.assignee ?? "\u2014"} ${t.title}`);
|
|
1354
|
+
return `Tasks pending review (created by @${me}):
|
|
1355
|
+
${lines.join("\n")}`;
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
spinner.stop();
|
|
1358
|
+
return `Error: ${err.message}`;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
var execute6 = (_input, config) => run6(config);
|
|
1362
|
+
var terminal6 = true;
|
|
1363
|
+
|
|
1364
|
+
// src/tools/refresh/index.ts
|
|
1365
|
+
var refresh_exports = {};
|
|
1366
|
+
__export(refresh_exports, {
|
|
1367
|
+
definition: () => definition7,
|
|
1368
|
+
execute: () => execute7,
|
|
1369
|
+
run: () => run7,
|
|
1370
|
+
terminal: () => terminal7
|
|
1371
|
+
});
|
|
1372
|
+
var definition7 = {
|
|
1373
|
+
type: "function",
|
|
1374
|
+
function: {
|
|
1375
|
+
name: "refresh",
|
|
1376
|
+
description: "Reload and display the full task list. Equivalent to /refresh.",
|
|
1377
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
async function run7(config) {
|
|
1381
|
+
const tasks = await printTaskList(config);
|
|
1382
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
1383
|
+
const lines = tasks.map((t) => {
|
|
1384
|
+
const status = getStatus(t);
|
|
1385
|
+
const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
|
|
1386
|
+
return `#${t.number} [${status}] ${assignee} ${t.title}`;
|
|
1387
|
+
});
|
|
1388
|
+
return `Tasks (${tasks.length}):
|
|
1389
|
+
${lines.join("\n")}`;
|
|
1390
|
+
}
|
|
1391
|
+
var execute7 = (_input, config) => run7(config);
|
|
1392
|
+
var terminal7 = true;
|
|
1393
|
+
|
|
1394
|
+
// src/tools/open-code/index.ts
|
|
1395
|
+
var open_code_exports = {};
|
|
1396
|
+
__export(open_code_exports, {
|
|
1397
|
+
definition: () => definition8,
|
|
1398
|
+
execute: () => execute8,
|
|
1399
|
+
run: () => run8,
|
|
1400
|
+
terminal: () => terminal8
|
|
1401
|
+
});
|
|
1402
|
+
init_github();
|
|
1403
|
+
var definition8 = {
|
|
1404
|
+
type: "function",
|
|
1405
|
+
function: {
|
|
1406
|
+
name: "open_code",
|
|
1407
|
+
description: "Launch Claude Code for the current task branch. Equivalent to /code.",
|
|
1408
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
async function run8(config) {
|
|
1412
|
+
let branch;
|
|
1413
|
+
try {
|
|
1414
|
+
branch = await getCurrentBranch();
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
return `Error: ${err.message}`;
|
|
1417
|
+
}
|
|
1418
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
1419
|
+
if (!match) return `Not on a task branch (current: ${branch}).`;
|
|
1420
|
+
const issueNum = parseInt(match[1], 10);
|
|
1421
|
+
let issue;
|
|
1422
|
+
try {
|
|
1423
|
+
issue = await getTask(config, issueNum);
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
return `Error: ${err.message}`;
|
|
1426
|
+
}
|
|
1427
|
+
await launchClaudeCode(issue, branch);
|
|
1428
|
+
return "Claude Code session ended.";
|
|
1429
|
+
}
|
|
1430
|
+
var execute8 = (_input, config) => run8(config);
|
|
1431
|
+
var terminal8 = true;
|
|
1432
|
+
|
|
1433
|
+
// src/tools/reject/index.ts
|
|
1434
|
+
var reject_exports = {};
|
|
1435
|
+
__export(reject_exports, {
|
|
1436
|
+
definition: () => definition9,
|
|
1437
|
+
execute: () => execute9,
|
|
1438
|
+
run: () => run9,
|
|
1439
|
+
terminal: () => terminal9
|
|
1440
|
+
});
|
|
1441
|
+
init_github();
|
|
1442
|
+
import chalk8 from "chalk";
|
|
1443
|
+
import { select as select5, input as promptInput3 } from "@inquirer/prompts";
|
|
1444
|
+
import ora7 from "ora";
|
|
1445
|
+
|
|
1446
|
+
// src/tools/reject/prompts.ts
|
|
1447
|
+
var REJECTION_FORMAT = `
|
|
1448
|
+
Write the rejection comment in the same language as the conversation. Use markdown. Include:
|
|
1449
|
+
|
|
1450
|
+
### \u274C \u6253\u56DE\u539F\u56E0 / Rejection Reason
|
|
1451
|
+
One paragraph: what was reviewed and what the main problem is.
|
|
1452
|
+
|
|
1453
|
+
### \u{1F527} \u9700\u8981\u4FEE\u6539\u7684\u5185\u5BB9 / Required Changes
|
|
1454
|
+
Numbered, specific, actionable items. Reference file names and function names.
|
|
1455
|
+
|
|
1456
|
+
### \u2705 \u672A\u901A\u8FC7\u7684\u9A8C\u6536\u6807\u51C6 / Failed Acceptance Criteria
|
|
1457
|
+
Re-list each criterion that was NOT met, prefixed with \u274C.
|
|
1458
|
+
|
|
1459
|
+
### \u{1F4CB} \u4E0B\u4E00\u6B65 / Next Steps
|
|
1460
|
+
Clear instruction on what to fix and how to re-submit (via /submit).
|
|
1461
|
+
`.trim();
|
|
1462
|
+
|
|
1463
|
+
// src/tools/reject/comment-generator.ts
|
|
1464
|
+
async function generateRejectionComment(config, issueNumber, userFeedback) {
|
|
1465
|
+
return runSubAgentLoop(
|
|
1466
|
+
config,
|
|
1467
|
+
"You are a senior engineer writing a structured code review rejection comment. Use get_task to read the acceptance criteria, get_diff or read_file to inspect the implementation, and get_comments to see prior discussion. Then write the rejection comment.\n\n" + REJECTION_FORMAT,
|
|
1468
|
+
`Write a rejection comment for issue #${issueNumber}.
|
|
1469
|
+
Reviewer feedback: ${userFeedback}`,
|
|
1470
|
+
["get_task", "get_comments", "get_diff", "read_file"]
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/tools/reject/index.ts
|
|
1475
|
+
var definition9 = {
|
|
1476
|
+
type: "function",
|
|
1477
|
+
function: {
|
|
1478
|
+
name: "reject",
|
|
1479
|
+
description: "Reject an in-review task: collects reviewer feedback, generates a structured rejection comment, shows a preview for confirmation, then posts the comment and marks the issue as changes-needed.",
|
|
1480
|
+
parameters: {
|
|
1481
|
+
type: "object",
|
|
1482
|
+
properties: {
|
|
1483
|
+
issue_number: { type: "number", description: "GitHub issue number to reject." },
|
|
1484
|
+
feedback: { type: "string", description: "Description of what is wrong with the submission." }
|
|
1485
|
+
},
|
|
1486
|
+
required: ["issue_number", "feedback"]
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
async function run9(config, opts) {
|
|
1491
|
+
const { issue_number: issueNumber } = opts;
|
|
1492
|
+
let feedback;
|
|
1493
|
+
try {
|
|
1494
|
+
feedback = await promptInput3({
|
|
1495
|
+
message: `What's wrong with #${issueNumber}? (brief description for the reviewer agent)`
|
|
1496
|
+
});
|
|
1497
|
+
} catch {
|
|
1498
|
+
return "Cancelled.";
|
|
1499
|
+
}
|
|
1500
|
+
if (!feedback.trim()) return "Cancelled.";
|
|
1501
|
+
const divider = chalk8.dim("\u2500".repeat(70));
|
|
1502
|
+
for (; ; ) {
|
|
1503
|
+
const spinner = ora7("Generating rejection comment\u2026").start();
|
|
1504
|
+
let comment;
|
|
1505
|
+
try {
|
|
1506
|
+
comment = await generateRejectionComment(config, issueNumber, feedback);
|
|
1507
|
+
spinner.stop();
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
spinner.stop();
|
|
1510
|
+
return `Error generating comment: ${err.message}`;
|
|
1511
|
+
}
|
|
1512
|
+
console.log("\n" + divider);
|
|
1513
|
+
console.log(chalk8.bold(` Rejection preview \u2014 issue #${issueNumber}`));
|
|
1514
|
+
console.log(divider);
|
|
1515
|
+
console.log(renderMarkdown(comment));
|
|
1516
|
+
console.log(divider + "\n");
|
|
1517
|
+
let decision;
|
|
1518
|
+
try {
|
|
1519
|
+
decision = await select5({
|
|
1520
|
+
message: `Post rejection and mark #${issueNumber} as changes-needed?`,
|
|
1521
|
+
choices: [
|
|
1522
|
+
{ name: "Post & Reject", value: "yes" },
|
|
1523
|
+
{ name: "Revise \u2014 describe what to change", value: "revise" },
|
|
1524
|
+
{ name: "Cancel", value: "cancel" }
|
|
1525
|
+
]
|
|
1526
|
+
});
|
|
1527
|
+
} catch {
|
|
1528
|
+
return "Cancelled.";
|
|
1529
|
+
}
|
|
1530
|
+
if (decision === "cancel") return "User cancelled rejection.";
|
|
1531
|
+
if (decision === "revise") {
|
|
1532
|
+
try {
|
|
1533
|
+
feedback = await promptInput3({ message: "What should be changed?" });
|
|
1534
|
+
} catch {
|
|
1535
|
+
return "Cancelled.";
|
|
1536
|
+
}
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
let spinner2 = ora7(`Posting rejection comment on #${issueNumber}\u2026`).start();
|
|
1540
|
+
try {
|
|
1541
|
+
await postComment(config, issueNumber, comment);
|
|
1542
|
+
spinner2.stop();
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
spinner2.stop();
|
|
1545
|
+
return `Error posting comment: ${err.message}`;
|
|
1546
|
+
}
|
|
1547
|
+
spinner2 = ora7(`Marking #${issueNumber} as changes-needed\u2026`).start();
|
|
1548
|
+
try {
|
|
1549
|
+
await rejectTask(config, issueNumber);
|
|
1550
|
+
spinner2.stop();
|
|
1551
|
+
} catch (err) {
|
|
1552
|
+
spinner2.stop();
|
|
1553
|
+
return `Comment posted but failed to update label: ${err.message}`;
|
|
1554
|
+
}
|
|
1555
|
+
return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
async function execute9(input, config) {
|
|
1559
|
+
const issueNumber = input["issue_number"];
|
|
1560
|
+
const feedback = input["feedback"];
|
|
1561
|
+
let comment;
|
|
1562
|
+
try {
|
|
1563
|
+
comment = await generateRejectionComment(config, issueNumber, feedback);
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
return `Error generating comment: ${err.message}`;
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
await postComment(config, issueNumber, comment);
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
return `Error posting comment: ${err.message}`;
|
|
1571
|
+
}
|
|
1572
|
+
try {
|
|
1573
|
+
await rejectTask(config, issueNumber);
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
return `Comment posted but failed to update label: ${err.message}`;
|
|
1576
|
+
}
|
|
1577
|
+
return `Task #${issueNumber} rejected.
|
|
1578
|
+
|
|
1579
|
+
Comment posted:
|
|
1580
|
+
${comment}`;
|
|
1581
|
+
}
|
|
1582
|
+
var terminal9 = true;
|
|
1583
|
+
|
|
1584
|
+
// src/tools/accept/index.ts
|
|
1585
|
+
var accept_exports = {};
|
|
1586
|
+
__export(accept_exports, {
|
|
1587
|
+
definition: () => definition10,
|
|
1588
|
+
execute: () => execute10,
|
|
1589
|
+
run: () => run10,
|
|
1590
|
+
terminal: () => terminal10
|
|
1591
|
+
});
|
|
1592
|
+
init_github();
|
|
1593
|
+
import chalk9 from "chalk";
|
|
1594
|
+
import { select as select6 } from "@inquirer/prompts";
|
|
1595
|
+
import ora8 from "ora";
|
|
1596
|
+
var definition10 = {
|
|
1597
|
+
type: "function",
|
|
1598
|
+
function: {
|
|
1599
|
+
name: "accept",
|
|
1600
|
+
description: "Accept an in-review task: merges the PR into the configured base branch and closes the issue.",
|
|
1601
|
+
parameters: {
|
|
1602
|
+
type: "object",
|
|
1603
|
+
properties: {
|
|
1604
|
+
issue_number: { type: "number", description: "GitHub issue number to accept" }
|
|
1605
|
+
},
|
|
1606
|
+
required: ["issue_number"]
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
async function run10(config, opts) {
|
|
1611
|
+
let issueNumber = opts?.issue_number;
|
|
1612
|
+
if (!issueNumber) {
|
|
1613
|
+
const spinner2 = ora8("Loading tasks for review\u2026").start();
|
|
1614
|
+
let tasks;
|
|
1615
|
+
let me;
|
|
1616
|
+
try {
|
|
1617
|
+
me = await getAuthenticatedUser(config);
|
|
1618
|
+
tasks = await listTasksForReview(config, me);
|
|
1619
|
+
spinner2.stop();
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
spinner2.stop();
|
|
1622
|
+
return `Error: ${err.message}`;
|
|
1623
|
+
}
|
|
1624
|
+
if (tasks.length === 0) return "No tasks pending review.";
|
|
1625
|
+
try {
|
|
1626
|
+
issueNumber = await select6({
|
|
1627
|
+
message: "Which task to accept?",
|
|
1628
|
+
choices: tasks.map((t) => ({
|
|
1629
|
+
name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
1630
|
+
value: t.number
|
|
1631
|
+
}))
|
|
1632
|
+
});
|
|
1633
|
+
} catch {
|
|
1634
|
+
return "Cancelled.";
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const baseBranch = config.github.baseBranch ?? "main";
|
|
1638
|
+
let confirmed;
|
|
1639
|
+
try {
|
|
1640
|
+
confirmed = await select6({
|
|
1641
|
+
message: `Merge PR for #${issueNumber} into ${chalk9.cyan(baseBranch)} and close issue?`,
|
|
1642
|
+
choices: [
|
|
1643
|
+
{ name: "Yes, accept", value: true },
|
|
1644
|
+
{ name: "Cancel", value: false }
|
|
1645
|
+
]
|
|
1646
|
+
});
|
|
1647
|
+
} catch {
|
|
1648
|
+
return "Cancelled.";
|
|
1649
|
+
}
|
|
1650
|
+
if (!confirmed) return "Cancelled.";
|
|
1651
|
+
const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
|
|
1652
|
+
try {
|
|
1653
|
+
const result = await acceptTask(config, issueNumber);
|
|
1654
|
+
spinner.succeed(`PR #${result.prNumber} merged into ${baseBranch}`);
|
|
1655
|
+
return `Task #${issueNumber} accepted.
|
|
1656
|
+
PR #${result.prNumber} merged \u2192 ${baseBranch}
|
|
1657
|
+
Issue closed.`;
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
spinner.fail("Failed");
|
|
1660
|
+
return `Error: ${err.message}`;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
async function execute10(input, config) {
|
|
1664
|
+
const issueNumber = input["issue_number"];
|
|
1665
|
+
const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
|
|
1666
|
+
try {
|
|
1667
|
+
const result = await acceptTask(config, issueNumber);
|
|
1668
|
+
spinner.stop();
|
|
1669
|
+
const baseBranch = config.github.baseBranch ?? "main";
|
|
1670
|
+
return `Task #${issueNumber} accepted.
|
|
1671
|
+
PR #${result.prNumber} merged \u2192 ${baseBranch}
|
|
1672
|
+
Issue closed.`;
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
spinner.stop();
|
|
1675
|
+
return `Error: ${err.message}`;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
var terminal10 = true;
|
|
1679
|
+
|
|
1680
|
+
// src/tools/get-task/index.ts
|
|
1681
|
+
var get_task_exports = {};
|
|
1682
|
+
__export(get_task_exports, {
|
|
1683
|
+
definition: () => definition11,
|
|
1684
|
+
execute: () => execute11
|
|
1685
|
+
});
|
|
1686
|
+
init_github();
|
|
1687
|
+
var definition11 = {
|
|
1688
|
+
type: "function",
|
|
1689
|
+
function: {
|
|
1690
|
+
name: "get_task",
|
|
1691
|
+
description: "Get full details of a specific GitHub issue: title, body, status, assignee.",
|
|
1692
|
+
parameters: {
|
|
1693
|
+
type: "object",
|
|
1694
|
+
properties: {
|
|
1695
|
+
issue_number: { type: "number", description: "GitHub issue number" }
|
|
1696
|
+
},
|
|
1697
|
+
required: ["issue_number"]
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
async function execute11(input, config) {
|
|
1702
|
+
const issue = await getTask(config, input["issue_number"]);
|
|
1703
|
+
const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
|
|
1704
|
+
const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
|
|
1705
|
+
const lines = [
|
|
1706
|
+
`#${issue.number} [${status}] ${assignee}`,
|
|
1707
|
+
`Title: ${issue.title}`,
|
|
1708
|
+
`URL: ${issue.htmlUrl}`
|
|
1709
|
+
];
|
|
1710
|
+
if (issue.body) lines.push(`
|
|
1711
|
+
${issue.body}`);
|
|
1712
|
+
return lines.join("\n");
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// src/tools/get-comments/index.ts
|
|
1716
|
+
var get_comments_exports = {};
|
|
1717
|
+
__export(get_comments_exports, {
|
|
1718
|
+
definition: () => definition12,
|
|
1719
|
+
execute: () => execute12
|
|
1720
|
+
});
|
|
1721
|
+
init_github();
|
|
1722
|
+
import ora9 from "ora";
|
|
1723
|
+
var definition12 = {
|
|
1724
|
+
type: "function",
|
|
1725
|
+
function: {
|
|
1726
|
+
name: "get_comments",
|
|
1727
|
+
description: "Get the latest comments on a GitHub issue. Useful for reading rejection feedback.",
|
|
1728
|
+
parameters: {
|
|
1729
|
+
type: "object",
|
|
1730
|
+
properties: {
|
|
1731
|
+
issue_number: { type: "number", description: "GitHub issue number" },
|
|
1732
|
+
limit: { type: "number", description: "Max number of latest comments to return (default 5)" }
|
|
1733
|
+
},
|
|
1734
|
+
required: ["issue_number"]
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
async function execute12(input, config) {
|
|
1739
|
+
const issueNumber = input["issue_number"];
|
|
1740
|
+
const limit = input["limit"] ?? 5;
|
|
1741
|
+
const spinner = ora9(`Loading comments for #${issueNumber}...`).start();
|
|
1742
|
+
try {
|
|
1743
|
+
const comments = await listComments(config, issueNumber, limit);
|
|
1744
|
+
spinner.stop();
|
|
1745
|
+
if (comments.length === 0) return `No comments on issue #${issueNumber}.`;
|
|
1746
|
+
const lines = comments.map((c) => `--- @${c.author} (${c.createdAt.slice(0, 10)}) ---
|
|
1747
|
+
${c.body}`);
|
|
1748
|
+
return `Latest ${comments.length} comment(s) on #${issueNumber}:
|
|
1749
|
+
|
|
1750
|
+
${lines.join("\n\n")}`;
|
|
1751
|
+
} catch (err) {
|
|
1752
|
+
spinner.stop();
|
|
1753
|
+
throw err;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// src/tools/get-diff/index.ts
|
|
1758
|
+
var get_diff_exports = {};
|
|
1759
|
+
__export(get_diff_exports, {
|
|
1760
|
+
definition: () => definition13,
|
|
1761
|
+
execute: () => execute13
|
|
1762
|
+
});
|
|
1763
|
+
import ora10 from "ora";
|
|
1764
|
+
var definition13 = {
|
|
1765
|
+
type: "function",
|
|
1766
|
+
function: {
|
|
1767
|
+
name: "get_diff",
|
|
1768
|
+
description: "Get the current git diff: changed files, diff vs HEAD, and any unpushed commits.",
|
|
1769
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
async function execute13(_input, _config) {
|
|
1773
|
+
const spinner = ora10("Reading git diff...").start();
|
|
1774
|
+
try {
|
|
1775
|
+
const diff = await getDiff(_config.github.baseBranch);
|
|
1776
|
+
spinner.stop();
|
|
1777
|
+
return diff;
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
spinner.stop();
|
|
1780
|
+
throw err;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// src/tools/run-command/index.ts
|
|
1785
|
+
var run_command_exports = {};
|
|
1786
|
+
__export(run_command_exports, {
|
|
1787
|
+
definition: () => definition14,
|
|
1788
|
+
execute: () => execute14
|
|
1789
|
+
});
|
|
1790
|
+
import { exec } from "child_process";
|
|
1791
|
+
import { promisify } from "util";
|
|
1792
|
+
import ora11 from "ora";
|
|
1793
|
+
var execAsync = promisify(exec);
|
|
1794
|
+
var definition14 = {
|
|
1795
|
+
type: "function",
|
|
1796
|
+
function: {
|
|
1797
|
+
name: "run_command",
|
|
1798
|
+
description: "Run a shell command in the project root directory. Use for building, testing, linting, or git status checks. stdout and stderr are both returned. Commands time out after 60 seconds.",
|
|
1799
|
+
parameters: {
|
|
1800
|
+
type: "object",
|
|
1801
|
+
properties: {
|
|
1802
|
+
command: { type: "string", description: "The shell command to run" }
|
|
1803
|
+
},
|
|
1804
|
+
required: ["command"]
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
async function execute14(input, _config) {
|
|
1809
|
+
const command = input["command"];
|
|
1810
|
+
const cwd = process.cwd();
|
|
1811
|
+
const spinner = ora11(`$ ${command}`).start();
|
|
1812
|
+
try {
|
|
1813
|
+
const { stdout, stderr } = await execAsync(command, { cwd, timeout: 6e4, maxBuffer: 1024 * 1024 });
|
|
1814
|
+
spinner.stop();
|
|
1815
|
+
const out = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
1816
|
+
const result = out.length > 5e3 ? out.slice(0, 5e3) + "\n... (truncated)" : out;
|
|
1817
|
+
return result || "(no output)";
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
spinner.stop();
|
|
1820
|
+
const e = err;
|
|
1821
|
+
const out = [e.stdout, e.stderr].filter(Boolean).join("\n").trim();
|
|
1822
|
+
return `Exit ${e.code ?? 1}:
|
|
1823
|
+
${out || e.message}`;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/tools/scan-project/index.ts
|
|
1828
|
+
var scan_project_exports = {};
|
|
1829
|
+
__export(scan_project_exports, {
|
|
1830
|
+
definition: () => definition15,
|
|
1831
|
+
execute: () => execute15
|
|
1832
|
+
});
|
|
1833
|
+
import ora12 from "ora";
|
|
1834
|
+
|
|
1835
|
+
// src/lib/project.ts
|
|
1836
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1837
|
+
import { existsSync } from "fs";
|
|
1838
|
+
import path2 from "path";
|
|
1839
|
+
import { globby } from "globby";
|
|
1840
|
+
import ignore from "ignore";
|
|
1841
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1842
|
+
".png",
|
|
1843
|
+
".jpg",
|
|
1844
|
+
".jpeg",
|
|
1845
|
+
".gif",
|
|
1846
|
+
".svg",
|
|
1847
|
+
".ico",
|
|
1848
|
+
".webp",
|
|
1849
|
+
".pdf",
|
|
1850
|
+
".zip",
|
|
1851
|
+
".tar",
|
|
1852
|
+
".gz",
|
|
1853
|
+
".bz2",
|
|
1854
|
+
".rar",
|
|
1855
|
+
".exe",
|
|
1856
|
+
".dll",
|
|
1857
|
+
".so",
|
|
1858
|
+
".dylib",
|
|
1859
|
+
".woff",
|
|
1860
|
+
".woff2",
|
|
1861
|
+
".ttf",
|
|
1862
|
+
".otf",
|
|
1863
|
+
".eot",
|
|
1864
|
+
".mp3",
|
|
1865
|
+
".mp4",
|
|
1866
|
+
".wav",
|
|
1867
|
+
".avi",
|
|
1868
|
+
".mov",
|
|
1869
|
+
".db",
|
|
1870
|
+
".sqlite",
|
|
1871
|
+
".lock"
|
|
1872
|
+
]);
|
|
1873
|
+
var ALWAYS_READ = [
|
|
1874
|
+
"README.md",
|
|
1875
|
+
"README.txt",
|
|
1876
|
+
"README",
|
|
1877
|
+
"package.json",
|
|
1878
|
+
"pyproject.toml",
|
|
1879
|
+
"go.mod",
|
|
1880
|
+
"Cargo.toml",
|
|
1881
|
+
"tsconfig.json",
|
|
1882
|
+
"vite.config.ts",
|
|
1883
|
+
"vite.config.js",
|
|
1884
|
+
"webpack.config.js",
|
|
1885
|
+
"rollup.config.js",
|
|
1886
|
+
".env.example",
|
|
1887
|
+
"docker-compose.yml",
|
|
1888
|
+
"Dockerfile"
|
|
1889
|
+
];
|
|
1890
|
+
var MAX_TOTAL_BYTES = 8e4;
|
|
1891
|
+
var MAX_FILE_BYTES = 15e3;
|
|
1892
|
+
async function buildIgnoreFilter(cwd) {
|
|
1893
|
+
const ig = ignore();
|
|
1894
|
+
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
1895
|
+
if (existsSync(gitignorePath)) {
|
|
1896
|
+
const content = await readFile2(gitignorePath, "utf-8");
|
|
1897
|
+
ig.add(content);
|
|
1898
|
+
}
|
|
1899
|
+
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "*.pyc", "build", "coverage"]);
|
|
1900
|
+
return ig;
|
|
1901
|
+
}
|
|
1902
|
+
async function safeReadFile(filePath, maxBytes = MAX_FILE_BYTES) {
|
|
1903
|
+
try {
|
|
1904
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1905
|
+
if (content.length > maxBytes) {
|
|
1906
|
+
return content.slice(0, maxBytes) + `
|
|
1907
|
+
... (truncated at ${maxBytes} chars)`;
|
|
1908
|
+
}
|
|
1909
|
+
return content;
|
|
1910
|
+
} catch {
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function isBinaryFile(filePath) {
|
|
1915
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
1916
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
1917
|
+
}
|
|
1918
|
+
function buildFileTree(files) {
|
|
1919
|
+
const tree = {};
|
|
1920
|
+
for (const file of files) {
|
|
1921
|
+
const dir = path2.dirname(file);
|
|
1922
|
+
if (!tree[dir]) tree[dir] = [];
|
|
1923
|
+
tree[dir].push(path2.basename(file));
|
|
1924
|
+
}
|
|
1925
|
+
const lines = [];
|
|
1926
|
+
const rootFiles = tree["."] ?? [];
|
|
1927
|
+
for (const f of rootFiles) lines.push(f);
|
|
1928
|
+
const dirs = Object.keys(tree).filter((d) => d !== ".").sort();
|
|
1929
|
+
for (const dir of dirs) {
|
|
1930
|
+
lines.push(`${dir}/`);
|
|
1931
|
+
for (const f of tree[dir]) {
|
|
1932
|
+
lines.push(` ${f}`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return lines.join("\n");
|
|
1936
|
+
}
|
|
1937
|
+
function scoreRelevance(filePath, keywords) {
|
|
1938
|
+
const lower = filePath.toLowerCase();
|
|
1939
|
+
let score = 0;
|
|
1940
|
+
for (const kw of keywords) {
|
|
1941
|
+
if (lower.includes(kw)) score += 1;
|
|
1942
|
+
}
|
|
1943
|
+
return score;
|
|
1944
|
+
}
|
|
1945
|
+
async function buildProjectContext(cwd, issueTitle, issueBody) {
|
|
1946
|
+
const ig = await buildIgnoreFilter(cwd);
|
|
1947
|
+
const allFiles = await globby("**/*", {
|
|
1948
|
+
cwd,
|
|
1949
|
+
gitignore: false,
|
|
1950
|
+
// We handle ignore ourselves
|
|
1951
|
+
dot: false,
|
|
1952
|
+
onlyFiles: true
|
|
1953
|
+
});
|
|
1954
|
+
const filtered = allFiles.filter((f) => !ig.ignores(f) && !isBinaryFile(f));
|
|
1955
|
+
const fileTree = buildFileTree(filtered);
|
|
1956
|
+
const issueText = `${issueTitle} ${issueBody}`.toLowerCase();
|
|
1957
|
+
const keywords = issueText.replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
1958
|
+
const keyFiles = {};
|
|
1959
|
+
let totalBytes = 0;
|
|
1960
|
+
for (const always of ALWAYS_READ) {
|
|
1961
|
+
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
1962
|
+
const fullPath = path2.join(cwd, always);
|
|
1963
|
+
if (!existsSync(fullPath)) continue;
|
|
1964
|
+
const content = await safeReadFile(fullPath);
|
|
1965
|
+
if (content !== null) {
|
|
1966
|
+
keyFiles[always] = content;
|
|
1967
|
+
totalBytes += content.length;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const scored = filtered.filter((f) => !ALWAYS_READ.includes(f) && !ALWAYS_READ.includes(path2.basename(f))).map((f) => ({ file: f, score: scoreRelevance(f, keywords) })).filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, 10);
|
|
1971
|
+
for (const { file } of scored) {
|
|
1972
|
+
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
1973
|
+
const fullPath = path2.join(cwd, file);
|
|
1974
|
+
const content = await safeReadFile(fullPath);
|
|
1975
|
+
if (content !== null) {
|
|
1976
|
+
keyFiles[file] = content;
|
|
1977
|
+
totalBytes += content.length;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return { fileTree, keyFiles };
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/tools/scan-project/index.ts
|
|
1984
|
+
var definition15 = {
|
|
1985
|
+
type: "function",
|
|
1986
|
+
function: {
|
|
1987
|
+
name: "scan_project",
|
|
1988
|
+
description: "Scan the current project directory: returns the file tree and contents of the most relevant files. Call this when creating a new task to understand the codebase before writing the task body and guide.",
|
|
1989
|
+
parameters: {
|
|
1990
|
+
type: "object",
|
|
1991
|
+
properties: {
|
|
1992
|
+
focus: {
|
|
1993
|
+
type: "string",
|
|
1994
|
+
description: "Keywords describing the task. Used to prioritise which files to read."
|
|
1995
|
+
}
|
|
1996
|
+
},
|
|
1997
|
+
required: []
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
async function execute15(input, _config) {
|
|
2002
|
+
const focus = input["focus"] ?? "";
|
|
2003
|
+
const spinner = ora12("Scanning project...").start();
|
|
2004
|
+
try {
|
|
2005
|
+
const cwd = process.cwd();
|
|
2006
|
+
const context = await buildProjectContext(cwd, focus, "");
|
|
2007
|
+
spinner.stop();
|
|
2008
|
+
const fileCount = context.fileTree.split("\n").filter(Boolean).length;
|
|
2009
|
+
const readCount = Object.keys(context.keyFiles).length;
|
|
2010
|
+
const totalBytes = Object.values(context.keyFiles).reduce((s, c) => s + c.length, 0);
|
|
2011
|
+
const summary = `Scanned ${fileCount} files \xB7 ${readCount} read \xB7 ${(totalBytes / 1024).toFixed(1)} KB`;
|
|
2012
|
+
const parts = [summary, `## File tree
|
|
2013
|
+
\`\`\`
|
|
2014
|
+
${context.fileTree}
|
|
2015
|
+
\`\`\``];
|
|
2016
|
+
for (const [filePath, content] of Object.entries(context.keyFiles)) {
|
|
2017
|
+
parts.push(`## ${filePath}
|
|
2018
|
+
\`\`\`
|
|
2019
|
+
${content}
|
|
2020
|
+
\`\`\``);
|
|
2021
|
+
}
|
|
2022
|
+
return parts.join("\n\n");
|
|
2023
|
+
} catch (err) {
|
|
2024
|
+
spinner.stop();
|
|
2025
|
+
throw err;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/tools/read-file/index.ts
|
|
2030
|
+
var read_file_exports = {};
|
|
2031
|
+
__export(read_file_exports, {
|
|
2032
|
+
definition: () => definition16,
|
|
2033
|
+
execute: () => execute16
|
|
2034
|
+
});
|
|
2035
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2036
|
+
import path3 from "path";
|
|
2037
|
+
var definition16 = {
|
|
2038
|
+
type: "function",
|
|
2039
|
+
function: {
|
|
2040
|
+
name: "read_file",
|
|
2041
|
+
description: "Read the full contents of a specific file in the project.",
|
|
2042
|
+
parameters: {
|
|
2043
|
+
type: "object",
|
|
2044
|
+
properties: {
|
|
2045
|
+
path: { type: "string", description: "File path relative to the project root" }
|
|
2046
|
+
},
|
|
2047
|
+
required: ["path"]
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
async function execute16(input, _config) {
|
|
2052
|
+
const filePath = input["path"];
|
|
2053
|
+
try {
|
|
2054
|
+
const fullPath = path3.join(process.cwd(), filePath);
|
|
2055
|
+
const content = await readFile3(fullPath, "utf-8");
|
|
2056
|
+
return content.length > 15e3 ? content.slice(0, 15e3) + "\n\n... (truncated)" : content;
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
return `Error reading file: ${err.message}`;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/tools/ask-user/index.ts
|
|
2063
|
+
var ask_user_exports = {};
|
|
2064
|
+
__export(ask_user_exports, {
|
|
2065
|
+
definition: () => definition17,
|
|
2066
|
+
execute: () => execute17
|
|
2067
|
+
});
|
|
2068
|
+
import chalk10 from "chalk";
|
|
2069
|
+
import { select as select7, input as promptInput4 } from "@inquirer/prompts";
|
|
2070
|
+
var definition17 = {
|
|
2071
|
+
type: "function",
|
|
2072
|
+
function: {
|
|
2073
|
+
name: "ask_user",
|
|
2074
|
+
description: "Ask the user to clarify something ambiguous \u2014 scope, expected behaviour, edge cases, or business decisions. Do NOT ask about technical implementation choices. Use at most 3 times per task.",
|
|
2075
|
+
parameters: {
|
|
2076
|
+
type: "object",
|
|
2077
|
+
properties: {
|
|
2078
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
2079
|
+
options: {
|
|
2080
|
+
type: "array",
|
|
2081
|
+
items: { type: "string" },
|
|
2082
|
+
description: "2\u20134 concrete answer choices"
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2085
|
+
required: ["question", "options"]
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
async function execute17(input, _config) {
|
|
2090
|
+
const question = input["question"];
|
|
2091
|
+
const options = input["options"];
|
|
2092
|
+
const OTHER = "__other__";
|
|
2093
|
+
console.log("");
|
|
2094
|
+
console.log(chalk10.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
|
|
2095
|
+
console.log(chalk10.dim(" \u2502"));
|
|
2096
|
+
for (const line of question.split("\n")) {
|
|
2097
|
+
console.log(chalk10.dim(" \u2502 ") + line);
|
|
2098
|
+
}
|
|
2099
|
+
console.log(chalk10.dim(" \u2514" + "\u2500".repeat(67)));
|
|
2100
|
+
let answer;
|
|
2101
|
+
try {
|
|
2102
|
+
const chosen = await select7({
|
|
2103
|
+
message: " ",
|
|
2104
|
+
choices: [
|
|
2105
|
+
...options.map((o) => ({ name: o, value: o })),
|
|
2106
|
+
{ name: chalk10.dim("Other (describe below)"), value: OTHER }
|
|
2107
|
+
]
|
|
2108
|
+
});
|
|
2109
|
+
answer = chosen === OTHER ? await promptInput4({ message: "Your answer:" }) : chosen;
|
|
2110
|
+
} catch {
|
|
2111
|
+
answer = "User skipped this question \u2014 use your best judgement.";
|
|
2112
|
+
}
|
|
2113
|
+
console.log("");
|
|
2114
|
+
return answer;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/tools/registry.ts
|
|
2118
|
+
var toolModules = [
|
|
2119
|
+
// Command tools
|
|
2120
|
+
pick_exports,
|
|
2121
|
+
new_task_exports,
|
|
2122
|
+
close_exports,
|
|
2123
|
+
submit_exports,
|
|
2124
|
+
my_status_exports,
|
|
2125
|
+
review_exports,
|
|
2126
|
+
refresh_exports,
|
|
2127
|
+
open_code_exports,
|
|
2128
|
+
reject_exports,
|
|
2129
|
+
accept_exports,
|
|
2130
|
+
// Low-level tools
|
|
2131
|
+
get_task_exports,
|
|
2132
|
+
get_comments_exports,
|
|
2133
|
+
get_diff_exports,
|
|
2134
|
+
run_command_exports,
|
|
2135
|
+
scan_project_exports,
|
|
2136
|
+
read_file_exports,
|
|
2137
|
+
ask_user_exports
|
|
2138
|
+
];
|
|
2139
|
+
|
|
2140
|
+
// src/lib/config.ts
|
|
2141
|
+
import Conf from "conf";
|
|
2142
|
+
import { z } from "zod";
|
|
2143
|
+
var configSchema = z.object({
|
|
2144
|
+
aiApiKey: z.string().min(1),
|
|
2145
|
+
aiBaseUrl: z.string().optional(),
|
|
2146
|
+
aiModel: z.string().optional(),
|
|
2147
|
+
githubToken: z.string().min(1),
|
|
2148
|
+
githubClientId: z.string().optional(),
|
|
2149
|
+
github: z.object({
|
|
2150
|
+
owner: z.string().min(1),
|
|
2151
|
+
repo: z.string().min(1),
|
|
2152
|
+
baseBranch: z.string().optional()
|
|
2153
|
+
})
|
|
2154
|
+
});
|
|
2155
|
+
var store = new Conf({
|
|
2156
|
+
projectName: "techunter",
|
|
2157
|
+
defaults: {}
|
|
2158
|
+
});
|
|
2159
|
+
function getConfig() {
|
|
2160
|
+
const raw = store.store;
|
|
2161
|
+
const result = configSchema.safeParse(raw);
|
|
2162
|
+
if (!result.success) {
|
|
2163
|
+
throw new Error("Configuration is missing or invalid.");
|
|
2164
|
+
}
|
|
2165
|
+
return result.data;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// src/mcp.ts
|
|
2169
|
+
var tools = toolModules.filter((m) => m.definition.function.name !== "ask_user");
|
|
2170
|
+
var server = new Server(
|
|
2171
|
+
{ name: "techunter", version: "0.1.0" },
|
|
2172
|
+
{ capabilities: { tools: {} } }
|
|
2173
|
+
);
|
|
2174
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2175
|
+
tools: tools.map((m) => ({
|
|
2176
|
+
name: m.definition.function.name,
|
|
2177
|
+
description: m.definition.function.description,
|
|
2178
|
+
inputSchema: m.definition.function.parameters
|
|
2179
|
+
}))
|
|
2180
|
+
}));
|
|
2181
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2182
|
+
const mod = tools.find((m) => m.definition.function.name === request.params.name);
|
|
2183
|
+
if (!mod) {
|
|
2184
|
+
return { content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }], isError: true };
|
|
2185
|
+
}
|
|
2186
|
+
try {
|
|
2187
|
+
const config = getConfig();
|
|
2188
|
+
const result = await mod.execute(request.params.arguments ?? {}, config);
|
|
2189
|
+
return { content: [{ type: "text", text: result }] };
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
var transport = new StdioServerTransport();
|
|
2195
|
+
await server.connect(transport);
|