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/index.js
ADDED
|
@@ -0,0 +1,2865 @@
|
|
|
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/index.ts
|
|
362
|
+
import chalk14 from "chalk";
|
|
363
|
+
import readline from "readline";
|
|
364
|
+
import { createRequire } from "module";
|
|
365
|
+
|
|
366
|
+
// src/commands/init.ts
|
|
367
|
+
import { input, password, select } from "@inquirer/prompts";
|
|
368
|
+
import chalk2 from "chalk";
|
|
369
|
+
import ora from "ora";
|
|
370
|
+
import open from "open";
|
|
371
|
+
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
|
|
372
|
+
|
|
373
|
+
// src/lib/config.ts
|
|
374
|
+
import Conf from "conf";
|
|
375
|
+
import { z } from "zod";
|
|
376
|
+
var configSchema = z.object({
|
|
377
|
+
aiApiKey: z.string().min(1),
|
|
378
|
+
aiBaseUrl: z.string().optional(),
|
|
379
|
+
aiModel: z.string().optional(),
|
|
380
|
+
githubToken: z.string().min(1),
|
|
381
|
+
githubClientId: z.string().optional(),
|
|
382
|
+
github: z.object({
|
|
383
|
+
owner: z.string().min(1),
|
|
384
|
+
repo: z.string().min(1),
|
|
385
|
+
baseBranch: z.string().optional()
|
|
386
|
+
})
|
|
387
|
+
});
|
|
388
|
+
var store = new Conf({
|
|
389
|
+
projectName: "techunter",
|
|
390
|
+
defaults: {}
|
|
391
|
+
});
|
|
392
|
+
function getConfig() {
|
|
393
|
+
const raw = store.store;
|
|
394
|
+
const result = configSchema.safeParse(raw);
|
|
395
|
+
if (!result.success) {
|
|
396
|
+
throw new Error("Configuration is missing or invalid.");
|
|
397
|
+
}
|
|
398
|
+
return result.data;
|
|
399
|
+
}
|
|
400
|
+
function setConfig(partial) {
|
|
401
|
+
const current = store.store;
|
|
402
|
+
if (partial.github) {
|
|
403
|
+
current["github"] = {
|
|
404
|
+
...current["github"] ?? {},
|
|
405
|
+
...partial.github
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (partial.aiApiKey !== void 0) {
|
|
409
|
+
current["aiApiKey"] = partial.aiApiKey;
|
|
410
|
+
}
|
|
411
|
+
if (partial.aiBaseUrl !== void 0) {
|
|
412
|
+
current["aiBaseUrl"] = partial.aiBaseUrl;
|
|
413
|
+
}
|
|
414
|
+
if (partial.aiModel !== void 0) {
|
|
415
|
+
current["aiModel"] = partial.aiModel;
|
|
416
|
+
}
|
|
417
|
+
if (partial.githubToken !== void 0) {
|
|
418
|
+
current["githubToken"] = partial.githubToken;
|
|
419
|
+
}
|
|
420
|
+
if (partial.githubClientId !== void 0) {
|
|
421
|
+
current["githubClientId"] = partial.githubClientId;
|
|
422
|
+
}
|
|
423
|
+
store.store = current;
|
|
424
|
+
}
|
|
425
|
+
function getConfigPath() {
|
|
426
|
+
return store.path;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/commands/init.ts
|
|
430
|
+
init_github();
|
|
431
|
+
|
|
432
|
+
// src/lib/git.ts
|
|
433
|
+
import chalk from "chalk";
|
|
434
|
+
import { simpleGit } from "simple-git";
|
|
435
|
+
var git = simpleGit();
|
|
436
|
+
async function getCurrentBranch() {
|
|
437
|
+
const summary = await git.branch();
|
|
438
|
+
return summary.current;
|
|
439
|
+
}
|
|
440
|
+
async function createAndSwitchBranch(name) {
|
|
441
|
+
await git.checkoutLocalBranch(name);
|
|
442
|
+
}
|
|
443
|
+
async function pushBranch(name) {
|
|
444
|
+
await git.push("origin", name, ["--set-upstream"]);
|
|
445
|
+
}
|
|
446
|
+
async function getRemoteUrl() {
|
|
447
|
+
try {
|
|
448
|
+
const remotes = await git.getRemotes(true);
|
|
449
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
450
|
+
return origin?.refs?.fetch ?? null;
|
|
451
|
+
} catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function parseOwnerRepo(remoteUrl) {
|
|
456
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^.]+?)(?:\.git)?$/);
|
|
457
|
+
if (sshMatch) {
|
|
458
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
459
|
+
}
|
|
460
|
+
const httpsMatch = remoteUrl.match(/https?:\/\/github\.com\/([^/]+)\/([^.]+?)(?:\.git)?$/);
|
|
461
|
+
if (httpsMatch) {
|
|
462
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
function makeBranchName(issueNumber, username) {
|
|
467
|
+
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
468
|
+
return `task-${issueNumber}-${slug}`;
|
|
469
|
+
}
|
|
470
|
+
async function findMergeBase(configuredBase) {
|
|
471
|
+
const candidates = configuredBase ? [`origin/${configuredBase}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
|
|
472
|
+
const unique = [...new Set(candidates)];
|
|
473
|
+
for (const base of unique) {
|
|
474
|
+
try {
|
|
475
|
+
const result = await git.raw(["merge-base", "HEAD", base]);
|
|
476
|
+
return result.trim();
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
async function getDiff(baseBranch) {
|
|
483
|
+
const status = await git.status();
|
|
484
|
+
const parts = [];
|
|
485
|
+
const fileLines = [
|
|
486
|
+
...status.modified.map((f) => ` M ${f}`),
|
|
487
|
+
...status.created.map((f) => ` A ${f}`),
|
|
488
|
+
...status.deleted.map((f) => ` D ${f}`),
|
|
489
|
+
...status.renamed.map((f) => ` R ${f.from} \u2192 ${f.to}`),
|
|
490
|
+
...status.not_added.map((f) => ` ? ${f}`)
|
|
491
|
+
];
|
|
492
|
+
if (fileLines.length > 0) {
|
|
493
|
+
parts.push("## Uncommitted changes\n" + fileLines.join("\n"));
|
|
494
|
+
const uncommitted = await git.diff(["HEAD"]);
|
|
495
|
+
if (uncommitted) {
|
|
496
|
+
const capped = uncommitted.length > 8e3 ? uncommitted.slice(0, 8e3) + "\n... (truncated)" : uncommitted;
|
|
497
|
+
parts.push("## Uncommitted diff\n```diff\n" + capped + "\n```");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const mergeBase = await findMergeBase(baseBranch);
|
|
501
|
+
if (mergeBase) {
|
|
502
|
+
const log = await git.log({ from: mergeBase, to: "HEAD" });
|
|
503
|
+
if (log.total > 0) {
|
|
504
|
+
const logLines = log.all.map((c) => ` ${c.hash.slice(0, 7)} ${c.message}`);
|
|
505
|
+
parts.push(`## Branch commits (${log.total} total)
|
|
506
|
+
` + logLines.join("\n"));
|
|
507
|
+
const branchDiff = await git.diff([mergeBase, "HEAD"]);
|
|
508
|
+
if (branchDiff) {
|
|
509
|
+
const capped = branchDiff.length > 12e3 ? branchDiff.slice(0, 12e3) + "\n... (truncated)" : branchDiff;
|
|
510
|
+
parts.push("## Full branch diff vs main\n```diff\n" + capped + "\n```");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return parts.length > 0 ? parts.join("\n\n") : "No changes detected.";
|
|
515
|
+
}
|
|
516
|
+
async function stageAllAndCommit(message) {
|
|
517
|
+
const status = await git.status();
|
|
518
|
+
if (!status.isClean()) {
|
|
519
|
+
await git.add(".");
|
|
520
|
+
await git.commit(message);
|
|
521
|
+
} else {
|
|
522
|
+
console.log(chalk.dim(" Working tree clean \u2014 no new commit created, pushing existing commits."));
|
|
523
|
+
}
|
|
524
|
+
const branch = (await git.branch()).current;
|
|
525
|
+
await git.push("origin", branch, ["--set-upstream"]);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/lib/client.ts
|
|
529
|
+
import OpenAI from "openai";
|
|
530
|
+
var DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
|
|
531
|
+
var DEFAULT_MODEL = "z-ai/glm-5";
|
|
532
|
+
function createClient(config) {
|
|
533
|
+
return new OpenAI({
|
|
534
|
+
baseURL: config.aiBaseUrl ?? DEFAULT_BASE_URL,
|
|
535
|
+
apiKey: config.aiApiKey
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function getModel(config) {
|
|
539
|
+
return config.aiModel ?? DEFAULT_MODEL;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/commands/init.ts
|
|
543
|
+
async function getGitHubTokenViaPAT() {
|
|
544
|
+
console.log(chalk2.dim("\n Create a token at: https://github.com/settings/tokens/new"));
|
|
545
|
+
console.log(chalk2.dim(" Required scopes: repo, read:user\n"));
|
|
546
|
+
const token = await password({
|
|
547
|
+
message: "GitHub Personal Access Token:",
|
|
548
|
+
mask: "*"
|
|
549
|
+
});
|
|
550
|
+
return { token: token.trim() };
|
|
551
|
+
}
|
|
552
|
+
var OAUTH_CLIENT_ID = "Ov23liW4zJ4r2RdZOsCJ";
|
|
553
|
+
async function getGitHubTokenViaDeviceFlow() {
|
|
554
|
+
let verificationUri = "";
|
|
555
|
+
let userCode = "";
|
|
556
|
+
const auth = createOAuthDeviceAuth({
|
|
557
|
+
clientType: "oauth-app",
|
|
558
|
+
clientId: OAUTH_CLIENT_ID,
|
|
559
|
+
scopes: ["repo"],
|
|
560
|
+
onVerification(verification) {
|
|
561
|
+
verificationUri = verification.verification_uri;
|
|
562
|
+
userCode = verification.user_code;
|
|
563
|
+
console.log("");
|
|
564
|
+
console.log(chalk2.bold(" 1. Open this URL in your browser:"));
|
|
565
|
+
console.log(" " + chalk2.cyan(verificationUri));
|
|
566
|
+
console.log("");
|
|
567
|
+
console.log(chalk2.bold(" 2. Enter this code:"));
|
|
568
|
+
console.log(" " + chalk2.yellow.bold(userCode));
|
|
569
|
+
console.log("");
|
|
570
|
+
open(verificationUri).catch(() => {
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
const spinner = ora("Waiting for authorization in browser...").start();
|
|
575
|
+
let token;
|
|
576
|
+
try {
|
|
577
|
+
const result = await auth({ type: "oauth" });
|
|
578
|
+
token = result.token;
|
|
579
|
+
spinner.succeed("Authorized!");
|
|
580
|
+
} catch (err) {
|
|
581
|
+
spinner.fail("Authorization failed");
|
|
582
|
+
throw err;
|
|
583
|
+
}
|
|
584
|
+
return { token, clientId: OAUTH_CLIENT_ID };
|
|
585
|
+
}
|
|
586
|
+
async function initCommand() {
|
|
587
|
+
console.log(chalk2.bold.cyan("\nTechunter \u2014 Initial Setup\n"));
|
|
588
|
+
let detectedOwner = "";
|
|
589
|
+
let detectedRepo = "";
|
|
590
|
+
const remoteUrl = await getRemoteUrl();
|
|
591
|
+
if (remoteUrl) {
|
|
592
|
+
const parsed = parseOwnerRepo(remoteUrl);
|
|
593
|
+
if (parsed) {
|
|
594
|
+
detectedOwner = parsed.owner;
|
|
595
|
+
detectedRepo = parsed.repo;
|
|
596
|
+
console.log(chalk2.dim(`Detected GitHub repo: ${detectedOwner}/${detectedRepo}
|
|
597
|
+
`));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const authMethod = await select({
|
|
601
|
+
message: "How would you like to authenticate with GitHub?",
|
|
602
|
+
choices: [
|
|
603
|
+
{
|
|
604
|
+
name: "Browser login (OAuth) \u2014 open a URL and click Authorize",
|
|
605
|
+
value: "device"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: "Personal Access Token (PAT) \u2014 paste a token from github.com/settings/tokens",
|
|
609
|
+
value: "pat"
|
|
610
|
+
}
|
|
611
|
+
]
|
|
612
|
+
});
|
|
613
|
+
let githubToken;
|
|
614
|
+
let githubClientId;
|
|
615
|
+
if (authMethod === "device") {
|
|
616
|
+
const result = await getGitHubTokenViaDeviceFlow();
|
|
617
|
+
githubToken = result.token;
|
|
618
|
+
githubClientId = result.clientId;
|
|
619
|
+
} else {
|
|
620
|
+
const result = await getGitHubTokenViaPAT();
|
|
621
|
+
githubToken = result.token;
|
|
622
|
+
}
|
|
623
|
+
const providerChoice = await select({
|
|
624
|
+
message: "AI provider:",
|
|
625
|
+
choices: [
|
|
626
|
+
{ name: `OpenRouter (recommended) ${chalk2.dim(`${DEFAULT_BASE_URL} \xB7 ${DEFAULT_MODEL}`)}`, value: "openrouter" },
|
|
627
|
+
{ name: "Custom (specify base URL and model)", value: "custom" }
|
|
628
|
+
]
|
|
629
|
+
});
|
|
630
|
+
let aiBaseUrl;
|
|
631
|
+
let aiModel;
|
|
632
|
+
if (providerChoice === "custom") {
|
|
633
|
+
aiBaseUrl = (await input({ message: "API base URL:", default: DEFAULT_BASE_URL })).trim();
|
|
634
|
+
aiModel = (await input({ message: "Model name:", default: DEFAULT_MODEL })).trim();
|
|
635
|
+
}
|
|
636
|
+
const apiKeyHint = providerChoice === "openrouter" ? chalk2.dim(" Get a key at: https://openrouter.ai/settings/keys\n") : chalk2.dim(" API key for your provider\n");
|
|
637
|
+
console.log(apiKeyHint);
|
|
638
|
+
const aiApiKey = await password({
|
|
639
|
+
message: "API Key:",
|
|
640
|
+
mask: "*"
|
|
641
|
+
});
|
|
642
|
+
let owner = detectedOwner;
|
|
643
|
+
let repo = detectedRepo;
|
|
644
|
+
if (!owner || !repo) {
|
|
645
|
+
owner = await input({
|
|
646
|
+
message: "GitHub repo owner (user or org):",
|
|
647
|
+
required: true
|
|
648
|
+
});
|
|
649
|
+
repo = await input({
|
|
650
|
+
message: "GitHub repo name:",
|
|
651
|
+
required: true
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const detectedDefault = "main";
|
|
655
|
+
const baseBranch = await input({
|
|
656
|
+
message: "Main branch to merge PRs into:",
|
|
657
|
+
default: detectedDefault
|
|
658
|
+
});
|
|
659
|
+
const config = {
|
|
660
|
+
githubToken,
|
|
661
|
+
githubClientId,
|
|
662
|
+
aiApiKey: aiApiKey.trim(),
|
|
663
|
+
...aiBaseUrl ? { aiBaseUrl } : {},
|
|
664
|
+
...aiModel ? { aiModel } : {},
|
|
665
|
+
github: {
|
|
666
|
+
owner: owner.trim(),
|
|
667
|
+
repo: repo.trim(),
|
|
668
|
+
baseBranch: baseBranch.trim() || detectedDefault
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
setConfig(config);
|
|
672
|
+
const spinner = ora("Setting up GitHub labels...").start();
|
|
673
|
+
try {
|
|
674
|
+
await ensureLabels(config);
|
|
675
|
+
spinner.succeed("GitHub labels created");
|
|
676
|
+
} catch (err) {
|
|
677
|
+
spinner.fail("Failed to create labels (check token permissions)");
|
|
678
|
+
console.error(chalk2.red(String(err)));
|
|
679
|
+
}
|
|
680
|
+
console.log(chalk2.green("\nSetup complete!"));
|
|
681
|
+
console.log(chalk2.dim(`Config saved to: ${getConfigPath()}
|
|
682
|
+
`));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/commands/config.ts
|
|
686
|
+
import { input as input2, password as password2, select as select2 } from "@inquirer/prompts";
|
|
687
|
+
import chalk3 from "chalk";
|
|
688
|
+
async function configCommand() {
|
|
689
|
+
let config;
|
|
690
|
+
try {
|
|
691
|
+
config = getConfig();
|
|
692
|
+
} catch {
|
|
693
|
+
console.error(chalk3.red("No config found. Run `tch init` first."));
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
console.log(chalk3.bold.cyan("\nTechunter \u2014 Settings\n"));
|
|
697
|
+
console.log(chalk3.dim(`Config file: ${getConfigPath()}
|
|
698
|
+
`));
|
|
699
|
+
const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
|
|
700
|
+
const currentModel = config.aiModel ?? DEFAULT_MODEL;
|
|
701
|
+
const field = await select2({
|
|
702
|
+
message: "Which setting to change?",
|
|
703
|
+
choices: [
|
|
704
|
+
{ name: `Base branch ${chalk3.dim(config.github.baseBranch ?? "(not set, uses repo default)")}`, value: "baseBranch" },
|
|
705
|
+
{ name: `GitHub repo ${chalk3.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
|
|
706
|
+
{ name: `AI base URL ${chalk3.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
|
|
707
|
+
{ name: `AI model ${chalk3.dim(currentModel)}`, value: "aiModel" },
|
|
708
|
+
{ name: `AI API Key ${chalk3.dim("(hidden)")}`, value: "aiApiKey" },
|
|
709
|
+
{ name: `GitHub Token ${chalk3.dim("(hidden)")}`, value: "githubToken" },
|
|
710
|
+
{ name: "Cancel", value: "cancel" }
|
|
711
|
+
]
|
|
712
|
+
});
|
|
713
|
+
if (field === "cancel") return;
|
|
714
|
+
if (field === "baseBranch") {
|
|
715
|
+
const val = await input2({
|
|
716
|
+
message: "Main branch to merge PRs into:",
|
|
717
|
+
default: config.github.baseBranch ?? "main"
|
|
718
|
+
});
|
|
719
|
+
setConfig({ github: { ...config.github, baseBranch: val.trim() || "main" } });
|
|
720
|
+
console.log(chalk3.green(`
|
|
721
|
+
Base branch set to: ${val.trim() || "main"}
|
|
722
|
+
`));
|
|
723
|
+
} else if (field === "repo") {
|
|
724
|
+
const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
|
|
725
|
+
const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
|
|
726
|
+
setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
|
|
727
|
+
console.log(chalk3.green(`
|
|
728
|
+
Repo set to: ${owner.trim()}/${repo.trim()}
|
|
729
|
+
`));
|
|
730
|
+
} else if (field === "aiBaseUrl") {
|
|
731
|
+
const val = await input2({ message: "AI base URL:", default: currentBaseUrl });
|
|
732
|
+
if (val.trim()) {
|
|
733
|
+
setConfig({ aiBaseUrl: val.trim() });
|
|
734
|
+
console.log(chalk3.green(`
|
|
735
|
+
AI base URL set to: ${val.trim()}
|
|
736
|
+
`));
|
|
737
|
+
}
|
|
738
|
+
} else if (field === "aiModel") {
|
|
739
|
+
const val = await input2({ message: "AI model name:", default: currentModel });
|
|
740
|
+
if (val.trim()) {
|
|
741
|
+
setConfig({ aiModel: val.trim() });
|
|
742
|
+
console.log(chalk3.green(`
|
|
743
|
+
AI model set to: ${val.trim()}
|
|
744
|
+
`));
|
|
745
|
+
}
|
|
746
|
+
} else if (field === "aiApiKey") {
|
|
747
|
+
const val = await password2({ message: "New AI API Key:", mask: "*" });
|
|
748
|
+
if (val.trim()) {
|
|
749
|
+
setConfig({ aiApiKey: val.trim() });
|
|
750
|
+
console.log(chalk3.green("\nAI API Key updated.\n"));
|
|
751
|
+
}
|
|
752
|
+
} else if (field === "githubToken") {
|
|
753
|
+
const val = await password2({ message: "New GitHub Token:", mask: "*" });
|
|
754
|
+
if (val.trim()) {
|
|
755
|
+
setConfig({ githubToken: val.trim() });
|
|
756
|
+
console.log(chalk3.green("\nGitHub Token updated.\n"));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/index.ts
|
|
762
|
+
init_github();
|
|
763
|
+
|
|
764
|
+
// src/lib/agent.ts
|
|
765
|
+
import ora14 from "ora";
|
|
766
|
+
import chalk13 from "chalk";
|
|
767
|
+
|
|
768
|
+
// src/tools/pick/index.ts
|
|
769
|
+
var pick_exports = {};
|
|
770
|
+
__export(pick_exports, {
|
|
771
|
+
definition: () => definition3,
|
|
772
|
+
execute: () => execute3,
|
|
773
|
+
run: () => run3,
|
|
774
|
+
terminal: () => terminal3
|
|
775
|
+
});
|
|
776
|
+
init_github();
|
|
777
|
+
import chalk8 from "chalk";
|
|
778
|
+
import ora4 from "ora";
|
|
779
|
+
import { select as select5 } from "@inquirer/prompts";
|
|
780
|
+
|
|
781
|
+
// src/lib/markdown.ts
|
|
782
|
+
import { marked } from "marked";
|
|
783
|
+
import { markedTerminal } from "marked-terminal";
|
|
784
|
+
marked.use(markedTerminal({ showSectionPrefix: false }));
|
|
785
|
+
function renderMarkdown(text) {
|
|
786
|
+
return marked(text);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/lib/display.ts
|
|
790
|
+
init_github();
|
|
791
|
+
import chalk4 from "chalk";
|
|
792
|
+
var LABEL_AVAILABLE2 = "techunter:available";
|
|
793
|
+
var LABEL_CLAIMED2 = "techunter:claimed";
|
|
794
|
+
var LABEL_IN_REVIEW2 = "techunter:in-review";
|
|
795
|
+
var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
|
|
796
|
+
function getStatus(issue) {
|
|
797
|
+
if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
|
|
798
|
+
if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
|
|
799
|
+
if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
|
|
800
|
+
if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
|
|
801
|
+
return "unknown";
|
|
802
|
+
}
|
|
803
|
+
function colorStatus(status) {
|
|
804
|
+
const padded = status.padEnd(14);
|
|
805
|
+
switch (status) {
|
|
806
|
+
case "available":
|
|
807
|
+
return chalk4.green(padded);
|
|
808
|
+
case "claimed":
|
|
809
|
+
return chalk4.yellow(padded);
|
|
810
|
+
case "in-review":
|
|
811
|
+
return chalk4.blue(padded);
|
|
812
|
+
case "changes-needed":
|
|
813
|
+
return chalk4.red(padded);
|
|
814
|
+
default:
|
|
815
|
+
return padded;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function printTaskDetail(issue) {
|
|
819
|
+
const divider = chalk4.dim("\u2500".repeat(70));
|
|
820
|
+
console.log("\n" + divider);
|
|
821
|
+
console.log(
|
|
822
|
+
chalk4.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk4.dim(issue.assignee ? `@${issue.assignee}` : "\u2014")
|
|
823
|
+
);
|
|
824
|
+
console.log(chalk4.bold("\n " + issue.title));
|
|
825
|
+
if (issue.body) {
|
|
826
|
+
console.log("");
|
|
827
|
+
console.log(renderMarkdown(issue.body));
|
|
828
|
+
}
|
|
829
|
+
console.log("\n " + chalk4.dim(issue.htmlUrl));
|
|
830
|
+
console.log(divider + "\n");
|
|
831
|
+
}
|
|
832
|
+
async function printTaskList(config) {
|
|
833
|
+
try {
|
|
834
|
+
const tasks = await listTasks(config);
|
|
835
|
+
const divider = chalk4.dim("\u2500".repeat(70));
|
|
836
|
+
console.log("");
|
|
837
|
+
console.log(chalk4.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
|
|
838
|
+
console.log(divider);
|
|
839
|
+
if (tasks.length === 0) {
|
|
840
|
+
console.log(chalk4.dim(" (no tasks)"));
|
|
841
|
+
} else {
|
|
842
|
+
for (const t of tasks) {
|
|
843
|
+
const num = `#${t.number}`.padEnd(5);
|
|
844
|
+
const status = colorStatus(getStatus(t));
|
|
845
|
+
const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
|
|
846
|
+
const title = t.title.length > 36 ? t.title.slice(0, 33) + "..." : t.title;
|
|
847
|
+
console.log(` ${num}${status}${assignee}${title}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
console.log(divider);
|
|
851
|
+
return tasks;
|
|
852
|
+
} catch (err) {
|
|
853
|
+
console.log(chalk4.yellow(`(Could not load tasks: ${err.message})`));
|
|
854
|
+
return [];
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async function printMyTasks(config) {
|
|
858
|
+
try {
|
|
859
|
+
const me = await getAuthenticatedUser(config);
|
|
860
|
+
const tasks = await listMyTasks(config, me);
|
|
861
|
+
if (tasks.length === 0) return;
|
|
862
|
+
const divider = chalk4.dim("\u2500".repeat(70));
|
|
863
|
+
console.log("");
|
|
864
|
+
console.log(chalk4.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + `My Tasks @${me}`));
|
|
865
|
+
console.log(divider);
|
|
866
|
+
for (const t of tasks) {
|
|
867
|
+
const num = `#${t.number}`.padEnd(5);
|
|
868
|
+
const status = colorStatus(getStatus(t));
|
|
869
|
+
const title = t.title.length > 46 ? t.title.slice(0, 43) + "..." : t.title;
|
|
870
|
+
console.log(` ${num}${status}${title}`);
|
|
871
|
+
}
|
|
872
|
+
console.log(divider);
|
|
873
|
+
const rejectedTasks = tasks.filter((t) => t.labels.includes(LABEL_CHANGES_NEEDED2));
|
|
874
|
+
if (rejectedTasks.length > 0) {
|
|
875
|
+
let currentBranch = "";
|
|
876
|
+
try {
|
|
877
|
+
currentBranch = await getCurrentBranch();
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
console.log("");
|
|
881
|
+
for (const t of rejectedTasks) {
|
|
882
|
+
const expectedBranch = makeBranchName(t.number, t.title);
|
|
883
|
+
const onCorrectBranch = currentBranch === expectedBranch;
|
|
884
|
+
console.log(
|
|
885
|
+
chalk4.red.bold(" \u26A0 Changes requested") + chalk4.red(` on #${t.number} "${t.title}"`)
|
|
886
|
+
);
|
|
887
|
+
if (!onCorrectBranch) {
|
|
888
|
+
console.log(
|
|
889
|
+
chalk4.dim(" Switch branch: ") + chalk4.cyan(`git checkout ${expectedBranch}`)
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
console.log("");
|
|
894
|
+
}
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/lib/launch.ts
|
|
900
|
+
import { spawn } from "child_process";
|
|
901
|
+
import chalk5 from "chalk";
|
|
902
|
+
function buildClaudePrompt(issue, branch) {
|
|
903
|
+
const lines = [
|
|
904
|
+
`You are working on task #${issue.number}: ${issue.title}`,
|
|
905
|
+
`Branch: ${branch}`,
|
|
906
|
+
""
|
|
907
|
+
];
|
|
908
|
+
if (issue.body) lines.push(issue.body.trim(), "");
|
|
909
|
+
lines.push(
|
|
910
|
+
"Implement the task. A detailed guide has been posted as a comment on the GitHub issue.",
|
|
911
|
+
"When done, return to tch and run /submit to review and deliver."
|
|
912
|
+
);
|
|
913
|
+
return lines.join("\n");
|
|
914
|
+
}
|
|
915
|
+
async function launchClaudeCode(issue, branch) {
|
|
916
|
+
const prompt = buildClaudePrompt(issue, branch);
|
|
917
|
+
console.log(chalk5.dim("\n Launching Claude Code\u2026\n"));
|
|
918
|
+
await new Promise((resolve) => {
|
|
919
|
+
const child = spawn("claude", [prompt], { stdio: "inherit", shell: true });
|
|
920
|
+
child.on("close", () => resolve());
|
|
921
|
+
child.on("error", () => {
|
|
922
|
+
console.log(
|
|
923
|
+
chalk5.yellow(
|
|
924
|
+
" Could not launch claude. Make sure Claude Code is installed:\n npm install -g @anthropic-ai/claude-code"
|
|
925
|
+
)
|
|
926
|
+
);
|
|
927
|
+
resolve();
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/tools/submit/index.ts
|
|
933
|
+
var submit_exports = {};
|
|
934
|
+
__export(submit_exports, {
|
|
935
|
+
definition: () => definition,
|
|
936
|
+
execute: () => execute,
|
|
937
|
+
run: () => run,
|
|
938
|
+
terminal: () => terminal
|
|
939
|
+
});
|
|
940
|
+
init_github();
|
|
941
|
+
import chalk7 from "chalk";
|
|
942
|
+
import ora2 from "ora";
|
|
943
|
+
import { select as select3, input as promptInput } from "@inquirer/prompts";
|
|
944
|
+
|
|
945
|
+
// src/lib/agent-ui.ts
|
|
946
|
+
import chalk6 from "chalk";
|
|
947
|
+
function formatInput(input3) {
|
|
948
|
+
return Object.entries(input3).map(([k, v]) => {
|
|
949
|
+
if (typeof v === "number") return `${k}=${v}`;
|
|
950
|
+
if (typeof v === "string") {
|
|
951
|
+
if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
|
|
952
|
+
return `${k}="${v}"`;
|
|
953
|
+
}
|
|
954
|
+
return `${k}=${JSON.stringify(v)}`;
|
|
955
|
+
}).join(" ");
|
|
956
|
+
}
|
|
957
|
+
function summarize(result) {
|
|
958
|
+
const first = result.split("\n").find((l) => l.trim()) ?? result;
|
|
959
|
+
return first.length > 100 ? first.slice(0, 97) + "..." : first;
|
|
960
|
+
}
|
|
961
|
+
function printToolCall(name, input3) {
|
|
962
|
+
const params = formatInput(input3);
|
|
963
|
+
console.log(` ${chalk6.cyan("\u2192")} ${chalk6.bold(name)}${params ? " " + chalk6.dim(params) : ""}`);
|
|
964
|
+
}
|
|
965
|
+
function printToolResult(result) {
|
|
966
|
+
const ok = !result.startsWith("Error:");
|
|
967
|
+
const icon = ok ? chalk6.green("\u2713") : chalk6.red("\u2717");
|
|
968
|
+
console.log(` ${icon} ${chalk6.dim(summarize(result))}`);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/lib/sub-agent.ts
|
|
972
|
+
async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
|
|
973
|
+
const client = createClient(config);
|
|
974
|
+
const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
|
|
975
|
+
const tools2 = selected.map((m) => m.definition);
|
|
976
|
+
const messages = [
|
|
977
|
+
{ role: "system", content: systemPrompt },
|
|
978
|
+
{ role: "user", content: userMessage }
|
|
979
|
+
];
|
|
980
|
+
const MAX_ITERATIONS = 20;
|
|
981
|
+
let iterations = 0;
|
|
982
|
+
for (; ; ) {
|
|
983
|
+
if (++iterations > MAX_ITERATIONS) {
|
|
984
|
+
throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
985
|
+
}
|
|
986
|
+
const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
|
|
987
|
+
const choice = res.choices[0];
|
|
988
|
+
messages.push({
|
|
989
|
+
role: "assistant",
|
|
990
|
+
content: choice.message.content ?? null,
|
|
991
|
+
...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
|
|
992
|
+
});
|
|
993
|
+
if (choice.finish_reason === "stop") {
|
|
994
|
+
return choice.message.content ?? "";
|
|
995
|
+
}
|
|
996
|
+
if (choice.finish_reason === "tool_calls") {
|
|
997
|
+
for (const tc of choice.message.tool_calls ?? []) {
|
|
998
|
+
let input3;
|
|
999
|
+
try {
|
|
1000
|
+
input3 = JSON.parse(tc.function.arguments);
|
|
1001
|
+
} catch {
|
|
1002
|
+
input3 = {};
|
|
1003
|
+
}
|
|
1004
|
+
printToolCall(tc.function.name, input3);
|
|
1005
|
+
const mod = selected.find((m) => m.definition.function.name === tc.function.name);
|
|
1006
|
+
const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
|
|
1007
|
+
printToolResult(result);
|
|
1008
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/tools/submit/prompts.ts
|
|
1015
|
+
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.";
|
|
1016
|
+
|
|
1017
|
+
// src/tools/submit/reviewer.ts
|
|
1018
|
+
async function reviewChanges(config, issueNumber, issue, diff) {
|
|
1019
|
+
return runSubAgentLoop(
|
|
1020
|
+
config,
|
|
1021
|
+
REVIEWER_SYSTEM_PROMPT,
|
|
1022
|
+
`Task #${issueNumber}: ${issue.title}
|
|
1023
|
+
|
|
1024
|
+
Acceptance Criteria:
|
|
1025
|
+
${issue.body ?? "(none)"}
|
|
1026
|
+
|
|
1027
|
+
Diff:
|
|
1028
|
+
${diff || "(no changes)"}`,
|
|
1029
|
+
["run_command", "read_file", "get_diff"]
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/tools/submit/index.ts
|
|
1034
|
+
var definition = {
|
|
1035
|
+
type: "function",
|
|
1036
|
+
function: {
|
|
1037
|
+
name: "submit",
|
|
1038
|
+
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.",
|
|
1039
|
+
parameters: {
|
|
1040
|
+
type: "object",
|
|
1041
|
+
properties: {
|
|
1042
|
+
commit_message: { type: "string", description: 'Commit message (optional \u2014 defaults to "complete: {task title}").' }
|
|
1043
|
+
},
|
|
1044
|
+
required: []
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
async function run(config) {
|
|
1049
|
+
const branch = await getCurrentBranch();
|
|
1050
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
1051
|
+
if (!match) {
|
|
1052
|
+
return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
|
|
1053
|
+
}
|
|
1054
|
+
const issueNumber = parseInt(match[1], 10);
|
|
1055
|
+
let spinner = ora2("Loading task and diff\u2026").start();
|
|
1056
|
+
const [issue, defaultBranch, diff] = await Promise.all([
|
|
1057
|
+
getTask(config, issueNumber),
|
|
1058
|
+
getBaseBranch(config),
|
|
1059
|
+
getDiff(config.github.baseBranch)
|
|
1060
|
+
]);
|
|
1061
|
+
spinner.stop();
|
|
1062
|
+
const reviewSpinner = ora2("Reviewing changes\u2026").start();
|
|
1063
|
+
let review = "";
|
|
1064
|
+
try {
|
|
1065
|
+
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
review = `(Review failed: ${err.message})`;
|
|
1068
|
+
}
|
|
1069
|
+
reviewSpinner.stop();
|
|
1070
|
+
const divider = chalk7.dim("\u2500".repeat(70));
|
|
1071
|
+
console.log("\n" + divider);
|
|
1072
|
+
console.log(chalk7.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
|
|
1073
|
+
console.log(divider);
|
|
1074
|
+
console.log(renderMarkdown(review));
|
|
1075
|
+
console.log(divider + "\n");
|
|
1076
|
+
let shouldProceed;
|
|
1077
|
+
try {
|
|
1078
|
+
shouldProceed = await select3({
|
|
1079
|
+
message: `Submit task #${issueNumber}?`,
|
|
1080
|
+
choices: [
|
|
1081
|
+
{ name: "Yes, submit", value: true },
|
|
1082
|
+
{ name: "No, not ready yet", value: false }
|
|
1083
|
+
]
|
|
1084
|
+
});
|
|
1085
|
+
} catch {
|
|
1086
|
+
return "Submit cancelled.";
|
|
1087
|
+
}
|
|
1088
|
+
if (!shouldProceed) return "Submit cancelled by user.";
|
|
1089
|
+
let commitMessage;
|
|
1090
|
+
try {
|
|
1091
|
+
commitMessage = await promptInput({
|
|
1092
|
+
message: "Commit message:",
|
|
1093
|
+
default: `complete: ${issue.title}`
|
|
1094
|
+
});
|
|
1095
|
+
} catch {
|
|
1096
|
+
return "Submit cancelled.";
|
|
1097
|
+
}
|
|
1098
|
+
if (!commitMessage.trim()) return "Submit cancelled.";
|
|
1099
|
+
spinner = ora2("Committing and pushing\u2026").start();
|
|
1100
|
+
try {
|
|
1101
|
+
await stageAllAndCommit(commitMessage.trim());
|
|
1102
|
+
spinner.stop();
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
spinner.stop();
|
|
1105
|
+
return `Commit failed: ${err.message}`;
|
|
1106
|
+
}
|
|
1107
|
+
spinner = ora2("Creating pull request\u2026").start();
|
|
1108
|
+
let prUrl;
|
|
1109
|
+
try {
|
|
1110
|
+
prUrl = await createPR(
|
|
1111
|
+
config,
|
|
1112
|
+
issue.title,
|
|
1113
|
+
`Closes #${issueNumber}
|
|
1114
|
+
|
|
1115
|
+
${issue.body ?? ""}`.trim(),
|
|
1116
|
+
branch,
|
|
1117
|
+
defaultBranch
|
|
1118
|
+
);
|
|
1119
|
+
spinner.stop();
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
spinner.stop();
|
|
1122
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
1123
|
+
}
|
|
1124
|
+
spinner = ora2("Marking as in-review\u2026").start();
|
|
1125
|
+
try {
|
|
1126
|
+
await markInReview(config, issueNumber);
|
|
1127
|
+
spinner.stop();
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
spinner.stop();
|
|
1130
|
+
return `PR created (${prUrl}) but failed to update label: ${err.message}`;
|
|
1131
|
+
}
|
|
1132
|
+
return `Task #${issueNumber} submitted.
|
|
1133
|
+
Commit: "${commitMessage.trim()}"
|
|
1134
|
+
PR: ${prUrl}`;
|
|
1135
|
+
}
|
|
1136
|
+
async function execute(input3, config) {
|
|
1137
|
+
const branch = await getCurrentBranch();
|
|
1138
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
1139
|
+
if (!match) return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
|
|
1140
|
+
const issueNumber = parseInt(match[1], 10);
|
|
1141
|
+
const [issue, defaultBranch, diff] = await Promise.all([
|
|
1142
|
+
getTask(config, issueNumber),
|
|
1143
|
+
getBaseBranch(config),
|
|
1144
|
+
getDiff(config.github.baseBranch)
|
|
1145
|
+
]);
|
|
1146
|
+
let review = "";
|
|
1147
|
+
try {
|
|
1148
|
+
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
review = `(Review failed: ${err.message})`;
|
|
1151
|
+
}
|
|
1152
|
+
const commitMessage = input3["commit_message"]?.trim() || `complete: ${issue.title}`;
|
|
1153
|
+
try {
|
|
1154
|
+
await stageAllAndCommit(commitMessage);
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
return `Commit failed: ${err.message}`;
|
|
1157
|
+
}
|
|
1158
|
+
let prUrl;
|
|
1159
|
+
try {
|
|
1160
|
+
prUrl = await createPR(
|
|
1161
|
+
config,
|
|
1162
|
+
issue.title,
|
|
1163
|
+
`Closes #${issueNumber}
|
|
1164
|
+
|
|
1165
|
+
${issue.body ?? ""}`.trim(),
|
|
1166
|
+
branch,
|
|
1167
|
+
defaultBranch
|
|
1168
|
+
);
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
1171
|
+
}
|
|
1172
|
+
try {
|
|
1173
|
+
await markInReview(config, issueNumber);
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
return `Task #${issueNumber} submitted.
|
|
1177
|
+
Review:
|
|
1178
|
+
${review}
|
|
1179
|
+
Commit: "${commitMessage}"
|
|
1180
|
+
PR: ${prUrl}`;
|
|
1181
|
+
}
|
|
1182
|
+
var terminal = true;
|
|
1183
|
+
|
|
1184
|
+
// src/tools/close/index.ts
|
|
1185
|
+
var close_exports = {};
|
|
1186
|
+
__export(close_exports, {
|
|
1187
|
+
definition: () => definition2,
|
|
1188
|
+
execute: () => execute2,
|
|
1189
|
+
run: () => run2,
|
|
1190
|
+
terminal: () => terminal2
|
|
1191
|
+
});
|
|
1192
|
+
init_github();
|
|
1193
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
1194
|
+
import ora3 from "ora";
|
|
1195
|
+
var definition2 = {
|
|
1196
|
+
type: "function",
|
|
1197
|
+
function: {
|
|
1198
|
+
name: "close",
|
|
1199
|
+
description: "Close a task (GitHub Issue). Equivalent to /close.",
|
|
1200
|
+
parameters: {
|
|
1201
|
+
type: "object",
|
|
1202
|
+
properties: {
|
|
1203
|
+
issue_number: { type: "number", description: "Issue number to close." }
|
|
1204
|
+
},
|
|
1205
|
+
required: ["issue_number"]
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
async function run2(config, opts = {}) {
|
|
1210
|
+
let issueNumber = opts.issue_number;
|
|
1211
|
+
if (!issueNumber) {
|
|
1212
|
+
let tasks;
|
|
1213
|
+
try {
|
|
1214
|
+
tasks = await listTasks(config);
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
return `Error loading tasks: ${err.message}`;
|
|
1217
|
+
}
|
|
1218
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
1219
|
+
try {
|
|
1220
|
+
issueNumber = await select4({
|
|
1221
|
+
message: "Select task to close:",
|
|
1222
|
+
choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
|
|
1223
|
+
});
|
|
1224
|
+
} catch {
|
|
1225
|
+
return "Cancelled.";
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
let confirmed;
|
|
1229
|
+
try {
|
|
1230
|
+
confirmed = await select4({
|
|
1231
|
+
message: `Close task #${issueNumber}?`,
|
|
1232
|
+
choices: [
|
|
1233
|
+
{ name: "Yes, close it", value: true },
|
|
1234
|
+
{ name: "No, cancel", value: false }
|
|
1235
|
+
]
|
|
1236
|
+
});
|
|
1237
|
+
} catch {
|
|
1238
|
+
return "Cancelled.";
|
|
1239
|
+
}
|
|
1240
|
+
if (!confirmed) return "Cancelled.";
|
|
1241
|
+
const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
|
|
1242
|
+
try {
|
|
1243
|
+
await closeTask(config, issueNumber);
|
|
1244
|
+
spinner.stop();
|
|
1245
|
+
return `Task #${issueNumber} closed.`;
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
spinner.stop();
|
|
1248
|
+
return `Error: ${err.message}`;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function execute2(input3, config) {
|
|
1252
|
+
const issueNumber = input3["issue_number"];
|
|
1253
|
+
const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
|
|
1254
|
+
try {
|
|
1255
|
+
await closeTask(config, issueNumber);
|
|
1256
|
+
spinner.stop();
|
|
1257
|
+
return `Task #${issueNumber} closed.`;
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
spinner.stop();
|
|
1260
|
+
return `Error: ${err.message}`;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
var terminal2 = true;
|
|
1264
|
+
|
|
1265
|
+
// src/tools/pick/index.ts
|
|
1266
|
+
var definition3 = {
|
|
1267
|
+
type: "function",
|
|
1268
|
+
function: {
|
|
1269
|
+
name: "pick",
|
|
1270
|
+
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.",
|
|
1271
|
+
parameters: {
|
|
1272
|
+
type: "object",
|
|
1273
|
+
properties: {
|
|
1274
|
+
issue_number: { type: "number", description: "Issue number to act on." },
|
|
1275
|
+
action: {
|
|
1276
|
+
type: "string",
|
|
1277
|
+
enum: ["claim", "view"],
|
|
1278
|
+
description: '"claim" to assign yourself and create a branch; "view" to return task details.'
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
required: ["issue_number", "action"]
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
async function run3(config, preselected) {
|
|
1286
|
+
let chosenNumber;
|
|
1287
|
+
if (preselected !== void 0) {
|
|
1288
|
+
chosenNumber = preselected;
|
|
1289
|
+
} else {
|
|
1290
|
+
let tasks;
|
|
1291
|
+
try {
|
|
1292
|
+
tasks = await listTasks(config);
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
return `Error: ${err.message}`;
|
|
1295
|
+
}
|
|
1296
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
1297
|
+
try {
|
|
1298
|
+
chosenNumber = await select5({
|
|
1299
|
+
message: "Select a task:",
|
|
1300
|
+
choices: tasks.map((t) => ({
|
|
1301
|
+
name: `#${String(t.number).padEnd(4)} ${colorStatus(getStatus(t))} ${t.title}`,
|
|
1302
|
+
value: t.number
|
|
1303
|
+
}))
|
|
1304
|
+
});
|
|
1305
|
+
} catch {
|
|
1306
|
+
return "Cancelled.";
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
let issue;
|
|
1310
|
+
try {
|
|
1311
|
+
issue = await getTask(config, chosenNumber);
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
return `Error loading task: ${err.message}`;
|
|
1314
|
+
}
|
|
1315
|
+
printTaskDetail(issue);
|
|
1316
|
+
const status = getStatus(issue);
|
|
1317
|
+
if (status === "changes-needed") {
|
|
1318
|
+
try {
|
|
1319
|
+
const comments = await listComments(config, issue.number, 1);
|
|
1320
|
+
if (comments.length > 0) {
|
|
1321
|
+
const c = comments[0];
|
|
1322
|
+
const divider = chalk8.dim("\u2500".repeat(70));
|
|
1323
|
+
console.log(
|
|
1324
|
+
chalk8.red.bold(" Latest rejection feedback") + chalk8.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
|
|
1325
|
+
);
|
|
1326
|
+
console.log(divider);
|
|
1327
|
+
console.log(renderMarkdown(c.body));
|
|
1328
|
+
console.log(divider + "\n");
|
|
1329
|
+
}
|
|
1330
|
+
} catch {
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const actions = [];
|
|
1334
|
+
if (status === "available") actions.push({ name: "Claim this task", value: "claim" });
|
|
1335
|
+
if (status === "claimed" || status === "changes-needed") actions.push({ name: "Submit this task", value: "submit" });
|
|
1336
|
+
actions.push({ name: "Close this task", value: "close" });
|
|
1337
|
+
actions.push({ name: "Nothing, just viewing", value: "none" });
|
|
1338
|
+
let action;
|
|
1339
|
+
try {
|
|
1340
|
+
action = await select5({ message: "Action:", choices: actions });
|
|
1341
|
+
} catch {
|
|
1342
|
+
return "Cancelled.";
|
|
1343
|
+
}
|
|
1344
|
+
if (action === "none") return `Viewed task #${issue.number}.`;
|
|
1345
|
+
if (action === "claim") {
|
|
1346
|
+
try {
|
|
1347
|
+
const { getAuthenticatedUser: getAuthenticatedUser2, listMyTasks: listMyTasks2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1348
|
+
const me = await getAuthenticatedUser2(config);
|
|
1349
|
+
const myTasks = await listMyTasks2(config, me);
|
|
1350
|
+
const activeTask = myTasks.find((t) => {
|
|
1351
|
+
const labels = t.labels;
|
|
1352
|
+
return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
|
|
1353
|
+
});
|
|
1354
|
+
if (activeTask) {
|
|
1355
|
+
return `You already have an active task: #${activeTask.number} "${activeTask.title}"
|
|
1356
|
+
Finish or submit it before claiming a new one.`;
|
|
1357
|
+
}
|
|
1358
|
+
let spinner = ora4(`Claiming #${issue.number}\u2026`).start();
|
|
1359
|
+
await claimTask(config, issue.number, me);
|
|
1360
|
+
spinner.stop();
|
|
1361
|
+
const branch = makeBranchName(issue.number, me);
|
|
1362
|
+
spinner = ora4(`Creating branch ${branch}\u2026`).start();
|
|
1363
|
+
try {
|
|
1364
|
+
await createAndSwitchBranch(branch);
|
|
1365
|
+
spinner.stop();
|
|
1366
|
+
} catch {
|
|
1367
|
+
spinner.warn(`Could not create branch ${branch}`);
|
|
1368
|
+
}
|
|
1369
|
+
spinner = ora4("Pushing branch\u2026").start();
|
|
1370
|
+
try {
|
|
1371
|
+
await pushBranch(branch);
|
|
1372
|
+
spinner.stop();
|
|
1373
|
+
} catch {
|
|
1374
|
+
spinner.warn("Could not push branch");
|
|
1375
|
+
}
|
|
1376
|
+
console.log(chalk8.green(`
|
|
1377
|
+
Claimed! Branch: ${branch}
|
|
1378
|
+
`));
|
|
1379
|
+
let openClaude;
|
|
1380
|
+
try {
|
|
1381
|
+
openClaude = await select5({
|
|
1382
|
+
message: "Open Claude Code for this task?",
|
|
1383
|
+
choices: [
|
|
1384
|
+
{ name: "Yes, start coding now", value: true },
|
|
1385
|
+
{ name: "No, return to tch", value: false }
|
|
1386
|
+
]
|
|
1387
|
+
});
|
|
1388
|
+
} catch {
|
|
1389
|
+
openClaude = false;
|
|
1390
|
+
}
|
|
1391
|
+
if (openClaude) await launchClaudeCode(issue, branch);
|
|
1392
|
+
return `Task #${issue.number} claimed. Branch: ${branch}`;
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
return `Error claiming task: ${err.message}`;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (action === "submit") return run(config);
|
|
1398
|
+
if (action === "close") return run2(config, { issue_number: issue.number });
|
|
1399
|
+
return "Cancelled.";
|
|
1400
|
+
}
|
|
1401
|
+
async function execute3(input3, config) {
|
|
1402
|
+
const issueNumber = input3["issue_number"];
|
|
1403
|
+
const action = input3["action"];
|
|
1404
|
+
let issue;
|
|
1405
|
+
try {
|
|
1406
|
+
issue = await getTask(config, issueNumber);
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
return `Error loading task: ${err.message}`;
|
|
1409
|
+
}
|
|
1410
|
+
if (action === "view") {
|
|
1411
|
+
const status = getStatus(issue);
|
|
1412
|
+
const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
|
|
1413
|
+
return [`#${issue.number} [${status}] ${assignee} ${issue.title}`, issue.body ?? ""].join("\n\n");
|
|
1414
|
+
}
|
|
1415
|
+
if (action === "claim") {
|
|
1416
|
+
const { getAuthenticatedUser: getAuthenticatedUser2, listMyTasks: listMyTasks2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1417
|
+
const me = await getAuthenticatedUser2(config);
|
|
1418
|
+
const myTasks = await listMyTasks2(config, me);
|
|
1419
|
+
const activeTask = myTasks.find((t) => {
|
|
1420
|
+
return t.labels.includes("techunter:claimed") || t.labels.includes("techunter:changes-needed");
|
|
1421
|
+
});
|
|
1422
|
+
if (activeTask) {
|
|
1423
|
+
return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
|
|
1424
|
+
}
|
|
1425
|
+
try {
|
|
1426
|
+
await claimTask(config, issueNumber, me);
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
return `Error claiming task: ${err.message}`;
|
|
1429
|
+
}
|
|
1430
|
+
const branch = makeBranchName(issueNumber, me);
|
|
1431
|
+
try {
|
|
1432
|
+
await createAndSwitchBranch(branch);
|
|
1433
|
+
} catch {
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
await pushBranch(branch);
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
return `Task #${issueNumber} claimed. Branch: ${branch}`;
|
|
1440
|
+
}
|
|
1441
|
+
return `Unknown action: ${action}`;
|
|
1442
|
+
}
|
|
1443
|
+
var terminal3 = true;
|
|
1444
|
+
|
|
1445
|
+
// src/tools/new-task/index.ts
|
|
1446
|
+
var new_task_exports = {};
|
|
1447
|
+
__export(new_task_exports, {
|
|
1448
|
+
definition: () => definition4,
|
|
1449
|
+
execute: () => execute4,
|
|
1450
|
+
run: () => run4,
|
|
1451
|
+
terminal: () => terminal4
|
|
1452
|
+
});
|
|
1453
|
+
init_github();
|
|
1454
|
+
import { select as select6, input as promptInput2 } from "@inquirer/prompts";
|
|
1455
|
+
import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
|
|
1456
|
+
import { spawn as spawn2 } from "child_process";
|
|
1457
|
+
import { tmpdir } from "os";
|
|
1458
|
+
import path from "path";
|
|
1459
|
+
import ora5 from "ora";
|
|
1460
|
+
import chalk9 from "chalk";
|
|
1461
|
+
import open2 from "open";
|
|
1462
|
+
|
|
1463
|
+
// src/tools/new-task/prompts.ts
|
|
1464
|
+
var GUIDE_FORMAT = `
|
|
1465
|
+
## Guide format
|
|
1466
|
+
Write the guide in the same language as the task title. Use plain markdown, no code blocks. Be concise. Include exactly these sections:
|
|
1467
|
+
|
|
1468
|
+
### \u4EFB\u52A1\u63CF\u8FF0
|
|
1469
|
+
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.
|
|
1470
|
+
|
|
1471
|
+
### \u6D89\u53CA\u6587\u4EF6
|
|
1472
|
+
List each file path with CREATE/MODIFY, and one sentence describing what changes.
|
|
1473
|
+
|
|
1474
|
+
### \u8F93\u5165 / \u8F93\u51FA
|
|
1475
|
+
What the feature/fix receives as input and what it produces or affects.
|
|
1476
|
+
|
|
1477
|
+
### \u9A8C\u6536\u6807\u51C6
|
|
1478
|
+
Checkbox list of testable conditions. Keep it short \u2014 3 to 6 items max.
|
|
1479
|
+
`.trim();
|
|
1480
|
+
|
|
1481
|
+
// src/tools/new-task/guide-generator.ts
|
|
1482
|
+
async function generateGuide(config, title, revise) {
|
|
1483
|
+
const userMessage = revise ? `Revise the following implementation guide for task: "${title}"
|
|
1484
|
+
|
|
1485
|
+
User feedback: ${revise.feedback}
|
|
1486
|
+
|
|
1487
|
+
Previous guide:
|
|
1488
|
+
${revise.previousGuide}` : `Write an implementation guide for this task: "${title}"`;
|
|
1489
|
+
return runSubAgentLoop(
|
|
1490
|
+
config,
|
|
1491
|
+
"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,
|
|
1492
|
+
userMessage,
|
|
1493
|
+
["scan_project", "read_file", "run_command", "ask_user"]
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/tools/new-task/index.ts
|
|
1498
|
+
async function openInEditor(content) {
|
|
1499
|
+
const dir = await mkdtemp(path.join(tmpdir(), "tch-guide-"));
|
|
1500
|
+
const file = path.join(dir, "guide.md");
|
|
1501
|
+
try {
|
|
1502
|
+
await writeFile(file, content, "utf-8");
|
|
1503
|
+
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? (process.platform === "win32" ? "notepad" : "vi");
|
|
1504
|
+
await new Promise((resolve, reject) => {
|
|
1505
|
+
const child = spawn2(editor, [file], { stdio: "inherit", shell: true });
|
|
1506
|
+
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Editor exited with code ${code}`)));
|
|
1507
|
+
child.on("error", reject);
|
|
1508
|
+
});
|
|
1509
|
+
return await readFile(file, "utf-8");
|
|
1510
|
+
} finally {
|
|
1511
|
+
await rm(dir, { recursive: true, force: true });
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
var definition4 = {
|
|
1515
|
+
type: "function",
|
|
1516
|
+
function: {
|
|
1517
|
+
name: "new_task",
|
|
1518
|
+
description: "Create a new task (GitHub Issue): scans the project, generates a full implementation guide, then creates the issue. Equivalent to /new.",
|
|
1519
|
+
parameters: {
|
|
1520
|
+
type: "object",
|
|
1521
|
+
properties: {
|
|
1522
|
+
title: { type: "string", description: "Task title." },
|
|
1523
|
+
feedback: { type: "string", description: "Optional feedback to revise the generated guide before creating the issue." }
|
|
1524
|
+
},
|
|
1525
|
+
required: ["title"]
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
async function run4(config, opts = {}) {
|
|
1530
|
+
let title = opts.title?.trim();
|
|
1531
|
+
if (!title) {
|
|
1532
|
+
try {
|
|
1533
|
+
title = (await promptInput2({ message: "Task title:" })).trim();
|
|
1534
|
+
} catch {
|
|
1535
|
+
return "Cancelled.";
|
|
1536
|
+
}
|
|
1537
|
+
if (!title) return "Cancelled.";
|
|
1538
|
+
}
|
|
1539
|
+
const spinner = ora5("Scanning project and generating guide\u2026").start();
|
|
1540
|
+
let guide;
|
|
1541
|
+
try {
|
|
1542
|
+
guide = await generateGuide(config, title);
|
|
1543
|
+
spinner.stop();
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
spinner.stop();
|
|
1546
|
+
return `Error generating guide: ${err.message}`;
|
|
1547
|
+
}
|
|
1548
|
+
const divider = chalk9.dim("\u2500".repeat(70));
|
|
1549
|
+
for (; ; ) {
|
|
1550
|
+
console.log("\n" + divider);
|
|
1551
|
+
console.log(chalk9.bold(" Generated guide preview"));
|
|
1552
|
+
console.log(divider);
|
|
1553
|
+
console.log(renderMarkdown(guide));
|
|
1554
|
+
console.log(divider + "\n");
|
|
1555
|
+
let action;
|
|
1556
|
+
try {
|
|
1557
|
+
action = await select6({
|
|
1558
|
+
message: "Create this task?",
|
|
1559
|
+
choices: [
|
|
1560
|
+
{ name: "Yes, create task", value: "create" },
|
|
1561
|
+
{ name: "Edit in editor", value: "edit" },
|
|
1562
|
+
{ name: "Let AI revise", value: "ai" },
|
|
1563
|
+
{ name: "Cancel", value: "cancel" }
|
|
1564
|
+
]
|
|
1565
|
+
});
|
|
1566
|
+
} catch {
|
|
1567
|
+
return "Cancelled.";
|
|
1568
|
+
}
|
|
1569
|
+
if (action === "cancel") return "Cancelled.";
|
|
1570
|
+
if (action === "create") break;
|
|
1571
|
+
if (action === "edit") {
|
|
1572
|
+
try {
|
|
1573
|
+
guide = await openInEditor(guide);
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
console.log(chalk9.yellow(` Editor error: ${err.message}`));
|
|
1576
|
+
}
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
let feedback;
|
|
1580
|
+
try {
|
|
1581
|
+
feedback = (await promptInput2({ message: "What should be changed?" })).trim();
|
|
1582
|
+
} catch {
|
|
1583
|
+
return "Cancelled.";
|
|
1584
|
+
}
|
|
1585
|
+
if (!feedback) continue;
|
|
1586
|
+
const reviseSpinner = ora5("Revising guide\u2026").start();
|
|
1587
|
+
try {
|
|
1588
|
+
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1589
|
+
reviseSpinner.stop();
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
reviseSpinner.stop();
|
|
1592
|
+
console.log(chalk9.yellow(` Revision error: ${err.message}`));
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
const createSpinner = ora5(`Creating "${title}"\u2026`).start();
|
|
1596
|
+
let htmlUrl;
|
|
1597
|
+
let issueNumber;
|
|
1598
|
+
let issueTitle;
|
|
1599
|
+
try {
|
|
1600
|
+
const issue = await createTask(config, title, guide);
|
|
1601
|
+
createSpinner.stop();
|
|
1602
|
+
htmlUrl = issue.htmlUrl;
|
|
1603
|
+
issueNumber = issue.number;
|
|
1604
|
+
issueTitle = issue.title;
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
createSpinner.stop();
|
|
1607
|
+
return `Error: ${err.message}`;
|
|
1608
|
+
}
|
|
1609
|
+
console.log(chalk9.green(`
|
|
1610
|
+
Created #${issueNumber} "${issueTitle}"
|
|
1611
|
+
${chalk9.dim(htmlUrl)}
|
|
1612
|
+
`));
|
|
1613
|
+
try {
|
|
1614
|
+
const openBrowser = await select6({
|
|
1615
|
+
message: "Open issue in browser?",
|
|
1616
|
+
choices: [
|
|
1617
|
+
{ name: "Yes", value: true },
|
|
1618
|
+
{ name: "No", value: false }
|
|
1619
|
+
]
|
|
1620
|
+
});
|
|
1621
|
+
if (openBrowser) await open2(htmlUrl);
|
|
1622
|
+
} catch {
|
|
1623
|
+
}
|
|
1624
|
+
return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
|
|
1625
|
+
}
|
|
1626
|
+
async function execute4(input3, config) {
|
|
1627
|
+
const title = input3["title"].trim();
|
|
1628
|
+
const feedback = input3["feedback"];
|
|
1629
|
+
let guide = await generateGuide(config, title);
|
|
1630
|
+
if (feedback) {
|
|
1631
|
+
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1632
|
+
}
|
|
1633
|
+
try {
|
|
1634
|
+
const issue = await createTask(config, title, guide);
|
|
1635
|
+
return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
|
|
1636
|
+
|
|
1637
|
+
Guide:
|
|
1638
|
+
${guide}`;
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
return `Error: ${err.message}`;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
var terminal4 = true;
|
|
1644
|
+
|
|
1645
|
+
// src/tools/my-status/index.ts
|
|
1646
|
+
var my_status_exports = {};
|
|
1647
|
+
__export(my_status_exports, {
|
|
1648
|
+
definition: () => definition5,
|
|
1649
|
+
execute: () => execute5,
|
|
1650
|
+
run: () => run5,
|
|
1651
|
+
terminal: () => terminal5
|
|
1652
|
+
});
|
|
1653
|
+
init_github();
|
|
1654
|
+
import ora6 from "ora";
|
|
1655
|
+
var definition5 = {
|
|
1656
|
+
type: "function",
|
|
1657
|
+
function: {
|
|
1658
|
+
name: "my_status",
|
|
1659
|
+
description: "Show all tasks currently assigned to the authenticated GitHub user. Equivalent to /status.",
|
|
1660
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
async function run5(config) {
|
|
1664
|
+
const spinner = ora6("Fetching your tasks\u2026").start();
|
|
1665
|
+
try {
|
|
1666
|
+
const me = await getAuthenticatedUser(config);
|
|
1667
|
+
const tasks = await listMyTasks(config, me);
|
|
1668
|
+
spinner.stop();
|
|
1669
|
+
if (tasks.length === 0) return `No tasks assigned to @${me}.`;
|
|
1670
|
+
const lines = tasks.map((t) => ` #${t.number} [${getStatus(t)}] ${t.title}`);
|
|
1671
|
+
return `Tasks assigned to @${me}:
|
|
1672
|
+
${lines.join("\n")}`;
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
spinner.stop();
|
|
1675
|
+
return `Error: ${err.message}`;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
var execute5 = (_input, config) => run5(config);
|
|
1679
|
+
var terminal5 = true;
|
|
1680
|
+
|
|
1681
|
+
// src/tools/review/index.ts
|
|
1682
|
+
var review_exports = {};
|
|
1683
|
+
__export(review_exports, {
|
|
1684
|
+
definition: () => definition6,
|
|
1685
|
+
execute: () => execute6,
|
|
1686
|
+
run: () => run6,
|
|
1687
|
+
terminal: () => terminal6
|
|
1688
|
+
});
|
|
1689
|
+
init_github();
|
|
1690
|
+
import ora7 from "ora";
|
|
1691
|
+
var definition6 = {
|
|
1692
|
+
type: "function",
|
|
1693
|
+
function: {
|
|
1694
|
+
name: "review",
|
|
1695
|
+
description: "List tasks waiting for your review (submitted by others, created by you). Equivalent to /review.",
|
|
1696
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
async function run6(config) {
|
|
1700
|
+
const spinner = ora7("Loading tasks for review\u2026").start();
|
|
1701
|
+
try {
|
|
1702
|
+
const me = await getAuthenticatedUser(config);
|
|
1703
|
+
const tasks = await listTasksForReview(config, me);
|
|
1704
|
+
spinner.stop();
|
|
1705
|
+
if (tasks.length === 0) return `No tasks pending review for @${me}.`;
|
|
1706
|
+
const lines = tasks.map((t) => ` #${t.number} [in-review] @${t.assignee ?? "\u2014"} ${t.title}`);
|
|
1707
|
+
return `Tasks pending review (created by @${me}):
|
|
1708
|
+
${lines.join("\n")}`;
|
|
1709
|
+
} catch (err) {
|
|
1710
|
+
spinner.stop();
|
|
1711
|
+
return `Error: ${err.message}`;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
var execute6 = (_input, config) => run6(config);
|
|
1715
|
+
var terminal6 = true;
|
|
1716
|
+
|
|
1717
|
+
// src/tools/refresh/index.ts
|
|
1718
|
+
var refresh_exports = {};
|
|
1719
|
+
__export(refresh_exports, {
|
|
1720
|
+
definition: () => definition7,
|
|
1721
|
+
execute: () => execute7,
|
|
1722
|
+
run: () => run7,
|
|
1723
|
+
terminal: () => terminal7
|
|
1724
|
+
});
|
|
1725
|
+
var definition7 = {
|
|
1726
|
+
type: "function",
|
|
1727
|
+
function: {
|
|
1728
|
+
name: "refresh",
|
|
1729
|
+
description: "Reload and display the full task list. Equivalent to /refresh.",
|
|
1730
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
async function run7(config) {
|
|
1734
|
+
const tasks = await printTaskList(config);
|
|
1735
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
1736
|
+
const lines = tasks.map((t) => {
|
|
1737
|
+
const status = getStatus(t);
|
|
1738
|
+
const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
|
|
1739
|
+
return `#${t.number} [${status}] ${assignee} ${t.title}`;
|
|
1740
|
+
});
|
|
1741
|
+
return `Tasks (${tasks.length}):
|
|
1742
|
+
${lines.join("\n")}`;
|
|
1743
|
+
}
|
|
1744
|
+
var execute7 = (_input, config) => run7(config);
|
|
1745
|
+
var terminal7 = true;
|
|
1746
|
+
|
|
1747
|
+
// src/tools/open-code/index.ts
|
|
1748
|
+
var open_code_exports = {};
|
|
1749
|
+
__export(open_code_exports, {
|
|
1750
|
+
definition: () => definition8,
|
|
1751
|
+
execute: () => execute8,
|
|
1752
|
+
run: () => run8,
|
|
1753
|
+
terminal: () => terminal8
|
|
1754
|
+
});
|
|
1755
|
+
init_github();
|
|
1756
|
+
var definition8 = {
|
|
1757
|
+
type: "function",
|
|
1758
|
+
function: {
|
|
1759
|
+
name: "open_code",
|
|
1760
|
+
description: "Launch Claude Code for the current task branch. Equivalent to /code.",
|
|
1761
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
async function run8(config) {
|
|
1765
|
+
let branch;
|
|
1766
|
+
try {
|
|
1767
|
+
branch = await getCurrentBranch();
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
return `Error: ${err.message}`;
|
|
1770
|
+
}
|
|
1771
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
1772
|
+
if (!match) return `Not on a task branch (current: ${branch}).`;
|
|
1773
|
+
const issueNum = parseInt(match[1], 10);
|
|
1774
|
+
let issue;
|
|
1775
|
+
try {
|
|
1776
|
+
issue = await getTask(config, issueNum);
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
return `Error: ${err.message}`;
|
|
1779
|
+
}
|
|
1780
|
+
await launchClaudeCode(issue, branch);
|
|
1781
|
+
return "Claude Code session ended.";
|
|
1782
|
+
}
|
|
1783
|
+
var execute8 = (_input, config) => run8(config);
|
|
1784
|
+
var terminal8 = true;
|
|
1785
|
+
|
|
1786
|
+
// src/tools/reject/index.ts
|
|
1787
|
+
var reject_exports = {};
|
|
1788
|
+
__export(reject_exports, {
|
|
1789
|
+
definition: () => definition9,
|
|
1790
|
+
execute: () => execute9,
|
|
1791
|
+
run: () => run9,
|
|
1792
|
+
terminal: () => terminal9
|
|
1793
|
+
});
|
|
1794
|
+
init_github();
|
|
1795
|
+
import chalk10 from "chalk";
|
|
1796
|
+
import { select as select7, input as promptInput3 } from "@inquirer/prompts";
|
|
1797
|
+
import ora8 from "ora";
|
|
1798
|
+
|
|
1799
|
+
// src/tools/reject/prompts.ts
|
|
1800
|
+
var REJECTION_FORMAT = `
|
|
1801
|
+
Write the rejection comment in the same language as the conversation. Use markdown. Include:
|
|
1802
|
+
|
|
1803
|
+
### \u274C \u6253\u56DE\u539F\u56E0 / Rejection Reason
|
|
1804
|
+
One paragraph: what was reviewed and what the main problem is.
|
|
1805
|
+
|
|
1806
|
+
### \u{1F527} \u9700\u8981\u4FEE\u6539\u7684\u5185\u5BB9 / Required Changes
|
|
1807
|
+
Numbered, specific, actionable items. Reference file names and function names.
|
|
1808
|
+
|
|
1809
|
+
### \u2705 \u672A\u901A\u8FC7\u7684\u9A8C\u6536\u6807\u51C6 / Failed Acceptance Criteria
|
|
1810
|
+
Re-list each criterion that was NOT met, prefixed with \u274C.
|
|
1811
|
+
|
|
1812
|
+
### \u{1F4CB} \u4E0B\u4E00\u6B65 / Next Steps
|
|
1813
|
+
Clear instruction on what to fix and how to re-submit (via /submit).
|
|
1814
|
+
`.trim();
|
|
1815
|
+
|
|
1816
|
+
// src/tools/reject/comment-generator.ts
|
|
1817
|
+
async function generateRejectionComment(config, issueNumber, userFeedback) {
|
|
1818
|
+
return runSubAgentLoop(
|
|
1819
|
+
config,
|
|
1820
|
+
"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,
|
|
1821
|
+
`Write a rejection comment for issue #${issueNumber}.
|
|
1822
|
+
Reviewer feedback: ${userFeedback}`,
|
|
1823
|
+
["get_task", "get_comments", "get_diff", "read_file"]
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/tools/reject/index.ts
|
|
1828
|
+
var definition9 = {
|
|
1829
|
+
type: "function",
|
|
1830
|
+
function: {
|
|
1831
|
+
name: "reject",
|
|
1832
|
+
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.",
|
|
1833
|
+
parameters: {
|
|
1834
|
+
type: "object",
|
|
1835
|
+
properties: {
|
|
1836
|
+
issue_number: { type: "number", description: "GitHub issue number to reject." },
|
|
1837
|
+
feedback: { type: "string", description: "Description of what is wrong with the submission." }
|
|
1838
|
+
},
|
|
1839
|
+
required: ["issue_number", "feedback"]
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
async function run9(config, opts) {
|
|
1844
|
+
const { issue_number: issueNumber } = opts;
|
|
1845
|
+
let feedback;
|
|
1846
|
+
try {
|
|
1847
|
+
feedback = await promptInput3({
|
|
1848
|
+
message: `What's wrong with #${issueNumber}? (brief description for the reviewer agent)`
|
|
1849
|
+
});
|
|
1850
|
+
} catch {
|
|
1851
|
+
return "Cancelled.";
|
|
1852
|
+
}
|
|
1853
|
+
if (!feedback.trim()) return "Cancelled.";
|
|
1854
|
+
const divider = chalk10.dim("\u2500".repeat(70));
|
|
1855
|
+
for (; ; ) {
|
|
1856
|
+
const spinner = ora8("Generating rejection comment\u2026").start();
|
|
1857
|
+
let comment;
|
|
1858
|
+
try {
|
|
1859
|
+
comment = await generateRejectionComment(config, issueNumber, feedback);
|
|
1860
|
+
spinner.stop();
|
|
1861
|
+
} catch (err) {
|
|
1862
|
+
spinner.stop();
|
|
1863
|
+
return `Error generating comment: ${err.message}`;
|
|
1864
|
+
}
|
|
1865
|
+
console.log("\n" + divider);
|
|
1866
|
+
console.log(chalk10.bold(` Rejection preview \u2014 issue #${issueNumber}`));
|
|
1867
|
+
console.log(divider);
|
|
1868
|
+
console.log(renderMarkdown(comment));
|
|
1869
|
+
console.log(divider + "\n");
|
|
1870
|
+
let decision;
|
|
1871
|
+
try {
|
|
1872
|
+
decision = await select7({
|
|
1873
|
+
message: `Post rejection and mark #${issueNumber} as changes-needed?`,
|
|
1874
|
+
choices: [
|
|
1875
|
+
{ name: "Post & Reject", value: "yes" },
|
|
1876
|
+
{ name: "Revise \u2014 describe what to change", value: "revise" },
|
|
1877
|
+
{ name: "Cancel", value: "cancel" }
|
|
1878
|
+
]
|
|
1879
|
+
});
|
|
1880
|
+
} catch {
|
|
1881
|
+
return "Cancelled.";
|
|
1882
|
+
}
|
|
1883
|
+
if (decision === "cancel") return "User cancelled rejection.";
|
|
1884
|
+
if (decision === "revise") {
|
|
1885
|
+
try {
|
|
1886
|
+
feedback = await promptInput3({ message: "What should be changed?" });
|
|
1887
|
+
} catch {
|
|
1888
|
+
return "Cancelled.";
|
|
1889
|
+
}
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
let spinner2 = ora8(`Posting rejection comment on #${issueNumber}\u2026`).start();
|
|
1893
|
+
try {
|
|
1894
|
+
await postComment(config, issueNumber, comment);
|
|
1895
|
+
spinner2.stop();
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
spinner2.stop();
|
|
1898
|
+
return `Error posting comment: ${err.message}`;
|
|
1899
|
+
}
|
|
1900
|
+
spinner2 = ora8(`Marking #${issueNumber} as changes-needed\u2026`).start();
|
|
1901
|
+
try {
|
|
1902
|
+
await rejectTask(config, issueNumber);
|
|
1903
|
+
spinner2.stop();
|
|
1904
|
+
} catch (err) {
|
|
1905
|
+
spinner2.stop();
|
|
1906
|
+
return `Comment posted but failed to update label: ${err.message}`;
|
|
1907
|
+
}
|
|
1908
|
+
return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
async function execute9(input3, config) {
|
|
1912
|
+
const issueNumber = input3["issue_number"];
|
|
1913
|
+
const feedback = input3["feedback"];
|
|
1914
|
+
let comment;
|
|
1915
|
+
try {
|
|
1916
|
+
comment = await generateRejectionComment(config, issueNumber, feedback);
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
return `Error generating comment: ${err.message}`;
|
|
1919
|
+
}
|
|
1920
|
+
try {
|
|
1921
|
+
await postComment(config, issueNumber, comment);
|
|
1922
|
+
} catch (err) {
|
|
1923
|
+
return `Error posting comment: ${err.message}`;
|
|
1924
|
+
}
|
|
1925
|
+
try {
|
|
1926
|
+
await rejectTask(config, issueNumber);
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
return `Comment posted but failed to update label: ${err.message}`;
|
|
1929
|
+
}
|
|
1930
|
+
return `Task #${issueNumber} rejected.
|
|
1931
|
+
|
|
1932
|
+
Comment posted:
|
|
1933
|
+
${comment}`;
|
|
1934
|
+
}
|
|
1935
|
+
var terminal9 = true;
|
|
1936
|
+
|
|
1937
|
+
// src/tools/accept/index.ts
|
|
1938
|
+
var accept_exports = {};
|
|
1939
|
+
__export(accept_exports, {
|
|
1940
|
+
definition: () => definition10,
|
|
1941
|
+
execute: () => execute10,
|
|
1942
|
+
run: () => run10,
|
|
1943
|
+
terminal: () => terminal10
|
|
1944
|
+
});
|
|
1945
|
+
init_github();
|
|
1946
|
+
import chalk11 from "chalk";
|
|
1947
|
+
import { select as select8 } from "@inquirer/prompts";
|
|
1948
|
+
import ora9 from "ora";
|
|
1949
|
+
var definition10 = {
|
|
1950
|
+
type: "function",
|
|
1951
|
+
function: {
|
|
1952
|
+
name: "accept",
|
|
1953
|
+
description: "Accept an in-review task: merges the PR into the configured base branch and closes the issue.",
|
|
1954
|
+
parameters: {
|
|
1955
|
+
type: "object",
|
|
1956
|
+
properties: {
|
|
1957
|
+
issue_number: { type: "number", description: "GitHub issue number to accept" }
|
|
1958
|
+
},
|
|
1959
|
+
required: ["issue_number"]
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
async function run10(config, opts) {
|
|
1964
|
+
let issueNumber = opts?.issue_number;
|
|
1965
|
+
if (!issueNumber) {
|
|
1966
|
+
const spinner2 = ora9("Loading tasks for review\u2026").start();
|
|
1967
|
+
let tasks;
|
|
1968
|
+
let me;
|
|
1969
|
+
try {
|
|
1970
|
+
me = await getAuthenticatedUser(config);
|
|
1971
|
+
tasks = await listTasksForReview(config, me);
|
|
1972
|
+
spinner2.stop();
|
|
1973
|
+
} catch (err) {
|
|
1974
|
+
spinner2.stop();
|
|
1975
|
+
return `Error: ${err.message}`;
|
|
1976
|
+
}
|
|
1977
|
+
if (tasks.length === 0) return "No tasks pending review.";
|
|
1978
|
+
try {
|
|
1979
|
+
issueNumber = await select8({
|
|
1980
|
+
message: "Which task to accept?",
|
|
1981
|
+
choices: tasks.map((t) => ({
|
|
1982
|
+
name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
1983
|
+
value: t.number
|
|
1984
|
+
}))
|
|
1985
|
+
});
|
|
1986
|
+
} catch {
|
|
1987
|
+
return "Cancelled.";
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const baseBranch = config.github.baseBranch ?? "main";
|
|
1991
|
+
let confirmed;
|
|
1992
|
+
try {
|
|
1993
|
+
confirmed = await select8({
|
|
1994
|
+
message: `Merge PR for #${issueNumber} into ${chalk11.cyan(baseBranch)} and close issue?`,
|
|
1995
|
+
choices: [
|
|
1996
|
+
{ name: "Yes, accept", value: true },
|
|
1997
|
+
{ name: "Cancel", value: false }
|
|
1998
|
+
]
|
|
1999
|
+
});
|
|
2000
|
+
} catch {
|
|
2001
|
+
return "Cancelled.";
|
|
2002
|
+
}
|
|
2003
|
+
if (!confirmed) return "Cancelled.";
|
|
2004
|
+
const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2005
|
+
try {
|
|
2006
|
+
const result = await acceptTask(config, issueNumber);
|
|
2007
|
+
spinner.succeed(`PR #${result.prNumber} merged into ${baseBranch}`);
|
|
2008
|
+
return `Task #${issueNumber} accepted.
|
|
2009
|
+
PR #${result.prNumber} merged \u2192 ${baseBranch}
|
|
2010
|
+
Issue closed.`;
|
|
2011
|
+
} catch (err) {
|
|
2012
|
+
spinner.fail("Failed");
|
|
2013
|
+
return `Error: ${err.message}`;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
async function execute10(input3, config) {
|
|
2017
|
+
const issueNumber = input3["issue_number"];
|
|
2018
|
+
const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2019
|
+
try {
|
|
2020
|
+
const result = await acceptTask(config, issueNumber);
|
|
2021
|
+
spinner.stop();
|
|
2022
|
+
const baseBranch = config.github.baseBranch ?? "main";
|
|
2023
|
+
return `Task #${issueNumber} accepted.
|
|
2024
|
+
PR #${result.prNumber} merged \u2192 ${baseBranch}
|
|
2025
|
+
Issue closed.`;
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
spinner.stop();
|
|
2028
|
+
return `Error: ${err.message}`;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
var terminal10 = true;
|
|
2032
|
+
|
|
2033
|
+
// src/tools/get-task/index.ts
|
|
2034
|
+
var get_task_exports = {};
|
|
2035
|
+
__export(get_task_exports, {
|
|
2036
|
+
definition: () => definition11,
|
|
2037
|
+
execute: () => execute11
|
|
2038
|
+
});
|
|
2039
|
+
init_github();
|
|
2040
|
+
var definition11 = {
|
|
2041
|
+
type: "function",
|
|
2042
|
+
function: {
|
|
2043
|
+
name: "get_task",
|
|
2044
|
+
description: "Get full details of a specific GitHub issue: title, body, status, assignee.",
|
|
2045
|
+
parameters: {
|
|
2046
|
+
type: "object",
|
|
2047
|
+
properties: {
|
|
2048
|
+
issue_number: { type: "number", description: "GitHub issue number" }
|
|
2049
|
+
},
|
|
2050
|
+
required: ["issue_number"]
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
async function execute11(input3, config) {
|
|
2055
|
+
const issue = await getTask(config, input3["issue_number"]);
|
|
2056
|
+
const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
|
|
2057
|
+
const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
|
|
2058
|
+
const lines = [
|
|
2059
|
+
`#${issue.number} [${status}] ${assignee}`,
|
|
2060
|
+
`Title: ${issue.title}`,
|
|
2061
|
+
`URL: ${issue.htmlUrl}`
|
|
2062
|
+
];
|
|
2063
|
+
if (issue.body) lines.push(`
|
|
2064
|
+
${issue.body}`);
|
|
2065
|
+
return lines.join("\n");
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/tools/get-comments/index.ts
|
|
2069
|
+
var get_comments_exports = {};
|
|
2070
|
+
__export(get_comments_exports, {
|
|
2071
|
+
definition: () => definition12,
|
|
2072
|
+
execute: () => execute12
|
|
2073
|
+
});
|
|
2074
|
+
init_github();
|
|
2075
|
+
import ora10 from "ora";
|
|
2076
|
+
var definition12 = {
|
|
2077
|
+
type: "function",
|
|
2078
|
+
function: {
|
|
2079
|
+
name: "get_comments",
|
|
2080
|
+
description: "Get the latest comments on a GitHub issue. Useful for reading rejection feedback.",
|
|
2081
|
+
parameters: {
|
|
2082
|
+
type: "object",
|
|
2083
|
+
properties: {
|
|
2084
|
+
issue_number: { type: "number", description: "GitHub issue number" },
|
|
2085
|
+
limit: { type: "number", description: "Max number of latest comments to return (default 5)" }
|
|
2086
|
+
},
|
|
2087
|
+
required: ["issue_number"]
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
async function execute12(input3, config) {
|
|
2092
|
+
const issueNumber = input3["issue_number"];
|
|
2093
|
+
const limit = input3["limit"] ?? 5;
|
|
2094
|
+
const spinner = ora10(`Loading comments for #${issueNumber}...`).start();
|
|
2095
|
+
try {
|
|
2096
|
+
const comments = await listComments(config, issueNumber, limit);
|
|
2097
|
+
spinner.stop();
|
|
2098
|
+
if (comments.length === 0) return `No comments on issue #${issueNumber}.`;
|
|
2099
|
+
const lines = comments.map((c) => `--- @${c.author} (${c.createdAt.slice(0, 10)}) ---
|
|
2100
|
+
${c.body}`);
|
|
2101
|
+
return `Latest ${comments.length} comment(s) on #${issueNumber}:
|
|
2102
|
+
|
|
2103
|
+
${lines.join("\n\n")}`;
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
spinner.stop();
|
|
2106
|
+
throw err;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// src/tools/get-diff/index.ts
|
|
2111
|
+
var get_diff_exports = {};
|
|
2112
|
+
__export(get_diff_exports, {
|
|
2113
|
+
definition: () => definition13,
|
|
2114
|
+
execute: () => execute13
|
|
2115
|
+
});
|
|
2116
|
+
import ora11 from "ora";
|
|
2117
|
+
var definition13 = {
|
|
2118
|
+
type: "function",
|
|
2119
|
+
function: {
|
|
2120
|
+
name: "get_diff",
|
|
2121
|
+
description: "Get the current git diff: changed files, diff vs HEAD, and any unpushed commits.",
|
|
2122
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
async function execute13(_input, _config) {
|
|
2126
|
+
const spinner = ora11("Reading git diff...").start();
|
|
2127
|
+
try {
|
|
2128
|
+
const diff = await getDiff(_config.github.baseBranch);
|
|
2129
|
+
spinner.stop();
|
|
2130
|
+
return diff;
|
|
2131
|
+
} catch (err) {
|
|
2132
|
+
spinner.stop();
|
|
2133
|
+
throw err;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// src/tools/run-command/index.ts
|
|
2138
|
+
var run_command_exports = {};
|
|
2139
|
+
__export(run_command_exports, {
|
|
2140
|
+
definition: () => definition14,
|
|
2141
|
+
execute: () => execute14
|
|
2142
|
+
});
|
|
2143
|
+
import { exec } from "child_process";
|
|
2144
|
+
import { promisify } from "util";
|
|
2145
|
+
import ora12 from "ora";
|
|
2146
|
+
var execAsync = promisify(exec);
|
|
2147
|
+
var definition14 = {
|
|
2148
|
+
type: "function",
|
|
2149
|
+
function: {
|
|
2150
|
+
name: "run_command",
|
|
2151
|
+
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.",
|
|
2152
|
+
parameters: {
|
|
2153
|
+
type: "object",
|
|
2154
|
+
properties: {
|
|
2155
|
+
command: { type: "string", description: "The shell command to run" }
|
|
2156
|
+
},
|
|
2157
|
+
required: ["command"]
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
async function execute14(input3, _config) {
|
|
2162
|
+
const command = input3["command"];
|
|
2163
|
+
const cwd = process.cwd();
|
|
2164
|
+
const spinner = ora12(`$ ${command}`).start();
|
|
2165
|
+
try {
|
|
2166
|
+
const { stdout, stderr } = await execAsync(command, { cwd, timeout: 6e4, maxBuffer: 1024 * 1024 });
|
|
2167
|
+
spinner.stop();
|
|
2168
|
+
const out = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
2169
|
+
const result = out.length > 5e3 ? out.slice(0, 5e3) + "\n... (truncated)" : out;
|
|
2170
|
+
return result || "(no output)";
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
spinner.stop();
|
|
2173
|
+
const e = err;
|
|
2174
|
+
const out = [e.stdout, e.stderr].filter(Boolean).join("\n").trim();
|
|
2175
|
+
return `Exit ${e.code ?? 1}:
|
|
2176
|
+
${out || e.message}`;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// src/tools/scan-project/index.ts
|
|
2181
|
+
var scan_project_exports = {};
|
|
2182
|
+
__export(scan_project_exports, {
|
|
2183
|
+
definition: () => definition15,
|
|
2184
|
+
execute: () => execute15
|
|
2185
|
+
});
|
|
2186
|
+
import ora13 from "ora";
|
|
2187
|
+
|
|
2188
|
+
// src/lib/project.ts
|
|
2189
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2190
|
+
import { existsSync } from "fs";
|
|
2191
|
+
import path2 from "path";
|
|
2192
|
+
import { globby } from "globby";
|
|
2193
|
+
import ignore from "ignore";
|
|
2194
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2195
|
+
".png",
|
|
2196
|
+
".jpg",
|
|
2197
|
+
".jpeg",
|
|
2198
|
+
".gif",
|
|
2199
|
+
".svg",
|
|
2200
|
+
".ico",
|
|
2201
|
+
".webp",
|
|
2202
|
+
".pdf",
|
|
2203
|
+
".zip",
|
|
2204
|
+
".tar",
|
|
2205
|
+
".gz",
|
|
2206
|
+
".bz2",
|
|
2207
|
+
".rar",
|
|
2208
|
+
".exe",
|
|
2209
|
+
".dll",
|
|
2210
|
+
".so",
|
|
2211
|
+
".dylib",
|
|
2212
|
+
".woff",
|
|
2213
|
+
".woff2",
|
|
2214
|
+
".ttf",
|
|
2215
|
+
".otf",
|
|
2216
|
+
".eot",
|
|
2217
|
+
".mp3",
|
|
2218
|
+
".mp4",
|
|
2219
|
+
".wav",
|
|
2220
|
+
".avi",
|
|
2221
|
+
".mov",
|
|
2222
|
+
".db",
|
|
2223
|
+
".sqlite",
|
|
2224
|
+
".lock"
|
|
2225
|
+
]);
|
|
2226
|
+
var ALWAYS_READ = [
|
|
2227
|
+
"README.md",
|
|
2228
|
+
"README.txt",
|
|
2229
|
+
"README",
|
|
2230
|
+
"package.json",
|
|
2231
|
+
"pyproject.toml",
|
|
2232
|
+
"go.mod",
|
|
2233
|
+
"Cargo.toml",
|
|
2234
|
+
"tsconfig.json",
|
|
2235
|
+
"vite.config.ts",
|
|
2236
|
+
"vite.config.js",
|
|
2237
|
+
"webpack.config.js",
|
|
2238
|
+
"rollup.config.js",
|
|
2239
|
+
".env.example",
|
|
2240
|
+
"docker-compose.yml",
|
|
2241
|
+
"Dockerfile"
|
|
2242
|
+
];
|
|
2243
|
+
var MAX_TOTAL_BYTES = 8e4;
|
|
2244
|
+
var MAX_FILE_BYTES = 15e3;
|
|
2245
|
+
async function buildIgnoreFilter(cwd) {
|
|
2246
|
+
const ig = ignore();
|
|
2247
|
+
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
2248
|
+
if (existsSync(gitignorePath)) {
|
|
2249
|
+
const content = await readFile2(gitignorePath, "utf-8");
|
|
2250
|
+
ig.add(content);
|
|
2251
|
+
}
|
|
2252
|
+
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "*.pyc", "build", "coverage"]);
|
|
2253
|
+
return ig;
|
|
2254
|
+
}
|
|
2255
|
+
async function safeReadFile(filePath, maxBytes = MAX_FILE_BYTES) {
|
|
2256
|
+
try {
|
|
2257
|
+
const content = await readFile2(filePath, "utf-8");
|
|
2258
|
+
if (content.length > maxBytes) {
|
|
2259
|
+
return content.slice(0, maxBytes) + `
|
|
2260
|
+
... (truncated at ${maxBytes} chars)`;
|
|
2261
|
+
}
|
|
2262
|
+
return content;
|
|
2263
|
+
} catch {
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
function isBinaryFile(filePath) {
|
|
2268
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
2269
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
2270
|
+
}
|
|
2271
|
+
function buildFileTree(files) {
|
|
2272
|
+
const tree = {};
|
|
2273
|
+
for (const file of files) {
|
|
2274
|
+
const dir = path2.dirname(file);
|
|
2275
|
+
if (!tree[dir]) tree[dir] = [];
|
|
2276
|
+
tree[dir].push(path2.basename(file));
|
|
2277
|
+
}
|
|
2278
|
+
const lines = [];
|
|
2279
|
+
const rootFiles = tree["."] ?? [];
|
|
2280
|
+
for (const f of rootFiles) lines.push(f);
|
|
2281
|
+
const dirs = Object.keys(tree).filter((d) => d !== ".").sort();
|
|
2282
|
+
for (const dir of dirs) {
|
|
2283
|
+
lines.push(`${dir}/`);
|
|
2284
|
+
for (const f of tree[dir]) {
|
|
2285
|
+
lines.push(` ${f}`);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
return lines.join("\n");
|
|
2289
|
+
}
|
|
2290
|
+
function scoreRelevance(filePath, keywords) {
|
|
2291
|
+
const lower = filePath.toLowerCase();
|
|
2292
|
+
let score = 0;
|
|
2293
|
+
for (const kw of keywords) {
|
|
2294
|
+
if (lower.includes(kw)) score += 1;
|
|
2295
|
+
}
|
|
2296
|
+
return score;
|
|
2297
|
+
}
|
|
2298
|
+
async function buildProjectContext(cwd, issueTitle, issueBody) {
|
|
2299
|
+
const ig = await buildIgnoreFilter(cwd);
|
|
2300
|
+
const allFiles = await globby("**/*", {
|
|
2301
|
+
cwd,
|
|
2302
|
+
gitignore: false,
|
|
2303
|
+
// We handle ignore ourselves
|
|
2304
|
+
dot: false,
|
|
2305
|
+
onlyFiles: true
|
|
2306
|
+
});
|
|
2307
|
+
const filtered = allFiles.filter((f) => !ig.ignores(f) && !isBinaryFile(f));
|
|
2308
|
+
const fileTree = buildFileTree(filtered);
|
|
2309
|
+
const issueText = `${issueTitle} ${issueBody}`.toLowerCase();
|
|
2310
|
+
const keywords = issueText.replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
2311
|
+
const keyFiles = {};
|
|
2312
|
+
let totalBytes = 0;
|
|
2313
|
+
for (const always of ALWAYS_READ) {
|
|
2314
|
+
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
2315
|
+
const fullPath = path2.join(cwd, always);
|
|
2316
|
+
if (!existsSync(fullPath)) continue;
|
|
2317
|
+
const content = await safeReadFile(fullPath);
|
|
2318
|
+
if (content !== null) {
|
|
2319
|
+
keyFiles[always] = content;
|
|
2320
|
+
totalBytes += content.length;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
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);
|
|
2324
|
+
for (const { file } of scored) {
|
|
2325
|
+
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
2326
|
+
const fullPath = path2.join(cwd, file);
|
|
2327
|
+
const content = await safeReadFile(fullPath);
|
|
2328
|
+
if (content !== null) {
|
|
2329
|
+
keyFiles[file] = content;
|
|
2330
|
+
totalBytes += content.length;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
return { fileTree, keyFiles };
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// src/tools/scan-project/index.ts
|
|
2337
|
+
var definition15 = {
|
|
2338
|
+
type: "function",
|
|
2339
|
+
function: {
|
|
2340
|
+
name: "scan_project",
|
|
2341
|
+
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.",
|
|
2342
|
+
parameters: {
|
|
2343
|
+
type: "object",
|
|
2344
|
+
properties: {
|
|
2345
|
+
focus: {
|
|
2346
|
+
type: "string",
|
|
2347
|
+
description: "Keywords describing the task. Used to prioritise which files to read."
|
|
2348
|
+
}
|
|
2349
|
+
},
|
|
2350
|
+
required: []
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
async function execute15(input3, _config) {
|
|
2355
|
+
const focus = input3["focus"] ?? "";
|
|
2356
|
+
const spinner = ora13("Scanning project...").start();
|
|
2357
|
+
try {
|
|
2358
|
+
const cwd = process.cwd();
|
|
2359
|
+
const context = await buildProjectContext(cwd, focus, "");
|
|
2360
|
+
spinner.stop();
|
|
2361
|
+
const fileCount = context.fileTree.split("\n").filter(Boolean).length;
|
|
2362
|
+
const readCount = Object.keys(context.keyFiles).length;
|
|
2363
|
+
const totalBytes = Object.values(context.keyFiles).reduce((s, c) => s + c.length, 0);
|
|
2364
|
+
const summary = `Scanned ${fileCount} files \xB7 ${readCount} read \xB7 ${(totalBytes / 1024).toFixed(1)} KB`;
|
|
2365
|
+
const parts = [summary, `## File tree
|
|
2366
|
+
\`\`\`
|
|
2367
|
+
${context.fileTree}
|
|
2368
|
+
\`\`\``];
|
|
2369
|
+
for (const [filePath, content] of Object.entries(context.keyFiles)) {
|
|
2370
|
+
parts.push(`## ${filePath}
|
|
2371
|
+
\`\`\`
|
|
2372
|
+
${content}
|
|
2373
|
+
\`\`\``);
|
|
2374
|
+
}
|
|
2375
|
+
return parts.join("\n\n");
|
|
2376
|
+
} catch (err) {
|
|
2377
|
+
spinner.stop();
|
|
2378
|
+
throw err;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// src/tools/read-file/index.ts
|
|
2383
|
+
var read_file_exports = {};
|
|
2384
|
+
__export(read_file_exports, {
|
|
2385
|
+
definition: () => definition16,
|
|
2386
|
+
execute: () => execute16
|
|
2387
|
+
});
|
|
2388
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2389
|
+
import path3 from "path";
|
|
2390
|
+
var definition16 = {
|
|
2391
|
+
type: "function",
|
|
2392
|
+
function: {
|
|
2393
|
+
name: "read_file",
|
|
2394
|
+
description: "Read the full contents of a specific file in the project.",
|
|
2395
|
+
parameters: {
|
|
2396
|
+
type: "object",
|
|
2397
|
+
properties: {
|
|
2398
|
+
path: { type: "string", description: "File path relative to the project root" }
|
|
2399
|
+
},
|
|
2400
|
+
required: ["path"]
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
async function execute16(input3, _config) {
|
|
2405
|
+
const filePath = input3["path"];
|
|
2406
|
+
try {
|
|
2407
|
+
const fullPath = path3.join(process.cwd(), filePath);
|
|
2408
|
+
const content = await readFile3(fullPath, "utf-8");
|
|
2409
|
+
return content.length > 15e3 ? content.slice(0, 15e3) + "\n\n... (truncated)" : content;
|
|
2410
|
+
} catch (err) {
|
|
2411
|
+
return `Error reading file: ${err.message}`;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// src/tools/ask-user/index.ts
|
|
2416
|
+
var ask_user_exports = {};
|
|
2417
|
+
__export(ask_user_exports, {
|
|
2418
|
+
definition: () => definition17,
|
|
2419
|
+
execute: () => execute17
|
|
2420
|
+
});
|
|
2421
|
+
import chalk12 from "chalk";
|
|
2422
|
+
import { select as select9, input as promptInput4 } from "@inquirer/prompts";
|
|
2423
|
+
var definition17 = {
|
|
2424
|
+
type: "function",
|
|
2425
|
+
function: {
|
|
2426
|
+
name: "ask_user",
|
|
2427
|
+
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.",
|
|
2428
|
+
parameters: {
|
|
2429
|
+
type: "object",
|
|
2430
|
+
properties: {
|
|
2431
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
2432
|
+
options: {
|
|
2433
|
+
type: "array",
|
|
2434
|
+
items: { type: "string" },
|
|
2435
|
+
description: "2\u20134 concrete answer choices"
|
|
2436
|
+
}
|
|
2437
|
+
},
|
|
2438
|
+
required: ["question", "options"]
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
async function execute17(input3, _config) {
|
|
2443
|
+
const question = input3["question"];
|
|
2444
|
+
const options = input3["options"];
|
|
2445
|
+
const OTHER = "__other__";
|
|
2446
|
+
console.log("");
|
|
2447
|
+
console.log(chalk12.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
|
|
2448
|
+
console.log(chalk12.dim(" \u2502"));
|
|
2449
|
+
for (const line of question.split("\n")) {
|
|
2450
|
+
console.log(chalk12.dim(" \u2502 ") + line);
|
|
2451
|
+
}
|
|
2452
|
+
console.log(chalk12.dim(" \u2514" + "\u2500".repeat(67)));
|
|
2453
|
+
let answer;
|
|
2454
|
+
try {
|
|
2455
|
+
const chosen = await select9({
|
|
2456
|
+
message: " ",
|
|
2457
|
+
choices: [
|
|
2458
|
+
...options.map((o) => ({ name: o, value: o })),
|
|
2459
|
+
{ name: chalk12.dim("Other (describe below)"), value: OTHER }
|
|
2460
|
+
]
|
|
2461
|
+
});
|
|
2462
|
+
answer = chosen === OTHER ? await promptInput4({ message: "Your answer:" }) : chosen;
|
|
2463
|
+
} catch {
|
|
2464
|
+
answer = "User skipped this question \u2014 use your best judgement.";
|
|
2465
|
+
}
|
|
2466
|
+
console.log("");
|
|
2467
|
+
return answer;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
// src/tools/registry.ts
|
|
2471
|
+
var toolModules = [
|
|
2472
|
+
// Command tools
|
|
2473
|
+
pick_exports,
|
|
2474
|
+
new_task_exports,
|
|
2475
|
+
close_exports,
|
|
2476
|
+
submit_exports,
|
|
2477
|
+
my_status_exports,
|
|
2478
|
+
review_exports,
|
|
2479
|
+
refresh_exports,
|
|
2480
|
+
open_code_exports,
|
|
2481
|
+
reject_exports,
|
|
2482
|
+
accept_exports,
|
|
2483
|
+
// Low-level tools
|
|
2484
|
+
get_task_exports,
|
|
2485
|
+
get_comments_exports,
|
|
2486
|
+
get_diff_exports,
|
|
2487
|
+
run_command_exports,
|
|
2488
|
+
scan_project_exports,
|
|
2489
|
+
read_file_exports,
|
|
2490
|
+
ask_user_exports
|
|
2491
|
+
];
|
|
2492
|
+
|
|
2493
|
+
// src/lib/agent.ts
|
|
2494
|
+
var tools = toolModules.map((m) => m.definition);
|
|
2495
|
+
async function executeTool(name, input3, config) {
|
|
2496
|
+
const mod = toolModules.find((m) => m.definition.function.name === name);
|
|
2497
|
+
if (!mod) return `Unknown tool: ${name}`;
|
|
2498
|
+
try {
|
|
2499
|
+
return await mod.execute(input3, config);
|
|
2500
|
+
} catch (err) {
|
|
2501
|
+
return `Error: ${err.message}`;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
async function runAgentLoop(config, messages) {
|
|
2505
|
+
const client = createClient(config);
|
|
2506
|
+
const { owner, repo } = config.github;
|
|
2507
|
+
const systemMessage = {
|
|
2508
|
+
role: "system",
|
|
2509
|
+
content: [
|
|
2510
|
+
"You are Techunter, an AI assistant managing GitHub tasks for a development team.",
|
|
2511
|
+
`Repository: ${owner}/${repo}`,
|
|
2512
|
+
"Respond in the same language the user writes in (Chinese or English).",
|
|
2513
|
+
"For conversational replies be concise. For task guides be thorough and detailed.",
|
|
2514
|
+
"Always use tools when the user requests an action.",
|
|
2515
|
+
"",
|
|
2516
|
+
"## Tool philosophy",
|
|
2517
|
+
"Command tools (pick, new_task, close, submit, my_status, review, refresh, open_code) run",
|
|
2518
|
+
"hardcoded interactive flows \u2014 always use these for user-facing actions.",
|
|
2519
|
+
"Low-level tools are for reasoning: run_command, scan_project, read_file, ask_user,",
|
|
2520
|
+
"get_task, get_comments, get_diff.",
|
|
2521
|
+
"",
|
|
2522
|
+
"## Creating a task",
|
|
2523
|
+
"If the task description is vague, call ask_user to clarify (max 3 times).",
|
|
2524
|
+
"Then call new_task with the title \u2014 the tool scans the project and generates the guide automatically.",
|
|
2525
|
+
"",
|
|
2526
|
+
"## Claiming a task",
|
|
2527
|
+
"Call pick with the issue_number \u2014 the tool handles everything.",
|
|
2528
|
+
"Do NOT scan or generate anything for claiming.",
|
|
2529
|
+
"",
|
|
2530
|
+
"## Rejecting a task",
|
|
2531
|
+
"Call reject with the issue_number \u2014 the tool collects feedback and generates the comment.",
|
|
2532
|
+
"",
|
|
2533
|
+
"## Submitting changes",
|
|
2534
|
+
"When the user asks to submit, deliver, push, or finish their work:",
|
|
2535
|
+
"Call submit directly \u2014 it handles AI review, confirmation, commit, and PR creation.",
|
|
2536
|
+
"",
|
|
2537
|
+
"## Reviewing submitted tasks",
|
|
2538
|
+
"When the user asks to review tasks or check what needs approval:",
|
|
2539
|
+
"1. Call review to list tasks pending your approval.",
|
|
2540
|
+
"2. Call get_task to read full details of the task to review.",
|
|
2541
|
+
"3. Call get_comments to read the implementation guide and any discussion.",
|
|
2542
|
+
"4. To approve: call close with the issue_number.",
|
|
2543
|
+
"5. To reject: write a structured rejection comment and call reject."
|
|
2544
|
+
].join("\n")
|
|
2545
|
+
};
|
|
2546
|
+
const MAX_ITERATIONS = 30;
|
|
2547
|
+
let iterations = 0;
|
|
2548
|
+
for (; ; ) {
|
|
2549
|
+
if (++iterations > MAX_ITERATIONS) {
|
|
2550
|
+
throw new Error(`Agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
2551
|
+
}
|
|
2552
|
+
const isWindows = process.platform === "win32";
|
|
2553
|
+
const spinner = isWindows ? null : ora14({ text: chalk13.dim("Thinking\u2026"), color: "cyan" }).start();
|
|
2554
|
+
if (isWindows) process.stdout.write(chalk13.dim(" Thinking\u2026"));
|
|
2555
|
+
let response;
|
|
2556
|
+
try {
|
|
2557
|
+
response = await client.chat.completions.create({
|
|
2558
|
+
model: getModel(config),
|
|
2559
|
+
tools,
|
|
2560
|
+
messages: [systemMessage, ...messages]
|
|
2561
|
+
});
|
|
2562
|
+
} catch (err) {
|
|
2563
|
+
if (spinner) spinner.stop();
|
|
2564
|
+
else process.stdout.write("\r" + " ".repeat(14) + "\r");
|
|
2565
|
+
throw err;
|
|
2566
|
+
}
|
|
2567
|
+
if (spinner) spinner.stop();
|
|
2568
|
+
else process.stdout.write("\r" + " ".repeat(14) + "\r");
|
|
2569
|
+
const choice = response.choices[0];
|
|
2570
|
+
const assistantMessage = {
|
|
2571
|
+
role: "assistant",
|
|
2572
|
+
content: choice.message.content ?? null,
|
|
2573
|
+
...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
|
|
2574
|
+
};
|
|
2575
|
+
messages.push(assistantMessage);
|
|
2576
|
+
if (choice.finish_reason === "stop") {
|
|
2577
|
+
return choice.message.content ?? "";
|
|
2578
|
+
}
|
|
2579
|
+
if (choice.finish_reason === "tool_calls") {
|
|
2580
|
+
const toolCalls = choice.message.tool_calls ?? [];
|
|
2581
|
+
for (const tc of toolCalls) {
|
|
2582
|
+
let parsed;
|
|
2583
|
+
try {
|
|
2584
|
+
parsed = JSON.parse(tc.function.arguments);
|
|
2585
|
+
} catch {
|
|
2586
|
+
parsed = {};
|
|
2587
|
+
}
|
|
2588
|
+
printToolCall(tc.function.name, parsed);
|
|
2589
|
+
}
|
|
2590
|
+
const results = await Promise.all(
|
|
2591
|
+
toolCalls.map((tc) => {
|
|
2592
|
+
let parsed;
|
|
2593
|
+
try {
|
|
2594
|
+
parsed = JSON.parse(tc.function.arguments);
|
|
2595
|
+
} catch {
|
|
2596
|
+
parsed = {};
|
|
2597
|
+
}
|
|
2598
|
+
return executeTool(tc.function.name, parsed, config);
|
|
2599
|
+
})
|
|
2600
|
+
);
|
|
2601
|
+
let terminal11 = false;
|
|
2602
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
2603
|
+
printToolResult(results[i]);
|
|
2604
|
+
messages.push({
|
|
2605
|
+
role: "tool",
|
|
2606
|
+
tool_call_id: toolCalls[i].id,
|
|
2607
|
+
content: results[i]
|
|
2608
|
+
});
|
|
2609
|
+
if (toolModules.find((m) => m.definition.function.name === toolCalls[i].function.name)?.terminal) {
|
|
2610
|
+
terminal11 = true;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
if (terminal11) return results[results.length - 1];
|
|
2614
|
+
} else {
|
|
2615
|
+
return choice.message.content ?? "";
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
// src/index.ts
|
|
2621
|
+
var _require = createRequire(import.meta.url);
|
|
2622
|
+
var { version } = _require("../package.json");
|
|
2623
|
+
var SLASH_NAMES = [
|
|
2624
|
+
"/help",
|
|
2625
|
+
"/h",
|
|
2626
|
+
"/refresh",
|
|
2627
|
+
"/r",
|
|
2628
|
+
"/pick",
|
|
2629
|
+
"/p",
|
|
2630
|
+
"/new",
|
|
2631
|
+
"/n",
|
|
2632
|
+
"/submit",
|
|
2633
|
+
"/s",
|
|
2634
|
+
"/close",
|
|
2635
|
+
"/d",
|
|
2636
|
+
"/review",
|
|
2637
|
+
"/rv",
|
|
2638
|
+
"/accept",
|
|
2639
|
+
"/ac",
|
|
2640
|
+
"/status",
|
|
2641
|
+
"/me",
|
|
2642
|
+
"/code",
|
|
2643
|
+
"/c",
|
|
2644
|
+
"/config",
|
|
2645
|
+
"/cfg"
|
|
2646
|
+
];
|
|
2647
|
+
function completer(line) {
|
|
2648
|
+
if (line.startsWith("/")) {
|
|
2649
|
+
const hits = SLASH_NAMES.filter((c) => c.startsWith(line));
|
|
2650
|
+
return [hits.length ? hits : SLASH_NAMES, line];
|
|
2651
|
+
}
|
|
2652
|
+
return [[], line];
|
|
2653
|
+
}
|
|
2654
|
+
var _rl = null;
|
|
2655
|
+
function promptUser() {
|
|
2656
|
+
return new Promise((resolve) => {
|
|
2657
|
+
_rl.question(chalk14.cyan("You") + chalk14.dim(" \u203A "), resolve);
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
var COMMANDS = [
|
|
2661
|
+
{ cmd: "/help", alias: "/h", desc: "Show available commands" },
|
|
2662
|
+
{ cmd: "/refresh", alias: "/r", desc: "Reload the task list" },
|
|
2663
|
+
{ cmd: "/pick", alias: "/p", desc: "Browse tasks and view details" },
|
|
2664
|
+
{ cmd: "/new", alias: "/n", desc: "Create a new task interactively" },
|
|
2665
|
+
{ cmd: "/close", alias: "/d", desc: "Close (delete) a task" },
|
|
2666
|
+
{ cmd: "/submit", alias: "/s", desc: "Commit, create PR, and mark in-review" },
|
|
2667
|
+
{ cmd: "/review", alias: "/rv", desc: "List tasks waiting for your approval" },
|
|
2668
|
+
{ cmd: "/accept", alias: "/ac", desc: "Accept a reviewed task: merge PR and close issue" },
|
|
2669
|
+
{ cmd: "/config", alias: "/cfg", desc: "Change settings (branch, repo, API keys)" },
|
|
2670
|
+
{ cmd: "/status", alias: "/me", desc: "Show tasks assigned to you" },
|
|
2671
|
+
{ cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" }
|
|
2672
|
+
];
|
|
2673
|
+
function cmdHelp() {
|
|
2674
|
+
console.log("");
|
|
2675
|
+
console.log(chalk14.bold(" Commands"));
|
|
2676
|
+
console.log(chalk14.dim(" \u2500".repeat(35)));
|
|
2677
|
+
for (const { cmd, alias, desc } of COMMANDS) {
|
|
2678
|
+
const left = (cmd + (alias ? ` ${chalk14.dim(alias)}` : "")).padEnd(22);
|
|
2679
|
+
console.log(` ${chalk14.cyan(cmd)}${alias ? " " + chalk14.dim(alias) : ""}`.padEnd(30) + chalk14.dim(desc));
|
|
2680
|
+
}
|
|
2681
|
+
console.log(chalk14.dim("\n Anything else is sent to the AI agent.\n"));
|
|
2682
|
+
}
|
|
2683
|
+
function printBanner(config) {
|
|
2684
|
+
const { owner, repo } = config.github;
|
|
2685
|
+
const g = chalk14.cyan;
|
|
2686
|
+
const b = chalk14.bold.white;
|
|
2687
|
+
const p = chalk14.yellow.bold;
|
|
2688
|
+
console.log("");
|
|
2689
|
+
console.log(" " + g("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
2690
|
+
console.log(p("\u25C6") + b("\u2550\u2550\u2550") + g("\u256C") + b(" TECHUNTER ") + g("\u256C") + b("\u2550\u2550\u2550\u25B6"));
|
|
2691
|
+
console.log(" " + g("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
2692
|
+
console.log("");
|
|
2693
|
+
console.log(
|
|
2694
|
+
" " + chalk14.bold("Techunter") + chalk14.dim(` v${version}`) + chalk14.dim(" \xB7 ") + chalk14.cyan("GLM-5") + chalk14.dim(" \xB7 zai-org") + chalk14.dim(" \xB7 ") + chalk14.dim(`${owner}/${repo}`)
|
|
2695
|
+
);
|
|
2696
|
+
console.log("");
|
|
2697
|
+
}
|
|
2698
|
+
async function main() {
|
|
2699
|
+
const args = process.argv.slice(2);
|
|
2700
|
+
if (args[0] === "config") {
|
|
2701
|
+
try {
|
|
2702
|
+
await configCommand();
|
|
2703
|
+
} catch (err) {
|
|
2704
|
+
console.error(chalk14.red(`
|
|
2705
|
+
Error: ${err.message}`));
|
|
2706
|
+
process.exit(1);
|
|
2707
|
+
}
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
let config;
|
|
2711
|
+
try {
|
|
2712
|
+
config = getConfig();
|
|
2713
|
+
} catch {
|
|
2714
|
+
try {
|
|
2715
|
+
await initCommand();
|
|
2716
|
+
config = getConfig();
|
|
2717
|
+
} catch (err) {
|
|
2718
|
+
console.error(chalk14.red(`
|
|
2719
|
+
Setup failed: ${err.message}`));
|
|
2720
|
+
process.exit(1);
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
const remoteUrl = await getRemoteUrl();
|
|
2725
|
+
if (remoteUrl) {
|
|
2726
|
+
const detected = parseOwnerRepo(remoteUrl);
|
|
2727
|
+
if (detected) {
|
|
2728
|
+
const isNewRepo = detected.owner !== config.github.owner || detected.repo !== config.github.repo;
|
|
2729
|
+
config = { ...config, github: { ...config.github, owner: detected.owner, repo: detected.repo, baseBranch: void 0 } };
|
|
2730
|
+
if (isNewRepo) {
|
|
2731
|
+
console.log(chalk14.dim(` Repo: ${detected.owner}/${detected.repo}`));
|
|
2732
|
+
ensureLabels(config).catch(() => {
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
} else if (!config.github.owner) {
|
|
2737
|
+
console.error(chalk14.red("\nNo git remote found and no repo configured. Run tch init."));
|
|
2738
|
+
process.exit(1);
|
|
2739
|
+
}
|
|
2740
|
+
printBanner(config);
|
|
2741
|
+
console.log(chalk14.dim(" Type /help for commands, or describe what you want.\n"));
|
|
2742
|
+
await printTaskList(config);
|
|
2743
|
+
await printMyTasks(config);
|
|
2744
|
+
_rl = readline.createInterface({
|
|
2745
|
+
input: process.stdin,
|
|
2746
|
+
output: process.stdout,
|
|
2747
|
+
completer,
|
|
2748
|
+
terminal: true
|
|
2749
|
+
});
|
|
2750
|
+
_rl.on("close", () => {
|
|
2751
|
+
console.log(chalk14.gray("\nGoodbye!"));
|
|
2752
|
+
process.exit(0);
|
|
2753
|
+
});
|
|
2754
|
+
_rl.on("SIGINT", () => {
|
|
2755
|
+
console.log(chalk14.gray("\nGoodbye!"));
|
|
2756
|
+
process.exit(0);
|
|
2757
|
+
});
|
|
2758
|
+
const messages = [];
|
|
2759
|
+
for (; ; ) {
|
|
2760
|
+
const userInput = (await promptUser()).trim();
|
|
2761
|
+
_rl.pause();
|
|
2762
|
+
if (!userInput) continue;
|
|
2763
|
+
const cmd = userInput.split(/\s+/)[0].toLowerCase();
|
|
2764
|
+
if (cmd.startsWith("/")) {
|
|
2765
|
+
switch (cmd) {
|
|
2766
|
+
case "/help":
|
|
2767
|
+
case "/h":
|
|
2768
|
+
cmdHelp();
|
|
2769
|
+
break;
|
|
2770
|
+
case "/refresh":
|
|
2771
|
+
case "/r":
|
|
2772
|
+
await run7(config);
|
|
2773
|
+
break;
|
|
2774
|
+
case "/pick":
|
|
2775
|
+
case "/p": {
|
|
2776
|
+
const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
|
|
2777
|
+
const preselected = arg ? parseInt(arg, 10) : void 0;
|
|
2778
|
+
const result = await run3(config, Number.isNaN(preselected) ? void 0 : preselected);
|
|
2779
|
+
if (result && result !== "Cancelled.") {
|
|
2780
|
+
console.log(chalk14.green(`
|
|
2781
|
+
${result}
|
|
2782
|
+
`));
|
|
2783
|
+
}
|
|
2784
|
+
await printTaskList(config);
|
|
2785
|
+
break;
|
|
2786
|
+
}
|
|
2787
|
+
case "/new":
|
|
2788
|
+
case "/n": {
|
|
2789
|
+
const result = await run4(config);
|
|
2790
|
+
console.log(chalk14.green(`
|
|
2791
|
+
${result}
|
|
2792
|
+
`));
|
|
2793
|
+
await printTaskList(config);
|
|
2794
|
+
break;
|
|
2795
|
+
}
|
|
2796
|
+
case "/submit":
|
|
2797
|
+
case "/s": {
|
|
2798
|
+
const result = await run(config);
|
|
2799
|
+
console.log("\n" + renderMarkdown(result));
|
|
2800
|
+
await printTaskList(config);
|
|
2801
|
+
break;
|
|
2802
|
+
}
|
|
2803
|
+
case "/close":
|
|
2804
|
+
case "/d": {
|
|
2805
|
+
const result = await run2(config);
|
|
2806
|
+
console.log(chalk14.green(`
|
|
2807
|
+
${result}
|
|
2808
|
+
`));
|
|
2809
|
+
await printTaskList(config);
|
|
2810
|
+
break;
|
|
2811
|
+
}
|
|
2812
|
+
case "/review":
|
|
2813
|
+
case "/rv": {
|
|
2814
|
+
const result = await run6(config);
|
|
2815
|
+
console.log("\n" + renderMarkdown(result));
|
|
2816
|
+
break;
|
|
2817
|
+
}
|
|
2818
|
+
case "/status":
|
|
2819
|
+
case "/me": {
|
|
2820
|
+
const result = await run5(config);
|
|
2821
|
+
console.log("\n" + renderMarkdown(result));
|
|
2822
|
+
break;
|
|
2823
|
+
}
|
|
2824
|
+
case "/accept":
|
|
2825
|
+
case "/ac": {
|
|
2826
|
+
const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
|
|
2827
|
+
const preselected = arg ? parseInt(arg, 10) : void 0;
|
|
2828
|
+
const result = await run10(config, Number.isNaN(preselected) ? void 0 : { issue_number: preselected });
|
|
2829
|
+
console.log(chalk14.green(`
|
|
2830
|
+
${result}
|
|
2831
|
+
`));
|
|
2832
|
+
await printTaskList(config);
|
|
2833
|
+
break;
|
|
2834
|
+
}
|
|
2835
|
+
case "/config":
|
|
2836
|
+
case "/cfg":
|
|
2837
|
+
await configCommand();
|
|
2838
|
+
break;
|
|
2839
|
+
case "/code":
|
|
2840
|
+
case "/c":
|
|
2841
|
+
await run8(config);
|
|
2842
|
+
break;
|
|
2843
|
+
default:
|
|
2844
|
+
console.log(chalk14.yellow(` Unknown command: ${cmd} (try /help)`));
|
|
2845
|
+
}
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2848
|
+
const prevLength = messages.length;
|
|
2849
|
+
messages.push({ role: "user", content: userInput });
|
|
2850
|
+
try {
|
|
2851
|
+
const response = await runAgentLoop(config, messages);
|
|
2852
|
+
console.log("\n" + chalk14.green("Techunter:") + "\n" + renderMarkdown(response));
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
messages.splice(prevLength);
|
|
2855
|
+
console.error(chalk14.red(`
|
|
2856
|
+
Error: ${err.message}
|
|
2857
|
+
`));
|
|
2858
|
+
}
|
|
2859
|
+
await printTaskList(config);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
main().catch((err) => {
|
|
2863
|
+
console.error(chalk14.red(`Fatal: ${err.message}`));
|
|
2864
|
+
process.exit(1);
|
|
2865
|
+
});
|