techunter 0.1.10 → 1.0.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/README.md +237 -209
- package/dist/index.js +1913 -1109
- package/dist/mcp.js +1228 -557
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,12 +39,20 @@ __export(github_exports, {
|
|
|
39
39
|
createTask: () => createTask,
|
|
40
40
|
editTask: () => editTask,
|
|
41
41
|
embedBaseCommit: () => embedBaseCommit,
|
|
42
|
+
embedTargetBranch: () => embedTargetBranch,
|
|
42
43
|
ensureLabels: () => ensureLabels,
|
|
44
|
+
ensureRemoteBranch: () => ensureRemoteBranch,
|
|
43
45
|
extractBaseCommit: () => extractBaseCommit,
|
|
46
|
+
extractTargetBranch: () => extractTargetBranch,
|
|
44
47
|
formatGuideAsMarkdown: () => formatGuideAsMarkdown,
|
|
45
48
|
getAuthenticatedUser: () => getAuthenticatedUser,
|
|
46
49
|
getDefaultBranch: () => getDefaultBranch,
|
|
50
|
+
getIssueNumberFromBranch: () => getIssueNumberFromBranch,
|
|
51
|
+
getOpenSubtasks: () => getOpenSubtasks,
|
|
52
|
+
getRepoFile: () => getRepoFile,
|
|
47
53
|
getTask: () => getTask,
|
|
54
|
+
getTaskPR: () => getTaskPR,
|
|
55
|
+
getTaskPRDiff: () => getTaskPRDiff,
|
|
48
56
|
isCollaborator: () => isCollaborator,
|
|
49
57
|
listComments: () => listComments,
|
|
50
58
|
listMyTasks: () => listMyTasks,
|
|
@@ -54,7 +62,8 @@ __export(github_exports, {
|
|
|
54
62
|
mergeWorkerIntoBase: () => mergeWorkerIntoBase,
|
|
55
63
|
postComment: () => postComment,
|
|
56
64
|
postGuideComment: () => postGuideComment,
|
|
57
|
-
rejectTask: () => rejectTask
|
|
65
|
+
rejectTask: () => rejectTask,
|
|
66
|
+
upsertRepoFile: () => upsertRepoFile
|
|
58
67
|
});
|
|
59
68
|
import { Octokit } from "@octokit/rest";
|
|
60
69
|
import { fetch as undiciFetch } from "undici";
|
|
@@ -117,11 +126,22 @@ function extractBaseCommit(body) {
|
|
|
117
126
|
const match = body.match(/<!-- techunter-base:([a-f0-9]{7,40}) -->/);
|
|
118
127
|
return match?.[1] ?? null;
|
|
119
128
|
}
|
|
120
|
-
|
|
129
|
+
function embedTargetBranch(body, branch) {
|
|
130
|
+
return `${body}
|
|
131
|
+
${TARGET_BRANCH_MARKER}${branch} -->`;
|
|
132
|
+
}
|
|
133
|
+
function extractTargetBranch(body) {
|
|
134
|
+
if (!body) return null;
|
|
135
|
+
const match = body.match(/<!-- techunter-target:([^\s>]+) -->/);
|
|
136
|
+
return match?.[1] ?? null;
|
|
137
|
+
}
|
|
138
|
+
async function createTask(config, title, body, baseCommit, targetBranch) {
|
|
121
139
|
const octokit = createOctokit(config.githubToken);
|
|
122
140
|
const { owner, repo } = config.github;
|
|
123
141
|
await ensureLabels(config);
|
|
124
|
-
|
|
142
|
+
let finalBody = body ?? "";
|
|
143
|
+
if (baseCommit) finalBody = embedBaseCommit(finalBody, baseCommit);
|
|
144
|
+
if (targetBranch) finalBody = embedTargetBranch(finalBody, targetBranch);
|
|
125
145
|
const { data } = await octokit.issues.create({
|
|
126
146
|
owner,
|
|
127
147
|
repo,
|
|
@@ -134,13 +154,22 @@ async function createTask(config, title, body, baseCommit) {
|
|
|
134
154
|
async function mergeWorkerIntoBase(config, workerBranch, baseBranch) {
|
|
135
155
|
const octokit = createOctokit(config.githubToken);
|
|
136
156
|
const { owner, repo } = config.github;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
try {
|
|
158
|
+
await octokit.repos.merge({
|
|
159
|
+
owner,
|
|
160
|
+
repo,
|
|
161
|
+
base: baseBranch,
|
|
162
|
+
head: workerBranch,
|
|
163
|
+
commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (err.status === 409) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Merge conflict: ${workerBranch} cannot be merged into ${baseBranch} cleanly. Resolve conflicts manually.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
144
173
|
}
|
|
145
174
|
async function claimTask(config, number, username) {
|
|
146
175
|
const octokit = createOctokit(config.githubToken);
|
|
@@ -216,6 +245,23 @@ async function postGuideComment(config, number, guide) {
|
|
|
216
245
|
body
|
|
217
246
|
});
|
|
218
247
|
}
|
|
248
|
+
async function ensureRemoteBranch(config, branchName, fallbackBase) {
|
|
249
|
+
const octokit = createOctokit(config.githubToken);
|
|
250
|
+
const { owner, repo } = config.github;
|
|
251
|
+
try {
|
|
252
|
+
await octokit.repos.getBranch({ owner, repo, branch: branchName });
|
|
253
|
+
return;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err.status !== 404) throw err;
|
|
256
|
+
}
|
|
257
|
+
const { data: baseRef } = await octokit.repos.getBranch({ owner, repo, branch: fallbackBase });
|
|
258
|
+
await octokit.git.createRef({
|
|
259
|
+
owner,
|
|
260
|
+
repo,
|
|
261
|
+
ref: `refs/heads/${branchName}`,
|
|
262
|
+
sha: baseRef.commit.sha
|
|
263
|
+
});
|
|
264
|
+
}
|
|
219
265
|
async function createPR(config, title, body, branch, base) {
|
|
220
266
|
const octokit = createOctokit(config.githubToken);
|
|
221
267
|
const { owner, repo } = config.github;
|
|
@@ -232,14 +278,11 @@ async function createPR(config, title, body, branch, base) {
|
|
|
232
278
|
async function markInReview(config, number) {
|
|
233
279
|
const octokit = createOctokit(config.githubToken);
|
|
234
280
|
const { owner, repo } = config.github;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
owner,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
name: LABEL_CLAIMED
|
|
241
|
-
});
|
|
242
|
-
} catch {
|
|
281
|
+
for (const label of [LABEL_CLAIMED, LABEL_CHANGES_NEEDED]) {
|
|
282
|
+
try {
|
|
283
|
+
await octokit.issues.removeLabel({ owner, repo, issue_number: number, name: label });
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
243
286
|
}
|
|
244
287
|
await octokit.issues.addLabels({
|
|
245
288
|
owner,
|
|
@@ -354,28 +397,115 @@ async function editTask(config, number, title, body) {
|
|
|
354
397
|
const { owner, repo } = config.github;
|
|
355
398
|
await octokit.issues.update({ owner, repo, issue_number: number, title, body });
|
|
356
399
|
}
|
|
400
|
+
async function upsertRepoFile(config, filePath, content, message) {
|
|
401
|
+
const octokit = createOctokit(config.githubToken);
|
|
402
|
+
const { owner, repo } = config.github;
|
|
403
|
+
let sha;
|
|
404
|
+
try {
|
|
405
|
+
const { data: data2 } = await octokit.repos.getContent({ owner, repo, path: filePath });
|
|
406
|
+
if (!Array.isArray(data2) && data2.type === "file") {
|
|
407
|
+
sha = data2.sha;
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
const { data } = await octokit.repos.createOrUpdateFileContents({
|
|
412
|
+
owner,
|
|
413
|
+
repo,
|
|
414
|
+
path: filePath,
|
|
415
|
+
message,
|
|
416
|
+
content: Buffer.from(content, "utf-8").toString("base64"),
|
|
417
|
+
...sha ? { sha } : {}
|
|
418
|
+
});
|
|
419
|
+
return data.content?.html_url ?? `https://github.com/${owner}/${repo}/blob/main/${filePath}`;
|
|
420
|
+
}
|
|
421
|
+
async function getRepoFile(config, filePath) {
|
|
422
|
+
const octokit = createOctokit(config.githubToken);
|
|
423
|
+
const { owner, repo } = config.github;
|
|
424
|
+
try {
|
|
425
|
+
const { data } = await octokit.repos.getContent({ owner, repo, path: filePath });
|
|
426
|
+
if (!Array.isArray(data) && data.type === "file" && "content" in data) {
|
|
427
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
357
434
|
async function getDefaultBranch(config) {
|
|
358
435
|
const octokit = createOctokit(config.githubToken);
|
|
359
436
|
const { owner, repo } = config.github;
|
|
360
437
|
const { data } = await octokit.repos.get({ owner, repo });
|
|
361
438
|
return data.default_branch;
|
|
362
439
|
}
|
|
363
|
-
async function
|
|
440
|
+
async function getTaskPR(config, issueNumber) {
|
|
364
441
|
const octokit = createOctokit(config.githubToken);
|
|
365
442
|
const { owner, repo } = config.github;
|
|
366
443
|
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
367
|
-
const pr =
|
|
368
|
-
|
|
369
|
-
|
|
444
|
+
const pr = prs.find(
|
|
445
|
+
(p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
|
|
446
|
+
);
|
|
447
|
+
if (!pr) return null;
|
|
448
|
+
return { number: pr.number, url: pr.html_url, body: pr.body ?? "", baseBranch: pr.base.ref };
|
|
449
|
+
}
|
|
450
|
+
async function getOpenSubtasks(config, targetBranch) {
|
|
451
|
+
const octokit = createOctokit(config.githubToken);
|
|
452
|
+
const { owner, repo } = config.github;
|
|
453
|
+
const { data } = await octokit.issues.listForRepo({
|
|
454
|
+
owner,
|
|
455
|
+
repo,
|
|
456
|
+
state: "open",
|
|
457
|
+
per_page: 100
|
|
458
|
+
});
|
|
459
|
+
return data.filter((issue) => !issue.pull_request).filter((issue) => extractTargetBranch(issue.body ?? null) === targetBranch).map((issue) => issue.number);
|
|
460
|
+
}
|
|
461
|
+
async function getIssueNumberFromBranch(config, branch) {
|
|
462
|
+
const octokit = createOctokit(config.githubToken);
|
|
463
|
+
const { owner, repo } = config.github;
|
|
464
|
+
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
465
|
+
const pr = prs.find((p) => p.head.ref === branch);
|
|
466
|
+
if (!pr) return null;
|
|
467
|
+
const match = (pr.body ?? "").match(/Closes #(\d+)/i);
|
|
468
|
+
if (!match) return null;
|
|
469
|
+
return { issueNumber: parseInt(match[1], 10), prUrl: pr.html_url };
|
|
470
|
+
}
|
|
471
|
+
async function getTaskPRDiff(config, prNumber) {
|
|
472
|
+
const octokit = createOctokit(config.githubToken);
|
|
473
|
+
const { owner, repo } = config.github;
|
|
474
|
+
const response = await octokit.pulls.get({
|
|
370
475
|
owner,
|
|
371
476
|
repo,
|
|
372
|
-
pull_number:
|
|
373
|
-
|
|
477
|
+
pull_number: prNumber,
|
|
478
|
+
mediaType: { format: "diff" }
|
|
374
479
|
});
|
|
375
|
-
|
|
376
|
-
|
|
480
|
+
return response.data;
|
|
481
|
+
}
|
|
482
|
+
async function acceptTask(config, issueNumber) {
|
|
483
|
+
const octokit = createOctokit(config.githubToken);
|
|
484
|
+
const { owner, repo } = config.github;
|
|
485
|
+
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
486
|
+
const pr = prs.find(
|
|
487
|
+
(p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
|
|
488
|
+
);
|
|
489
|
+
if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
|
|
490
|
+
try {
|
|
491
|
+
const { data: merge } = await octokit.pulls.merge({
|
|
492
|
+
owner,
|
|
493
|
+
repo,
|
|
494
|
+
pull_number: pr.number,
|
|
495
|
+
merge_method: "merge"
|
|
496
|
+
});
|
|
497
|
+
await closeTask(config, issueNumber);
|
|
498
|
+
return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "", baseBranch: pr.base.ref };
|
|
499
|
+
} catch (err) {
|
|
500
|
+
if (err.status === 405) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`PR #${pr.number} cannot be merged \u2014 may have conflicts or is not in a mergeable state.`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
377
507
|
}
|
|
378
|
-
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER;
|
|
508
|
+
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER, TARGET_BRANCH_MARKER;
|
|
379
509
|
var init_github = __esm({
|
|
380
510
|
"src/lib/github.ts"() {
|
|
381
511
|
"use strict";
|
|
@@ -392,19 +522,20 @@ var init_github = __esm({
|
|
|
392
522
|
];
|
|
393
523
|
TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
|
|
394
524
|
BASE_COMMIT_MARKER = "<!-- techunter-base:";
|
|
525
|
+
TARGET_BRANCH_MARKER = "<!-- techunter-target:";
|
|
395
526
|
}
|
|
396
527
|
});
|
|
397
528
|
|
|
398
529
|
// src/index.ts
|
|
399
|
-
import
|
|
530
|
+
import chalk17 from "chalk";
|
|
400
531
|
import readline from "readline";
|
|
401
532
|
import { createRequire } from "module";
|
|
402
533
|
|
|
403
534
|
// src/commands/init.ts
|
|
404
|
-
import { input, password, select } from "@inquirer/prompts";
|
|
405
|
-
import
|
|
406
|
-
import
|
|
407
|
-
import
|
|
535
|
+
import { input, password, select as select11 } from "@inquirer/prompts";
|
|
536
|
+
import chalk13 from "chalk";
|
|
537
|
+
import ora14 from "ora";
|
|
538
|
+
import open2 from "open";
|
|
408
539
|
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
|
|
409
540
|
|
|
410
541
|
// src/lib/config.ts
|
|
@@ -423,7 +554,8 @@ var configSchema = z.object({
|
|
|
423
554
|
}),
|
|
424
555
|
taskState: z.object({
|
|
425
556
|
activeIssueNumber: z.number().optional(),
|
|
426
|
-
baseCommit: z.string().optional()
|
|
557
|
+
baseCommit: z.string().optional(),
|
|
558
|
+
activeBranch: z.string().optional()
|
|
427
559
|
}).optional()
|
|
428
560
|
});
|
|
429
561
|
var store = new Conf({
|
|
@@ -510,14 +642,21 @@ function parseOwnerRepo(remoteUrl) {
|
|
|
510
642
|
}
|
|
511
643
|
return null;
|
|
512
644
|
}
|
|
513
|
-
function makeBranchName(issueNumber, username) {
|
|
514
|
-
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
515
|
-
return `task-${issueNumber}-${slug}`;
|
|
516
|
-
}
|
|
517
645
|
function makeWorkerBranchName(username) {
|
|
518
646
|
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
519
647
|
return `worker-${slug}`;
|
|
520
648
|
}
|
|
649
|
+
function makeTaskBranchName(issueNumber, username) {
|
|
650
|
+
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
651
|
+
return `task-${issueNumber}-${slug}`;
|
|
652
|
+
}
|
|
653
|
+
function isTaskBranch(branch) {
|
|
654
|
+
return /^task-\d+-/.test(branch);
|
|
655
|
+
}
|
|
656
|
+
function parseIssueNumberFromBranch(branch) {
|
|
657
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
658
|
+
return match ? parseInt(match[1], 10) : null;
|
|
659
|
+
}
|
|
521
660
|
async function getCurrentCommit() {
|
|
522
661
|
return (await git.revparse(["HEAD"])).trim();
|
|
523
662
|
}
|
|
@@ -638,16 +777,27 @@ async function getRemoteHeadSha(baseBranch) {
|
|
|
638
777
|
await git.fetch("origin", baseBranch);
|
|
639
778
|
return (await git.revparse([`origin/${baseBranch}`])).trim();
|
|
640
779
|
}
|
|
641
|
-
async function
|
|
642
|
-
const branches = await git.branch();
|
|
643
|
-
const
|
|
644
|
-
|
|
780
|
+
async function checkoutFromCommit(branchName, sha) {
|
|
781
|
+
const branches = await git.branch(["-a"]);
|
|
782
|
+
const exists = Object.keys(branches.branches).some(
|
|
783
|
+
(b) => b === branchName || b === `remotes/origin/${branchName}`
|
|
784
|
+
);
|
|
785
|
+
if (exists) {
|
|
645
786
|
await git.checkout(branchName);
|
|
646
|
-
await git.reset(["--hard", sha]);
|
|
647
787
|
} else {
|
|
648
788
|
await git.checkoutBranch(branchName, sha);
|
|
649
789
|
}
|
|
650
790
|
}
|
|
791
|
+
async function hasUncommittedChanges() {
|
|
792
|
+
const status = await git.status();
|
|
793
|
+
return !status.isClean();
|
|
794
|
+
}
|
|
795
|
+
async function stash(message) {
|
|
796
|
+
await git.stash(["push", "-u", "-m", message]);
|
|
797
|
+
}
|
|
798
|
+
async function stashPop() {
|
|
799
|
+
await git.stash(["pop"]);
|
|
800
|
+
}
|
|
651
801
|
|
|
652
802
|
// src/lib/client.ts
|
|
653
803
|
init_proxy();
|
|
@@ -665,349 +815,165 @@ function getModel(config) {
|
|
|
665
815
|
return config.aiModel ?? DEFAULT_MODEL;
|
|
666
816
|
}
|
|
667
817
|
|
|
668
|
-
// src/
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
818
|
+
// src/tools/pick/index.ts
|
|
819
|
+
var pick_exports = {};
|
|
820
|
+
__export(pick_exports, {
|
|
821
|
+
definition: () => definition3,
|
|
822
|
+
execute: () => execute3,
|
|
823
|
+
run: () => run3,
|
|
824
|
+
terminal: () => terminal3
|
|
825
|
+
});
|
|
826
|
+
init_github();
|
|
827
|
+
import chalk5 from "chalk";
|
|
828
|
+
import ora3 from "ora";
|
|
829
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
830
|
+
init_github();
|
|
831
|
+
|
|
832
|
+
// src/lib/markdown.ts
|
|
833
|
+
import { marked } from "marked";
|
|
834
|
+
import { markedTerminal } from "marked-terminal";
|
|
835
|
+
marked.use(markedTerminal({ showSectionPrefix: false }));
|
|
836
|
+
function renderMarkdown(text) {
|
|
837
|
+
return marked(text);
|
|
677
838
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
839
|
+
|
|
840
|
+
// src/lib/display.ts
|
|
841
|
+
init_github();
|
|
842
|
+
import chalk2 from "chalk";
|
|
843
|
+
var LABEL_AVAILABLE2 = "techunter:available";
|
|
844
|
+
var LABEL_CLAIMED2 = "techunter:claimed";
|
|
845
|
+
var LABEL_IN_REVIEW2 = "techunter:in-review";
|
|
846
|
+
var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
|
|
847
|
+
function getStatus(issue) {
|
|
848
|
+
if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
|
|
849
|
+
if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
|
|
850
|
+
if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
|
|
851
|
+
if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
|
|
852
|
+
return "unknown";
|
|
853
|
+
}
|
|
854
|
+
function colorStatus(status) {
|
|
855
|
+
const padded = status.padEnd(14);
|
|
856
|
+
switch (status) {
|
|
857
|
+
case "available":
|
|
858
|
+
return chalk2.green(padded);
|
|
859
|
+
case "claimed":
|
|
860
|
+
return chalk2.yellow(padded);
|
|
861
|
+
case "in-review":
|
|
862
|
+
return chalk2.blue(padded);
|
|
863
|
+
case "changes-needed":
|
|
864
|
+
return chalk2.red(padded);
|
|
865
|
+
default:
|
|
866
|
+
return padded;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function parentIssueFromBranch(branch) {
|
|
870
|
+
if (!isTaskBranch(branch)) return null;
|
|
871
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
872
|
+
return match ? parseInt(match[1], 10) : null;
|
|
873
|
+
}
|
|
874
|
+
function getParentIssueNumber(issue) {
|
|
875
|
+
const target = extractTargetBranch(issue.body);
|
|
876
|
+
if (!target) return null;
|
|
877
|
+
return parentIssueFromBranch(target);
|
|
878
|
+
}
|
|
879
|
+
function printTaskDetail(issue) {
|
|
880
|
+
const divider = chalk2.dim("\u2500".repeat(70));
|
|
881
|
+
const parentNum = getParentIssueNumber(issue);
|
|
882
|
+
console.log("\n" + divider);
|
|
883
|
+
console.log(
|
|
884
|
+
chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014") + (parentNum ? chalk2.dim(` sub-task of #${parentNum}`) : "")
|
|
885
|
+
);
|
|
886
|
+
console.log(chalk2.bold("\n " + issue.title));
|
|
887
|
+
if (issue.body) {
|
|
888
|
+
console.log("");
|
|
889
|
+
console.log(renderMarkdown(issue.body));
|
|
890
|
+
}
|
|
891
|
+
console.log("\n " + chalk2.dim(issue.htmlUrl));
|
|
892
|
+
console.log(divider + "\n");
|
|
893
|
+
}
|
|
894
|
+
async function printTaskList(config) {
|
|
702
895
|
try {
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
896
|
+
const tasks = await listTasks(config);
|
|
897
|
+
const divider = chalk2.dim("\u2500".repeat(70));
|
|
898
|
+
console.log("");
|
|
899
|
+
console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
|
|
900
|
+
console.log(divider);
|
|
901
|
+
if (tasks.length === 0) {
|
|
902
|
+
console.log(chalk2.dim(" (no tasks)"));
|
|
903
|
+
} else {
|
|
904
|
+
let printTask2 = function(t, indent, connector, isLast) {
|
|
905
|
+
const num = `#${t.number}`.padEnd(5);
|
|
906
|
+
const status = colorStatus(getStatus(t));
|
|
907
|
+
const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
|
|
908
|
+
const fullPrefix = indent + connector;
|
|
909
|
+
const maxTitle = 36 - fullPrefix.length;
|
|
910
|
+
const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
|
|
911
|
+
console.log(` ${num}${status}${assignee}${chalk2.dim(fullPrefix)}${title}`);
|
|
912
|
+
const children = childrenOf.get(t.number) ?? [];
|
|
913
|
+
const childIndent = indent + (isLast ? " " : "\u2502 ");
|
|
914
|
+
for (let i = 0; i < children.length; i++) {
|
|
915
|
+
const childIsLast = i === children.length - 1;
|
|
916
|
+
printTask2(children[i], childIndent, childIsLast ? "\u2514\u2500 " : "\u251C\u2500 ", childIsLast);
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
var printTask = printTask2;
|
|
920
|
+
const taskMap = new Map(tasks.map((t) => [t.number, t]));
|
|
921
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
922
|
+
for (const t of tasks) {
|
|
923
|
+
const parentNum = getParentIssueNumber(t);
|
|
924
|
+
const key = parentNum !== null && taskMap.has(parentNum) ? parentNum : null;
|
|
925
|
+
if (!childrenOf.has(key)) childrenOf.set(key, []);
|
|
926
|
+
childrenOf.get(key).push(t);
|
|
927
|
+
}
|
|
928
|
+
const roots = childrenOf.get(null) ?? [];
|
|
929
|
+
for (let i = 0; i < roots.length; i++) {
|
|
930
|
+
const isLast = i === roots.length - 1;
|
|
931
|
+
printTask2(roots[i], "", isLast ? "\u2514\u2500 " : "\u251C\u2500 ", isLast);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
console.log(divider);
|
|
935
|
+
return tasks;
|
|
706
936
|
} catch (err) {
|
|
707
|
-
|
|
708
|
-
|
|
937
|
+
console.log(chalk2.yellow(`(Could not load tasks: ${err.message})`));
|
|
938
|
+
return [];
|
|
709
939
|
}
|
|
710
|
-
return { token, clientId: OAUTH_CLIENT_ID };
|
|
711
940
|
}
|
|
712
|
-
async function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
941
|
+
async function printMyTasks(config) {
|
|
942
|
+
try {
|
|
943
|
+
const me = await getAuthenticatedUser(config);
|
|
944
|
+
const tasks = await listMyTasks(config, me);
|
|
945
|
+
if (tasks.length === 0) return;
|
|
946
|
+
const divider = chalk2.dim("\u2500".repeat(70));
|
|
947
|
+
console.log("");
|
|
948
|
+
console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + `My Tasks @${me}`));
|
|
949
|
+
console.log(divider);
|
|
950
|
+
for (const t of tasks) {
|
|
951
|
+
const num = `#${t.number}`.padEnd(5);
|
|
952
|
+
const status = colorStatus(getStatus(t));
|
|
953
|
+
const parentNum = getParentIssueNumber(t);
|
|
954
|
+
const parentTag = parentNum ? chalk2.dim(` (sub of #${parentNum})`) : "";
|
|
955
|
+
const maxTitle = parentNum ? 34 : 46;
|
|
956
|
+
const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
|
|
957
|
+
console.log(` ${num}${status}${title}${parentTag}`);
|
|
724
958
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
{
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
},
|
|
733
|
-
{
|
|
734
|
-
name: "Personal Access Token (PAT) \u2014 paste a token from github.com/settings/tokens",
|
|
735
|
-
value: "pat"
|
|
736
|
-
}
|
|
737
|
-
]
|
|
738
|
-
});
|
|
739
|
-
let githubToken;
|
|
740
|
-
let githubClientId;
|
|
741
|
-
if (authMethod === "device") {
|
|
742
|
-
const result = await getGitHubTokenViaDeviceFlow();
|
|
743
|
-
githubToken = result.token;
|
|
744
|
-
githubClientId = result.clientId;
|
|
745
|
-
} else {
|
|
746
|
-
const result = await getGitHubTokenViaPAT();
|
|
747
|
-
githubToken = result.token;
|
|
748
|
-
}
|
|
749
|
-
const providerChoice = await select({
|
|
750
|
-
message: "AI provider:",
|
|
751
|
-
choices: [
|
|
752
|
-
{ name: `OpenRouter (recommended) ${chalk2.dim(`${DEFAULT_BASE_URL} \xB7 ${DEFAULT_MODEL}`)}`, value: "openrouter" },
|
|
753
|
-
{ name: "Custom (specify base URL and model)", value: "custom" }
|
|
754
|
-
]
|
|
755
|
-
});
|
|
756
|
-
let aiBaseUrl;
|
|
757
|
-
let aiModel;
|
|
758
|
-
if (providerChoice === "custom") {
|
|
759
|
-
aiBaseUrl = (await input({ message: "API base URL:", default: DEFAULT_BASE_URL })).trim();
|
|
760
|
-
aiModel = (await input({ message: "Model name:", default: DEFAULT_MODEL })).trim();
|
|
761
|
-
}
|
|
762
|
-
const apiKeyHint = providerChoice === "openrouter" ? chalk2.dim(" Get a key at: https://openrouter.ai/settings/keys\n") : chalk2.dim(" API key for your provider\n");
|
|
763
|
-
console.log(apiKeyHint);
|
|
764
|
-
const aiApiKey = await password({
|
|
765
|
-
message: "API Key:",
|
|
766
|
-
mask: "*"
|
|
767
|
-
});
|
|
768
|
-
let owner = detectedOwner;
|
|
769
|
-
let repo = detectedRepo;
|
|
770
|
-
if (!owner || !repo) {
|
|
771
|
-
owner = await input({
|
|
772
|
-
message: "GitHub repo owner (user or org):",
|
|
773
|
-
required: true
|
|
774
|
-
});
|
|
775
|
-
repo = await input({
|
|
776
|
-
message: "GitHub repo name:",
|
|
777
|
-
required: true
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
const config = {
|
|
781
|
-
githubToken,
|
|
782
|
-
githubClientId,
|
|
783
|
-
aiApiKey: aiApiKey.trim(),
|
|
784
|
-
...aiBaseUrl ? { aiBaseUrl } : {},
|
|
785
|
-
...aiModel ? { aiModel } : {},
|
|
786
|
-
github: {
|
|
787
|
-
owner: owner.trim(),
|
|
788
|
-
repo: repo.trim()
|
|
789
|
-
}
|
|
790
|
-
};
|
|
791
|
-
setConfig(config);
|
|
792
|
-
const spinner = ora("Setting up GitHub labels...").start();
|
|
793
|
-
try {
|
|
794
|
-
await ensureLabels(config);
|
|
795
|
-
spinner.succeed("GitHub labels created");
|
|
796
|
-
} catch (err) {
|
|
797
|
-
spinner.fail("Failed to create labels (check token permissions)");
|
|
798
|
-
console.error(chalk2.red(String(err)));
|
|
799
|
-
}
|
|
800
|
-
console.log(chalk2.green("\nSetup complete!"));
|
|
801
|
-
console.log(chalk2.dim(`Config saved to: ${getConfigPath()}
|
|
802
|
-
`));
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// src/commands/config.ts
|
|
806
|
-
import { input as input2, password as password2, select as select2 } from "@inquirer/prompts";
|
|
807
|
-
import chalk3 from "chalk";
|
|
808
|
-
async function configCommand() {
|
|
809
|
-
let config;
|
|
810
|
-
try {
|
|
811
|
-
config = getConfig();
|
|
812
|
-
} catch {
|
|
813
|
-
console.error(chalk3.red("No config found. Run `tch init` first."));
|
|
814
|
-
process.exit(1);
|
|
815
|
-
}
|
|
816
|
-
console.log(chalk3.bold.cyan("\nTechunter \u2014 Settings\n"));
|
|
817
|
-
console.log(chalk3.dim(`Config file: ${getConfigPath()}
|
|
818
|
-
`));
|
|
819
|
-
const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
|
|
820
|
-
const currentModel = config.aiModel ?? DEFAULT_MODEL;
|
|
821
|
-
const currentBaseBranch = config.baseBranch ?? "main";
|
|
822
|
-
const field = await select2({
|
|
823
|
-
message: "Which setting to change?",
|
|
824
|
-
choices: [
|
|
825
|
-
{ name: `GitHub repo ${chalk3.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
|
|
826
|
-
{ name: `Base branch ${chalk3.dim(currentBaseBranch)}`, value: "baseBranch" },
|
|
827
|
-
{ name: `AI base URL ${chalk3.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
|
|
828
|
-
{ name: `AI model ${chalk3.dim(currentModel)}`, value: "aiModel" },
|
|
829
|
-
{ name: `AI API Key ${chalk3.dim("(hidden)")}`, value: "aiApiKey" },
|
|
830
|
-
{ name: `GitHub Token ${chalk3.dim("(hidden)")}`, value: "githubToken" },
|
|
831
|
-
{ name: "Cancel", value: "cancel" }
|
|
832
|
-
]
|
|
833
|
-
});
|
|
834
|
-
if (field === "cancel") return;
|
|
835
|
-
if (field === "baseBranch") {
|
|
836
|
-
const val = await input2({ message: "Base branch name:", default: currentBaseBranch });
|
|
837
|
-
if (val.trim()) {
|
|
838
|
-
setConfig({ baseBranch: val.trim() });
|
|
839
|
-
console.log(chalk3.green(`
|
|
840
|
-
Base branch set to: ${val.trim()}
|
|
841
|
-
`));
|
|
842
|
-
}
|
|
843
|
-
} else if (field === "repo") {
|
|
844
|
-
const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
|
|
845
|
-
const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
|
|
846
|
-
setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
|
|
847
|
-
console.log(chalk3.green(`
|
|
848
|
-
Repo set to: ${owner.trim()}/${repo.trim()}
|
|
849
|
-
`));
|
|
850
|
-
} else if (field === "aiBaseUrl") {
|
|
851
|
-
const val = await input2({ message: "AI base URL:", default: currentBaseUrl });
|
|
852
|
-
if (val.trim()) {
|
|
853
|
-
setConfig({ aiBaseUrl: val.trim() });
|
|
854
|
-
console.log(chalk3.green(`
|
|
855
|
-
AI base URL set to: ${val.trim()}
|
|
856
|
-
`));
|
|
857
|
-
}
|
|
858
|
-
} else if (field === "aiModel") {
|
|
859
|
-
const val = await input2({ message: "AI model name:", default: currentModel });
|
|
860
|
-
if (val.trim()) {
|
|
861
|
-
setConfig({ aiModel: val.trim() });
|
|
862
|
-
console.log(chalk3.green(`
|
|
863
|
-
AI model set to: ${val.trim()}
|
|
864
|
-
`));
|
|
865
|
-
}
|
|
866
|
-
} else if (field === "aiApiKey") {
|
|
867
|
-
const val = await password2({ message: "New AI API Key:", mask: "*" });
|
|
868
|
-
if (val.trim()) {
|
|
869
|
-
setConfig({ aiApiKey: val.trim() });
|
|
870
|
-
console.log(chalk3.green("\nAI API Key updated.\n"));
|
|
871
|
-
}
|
|
872
|
-
} else if (field === "githubToken") {
|
|
873
|
-
const val = await password2({ message: "New GitHub Token:", mask: "*" });
|
|
874
|
-
if (val.trim()) {
|
|
875
|
-
setConfig({ githubToken: val.trim() });
|
|
876
|
-
console.log(chalk3.green("\nGitHub Token updated.\n"));
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// src/index.ts
|
|
882
|
-
init_github();
|
|
883
|
-
|
|
884
|
-
// src/lib/agent.ts
|
|
885
|
-
import ora15 from "ora";
|
|
886
|
-
import chalk13 from "chalk";
|
|
887
|
-
|
|
888
|
-
// src/tools/pick/index.ts
|
|
889
|
-
var pick_exports = {};
|
|
890
|
-
__export(pick_exports, {
|
|
891
|
-
definition: () => definition3,
|
|
892
|
-
execute: () => execute3,
|
|
893
|
-
run: () => run3,
|
|
894
|
-
terminal: () => terminal3
|
|
895
|
-
});
|
|
896
|
-
init_github();
|
|
897
|
-
import chalk8 from "chalk";
|
|
898
|
-
import ora4 from "ora";
|
|
899
|
-
import { select as select5 } from "@inquirer/prompts";
|
|
900
|
-
init_github();
|
|
901
|
-
|
|
902
|
-
// src/lib/markdown.ts
|
|
903
|
-
import { marked } from "marked";
|
|
904
|
-
import { markedTerminal } from "marked-terminal";
|
|
905
|
-
marked.use(markedTerminal({ showSectionPrefix: false }));
|
|
906
|
-
function renderMarkdown(text) {
|
|
907
|
-
return marked(text);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// src/lib/display.ts
|
|
911
|
-
init_github();
|
|
912
|
-
import chalk4 from "chalk";
|
|
913
|
-
var LABEL_AVAILABLE2 = "techunter:available";
|
|
914
|
-
var LABEL_CLAIMED2 = "techunter:claimed";
|
|
915
|
-
var LABEL_IN_REVIEW2 = "techunter:in-review";
|
|
916
|
-
var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
|
|
917
|
-
function getStatus(issue) {
|
|
918
|
-
if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
|
|
919
|
-
if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
|
|
920
|
-
if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
|
|
921
|
-
if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
|
|
922
|
-
return "unknown";
|
|
923
|
-
}
|
|
924
|
-
function colorStatus(status) {
|
|
925
|
-
const padded = status.padEnd(14);
|
|
926
|
-
switch (status) {
|
|
927
|
-
case "available":
|
|
928
|
-
return chalk4.green(padded);
|
|
929
|
-
case "claimed":
|
|
930
|
-
return chalk4.yellow(padded);
|
|
931
|
-
case "in-review":
|
|
932
|
-
return chalk4.blue(padded);
|
|
933
|
-
case "changes-needed":
|
|
934
|
-
return chalk4.red(padded);
|
|
935
|
-
default:
|
|
936
|
-
return padded;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
function printTaskDetail(issue) {
|
|
940
|
-
const divider = chalk4.dim("\u2500".repeat(70));
|
|
941
|
-
console.log("\n" + divider);
|
|
942
|
-
console.log(
|
|
943
|
-
chalk4.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk4.dim(issue.assignee ? `@${issue.assignee}` : "\u2014")
|
|
944
|
-
);
|
|
945
|
-
console.log(chalk4.bold("\n " + issue.title));
|
|
946
|
-
if (issue.body) {
|
|
947
|
-
console.log("");
|
|
948
|
-
console.log(renderMarkdown(issue.body));
|
|
949
|
-
}
|
|
950
|
-
console.log("\n " + chalk4.dim(issue.htmlUrl));
|
|
951
|
-
console.log(divider + "\n");
|
|
952
|
-
}
|
|
953
|
-
async function printTaskList(config) {
|
|
954
|
-
try {
|
|
955
|
-
const tasks = await listTasks(config);
|
|
956
|
-
const divider = chalk4.dim("\u2500".repeat(70));
|
|
957
|
-
console.log("");
|
|
958
|
-
console.log(chalk4.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
|
|
959
|
-
console.log(divider);
|
|
960
|
-
if (tasks.length === 0) {
|
|
961
|
-
console.log(chalk4.dim(" (no tasks)"));
|
|
962
|
-
} else {
|
|
963
|
-
for (const t of tasks) {
|
|
964
|
-
const num = `#${t.number}`.padEnd(5);
|
|
965
|
-
const status = colorStatus(getStatus(t));
|
|
966
|
-
const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
|
|
967
|
-
const title = t.title.length > 36 ? t.title.slice(0, 33) + "..." : t.title;
|
|
968
|
-
console.log(` ${num}${status}${assignee}${title}`);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
console.log(divider);
|
|
972
|
-
return tasks;
|
|
973
|
-
} catch (err) {
|
|
974
|
-
console.log(chalk4.yellow(`(Could not load tasks: ${err.message})`));
|
|
975
|
-
return [];
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
async function printMyTasks(config) {
|
|
979
|
-
try {
|
|
980
|
-
const me = await getAuthenticatedUser(config);
|
|
981
|
-
const tasks = await listMyTasks(config, me);
|
|
982
|
-
if (tasks.length === 0) return;
|
|
983
|
-
const divider = chalk4.dim("\u2500".repeat(70));
|
|
984
|
-
console.log("");
|
|
985
|
-
console.log(chalk4.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + `My Tasks @${me}`));
|
|
986
|
-
console.log(divider);
|
|
987
|
-
for (const t of tasks) {
|
|
988
|
-
const num = `#${t.number}`.padEnd(5);
|
|
989
|
-
const status = colorStatus(getStatus(t));
|
|
990
|
-
const title = t.title.length > 46 ? t.title.slice(0, 43) + "..." : t.title;
|
|
991
|
-
console.log(` ${num}${status}${title}`);
|
|
992
|
-
}
|
|
993
|
-
console.log(divider);
|
|
994
|
-
const rejectedTasks = tasks.filter((t) => t.labels.includes(LABEL_CHANGES_NEEDED2));
|
|
995
|
-
if (rejectedTasks.length > 0) {
|
|
996
|
-
let currentBranch = "";
|
|
997
|
-
try {
|
|
998
|
-
currentBranch = await getCurrentBranch();
|
|
999
|
-
} catch {
|
|
959
|
+
console.log(divider);
|
|
960
|
+
const rejectedTasks = tasks.filter((t) => t.labels.includes(LABEL_CHANGES_NEEDED2));
|
|
961
|
+
if (rejectedTasks.length > 0) {
|
|
962
|
+
let currentBranch = "";
|
|
963
|
+
try {
|
|
964
|
+
currentBranch = await getCurrentBranch();
|
|
965
|
+
} catch {
|
|
1000
966
|
}
|
|
1001
967
|
console.log("");
|
|
1002
968
|
for (const t of rejectedTasks) {
|
|
1003
|
-
const
|
|
1004
|
-
const onCorrectBranch = currentBranch ===
|
|
969
|
+
const taskBranch = t.assignee ? makeTaskBranchName(t.number, t.assignee) : `task-${t.number}`;
|
|
970
|
+
const onCorrectBranch = currentBranch === taskBranch;
|
|
1005
971
|
console.log(
|
|
1006
|
-
|
|
972
|
+
chalk2.red.bold(" \u26A0 Changes requested") + chalk2.red(` on #${t.number} "${t.title}"`)
|
|
1007
973
|
);
|
|
1008
974
|
if (!onCorrectBranch) {
|
|
1009
975
|
console.log(
|
|
1010
|
-
|
|
976
|
+
chalk2.dim(" Switch branch: ") + chalk2.cyan(`git checkout ${taskBranch}`)
|
|
1011
977
|
);
|
|
1012
978
|
}
|
|
1013
979
|
}
|
|
@@ -1019,7 +985,7 @@ async function printMyTasks(config) {
|
|
|
1019
985
|
|
|
1020
986
|
// src/lib/launch.ts
|
|
1021
987
|
import { spawn } from "child_process";
|
|
1022
|
-
import
|
|
988
|
+
import chalk3 from "chalk";
|
|
1023
989
|
function buildClaudePrompt(issue, branch) {
|
|
1024
990
|
const lines = [
|
|
1025
991
|
`You are working on task #${issue.number}: ${issue.title}`,
|
|
@@ -1035,14 +1001,14 @@ function buildClaudePrompt(issue, branch) {
|
|
|
1035
1001
|
}
|
|
1036
1002
|
async function launchClaudeCode(issue, branch) {
|
|
1037
1003
|
const prompt = buildClaudePrompt(issue, branch);
|
|
1038
|
-
console.log(
|
|
1004
|
+
console.log(chalk3.dim("\n Launching Claude Code\u2026\n"));
|
|
1039
1005
|
await new Promise((resolve) => {
|
|
1040
1006
|
const safePrompt = prompt.replace(/\r?\n/g, " ").replace(/"/g, "'");
|
|
1041
1007
|
const child = spawn(`claude "${safePrompt}"`, [], { stdio: "inherit", shell: true });
|
|
1042
1008
|
child.on("close", () => resolve());
|
|
1043
1009
|
child.on("error", () => {
|
|
1044
1010
|
console.log(
|
|
1045
|
-
|
|
1011
|
+
chalk3.yellow(
|
|
1046
1012
|
" Could not launch claude. Make sure Claude Code is installed:\n npm install -g @anthropic-ai/claude-code"
|
|
1047
1013
|
)
|
|
1048
1014
|
);
|
|
@@ -1060,81 +1026,12 @@ __export(submit_exports, {
|
|
|
1060
1026
|
terminal: () => terminal
|
|
1061
1027
|
});
|
|
1062
1028
|
init_github();
|
|
1063
|
-
import
|
|
1064
|
-
import
|
|
1065
|
-
import { select
|
|
1029
|
+
import chalk4 from "chalk";
|
|
1030
|
+
import ora from "ora";
|
|
1031
|
+
import { select, input as promptInput } from "@inquirer/prompts";
|
|
1066
1032
|
|
|
1067
|
-
// src/
|
|
1068
|
-
|
|
1069
|
-
function formatInput(input3) {
|
|
1070
|
-
return Object.entries(input3).map(([k, v]) => {
|
|
1071
|
-
if (typeof v === "number") return `${k}=${v}`;
|
|
1072
|
-
if (typeof v === "string") {
|
|
1073
|
-
if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
|
|
1074
|
-
return `${k}="${v}"`;
|
|
1075
|
-
}
|
|
1076
|
-
return `${k}=${JSON.stringify(v)}`;
|
|
1077
|
-
}).join(" ");
|
|
1078
|
-
}
|
|
1079
|
-
function summarize(result) {
|
|
1080
|
-
const first = result.split("\n").find((l) => l.trim()) ?? result;
|
|
1081
|
-
return first.length > 100 ? first.slice(0, 97) + "..." : first;
|
|
1082
|
-
}
|
|
1083
|
-
function printToolCall(name, input3) {
|
|
1084
|
-
const params = formatInput(input3);
|
|
1085
|
-
console.log(` ${chalk6.cyan("\u2192")} ${chalk6.bold(name)}${params ? " " + chalk6.dim(params) : ""}`);
|
|
1086
|
-
}
|
|
1087
|
-
function printToolResult(result) {
|
|
1088
|
-
const ok = !result.startsWith("Error:");
|
|
1089
|
-
const icon = ok ? chalk6.green("\u2713") : chalk6.red("\u2717");
|
|
1090
|
-
console.log(` ${icon} ${chalk6.dim(summarize(result))}`);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// src/lib/sub-agent.ts
|
|
1094
|
-
async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
|
|
1095
|
-
const client = createClient(config);
|
|
1096
|
-
const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
|
|
1097
|
-
const tools2 = selected.map((m) => m.definition);
|
|
1098
|
-
const messages = [
|
|
1099
|
-
{ role: "system", content: systemPrompt },
|
|
1100
|
-
{ role: "user", content: userMessage }
|
|
1101
|
-
];
|
|
1102
|
-
const MAX_ITERATIONS = 100;
|
|
1103
|
-
let iterations = 0;
|
|
1104
|
-
for (; ; ) {
|
|
1105
|
-
if (++iterations > MAX_ITERATIONS) {
|
|
1106
|
-
throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
1107
|
-
}
|
|
1108
|
-
const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
|
|
1109
|
-
const choice = res.choices[0];
|
|
1110
|
-
messages.push({
|
|
1111
|
-
role: "assistant",
|
|
1112
|
-
content: choice.message.content ?? null,
|
|
1113
|
-
...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
|
|
1114
|
-
});
|
|
1115
|
-
if (choice.finish_reason === "stop") {
|
|
1116
|
-
return choice.message.content ?? "";
|
|
1117
|
-
}
|
|
1118
|
-
if (choice.finish_reason === "tool_calls") {
|
|
1119
|
-
for (const tc of choice.message.tool_calls ?? []) {
|
|
1120
|
-
let input3;
|
|
1121
|
-
try {
|
|
1122
|
-
input3 = JSON.parse(tc.function.arguments);
|
|
1123
|
-
} catch {
|
|
1124
|
-
input3 = {};
|
|
1125
|
-
}
|
|
1126
|
-
printToolCall(tc.function.name, input3);
|
|
1127
|
-
const mod = selected.find((m) => m.definition.function.name === tc.function.name);
|
|
1128
|
-
const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
|
|
1129
|
-
printToolResult(result);
|
|
1130
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// src/tools/submit/prompts.ts
|
|
1137
|
-
var REVIEWER_SYSTEM_PROMPT = "You are a concise code reviewer. The diff provided shows all changes on this worker branch since the task was claimed. The branch is shared across tasks, so the diff may contain changes unrelated to this specific task \u2014 use the task title and acceptance criteria to identify which changes are relevant, and ignore the rest. 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.";
|
|
1033
|
+
// src/tools/submit/prompts.ts
|
|
1034
|
+
var REVIEWER_SYSTEM_PROMPT = "You are a concise code reviewer. The diff provided shows all changes on this worker branch since the task was claimed. The branch is shared across tasks, so the diff may contain changes unrelated to this specific task \u2014 use the task title and acceptance criteria to identify which changes are relevant, and ignore the rest. 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.";
|
|
1138
1035
|
|
|
1139
1036
|
// src/tools/submit/reviewer.ts
|
|
1140
1037
|
async function reviewChanges(config, issueNumber, issue, diff) {
|
|
@@ -1148,7 +1045,7 @@ ${issue.body ?? "(none)"}
|
|
|
1148
1045
|
|
|
1149
1046
|
Diff:
|
|
1150
1047
|
${diff || "(no changes)"}`,
|
|
1151
|
-
["run_command", "
|
|
1048
|
+
["run_command", "grep_code", "get_diff"]
|
|
1152
1049
|
);
|
|
1153
1050
|
}
|
|
1154
1051
|
|
|
@@ -1169,11 +1066,21 @@ var definition = {
|
|
|
1169
1066
|
};
|
|
1170
1067
|
async function run(_input, config) {
|
|
1171
1068
|
const taskState = getConfig().taskState;
|
|
1172
|
-
const
|
|
1069
|
+
const currentBranch = await getCurrentBranch();
|
|
1070
|
+
let issueNumber = taskState?.activeIssueNumber && taskState?.activeBranch && currentBranch === taskState.activeBranch ? taskState.activeIssueNumber : void 0;
|
|
1173
1071
|
if (!issueNumber) {
|
|
1174
|
-
|
|
1072
|
+
const fromBranch = parseIssueNumberFromBranch(currentBranch);
|
|
1073
|
+
if (fromBranch) {
|
|
1074
|
+
issueNumber = fromBranch;
|
|
1075
|
+
} else {
|
|
1076
|
+
const found = await getIssueNumberFromBranch(config, currentBranch);
|
|
1077
|
+
if (!found) {
|
|
1078
|
+
return "No active task found. Claim a task first with /pick.";
|
|
1079
|
+
}
|
|
1080
|
+
issueNumber = found.issueNumber;
|
|
1081
|
+
}
|
|
1175
1082
|
}
|
|
1176
|
-
let spinner =
|
|
1083
|
+
let spinner = ora("Loading task and diff\u2026").start();
|
|
1177
1084
|
const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
|
|
1178
1085
|
const [issue, diff, me] = await Promise.all([
|
|
1179
1086
|
getTask(config, issueNumber),
|
|
@@ -1181,12 +1088,19 @@ async function run(_input, config) {
|
|
|
1181
1088
|
getAuthenticatedUser(config)
|
|
1182
1089
|
]);
|
|
1183
1090
|
spinner.stop();
|
|
1184
|
-
const
|
|
1091
|
+
const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
|
|
1185
1092
|
const branch = await getCurrentBranch();
|
|
1186
1093
|
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
1094
|
+
spinner = ora("Checking for open sub-tasks\u2026").start();
|
|
1095
|
+
const openSubtaskNumbers = await getOpenSubtasks(config, branch);
|
|
1096
|
+
spinner.stop();
|
|
1097
|
+
if (openSubtaskNumbers.length > 0) {
|
|
1098
|
+
return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open:
|
|
1099
|
+
` + openSubtaskNumbers.map((n) => ` - #${n}`).join("\n") + "\nComplete all sub-tasks before submitting.";
|
|
1100
|
+
}
|
|
1187
1101
|
let review = "";
|
|
1188
1102
|
if (!isSelfSubmit) {
|
|
1189
|
-
const reviewSpinner =
|
|
1103
|
+
const reviewSpinner = ora("Reviewing changes\u2026").start();
|
|
1190
1104
|
try {
|
|
1191
1105
|
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
1192
1106
|
} catch (err) {
|
|
@@ -1194,19 +1108,19 @@ async function run(_input, config) {
|
|
|
1194
1108
|
}
|
|
1195
1109
|
reviewSpinner.stop();
|
|
1196
1110
|
}
|
|
1197
|
-
const divider =
|
|
1111
|
+
const divider = chalk4.dim("\u2500".repeat(70));
|
|
1198
1112
|
console.log("\n" + divider);
|
|
1199
1113
|
if (isSelfSubmit) {
|
|
1200
|
-
console.log(
|
|
1114
|
+
console.log(chalk4.yellow(` Self-submit detected \u2014 AI review skipped.`));
|
|
1201
1115
|
} else {
|
|
1202
|
-
console.log(
|
|
1116
|
+
console.log(chalk4.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
|
|
1203
1117
|
console.log(divider);
|
|
1204
1118
|
console.log(renderMarkdown(review));
|
|
1205
1119
|
}
|
|
1206
1120
|
console.log(divider + "\n");
|
|
1207
1121
|
let shouldProceed;
|
|
1208
1122
|
try {
|
|
1209
|
-
shouldProceed = await
|
|
1123
|
+
shouldProceed = await select({
|
|
1210
1124
|
message: `Submit task #${issueNumber}?`,
|
|
1211
1125
|
choices: [
|
|
1212
1126
|
{ name: "Yes, submit", value: true },
|
|
@@ -1227,7 +1141,7 @@ async function run(_input, config) {
|
|
|
1227
1141
|
return "Submit cancelled.";
|
|
1228
1142
|
}
|
|
1229
1143
|
if (!commitMessage.trim()) return "Submit cancelled.";
|
|
1230
|
-
spinner =
|
|
1144
|
+
spinner = ora("Committing and pushing\u2026").start();
|
|
1231
1145
|
try {
|
|
1232
1146
|
await stageAllAndCommit(commitMessage.trim());
|
|
1233
1147
|
spinner.stop();
|
|
@@ -1236,52 +1150,71 @@ async function run(_input, config) {
|
|
|
1236
1150
|
return `Commit failed: ${err.message}`;
|
|
1237
1151
|
}
|
|
1238
1152
|
if (isSelfSubmit) {
|
|
1239
|
-
spinner =
|
|
1153
|
+
spinner = ora("Closing issue\u2026").start();
|
|
1240
1154
|
try {
|
|
1241
1155
|
await closeTask(config, issueNumber);
|
|
1242
1156
|
spinner.stop();
|
|
1243
1157
|
} catch (err) {
|
|
1244
1158
|
spinner.stop();
|
|
1245
|
-
console.error(
|
|
1159
|
+
console.error(chalk4.yellow(`Warning: failed to close issue: ${err.message}`));
|
|
1246
1160
|
}
|
|
1247
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1161
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1248
1162
|
return `Task #${issueNumber} committed and closed.
|
|
1249
1163
|
Commit: "${commitMessage.trim()}"`;
|
|
1250
1164
|
}
|
|
1251
|
-
spinner =
|
|
1165
|
+
spinner = ora("Checking for existing PR\u2026").start();
|
|
1166
|
+
const existingPR = await getTaskPR(config, issueNumber);
|
|
1167
|
+
spinner.stop();
|
|
1252
1168
|
let prUrl;
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1169
|
+
if (existingPR) {
|
|
1170
|
+
prUrl = existingPR.url;
|
|
1171
|
+
console.log(chalk4.dim(` Existing PR found: ${prUrl} \u2014 updating.`));
|
|
1172
|
+
} else {
|
|
1173
|
+
spinner = ora("Creating pull request\u2026").start();
|
|
1174
|
+
try {
|
|
1175
|
+
await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
|
|
1176
|
+
const prBody = [
|
|
1177
|
+
`Closes #${issueNumber}`,
|
|
1178
|
+
issue.body ? `
|
|
1257
1179
|
${issue.body}` : "",
|
|
1258
|
-
|
|
1180
|
+
review ? `
|
|
1259
1181
|
## AI Review
|
|
1260
1182
|
${review}` : ""
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1183
|
+
].join("\n").trim();
|
|
1184
|
+
prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
|
|
1185
|
+
spinner.stop();
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
spinner.stop();
|
|
1188
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
1189
|
+
}
|
|
1267
1190
|
}
|
|
1268
|
-
spinner =
|
|
1191
|
+
spinner = ora("Marking as in-review\u2026").start();
|
|
1269
1192
|
try {
|
|
1270
1193
|
await markInReview(config, issueNumber);
|
|
1271
1194
|
spinner.stop();
|
|
1272
1195
|
} catch (err) {
|
|
1273
1196
|
spinner.stop();
|
|
1274
|
-
return `PR created (${prUrl}) but failed to update label: ${err.message}`;
|
|
1197
|
+
return `PR ${existingPR ? "updated" : "created"} (${prUrl}) but failed to update label: ${err.message}`;
|
|
1275
1198
|
}
|
|
1276
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1277
|
-
return `Task #${issueNumber} submitted.
|
|
1199
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1200
|
+
return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
|
|
1278
1201
|
Commit: "${commitMessage.trim()}"
|
|
1279
1202
|
PR: ${prUrl}`;
|
|
1280
1203
|
}
|
|
1281
1204
|
async function execute(input3, config) {
|
|
1282
1205
|
const taskState = getConfig().taskState;
|
|
1283
|
-
|
|
1284
|
-
if (!issueNumber)
|
|
1206
|
+
let issueNumber = taskState?.activeIssueNumber;
|
|
1207
|
+
if (!issueNumber) {
|
|
1208
|
+
const currentBranch = await getCurrentBranch();
|
|
1209
|
+
const fromBranch = parseIssueNumberFromBranch(currentBranch);
|
|
1210
|
+
if (fromBranch) {
|
|
1211
|
+
issueNumber = fromBranch;
|
|
1212
|
+
} else {
|
|
1213
|
+
const found = await getIssueNumberFromBranch(config, currentBranch);
|
|
1214
|
+
if (!found) return "No active task found. Claim a task first.";
|
|
1215
|
+
issueNumber = found.issueNumber;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1285
1218
|
const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
|
|
1286
1219
|
const [issue, diff, branch, me] = await Promise.all([
|
|
1287
1220
|
getTask(config, issueNumber),
|
|
@@ -1289,7 +1222,11 @@ async function execute(input3, config) {
|
|
|
1289
1222
|
getCurrentBranch(),
|
|
1290
1223
|
getAuthenticatedUser(config)
|
|
1291
1224
|
]);
|
|
1292
|
-
const
|
|
1225
|
+
const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
|
|
1226
|
+
const openSubtaskNumbers = await getOpenSubtasks(config, branch);
|
|
1227
|
+
if (openSubtaskNumbers.length > 0) {
|
|
1228
|
+
return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open: ` + openSubtaskNumbers.map((n) => `#${n}`).join(", ");
|
|
1229
|
+
}
|
|
1293
1230
|
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
1294
1231
|
let review = "";
|
|
1295
1232
|
if (!isSelfSubmit) {
|
|
@@ -1310,29 +1247,36 @@ async function execute(input3, config) {
|
|
|
1310
1247
|
await closeTask(config, issueNumber);
|
|
1311
1248
|
} catch {
|
|
1312
1249
|
}
|
|
1313
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1250
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1314
1251
|
return `Task #${issueNumber} committed and closed.
|
|
1315
1252
|
Commit: "${commitMessage}"`;
|
|
1316
1253
|
}
|
|
1254
|
+
const existingPR = await getTaskPR(config, issueNumber);
|
|
1317
1255
|
let prUrl;
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1256
|
+
if (existingPR) {
|
|
1257
|
+
prUrl = existingPR.url;
|
|
1258
|
+
} else {
|
|
1259
|
+
try {
|
|
1260
|
+
await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
|
|
1261
|
+
const prBody = [
|
|
1262
|
+
`Closes #${issueNumber}`,
|
|
1263
|
+
issue.body ? `
|
|
1322
1264
|
${issue.body}` : "",
|
|
1323
|
-
|
|
1265
|
+
review ? `
|
|
1324
1266
|
## AI Review
|
|
1325
1267
|
${review}` : ""
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1268
|
+
].join("\n").trim();
|
|
1269
|
+
prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
1272
|
+
}
|
|
1330
1273
|
}
|
|
1331
1274
|
try {
|
|
1332
1275
|
await markInReview(config, issueNumber);
|
|
1333
1276
|
} catch {
|
|
1334
1277
|
}
|
|
1335
|
-
|
|
1278
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1279
|
+
return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
|
|
1336
1280
|
Review:
|
|
1337
1281
|
${review}
|
|
1338
1282
|
Commit: "${commitMessage}"
|
|
@@ -1349,8 +1293,8 @@ __export(close_exports, {
|
|
|
1349
1293
|
terminal: () => terminal2
|
|
1350
1294
|
});
|
|
1351
1295
|
init_github();
|
|
1352
|
-
import { select as
|
|
1353
|
-
import
|
|
1296
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
1297
|
+
import ora2 from "ora";
|
|
1354
1298
|
var definition2 = {
|
|
1355
1299
|
type: "function",
|
|
1356
1300
|
function: {
|
|
@@ -1376,7 +1320,7 @@ async function run2(input3, config) {
|
|
|
1376
1320
|
}
|
|
1377
1321
|
if (tasks.length === 0) return "No tasks found.";
|
|
1378
1322
|
try {
|
|
1379
|
-
issueNumber = await
|
|
1323
|
+
issueNumber = await select2({
|
|
1380
1324
|
message: "Select task to close:",
|
|
1381
1325
|
choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
|
|
1382
1326
|
});
|
|
@@ -1386,7 +1330,7 @@ async function run2(input3, config) {
|
|
|
1386
1330
|
}
|
|
1387
1331
|
let confirmed;
|
|
1388
1332
|
try {
|
|
1389
|
-
confirmed = await
|
|
1333
|
+
confirmed = await select2({
|
|
1390
1334
|
message: `Close task #${issueNumber}?`,
|
|
1391
1335
|
choices: [
|
|
1392
1336
|
{ name: "Yes, close it", value: true },
|
|
@@ -1397,7 +1341,7 @@ async function run2(input3, config) {
|
|
|
1397
1341
|
return "Cancelled.";
|
|
1398
1342
|
}
|
|
1399
1343
|
if (!confirmed) return "Cancelled.";
|
|
1400
|
-
const spinner =
|
|
1344
|
+
const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
|
|
1401
1345
|
try {
|
|
1402
1346
|
await closeTask(config, issueNumber);
|
|
1403
1347
|
spinner.stop();
|
|
@@ -1409,7 +1353,7 @@ async function run2(input3, config) {
|
|
|
1409
1353
|
}
|
|
1410
1354
|
async function execute2(input3, config) {
|
|
1411
1355
|
const issueNumber = input3["issue_number"];
|
|
1412
|
-
const spinner =
|
|
1356
|
+
const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
|
|
1413
1357
|
try {
|
|
1414
1358
|
await closeTask(config, issueNumber);
|
|
1415
1359
|
spinner.stop();
|
|
@@ -1455,7 +1399,7 @@ async function run3(input3, config) {
|
|
|
1455
1399
|
}
|
|
1456
1400
|
if (tasks.length === 0) return "No tasks found.";
|
|
1457
1401
|
try {
|
|
1458
|
-
chosenNumber = await
|
|
1402
|
+
chosenNumber = await select3({
|
|
1459
1403
|
message: "Select a task:",
|
|
1460
1404
|
choices: tasks.map((t) => ({
|
|
1461
1405
|
name: `#${String(t.number).padEnd(4)} ${colorStatus(getStatus(t))} ${t.title}`,
|
|
@@ -1479,9 +1423,9 @@ async function run3(input3, config) {
|
|
|
1479
1423
|
const comments = await listComments(config, issue.number, 1);
|
|
1480
1424
|
if (comments.length > 0) {
|
|
1481
1425
|
const c = comments[0];
|
|
1482
|
-
const divider =
|
|
1426
|
+
const divider = chalk5.dim("\u2500".repeat(70));
|
|
1483
1427
|
console.log(
|
|
1484
|
-
|
|
1428
|
+
chalk5.red.bold(" Latest rejection feedback") + chalk5.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
|
|
1485
1429
|
);
|
|
1486
1430
|
console.log(divider);
|
|
1487
1431
|
console.log(renderMarkdown(c.body));
|
|
@@ -1491,22 +1435,36 @@ async function run3(input3, config) {
|
|
|
1491
1435
|
}
|
|
1492
1436
|
}
|
|
1493
1437
|
const actions = [];
|
|
1494
|
-
if (status === "available")
|
|
1495
|
-
|
|
1438
|
+
if (status === "available") {
|
|
1439
|
+
actions.push({ name: "Claim this task", value: "claim" });
|
|
1440
|
+
}
|
|
1441
|
+
if (status === "claimed") {
|
|
1442
|
+
actions.push({ name: "Submit this task", value: "submit" });
|
|
1443
|
+
}
|
|
1444
|
+
if (status === "changes-needed") {
|
|
1445
|
+
const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1446
|
+
const me = await getAuthenticatedUser2(config);
|
|
1447
|
+
const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
|
|
1448
|
+
const currentBranch = await getCurrentBranch();
|
|
1449
|
+
if (currentBranch === taskBranch) {
|
|
1450
|
+
actions.push({ name: "Submit this task (fixes done)", value: "submit" });
|
|
1451
|
+
} else {
|
|
1452
|
+
actions.push({ name: `Switch to ${taskBranch} to fix`, value: "switch-fix" });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1496
1455
|
actions.push({ name: "Close this task", value: "close" });
|
|
1497
1456
|
actions.push({ name: "Nothing, just viewing", value: "none" });
|
|
1498
1457
|
let action;
|
|
1499
1458
|
try {
|
|
1500
|
-
action = await
|
|
1459
|
+
action = await select3({ message: "Action:", choices: actions });
|
|
1501
1460
|
} catch {
|
|
1502
1461
|
return "Cancelled.";
|
|
1503
1462
|
}
|
|
1504
1463
|
if (action === "none") return `Viewed task #${issue.number}.`;
|
|
1505
1464
|
if (action === "claim") {
|
|
1506
1465
|
try {
|
|
1507
|
-
const
|
|
1508
|
-
const
|
|
1509
|
-
const myTasks = await listMyTasks2(config, me);
|
|
1466
|
+
const me = await getAuthenticatedUser(config);
|
|
1467
|
+
const myTasks = await listMyTasks(config, me);
|
|
1510
1468
|
const activeTask = myTasks.find((t) => {
|
|
1511
1469
|
const labels = t.labels;
|
|
1512
1470
|
return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
|
|
@@ -1515,37 +1473,65 @@ async function run3(input3, config) {
|
|
|
1515
1473
|
return `You already have an active task: #${activeTask.number} "${activeTask.title}"
|
|
1516
1474
|
Finish or submit it before claiming a new one.`;
|
|
1517
1475
|
}
|
|
1518
|
-
let
|
|
1476
|
+
let stashed = false;
|
|
1477
|
+
if (await hasUncommittedChanges()) {
|
|
1478
|
+
let choice;
|
|
1479
|
+
try {
|
|
1480
|
+
choice = await select3({
|
|
1481
|
+
message: "You have uncommitted changes. What would you like to do?",
|
|
1482
|
+
choices: [
|
|
1483
|
+
{ name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
|
|
1484
|
+
{ name: "Cancel", value: "cancel" }
|
|
1485
|
+
]
|
|
1486
|
+
});
|
|
1487
|
+
} catch {
|
|
1488
|
+
choice = "cancel";
|
|
1489
|
+
}
|
|
1490
|
+
if (choice === "cancel") return "Cancelled.";
|
|
1491
|
+
await stash(`tch: before claiming #${issue.number}`);
|
|
1492
|
+
stashed = true;
|
|
1493
|
+
console.log(chalk5.dim(" Changes stashed. Run `git stash pop` after you finish this task to restore them."));
|
|
1494
|
+
}
|
|
1495
|
+
let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
|
|
1519
1496
|
await claimTask(config, issue.number, me);
|
|
1520
1497
|
spinner.stop();
|
|
1521
|
-
const
|
|
1498
|
+
const taskBranch = makeTaskBranchName(issue.number, me);
|
|
1522
1499
|
const taskBase = extractBaseCommit(issue.body);
|
|
1523
|
-
spinner =
|
|
1500
|
+
spinner = ora3(`Creating branch ${taskBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
|
|
1524
1501
|
try {
|
|
1525
1502
|
if (taskBase) {
|
|
1526
|
-
await
|
|
1503
|
+
await checkoutFromCommit(taskBranch, taskBase);
|
|
1527
1504
|
} else {
|
|
1528
|
-
await switchToBranchOrCreate(
|
|
1505
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1529
1506
|
}
|
|
1530
1507
|
spinner.stop();
|
|
1531
|
-
spinner =
|
|
1508
|
+
spinner = ora3("Pushing task branch\u2026").start();
|
|
1532
1509
|
try {
|
|
1533
|
-
await pushBranch(
|
|
1510
|
+
await pushBranch(taskBranch);
|
|
1534
1511
|
spinner.stop();
|
|
1535
1512
|
} catch {
|
|
1536
|
-
spinner.warn("Could not push
|
|
1513
|
+
spinner.warn("Could not push task branch \u2014 will push on submit");
|
|
1537
1514
|
}
|
|
1538
|
-
} catch {
|
|
1539
|
-
spinner.warn(`Could not switch to ${
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
spinner.warn(`Could not switch to ${taskBranch}`);
|
|
1517
|
+
if (stashed) {
|
|
1518
|
+
try {
|
|
1519
|
+
await stashPop();
|
|
1520
|
+
console.log(chalk5.dim(" Restored stashed changes."));
|
|
1521
|
+
} catch {
|
|
1522
|
+
console.log(chalk5.yellow(" Warning: could not restore stash automatically. Run `git stash pop` manually."));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
throw err;
|
|
1540
1526
|
}
|
|
1541
1527
|
const baseCommit = await getCurrentCommit();
|
|
1542
|
-
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
|
|
1543
|
-
console.log(
|
|
1544
|
-
Claimed! Branch: ${
|
|
1528
|
+
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
|
|
1529
|
+
console.log(chalk5.green(`
|
|
1530
|
+
Claimed! Branch: ${taskBranch} (base: ${baseCommit.slice(0, 7)})
|
|
1545
1531
|
`));
|
|
1546
1532
|
let openClaude;
|
|
1547
1533
|
try {
|
|
1548
|
-
openClaude = await
|
|
1534
|
+
openClaude = await select3({
|
|
1549
1535
|
message: "Open Claude Code for this task?",
|
|
1550
1536
|
choices: [
|
|
1551
1537
|
{ name: "Yes, start coding now", value: true },
|
|
@@ -1555,12 +1541,58 @@ Finish or submit it before claiming a new one.`;
|
|
|
1555
1541
|
} catch {
|
|
1556
1542
|
openClaude = false;
|
|
1557
1543
|
}
|
|
1558
|
-
if (openClaude) await launchClaudeCode(issue,
|
|
1559
|
-
return `Task #${issue.number} claimed. Branch: ${
|
|
1544
|
+
if (openClaude) await launchClaudeCode(issue, taskBranch);
|
|
1545
|
+
return `Task #${issue.number} claimed. Branch: ${taskBranch}`;
|
|
1560
1546
|
} catch (err) {
|
|
1561
1547
|
return `Error claiming task: ${err.message}`;
|
|
1562
1548
|
}
|
|
1563
1549
|
}
|
|
1550
|
+
if (action === "switch-fix") {
|
|
1551
|
+
const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1552
|
+
const me = await getAuthenticatedUser2(config);
|
|
1553
|
+
const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
|
|
1554
|
+
let stashed = false;
|
|
1555
|
+
if (await hasUncommittedChanges()) {
|
|
1556
|
+
let choice;
|
|
1557
|
+
try {
|
|
1558
|
+
choice = await select3({
|
|
1559
|
+
message: "You have uncommitted changes. What would you like to do?",
|
|
1560
|
+
choices: [
|
|
1561
|
+
{ name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
|
|
1562
|
+
{ name: "Cancel", value: "cancel" }
|
|
1563
|
+
]
|
|
1564
|
+
});
|
|
1565
|
+
} catch {
|
|
1566
|
+
choice = "cancel";
|
|
1567
|
+
}
|
|
1568
|
+
if (choice === "cancel") return "Cancelled.";
|
|
1569
|
+
await stash(`tch: before switching to ${taskBranch}`);
|
|
1570
|
+
stashed = true;
|
|
1571
|
+
console.log(chalk5.dim(" Changes stashed. Run `git stash pop` to restore them later."));
|
|
1572
|
+
}
|
|
1573
|
+
const spinner = ora3(`Switching to ${taskBranch}\u2026`).start();
|
|
1574
|
+
try {
|
|
1575
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1576
|
+
spinner.stop();
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
spinner.warn(`Could not switch to ${taskBranch}: ${err.message}`);
|
|
1579
|
+
if (stashed) {
|
|
1580
|
+
try {
|
|
1581
|
+
await stashPop();
|
|
1582
|
+
console.log(chalk5.dim(" Restored stashed changes."));
|
|
1583
|
+
} catch {
|
|
1584
|
+
console.log(chalk5.yellow(" Run `git stash pop` manually to restore your changes."));
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return `Error: ${err.message}`;
|
|
1588
|
+
}
|
|
1589
|
+
const baseCommit = extractBaseCommit(issue.body) ?? await getCurrentCommit();
|
|
1590
|
+
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
|
|
1591
|
+
console.log(chalk5.green(`
|
|
1592
|
+
Switched to ${taskBranch}. Fix the issues then run /submit.
|
|
1593
|
+
`));
|
|
1594
|
+
return `Switched to ${taskBranch} for task #${issue.number}.`;
|
|
1595
|
+
}
|
|
1564
1596
|
if (action === "submit") return run({}, config);
|
|
1565
1597
|
if (action === "close") return run2({ issue_number: issue.number }, config);
|
|
1566
1598
|
return "Cancelled.";
|
|
@@ -1589,28 +1621,31 @@ async function execute3(input3, config) {
|
|
|
1589
1621
|
if (activeTask) {
|
|
1590
1622
|
return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
|
|
1591
1623
|
}
|
|
1624
|
+
if (await hasUncommittedChanges()) {
|
|
1625
|
+
return "Cannot claim: you have uncommitted changes. Commit or stash them first (git stash).";
|
|
1626
|
+
}
|
|
1592
1627
|
try {
|
|
1593
1628
|
await claimTask(config, issueNumber, me);
|
|
1594
1629
|
} catch (err) {
|
|
1595
1630
|
return `Error claiming task: ${err.message}`;
|
|
1596
1631
|
}
|
|
1597
|
-
const
|
|
1632
|
+
const taskBranch = makeTaskBranchName(issue.number, me);
|
|
1598
1633
|
const taskBase = extractBaseCommit(issue.body);
|
|
1599
1634
|
try {
|
|
1600
1635
|
if (taskBase) {
|
|
1601
|
-
await
|
|
1636
|
+
await checkoutFromCommit(taskBranch, taskBase);
|
|
1602
1637
|
} else {
|
|
1603
|
-
await switchToBranchOrCreate(
|
|
1638
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1604
1639
|
}
|
|
1605
1640
|
} catch {
|
|
1606
1641
|
}
|
|
1607
1642
|
try {
|
|
1608
|
-
await pushBranch(
|
|
1643
|
+
await pushBranch(taskBranch);
|
|
1609
1644
|
} catch {
|
|
1610
1645
|
}
|
|
1611
1646
|
const baseCommit = await getCurrentCommit();
|
|
1612
|
-
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
|
|
1613
|
-
return `Task #${issueNumber} claimed. Branch: ${
|
|
1647
|
+
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit, activeBranch: taskBranch } });
|
|
1648
|
+
return `Task #${issueNumber} claimed. Branch: ${taskBranch} (base commit: ${baseCommit.slice(0, 7)})`;
|
|
1614
1649
|
}
|
|
1615
1650
|
return `Unknown action: ${action}`;
|
|
1616
1651
|
}
|
|
@@ -1625,14 +1660,14 @@ __export(new_task_exports, {
|
|
|
1625
1660
|
terminal: () => terminal4
|
|
1626
1661
|
});
|
|
1627
1662
|
init_github();
|
|
1628
|
-
import { select as
|
|
1663
|
+
import { select as select4, input as promptInput2 } from "@inquirer/prompts";
|
|
1629
1664
|
import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
|
|
1630
1665
|
import { spawn as spawn2 } from "child_process";
|
|
1631
1666
|
import { tmpdir } from "os";
|
|
1632
1667
|
import path from "path";
|
|
1633
|
-
import
|
|
1634
|
-
import
|
|
1635
|
-
import
|
|
1668
|
+
import ora4 from "ora";
|
|
1669
|
+
import chalk6 from "chalk";
|
|
1670
|
+
import open from "open";
|
|
1636
1671
|
|
|
1637
1672
|
// src/tools/new-task/prompts.ts
|
|
1638
1673
|
var GUIDE_FORMAT = `
|
|
@@ -1662,9 +1697,9 @@ Previous guide:
|
|
|
1662
1697
|
${revise.previousGuide}` : `Write an implementation guide for this task: "${title}"`;
|
|
1663
1698
|
return runSubAgentLoop(
|
|
1664
1699
|
config,
|
|
1665
|
-
"You are a senior engineer writing a brief task guide for a developer. Use
|
|
1700
|
+
"You are a senior engineer writing a brief task guide for a developer. Use list_files then grep_code 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,
|
|
1666
1701
|
userMessage,
|
|
1667
|
-
["
|
|
1702
|
+
["list_files", "grep_code", "run_command", "ask_user"]
|
|
1668
1703
|
);
|
|
1669
1704
|
}
|
|
1670
1705
|
|
|
@@ -1685,6 +1720,77 @@ async function openInEditor(content) {
|
|
|
1685
1720
|
await rm(dir, { recursive: true, force: true });
|
|
1686
1721
|
}
|
|
1687
1722
|
}
|
|
1723
|
+
async function resolveBaseAndTarget(config, me, interactive) {
|
|
1724
|
+
const currentBranch = await getCurrentBranch();
|
|
1725
|
+
if (isTaskBranch(currentBranch)) {
|
|
1726
|
+
if (await hasUncommittedChanges()) {
|
|
1727
|
+
if (!interactive) {
|
|
1728
|
+
throw new Error("Cannot create sub-task: you have uncommitted changes. Commit them first so the executor starts from the correct base.");
|
|
1729
|
+
}
|
|
1730
|
+
const { select: inquirerSelect } = await import("@inquirer/prompts");
|
|
1731
|
+
let choice;
|
|
1732
|
+
try {
|
|
1733
|
+
choice = await inquirerSelect({
|
|
1734
|
+
message: "You have uncommitted changes. The sub-task executor will start from the last commit \u2014 they won't see your current unsaved work.",
|
|
1735
|
+
choices: [
|
|
1736
|
+
{ name: "Commit first (cancel and commit manually)", value: "cancel" },
|
|
1737
|
+
{ name: "Continue anyway (executor starts without my unsaved changes)", value: "continue" }
|
|
1738
|
+
]
|
|
1739
|
+
});
|
|
1740
|
+
} catch {
|
|
1741
|
+
choice = "cancel";
|
|
1742
|
+
}
|
|
1743
|
+
if (choice === "cancel") throw new Error("Cancelled. Commit your changes first, then create the sub-task.");
|
|
1744
|
+
}
|
|
1745
|
+
const baseCommit2 = await getCurrentCommit();
|
|
1746
|
+
return { baseCommit: baseCommit2, targetBranch: currentBranch, isSubtask: true };
|
|
1747
|
+
}
|
|
1748
|
+
let stashedForSync = false;
|
|
1749
|
+
if (await hasUncommittedChanges()) {
|
|
1750
|
+
if (!interactive) {
|
|
1751
|
+
throw new Error("Cannot create task: you have uncommitted changes. Commit or stash them first (git stash).");
|
|
1752
|
+
}
|
|
1753
|
+
const { select: inquirerSelect } = await import("@inquirer/prompts");
|
|
1754
|
+
let choice;
|
|
1755
|
+
try {
|
|
1756
|
+
choice = await inquirerSelect({
|
|
1757
|
+
message: "You have uncommitted changes. Syncing with main requires a clean working tree.",
|
|
1758
|
+
choices: [
|
|
1759
|
+
{ name: "Stash changes and continue (restore with: git stash pop)", value: "stash" },
|
|
1760
|
+
{ name: "Cancel", value: "cancel" }
|
|
1761
|
+
]
|
|
1762
|
+
});
|
|
1763
|
+
} catch {
|
|
1764
|
+
choice = "cancel";
|
|
1765
|
+
}
|
|
1766
|
+
if (choice === "cancel") throw new Error("Cancelled.");
|
|
1767
|
+
await stash("tch: before creating new task");
|
|
1768
|
+
stashedForSync = true;
|
|
1769
|
+
console.log(chalk6.dim(" Changes stashed. Run `git stash pop` after creating the task."));
|
|
1770
|
+
}
|
|
1771
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
1772
|
+
let baseCommit;
|
|
1773
|
+
const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
|
|
1774
|
+
try {
|
|
1775
|
+
await syncWithBase(baseBranch);
|
|
1776
|
+
baseCommit = await getCurrentCommit();
|
|
1777
|
+
syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
|
|
1778
|
+
} catch {
|
|
1779
|
+
syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
|
|
1780
|
+
try {
|
|
1781
|
+
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1782
|
+
} catch {
|
|
1783
|
+
}
|
|
1784
|
+
if (stashedForSync) {
|
|
1785
|
+
try {
|
|
1786
|
+
await stashPop();
|
|
1787
|
+
} catch {
|
|
1788
|
+
}
|
|
1789
|
+
throw new Error(`Could not sync with ${baseBranch}. Your changes have been restored from stash.`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return { baseCommit, targetBranch: makeWorkerBranchName(me), isSubtask: false };
|
|
1793
|
+
}
|
|
1688
1794
|
var definition4 = {
|
|
1689
1795
|
type: "function",
|
|
1690
1796
|
function: {
|
|
@@ -1701,7 +1807,7 @@ var definition4 = {
|
|
|
1701
1807
|
}
|
|
1702
1808
|
};
|
|
1703
1809
|
async function run4(input3, config) {
|
|
1704
|
-
const authSpinner =
|
|
1810
|
+
const authSpinner = ora4("Checking permissions\u2026").start();
|
|
1705
1811
|
let me;
|
|
1706
1812
|
let allowed;
|
|
1707
1813
|
try {
|
|
@@ -1724,7 +1830,7 @@ async function run4(input3, config) {
|
|
|
1724
1830
|
}
|
|
1725
1831
|
if (!title) return "Cancelled.";
|
|
1726
1832
|
}
|
|
1727
|
-
const spinner =
|
|
1833
|
+
const spinner = ora4("Scanning project and generating guide\u2026").start();
|
|
1728
1834
|
let guide;
|
|
1729
1835
|
try {
|
|
1730
1836
|
guide = await generateGuide(config, title);
|
|
@@ -1733,16 +1839,16 @@ async function run4(input3, config) {
|
|
|
1733
1839
|
spinner.stop();
|
|
1734
1840
|
return `Error generating guide: ${err.message}`;
|
|
1735
1841
|
}
|
|
1736
|
-
const divider =
|
|
1842
|
+
const divider = chalk6.dim("\u2500".repeat(70));
|
|
1737
1843
|
for (; ; ) {
|
|
1738
1844
|
console.log("\n" + divider);
|
|
1739
|
-
console.log(
|
|
1845
|
+
console.log(chalk6.bold(" Generated guide preview"));
|
|
1740
1846
|
console.log(divider);
|
|
1741
1847
|
console.log(renderMarkdown(guide));
|
|
1742
1848
|
console.log(divider + "\n");
|
|
1743
1849
|
let action;
|
|
1744
1850
|
try {
|
|
1745
|
-
action = await
|
|
1851
|
+
action = await select4({
|
|
1746
1852
|
message: "Create this task?",
|
|
1747
1853
|
choices: [
|
|
1748
1854
|
{ name: "Yes, create task", value: "create" },
|
|
@@ -1760,7 +1866,7 @@ async function run4(input3, config) {
|
|
|
1760
1866
|
try {
|
|
1761
1867
|
guide = await openInEditor(guide);
|
|
1762
1868
|
} catch (err) {
|
|
1763
|
-
console.log(
|
|
1869
|
+
console.log(chalk6.yellow(` Editor error: ${err.message}`));
|
|
1764
1870
|
}
|
|
1765
1871
|
continue;
|
|
1766
1872
|
}
|
|
@@ -1771,35 +1877,32 @@ async function run4(input3, config) {
|
|
|
1771
1877
|
return "Cancelled.";
|
|
1772
1878
|
}
|
|
1773
1879
|
if (!feedback) continue;
|
|
1774
|
-
const reviseSpinner =
|
|
1880
|
+
const reviseSpinner = ora4("Revising guide\u2026").start();
|
|
1775
1881
|
try {
|
|
1776
1882
|
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1777
1883
|
reviseSpinner.stop();
|
|
1778
1884
|
} catch (err) {
|
|
1779
1885
|
reviseSpinner.stop();
|
|
1780
|
-
console.log(
|
|
1886
|
+
console.log(chalk6.yellow(` Revision error: ${err.message}`));
|
|
1781
1887
|
}
|
|
1782
1888
|
}
|
|
1783
|
-
const baseBranch = config.baseBranch ?? "main";
|
|
1784
1889
|
let baseCommit;
|
|
1785
|
-
|
|
1890
|
+
let targetBranch;
|
|
1891
|
+
let isSubtask;
|
|
1786
1892
|
try {
|
|
1787
|
-
await
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
} catch {
|
|
1791
|
-
syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
|
|
1792
|
-
try {
|
|
1793
|
-
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1794
|
-
} catch {
|
|
1795
|
-
}
|
|
1893
|
+
({ baseCommit, targetBranch, isSubtask } = await resolveBaseAndTarget(config, me, true));
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
return err.message;
|
|
1796
1896
|
}
|
|
1797
|
-
|
|
1897
|
+
if (isSubtask) {
|
|
1898
|
+
console.log(chalk6.dim(` Sub-task: will target branch ${chalk6.cyan(targetBranch)} (base: ${baseCommit?.slice(0, 7) ?? "HEAD"})`));
|
|
1899
|
+
}
|
|
1900
|
+
const createSpinner = ora4(`Creating "${title}"\u2026`).start();
|
|
1798
1901
|
let htmlUrl;
|
|
1799
1902
|
let issueNumber;
|
|
1800
1903
|
let issueTitle;
|
|
1801
1904
|
try {
|
|
1802
|
-
const issue = await createTask(config, title, guide, baseCommit);
|
|
1905
|
+
const issue = await createTask(config, title, guide, baseCommit, targetBranch);
|
|
1803
1906
|
createSpinner.stop();
|
|
1804
1907
|
htmlUrl = issue.htmlUrl;
|
|
1805
1908
|
issueNumber = issue.number;
|
|
@@ -1808,19 +1911,19 @@ async function run4(input3, config) {
|
|
|
1808
1911
|
createSpinner.stop();
|
|
1809
1912
|
return `Error: ${err.message}`;
|
|
1810
1913
|
}
|
|
1811
|
-
console.log(
|
|
1914
|
+
console.log(chalk6.green(`
|
|
1812
1915
|
Created #${issueNumber} "${issueTitle}"
|
|
1813
|
-
${
|
|
1916
|
+
${chalk6.dim(htmlUrl)}
|
|
1814
1917
|
`));
|
|
1815
1918
|
try {
|
|
1816
|
-
const openBrowser = await
|
|
1919
|
+
const openBrowser = await select4({
|
|
1817
1920
|
message: "Open issue in browser?",
|
|
1818
1921
|
choices: [
|
|
1819
1922
|
{ name: "Yes", value: true },
|
|
1820
1923
|
{ name: "No", value: false }
|
|
1821
1924
|
]
|
|
1822
1925
|
});
|
|
1823
|
-
if (openBrowser) await
|
|
1926
|
+
if (openBrowser) await open(htmlUrl);
|
|
1824
1927
|
} catch {
|
|
1825
1928
|
}
|
|
1826
1929
|
return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
|
|
@@ -1836,19 +1939,9 @@ async function execute4(input3, config) {
|
|
|
1836
1939
|
if (feedback) {
|
|
1837
1940
|
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1838
1941
|
}
|
|
1839
|
-
const
|
|
1840
|
-
let baseCommit;
|
|
1841
|
-
try {
|
|
1842
|
-
await syncWithBase(baseBranch);
|
|
1843
|
-
baseCommit = await getCurrentCommit();
|
|
1844
|
-
} catch {
|
|
1845
|
-
try {
|
|
1846
|
-
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1847
|
-
} catch {
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1942
|
+
const { baseCommit, targetBranch } = await resolveBaseAndTarget(config, me, false);
|
|
1850
1943
|
try {
|
|
1851
|
-
const issue = await createTask(config, title, guide, baseCommit);
|
|
1944
|
+
const issue = await createTask(config, title, guide, baseCommit, targetBranch);
|
|
1852
1945
|
return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
|
|
1853
1946
|
|
|
1854
1947
|
Guide:
|
|
@@ -1868,7 +1961,7 @@ __export(my_status_exports, {
|
|
|
1868
1961
|
terminal: () => terminal5
|
|
1869
1962
|
});
|
|
1870
1963
|
init_github();
|
|
1871
|
-
import
|
|
1964
|
+
import ora5 from "ora";
|
|
1872
1965
|
var definition5 = {
|
|
1873
1966
|
type: "function",
|
|
1874
1967
|
function: {
|
|
@@ -1878,7 +1971,7 @@ var definition5 = {
|
|
|
1878
1971
|
}
|
|
1879
1972
|
};
|
|
1880
1973
|
async function run5(_input, config) {
|
|
1881
|
-
const spinner =
|
|
1974
|
+
const spinner = ora5("Fetching your tasks\u2026").start();
|
|
1882
1975
|
try {
|
|
1883
1976
|
const me = await getAuthenticatedUser(config);
|
|
1884
1977
|
const tasks = await listMyTasks(config, me);
|
|
@@ -1898,120 +1991,193 @@ var terminal5 = true;
|
|
|
1898
1991
|
// src/tools/review/index.ts
|
|
1899
1992
|
var review_exports = {};
|
|
1900
1993
|
__export(review_exports, {
|
|
1994
|
+
definition: () => definition8,
|
|
1995
|
+
execute: () => execute8,
|
|
1996
|
+
run: () => run8,
|
|
1997
|
+
terminal: () => terminal8
|
|
1998
|
+
});
|
|
1999
|
+
init_github();
|
|
2000
|
+
import chalk9 from "chalk";
|
|
2001
|
+
import ora8 from "ora";
|
|
2002
|
+
import { select as select7 } from "@inquirer/prompts";
|
|
2003
|
+
|
|
2004
|
+
// src/tools/accept/index.ts
|
|
2005
|
+
var accept_exports = {};
|
|
2006
|
+
__export(accept_exports, {
|
|
1901
2007
|
definition: () => definition6,
|
|
1902
2008
|
execute: () => execute6,
|
|
1903
2009
|
run: () => run6,
|
|
1904
2010
|
terminal: () => terminal6
|
|
1905
2011
|
});
|
|
1906
2012
|
init_github();
|
|
1907
|
-
import
|
|
2013
|
+
import chalk7 from "chalk";
|
|
2014
|
+
import { select as select5 } from "@inquirer/prompts";
|
|
2015
|
+
import ora6 from "ora";
|
|
1908
2016
|
var definition6 = {
|
|
1909
2017
|
type: "function",
|
|
1910
2018
|
function: {
|
|
1911
|
-
name: "
|
|
1912
|
-
description: "
|
|
1913
|
-
parameters: {
|
|
2019
|
+
name: "accept",
|
|
2020
|
+
description: "Accept an in-review task: merges the PR into the target branch and closes the issue.",
|
|
2021
|
+
parameters: {
|
|
2022
|
+
type: "object",
|
|
2023
|
+
properties: {
|
|
2024
|
+
issue_number: { type: "number", description: "GitHub issue number to accept" }
|
|
2025
|
+
},
|
|
2026
|
+
required: ["issue_number"]
|
|
2027
|
+
}
|
|
1914
2028
|
}
|
|
1915
2029
|
};
|
|
1916
|
-
async function run6(
|
|
1917
|
-
|
|
2030
|
+
async function run6(input3, config) {
|
|
2031
|
+
let issueNumber = input3["issue_number"];
|
|
2032
|
+
if (!issueNumber) {
|
|
2033
|
+
const spinner3 = ora6("Loading tasks for review\u2026").start();
|
|
2034
|
+
let tasks;
|
|
2035
|
+
let me;
|
|
2036
|
+
try {
|
|
2037
|
+
me = await getAuthenticatedUser(config);
|
|
2038
|
+
tasks = await listTasksForReview(config, me);
|
|
2039
|
+
spinner3.stop();
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
spinner3.stop();
|
|
2042
|
+
return `Error: ${err.message}`;
|
|
2043
|
+
}
|
|
2044
|
+
if (tasks.length === 0) return "No tasks pending review.";
|
|
2045
|
+
try {
|
|
2046
|
+
issueNumber = await select5({
|
|
2047
|
+
message: "Which task to accept?",
|
|
2048
|
+
choices: tasks.map((t) => ({
|
|
2049
|
+
name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
2050
|
+
value: t.number
|
|
2051
|
+
}))
|
|
2052
|
+
});
|
|
2053
|
+
} catch {
|
|
2054
|
+
return "Cancelled.";
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
const spinner2 = ora6("Verifying permissions\u2026").start();
|
|
2058
|
+
let me2;
|
|
2059
|
+
let issue;
|
|
1918
2060
|
try {
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
return `Tasks pending review (created by @${me}):
|
|
1925
|
-
${lines.join("\n")}`;
|
|
2061
|
+
[me2, issue] = await Promise.all([
|
|
2062
|
+
getAuthenticatedUser(config),
|
|
2063
|
+
getTask(config, issueNumber)
|
|
2064
|
+
]);
|
|
2065
|
+
spinner2.stop();
|
|
1926
2066
|
} catch (err) {
|
|
1927
|
-
|
|
2067
|
+
spinner2.stop();
|
|
1928
2068
|
return `Error: ${err.message}`;
|
|
1929
2069
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
var terminal6 = true;
|
|
1933
|
-
|
|
1934
|
-
// src/tools/refresh/index.ts
|
|
1935
|
-
var refresh_exports = {};
|
|
1936
|
-
__export(refresh_exports, {
|
|
1937
|
-
definition: () => definition7,
|
|
1938
|
-
execute: () => execute7,
|
|
1939
|
-
run: () => run7,
|
|
1940
|
-
terminal: () => terminal7
|
|
1941
|
-
});
|
|
1942
|
-
var definition7 = {
|
|
1943
|
-
type: "function",
|
|
1944
|
-
function: {
|
|
1945
|
-
name: "refresh",
|
|
1946
|
-
description: "Reload and display the full task list. Equivalent to /refresh.",
|
|
1947
|
-
parameters: { type: "object", properties: {}, required: [] }
|
|
1948
|
-
}
|
|
1949
|
-
};
|
|
1950
|
-
async function run7(_input, config) {
|
|
1951
|
-
const tasks = await printTaskList(config);
|
|
1952
|
-
if (tasks.length === 0) return "No tasks found.";
|
|
1953
|
-
const lines = tasks.map((t) => {
|
|
1954
|
-
const status = getStatus(t);
|
|
1955
|
-
const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
|
|
1956
|
-
return `#${t.number} [${status}] ${assignee} ${t.title}`;
|
|
1957
|
-
});
|
|
1958
|
-
return `Tasks (${tasks.length}):
|
|
1959
|
-
${lines.join("\n")}`;
|
|
1960
|
-
}
|
|
1961
|
-
var execute7 = run7;
|
|
1962
|
-
var terminal7 = true;
|
|
1963
|
-
|
|
1964
|
-
// src/tools/open-code/index.ts
|
|
1965
|
-
var open_code_exports = {};
|
|
1966
|
-
__export(open_code_exports, {
|
|
1967
|
-
definition: () => definition8,
|
|
1968
|
-
execute: () => execute8,
|
|
1969
|
-
run: () => run8,
|
|
1970
|
-
terminal: () => terminal8
|
|
1971
|
-
});
|
|
1972
|
-
init_github();
|
|
1973
|
-
var definition8 = {
|
|
1974
|
-
type: "function",
|
|
1975
|
-
function: {
|
|
1976
|
-
name: "open_code",
|
|
1977
|
-
description: "Launch Claude Code for the current task branch. Equivalent to /code.",
|
|
1978
|
-
parameters: { type: "object", properties: {}, required: [] }
|
|
2070
|
+
if (issue.author && issue.author !== me2) {
|
|
2071
|
+
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
1979
2072
|
}
|
|
1980
|
-
|
|
1981
|
-
async function run8(_input, config) {
|
|
1982
|
-
let branch;
|
|
2073
|
+
let confirmed;
|
|
1983
2074
|
try {
|
|
1984
|
-
|
|
2075
|
+
confirmed = await select5({
|
|
2076
|
+
message: `Merge PR for #${issueNumber} and close issue?`,
|
|
2077
|
+
choices: [
|
|
2078
|
+
{ name: "Yes, accept", value: true },
|
|
2079
|
+
{ name: "Cancel", value: false }
|
|
2080
|
+
]
|
|
2081
|
+
});
|
|
2082
|
+
} catch {
|
|
2083
|
+
return "Cancelled.";
|
|
2084
|
+
}
|
|
2085
|
+
if (!confirmed) return "Cancelled.";
|
|
2086
|
+
const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2087
|
+
let result;
|
|
2088
|
+
try {
|
|
2089
|
+
result = await acceptTask(config, issueNumber);
|
|
2090
|
+
spinner.succeed(`PR #${result.prNumber} merged \u2192 ${chalk7.cyan(result.baseBranch)}`);
|
|
1985
2091
|
} catch (err) {
|
|
2092
|
+
spinner.fail("Failed");
|
|
1986
2093
|
return `Error: ${err.message}`;
|
|
1987
2094
|
}
|
|
1988
|
-
const
|
|
1989
|
-
if (!
|
|
1990
|
-
|
|
1991
|
-
|
|
2095
|
+
const mergedIntoTaskBranch = isTaskBranch(result.baseBranch);
|
|
2096
|
+
if (!mergedIntoTaskBranch) {
|
|
2097
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
2098
|
+
let pushToMain;
|
|
2099
|
+
try {
|
|
2100
|
+
pushToMain = await select5({
|
|
2101
|
+
message: `Push ${chalk7.cyan(result.baseBranch)} \u2192 ${chalk7.cyan(baseBranch)}?`,
|
|
2102
|
+
choices: [
|
|
2103
|
+
{ name: `Yes, push to ${baseBranch}`, value: true },
|
|
2104
|
+
{ name: "No, keep in worker branch", value: false }
|
|
2105
|
+
]
|
|
2106
|
+
});
|
|
2107
|
+
} catch {
|
|
2108
|
+
pushToMain = false;
|
|
2109
|
+
}
|
|
2110
|
+
if (pushToMain) {
|
|
2111
|
+
const mergeSpinner = ora6(`Merging ${result.baseBranch} \u2192 ${baseBranch}\u2026`).start();
|
|
2112
|
+
try {
|
|
2113
|
+
await mergeWorkerIntoBase(config, result.baseBranch, baseBranch);
|
|
2114
|
+
mergeSpinner.succeed(`Merged ${result.baseBranch} \u2192 ${baseBranch}`);
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
let updateWiki = false;
|
|
1992
2121
|
try {
|
|
1993
|
-
|
|
2122
|
+
updateWiki = await select5({
|
|
2123
|
+
message: "Update TECHUNTER.md project overview?",
|
|
2124
|
+
choices: [
|
|
2125
|
+
{ name: "Yes, regenerate", value: true },
|
|
2126
|
+
{ name: "No, skip", value: false }
|
|
2127
|
+
]
|
|
2128
|
+
});
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
if (updateWiki) {
|
|
2132
|
+
const wikiSpinner = ora6("Regenerating TECHUNTER.md\u2026").start();
|
|
2133
|
+
try {
|
|
2134
|
+
const content = await generateWiki(config);
|
|
2135
|
+
await upsertRepoFile(config, "TECHUNTER.md", content, "docs: update TECHUNTER.md project overview");
|
|
2136
|
+
wikiSpinner.succeed("TECHUNTER.md updated");
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
wikiSpinner.fail(`Wiki update failed: ${err.message}`);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
const mergeTarget = mergedIntoTaskBranch ? `${result.baseBranch} (sub-task merged, no push to main)` : result.baseBranch;
|
|
2142
|
+
return `Task #${issueNumber} accepted.
|
|
2143
|
+
PR #${result.prNumber} merged \u2192 ${mergeTarget}
|
|
2144
|
+
Issue closed.`;
|
|
2145
|
+
}
|
|
2146
|
+
async function execute6(input3, config) {
|
|
2147
|
+
const issueNumber = input3["issue_number"];
|
|
2148
|
+
const [me, issue] = await Promise.all([
|
|
2149
|
+
getAuthenticatedUser(config),
|
|
2150
|
+
getTask(config, issueNumber)
|
|
2151
|
+
]);
|
|
2152
|
+
if (issue.author && issue.author !== me) {
|
|
2153
|
+
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
2154
|
+
}
|
|
2155
|
+
const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2156
|
+
try {
|
|
2157
|
+
const result = await acceptTask(config, issueNumber);
|
|
2158
|
+
spinner.stop();
|
|
2159
|
+
return `Task #${issueNumber} accepted.
|
|
2160
|
+
PR #${result.prNumber} merged \u2192 ${result.baseBranch}
|
|
2161
|
+
Issue closed.`;
|
|
1994
2162
|
} catch (err) {
|
|
2163
|
+
spinner.stop();
|
|
1995
2164
|
return `Error: ${err.message}`;
|
|
1996
2165
|
}
|
|
1997
|
-
await launchClaudeCode(issue, branch);
|
|
1998
|
-
return "Claude Code session ended.";
|
|
1999
2166
|
}
|
|
2000
|
-
var
|
|
2001
|
-
var terminal8 = true;
|
|
2167
|
+
var terminal6 = true;
|
|
2002
2168
|
|
|
2003
2169
|
// src/tools/reject/index.ts
|
|
2004
2170
|
var reject_exports = {};
|
|
2005
2171
|
__export(reject_exports, {
|
|
2006
|
-
definition: () =>
|
|
2007
|
-
execute: () =>
|
|
2008
|
-
run: () =>
|
|
2009
|
-
terminal: () =>
|
|
2172
|
+
definition: () => definition7,
|
|
2173
|
+
execute: () => execute7,
|
|
2174
|
+
run: () => run7,
|
|
2175
|
+
terminal: () => terminal7
|
|
2010
2176
|
});
|
|
2011
2177
|
init_github();
|
|
2012
|
-
import
|
|
2013
|
-
import { select as
|
|
2014
|
-
import
|
|
2178
|
+
import chalk8 from "chalk";
|
|
2179
|
+
import { select as select6, input as promptInput3 } from "@inquirer/prompts";
|
|
2180
|
+
import ora7 from "ora";
|
|
2015
2181
|
|
|
2016
2182
|
// src/tools/reject/prompts.ts
|
|
2017
2183
|
var REJECTION_FORMAT = `
|
|
@@ -2034,15 +2200,15 @@ Clear instruction on what to fix and how to re-submit (via /submit).
|
|
|
2034
2200
|
async function generateRejectionComment(config, issueNumber, userFeedback) {
|
|
2035
2201
|
return runSubAgentLoop(
|
|
2036
2202
|
config,
|
|
2037
|
-
"You are a senior engineer writing a structured code review rejection comment. Use get_task to read the acceptance criteria, get_diff or
|
|
2203
|
+
"You are a senior engineer writing a structured code review rejection comment. Use get_task to read the acceptance criteria, get_diff or grep_code to inspect the implementation, and get_comments to see prior discussion. Then write the rejection comment.\n\n" + REJECTION_FORMAT,
|
|
2038
2204
|
`Write a rejection comment for issue #${issueNumber}.
|
|
2039
2205
|
Reviewer feedback: ${userFeedback}`,
|
|
2040
|
-
["get_task", "get_comments", "get_diff", "
|
|
2206
|
+
["get_task", "get_comments", "get_diff", "grep_code"]
|
|
2041
2207
|
);
|
|
2042
2208
|
}
|
|
2043
2209
|
|
|
2044
2210
|
// src/tools/reject/index.ts
|
|
2045
|
-
var
|
|
2211
|
+
var definition7 = {
|
|
2046
2212
|
type: "function",
|
|
2047
2213
|
function: {
|
|
2048
2214
|
name: "reject",
|
|
@@ -2057,7 +2223,7 @@ var definition9 = {
|
|
|
2057
2223
|
}
|
|
2058
2224
|
}
|
|
2059
2225
|
};
|
|
2060
|
-
async function
|
|
2226
|
+
async function run7(input3, config) {
|
|
2061
2227
|
const issueNumber = input3["issue_number"];
|
|
2062
2228
|
const [me, issue] = await Promise.all([
|
|
2063
2229
|
getAuthenticatedUser(config),
|
|
@@ -2075,9 +2241,9 @@ async function run9(input3, config) {
|
|
|
2075
2241
|
return "Cancelled.";
|
|
2076
2242
|
}
|
|
2077
2243
|
if (!feedback.trim()) return "Cancelled.";
|
|
2078
|
-
const divider =
|
|
2244
|
+
const divider = chalk8.dim("\u2500".repeat(70));
|
|
2079
2245
|
for (; ; ) {
|
|
2080
|
-
const spinner =
|
|
2246
|
+
const spinner = ora7("Generating rejection comment\u2026").start();
|
|
2081
2247
|
let comment;
|
|
2082
2248
|
try {
|
|
2083
2249
|
comment = await generateRejectionComment(config, issueNumber, feedback);
|
|
@@ -2087,13 +2253,13 @@ async function run9(input3, config) {
|
|
|
2087
2253
|
return `Error generating comment: ${err.message}`;
|
|
2088
2254
|
}
|
|
2089
2255
|
console.log("\n" + divider);
|
|
2090
|
-
console.log(
|
|
2256
|
+
console.log(chalk8.bold(` Rejection preview \u2014 issue #${issueNumber}`));
|
|
2091
2257
|
console.log(divider);
|
|
2092
2258
|
console.log(renderMarkdown(comment));
|
|
2093
2259
|
console.log(divider + "\n");
|
|
2094
2260
|
let decision;
|
|
2095
2261
|
try {
|
|
2096
|
-
decision = await
|
|
2262
|
+
decision = await select6({
|
|
2097
2263
|
message: `Post rejection and mark #${issueNumber} as changes-needed?`,
|
|
2098
2264
|
choices: [
|
|
2099
2265
|
{ name: "Post & Reject", value: "yes" },
|
|
@@ -2113,7 +2279,7 @@ async function run9(input3, config) {
|
|
|
2113
2279
|
}
|
|
2114
2280
|
continue;
|
|
2115
2281
|
}
|
|
2116
|
-
let spinner2 =
|
|
2282
|
+
let spinner2 = ora7(`Posting rejection comment on #${issueNumber}\u2026`).start();
|
|
2117
2283
|
try {
|
|
2118
2284
|
await postComment(config, issueNumber, comment);
|
|
2119
2285
|
spinner2.stop();
|
|
@@ -2121,7 +2287,7 @@ async function run9(input3, config) {
|
|
|
2121
2287
|
spinner2.stop();
|
|
2122
2288
|
return `Error posting comment: ${err.message}`;
|
|
2123
2289
|
}
|
|
2124
|
-
spinner2 =
|
|
2290
|
+
spinner2 = ora7(`Marking #${issueNumber} as changes-needed\u2026`).start();
|
|
2125
2291
|
try {
|
|
2126
2292
|
await rejectTask(config, issueNumber);
|
|
2127
2293
|
spinner2.stop();
|
|
@@ -2132,7 +2298,7 @@ async function run9(input3, config) {
|
|
|
2132
2298
|
return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
|
|
2133
2299
|
}
|
|
2134
2300
|
}
|
|
2135
|
-
async function
|
|
2301
|
+
async function execute7(input3, config) {
|
|
2136
2302
|
const issueNumber = input3["issue_number"];
|
|
2137
2303
|
const feedback = input3["feedback"];
|
|
2138
2304
|
const [me, issue] = await Promise.all([
|
|
@@ -2163,171 +2329,202 @@ async function execute9(input3, config) {
|
|
|
2163
2329
|
Comment posted:
|
|
2164
2330
|
${comment}`;
|
|
2165
2331
|
}
|
|
2166
|
-
var
|
|
2332
|
+
var terminal7 = true;
|
|
2167
2333
|
|
|
2168
|
-
// src/tools/
|
|
2169
|
-
var
|
|
2170
|
-
__export(accept_exports, {
|
|
2171
|
-
definition: () => definition10,
|
|
2172
|
-
execute: () => execute10,
|
|
2173
|
-
run: () => run10,
|
|
2174
|
-
terminal: () => terminal10
|
|
2175
|
-
});
|
|
2176
|
-
init_github();
|
|
2177
|
-
import chalk11 from "chalk";
|
|
2178
|
-
import { select as select8 } from "@inquirer/prompts";
|
|
2179
|
-
import ora9 from "ora";
|
|
2180
|
-
var definition10 = {
|
|
2334
|
+
// src/tools/review/index.ts
|
|
2335
|
+
var definition8 = {
|
|
2181
2336
|
type: "function",
|
|
2182
2337
|
function: {
|
|
2183
|
-
name: "
|
|
2184
|
-
description: "
|
|
2185
|
-
parameters: {
|
|
2186
|
-
type: "object",
|
|
2187
|
-
properties: {
|
|
2188
|
-
issue_number: { type: "number", description: "GitHub issue number to accept" }
|
|
2189
|
-
},
|
|
2190
|
-
required: ["issue_number"]
|
|
2191
|
-
}
|
|
2338
|
+
name: "review",
|
|
2339
|
+
description: "List tasks waiting for your review (submitted by others, created by you), then let you accept or reject one. Equivalent to /review.",
|
|
2340
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
2192
2341
|
}
|
|
2193
2342
|
};
|
|
2194
|
-
async function
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
let tasks;
|
|
2199
|
-
let me;
|
|
2200
|
-
try {
|
|
2201
|
-
me = await getAuthenticatedUser(config);
|
|
2202
|
-
tasks = await listTasksForReview(config, me);
|
|
2203
|
-
spinner3.stop();
|
|
2204
|
-
} catch (err) {
|
|
2205
|
-
spinner3.stop();
|
|
2206
|
-
return `Error: ${err.message}`;
|
|
2207
|
-
}
|
|
2208
|
-
if (tasks.length === 0) return "No tasks pending review.";
|
|
2209
|
-
try {
|
|
2210
|
-
issueNumber = await select8({
|
|
2211
|
-
message: "Which task to accept?",
|
|
2212
|
-
choices: tasks.map((t) => ({
|
|
2213
|
-
name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
2214
|
-
value: t.number
|
|
2215
|
-
}))
|
|
2216
|
-
});
|
|
2217
|
-
} catch {
|
|
2218
|
-
return "Cancelled.";
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
const spinner2 = ora9("Verifying permissions\u2026").start();
|
|
2222
|
-
let me2;
|
|
2223
|
-
let issue;
|
|
2343
|
+
async function run8(_input, config) {
|
|
2344
|
+
const spinner = ora8("Loading tasks for review\u2026").start();
|
|
2345
|
+
let me;
|
|
2346
|
+
let tasks;
|
|
2224
2347
|
try {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
]);
|
|
2229
|
-
spinner2.stop();
|
|
2348
|
+
me = await getAuthenticatedUser(config);
|
|
2349
|
+
tasks = await listTasksForReview(config, me);
|
|
2350
|
+
spinner.stop();
|
|
2230
2351
|
} catch (err) {
|
|
2231
|
-
|
|
2352
|
+
spinner.stop();
|
|
2232
2353
|
return `Error: ${err.message}`;
|
|
2233
2354
|
}
|
|
2234
|
-
if (
|
|
2235
|
-
|
|
2236
|
-
}
|
|
2237
|
-
const targetBranch = makeWorkerBranchName(me2);
|
|
2238
|
-
let confirmed;
|
|
2355
|
+
if (tasks.length === 0) return `No tasks pending review for @${me}.`;
|
|
2356
|
+
let issueNumber;
|
|
2239
2357
|
try {
|
|
2240
|
-
|
|
2241
|
-
message:
|
|
2242
|
-
choices:
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2358
|
+
issueNumber = await select7({
|
|
2359
|
+
message: "Select a task to review:",
|
|
2360
|
+
choices: tasks.map((t) => ({
|
|
2361
|
+
name: `#${String(t.number).padEnd(4)} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
2362
|
+
value: t.number
|
|
2363
|
+
}))
|
|
2246
2364
|
});
|
|
2247
2365
|
} catch {
|
|
2248
2366
|
return "Cancelled.";
|
|
2249
2367
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
let result;
|
|
2368
|
+
const spinner2 = ora8(`Loading #${issueNumber}\u2026`).start();
|
|
2369
|
+
let pr;
|
|
2253
2370
|
try {
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
|
|
2371
|
+
pr = await getTaskPR(config, issueNumber);
|
|
2372
|
+
spinner2.stop();
|
|
2257
2373
|
} catch (err) {
|
|
2258
|
-
|
|
2259
|
-
return `Error: ${err.message}`;
|
|
2374
|
+
spinner2.stop();
|
|
2375
|
+
return `Error loading PR: ${err.message}`;
|
|
2260
2376
|
}
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
]
|
|
2270
|
-
});
|
|
2271
|
-
} catch {
|
|
2272
|
-
pushToMain = false;
|
|
2377
|
+
const divider = chalk9.dim("\u2500".repeat(70));
|
|
2378
|
+
console.log("\n" + divider);
|
|
2379
|
+
if (pr) {
|
|
2380
|
+
console.log(chalk9.bold(` PR #${pr.number}`) + " " + chalk9.dim(pr.url));
|
|
2381
|
+
console.log(divider);
|
|
2382
|
+
console.log(renderMarkdown(pr.body));
|
|
2383
|
+
} else {
|
|
2384
|
+
console.log(chalk9.yellow(` No open PR found for task #${issueNumber}`));
|
|
2273
2385
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2386
|
+
console.log(divider + "\n");
|
|
2387
|
+
for (; ; ) {
|
|
2388
|
+
let action;
|
|
2276
2389
|
try {
|
|
2277
|
-
await
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2390
|
+
action = await select7({
|
|
2391
|
+
message: "Review action:",
|
|
2392
|
+
choices: [
|
|
2393
|
+
...pr ? [{ name: "View diff", value: "diff" }] : [],
|
|
2394
|
+
{ name: chalk9.green("Accept") + " \u2014 merge PR and close issue", value: "accept" },
|
|
2395
|
+
{ name: chalk9.red("Reject") + " \u2014 request changes", value: "reject" },
|
|
2396
|
+
{ name: "Nothing, just viewing", value: "none" }
|
|
2397
|
+
]
|
|
2398
|
+
});
|
|
2399
|
+
} catch {
|
|
2400
|
+
return "Cancelled.";
|
|
2401
|
+
}
|
|
2402
|
+
if (action === "none") return `Viewed task #${issueNumber}.`;
|
|
2403
|
+
if (action === "accept") return run6({ issue_number: issueNumber }, config);
|
|
2404
|
+
if (action === "reject") return run7({ issue_number: issueNumber }, config);
|
|
2405
|
+
if (action === "diff") {
|
|
2406
|
+
const diffSpinner = ora8("Fetching diff\u2026").start();
|
|
2407
|
+
let diff;
|
|
2408
|
+
try {
|
|
2409
|
+
diff = await getTaskPRDiff(config, pr.number);
|
|
2410
|
+
diffSpinner.stop();
|
|
2411
|
+
} catch (err) {
|
|
2412
|
+
diffSpinner.stop();
|
|
2413
|
+
console.log(chalk9.red(`Error fetching diff: ${err.message}`));
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
console.log("\n" + divider);
|
|
2417
|
+
for (const line of diff.split("\n")) {
|
|
2418
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2419
|
+
process.stdout.write(chalk9.green(line) + "\n");
|
|
2420
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2421
|
+
process.stdout.write(chalk9.red(line) + "\n");
|
|
2422
|
+
} else if (line.startsWith("@@")) {
|
|
2423
|
+
process.stdout.write(chalk9.cyan(line) + "\n");
|
|
2424
|
+
} else if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("+++") || line.startsWith("---")) {
|
|
2425
|
+
process.stdout.write(chalk9.bold(line) + "\n");
|
|
2426
|
+
} else {
|
|
2427
|
+
process.stdout.write(line + "\n");
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
console.log(divider + "\n");
|
|
2281
2431
|
}
|
|
2282
|
-
}
|
|
2283
|
-
return `Task #${issueNumber} accepted.
|
|
2284
|
-
PR #${result.prNumber} merged \u2192 ${targetBranch}${pushToMain ? ` \u2192 ${baseBranch}` : ""}
|
|
2285
|
-
Issue closed.`;
|
|
2286
|
-
}
|
|
2287
|
-
async function execute10(input3, config) {
|
|
2288
|
-
const issueNumber = input3["issue_number"];
|
|
2289
|
-
const [me, issue] = await Promise.all([
|
|
2290
|
-
getAuthenticatedUser(config),
|
|
2291
|
-
getTask(config, issueNumber)
|
|
2292
|
-
]);
|
|
2293
|
-
if (issue.author && issue.author !== me) {
|
|
2294
|
-
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
2295
|
-
}
|
|
2296
|
-
const targetBranch = makeWorkerBranchName(me);
|
|
2297
|
-
const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2298
|
-
try {
|
|
2299
|
-
const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
|
|
2300
|
-
const result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
|
|
2301
|
-
spinner.stop();
|
|
2302
|
-
return `Task #${issueNumber} accepted.
|
|
2303
|
-
PR #${result.prNumber} merged \u2192 ${targetBranch}
|
|
2304
|
-
Issue closed.`;
|
|
2305
|
-
} catch (err) {
|
|
2306
|
-
spinner.stop();
|
|
2307
|
-
return `Error: ${err.message}`;
|
|
2308
2432
|
}
|
|
2309
2433
|
}
|
|
2310
|
-
var
|
|
2434
|
+
var execute8 = run8;
|
|
2435
|
+
var terminal8 = true;
|
|
2311
2436
|
|
|
2312
|
-
// src/tools/
|
|
2313
|
-
var
|
|
2314
|
-
__export(
|
|
2315
|
-
definition: () =>
|
|
2316
|
-
execute: () =>
|
|
2317
|
-
run: () =>
|
|
2318
|
-
terminal: () =>
|
|
2437
|
+
// src/tools/refresh/index.ts
|
|
2438
|
+
var refresh_exports = {};
|
|
2439
|
+
__export(refresh_exports, {
|
|
2440
|
+
definition: () => definition9,
|
|
2441
|
+
execute: () => execute9,
|
|
2442
|
+
run: () => run9,
|
|
2443
|
+
terminal: () => terminal9
|
|
2319
2444
|
});
|
|
2320
|
-
|
|
2321
|
-
import { select as select9, input as promptInput4 } from "@inquirer/prompts";
|
|
2322
|
-
import ora10 from "ora";
|
|
2323
|
-
var definition11 = {
|
|
2445
|
+
var definition9 = {
|
|
2324
2446
|
type: "function",
|
|
2325
2447
|
function: {
|
|
2326
|
-
name: "
|
|
2327
|
-
description: "
|
|
2328
|
-
parameters: {
|
|
2329
|
-
|
|
2330
|
-
|
|
2448
|
+
name: "refresh",
|
|
2449
|
+
description: "Reload and display the full task list. Equivalent to /refresh.",
|
|
2450
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
async function run9(_input, config) {
|
|
2454
|
+
const tasks = await printTaskList(config);
|
|
2455
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
2456
|
+
const lines = tasks.map((t) => {
|
|
2457
|
+
const status = getStatus(t);
|
|
2458
|
+
const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
|
|
2459
|
+
return `#${t.number} [${status}] ${assignee} ${t.title}`;
|
|
2460
|
+
});
|
|
2461
|
+
return `Tasks (${tasks.length}):
|
|
2462
|
+
${lines.join("\n")}`;
|
|
2463
|
+
}
|
|
2464
|
+
var execute9 = run9;
|
|
2465
|
+
var terminal9 = true;
|
|
2466
|
+
|
|
2467
|
+
// src/tools/open-code/index.ts
|
|
2468
|
+
var open_code_exports = {};
|
|
2469
|
+
__export(open_code_exports, {
|
|
2470
|
+
definition: () => definition10,
|
|
2471
|
+
execute: () => execute10,
|
|
2472
|
+
run: () => run10,
|
|
2473
|
+
terminal: () => terminal10
|
|
2474
|
+
});
|
|
2475
|
+
init_github();
|
|
2476
|
+
var definition10 = {
|
|
2477
|
+
type: "function",
|
|
2478
|
+
function: {
|
|
2479
|
+
name: "open_code",
|
|
2480
|
+
description: "Launch Claude Code for the current task branch. Equivalent to /code.",
|
|
2481
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
async function run10(_input, config) {
|
|
2485
|
+
let branch;
|
|
2486
|
+
try {
|
|
2487
|
+
branch = await getCurrentBranch();
|
|
2488
|
+
} catch (err) {
|
|
2489
|
+
return `Error: ${err.message}`;
|
|
2490
|
+
}
|
|
2491
|
+
let issueNum = getConfig().taskState?.activeIssueNumber;
|
|
2492
|
+
if (!issueNum) {
|
|
2493
|
+
const found = await getIssueNumberFromBranch(config, branch);
|
|
2494
|
+
if (!found) return `No active task found (current branch: ${branch}).`;
|
|
2495
|
+
issueNum = found.issueNumber;
|
|
2496
|
+
}
|
|
2497
|
+
let issue;
|
|
2498
|
+
try {
|
|
2499
|
+
issue = await getTask(config, issueNum);
|
|
2500
|
+
} catch (err) {
|
|
2501
|
+
return `Error: ${err.message}`;
|
|
2502
|
+
}
|
|
2503
|
+
await launchClaudeCode(issue, branch);
|
|
2504
|
+
return "Claude Code session ended.";
|
|
2505
|
+
}
|
|
2506
|
+
var execute10 = run10;
|
|
2507
|
+
var terminal10 = true;
|
|
2508
|
+
|
|
2509
|
+
// src/tools/edit-task/index.ts
|
|
2510
|
+
var edit_task_exports = {};
|
|
2511
|
+
__export(edit_task_exports, {
|
|
2512
|
+
definition: () => definition11,
|
|
2513
|
+
execute: () => execute11,
|
|
2514
|
+
run: () => run11,
|
|
2515
|
+
terminal: () => terminal11
|
|
2516
|
+
});
|
|
2517
|
+
init_github();
|
|
2518
|
+
import { select as select8, input as promptInput4 } from "@inquirer/prompts";
|
|
2519
|
+
import ora9 from "ora";
|
|
2520
|
+
var definition11 = {
|
|
2521
|
+
type: "function",
|
|
2522
|
+
function: {
|
|
2523
|
+
name: "edit_task",
|
|
2524
|
+
description: "Edit the title and/or body of an existing task (GitHub Issue). Equivalent to /edit.",
|
|
2525
|
+
parameters: {
|
|
2526
|
+
type: "object",
|
|
2527
|
+
properties: {
|
|
2331
2528
|
issue_number: { type: "number", description: "Issue number to edit." },
|
|
2332
2529
|
title: { type: "string", description: "New title." },
|
|
2333
2530
|
body: { type: "string", description: "New body/description." }
|
|
@@ -2347,7 +2544,7 @@ async function run11(input3, config) {
|
|
|
2347
2544
|
}
|
|
2348
2545
|
if (tasks.length === 0) return "No tasks found.";
|
|
2349
2546
|
try {
|
|
2350
|
-
issueNumber = await
|
|
2547
|
+
issueNumber = await select8({
|
|
2351
2548
|
message: "Select task to edit:",
|
|
2352
2549
|
choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
|
|
2353
2550
|
});
|
|
@@ -2378,7 +2575,7 @@ async function run11(input3, config) {
|
|
|
2378
2575
|
if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
|
|
2379
2576
|
return "No changes made.";
|
|
2380
2577
|
}
|
|
2381
|
-
const spinner =
|
|
2578
|
+
const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
|
|
2382
2579
|
try {
|
|
2383
2580
|
await editTask(config, issueNumber, title.trim() || issue.title, body.trim());
|
|
2384
2581
|
spinner.stop();
|
|
@@ -2392,7 +2589,7 @@ async function execute11(input3, config) {
|
|
|
2392
2589
|
const issueNumber = input3["issue_number"];
|
|
2393
2590
|
const title = input3["title"];
|
|
2394
2591
|
const body = input3["body"];
|
|
2395
|
-
const spinner =
|
|
2592
|
+
const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
|
|
2396
2593
|
try {
|
|
2397
2594
|
await editTask(config, issueNumber, title, body);
|
|
2398
2595
|
spinner.stop();
|
|
@@ -2404,14 +2601,140 @@ async function execute11(input3, config) {
|
|
|
2404
2601
|
}
|
|
2405
2602
|
var terminal11 = true;
|
|
2406
2603
|
|
|
2604
|
+
// src/tools/wiki/index.ts
|
|
2605
|
+
var wiki_exports = {};
|
|
2606
|
+
__export(wiki_exports, {
|
|
2607
|
+
definition: () => definition12,
|
|
2608
|
+
execute: () => execute12,
|
|
2609
|
+
run: () => run12,
|
|
2610
|
+
terminal: () => terminal12
|
|
2611
|
+
});
|
|
2612
|
+
import ora10 from "ora";
|
|
2613
|
+
import chalk10 from "chalk";
|
|
2614
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2615
|
+
import { select as select9 } from "@inquirer/prompts";
|
|
2616
|
+
init_github();
|
|
2617
|
+
var WIKI_PATH = "TECHUNTER.md";
|
|
2618
|
+
var definition12 = {
|
|
2619
|
+
type: "function",
|
|
2620
|
+
function: {
|
|
2621
|
+
name: "update_wiki",
|
|
2622
|
+
description: "Generate or refresh the project overview document (TECHUNTER.md) by scanning the codebase. The document helps new team members understand what the project does, how it is architected, and how to start contributing. Equivalent to /wiki.",
|
|
2623
|
+
parameters: {
|
|
2624
|
+
type: "object",
|
|
2625
|
+
properties: {},
|
|
2626
|
+
required: []
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
async function readWikiContent(config) {
|
|
2631
|
+
try {
|
|
2632
|
+
return await readFile2(WIKI_PATH, "utf-8");
|
|
2633
|
+
} catch {
|
|
2634
|
+
}
|
|
2635
|
+
return getRepoFile(config, WIKI_PATH);
|
|
2636
|
+
}
|
|
2637
|
+
function printWiki(content) {
|
|
2638
|
+
const divider = chalk10.dim("\u2500".repeat(70));
|
|
2639
|
+
console.log("\n" + divider);
|
|
2640
|
+
console.log(chalk10.bold(" TECHUNTER.md"));
|
|
2641
|
+
console.log(divider);
|
|
2642
|
+
console.log(renderMarkdown(content));
|
|
2643
|
+
console.log(divider + "\n");
|
|
2644
|
+
}
|
|
2645
|
+
async function run12(_input, config) {
|
|
2646
|
+
const fetchSpinner = ora10("Checking for existing wiki\u2026").start();
|
|
2647
|
+
const existing = await readWikiContent(config).catch(() => null);
|
|
2648
|
+
fetchSpinner.stop();
|
|
2649
|
+
let action;
|
|
2650
|
+
try {
|
|
2651
|
+
action = await select9({
|
|
2652
|
+
message: "TECHUNTER.md \u2014 what would you like to do?",
|
|
2653
|
+
choices: [
|
|
2654
|
+
...existing ? [{ name: "View current wiki", value: "view" }] : [],
|
|
2655
|
+
{ name: existing ? "Regenerate & commit to repo" : "Generate & commit to repo", value: "generate" },
|
|
2656
|
+
{ name: "Cancel", value: "cancel" }
|
|
2657
|
+
]
|
|
2658
|
+
});
|
|
2659
|
+
} catch {
|
|
2660
|
+
return "Cancelled.";
|
|
2661
|
+
}
|
|
2662
|
+
if (action === "cancel") return "Cancelled.";
|
|
2663
|
+
if (action === "view") {
|
|
2664
|
+
printWiki(existing);
|
|
2665
|
+
return "Displayed TECHUNTER.md.";
|
|
2666
|
+
}
|
|
2667
|
+
const authSpinner = ora10("Checking permissions\u2026").start();
|
|
2668
|
+
let me;
|
|
2669
|
+
let allowed;
|
|
2670
|
+
try {
|
|
2671
|
+
me = await getAuthenticatedUser(config);
|
|
2672
|
+
allowed = await isCollaborator(config, me);
|
|
2673
|
+
authSpinner.stop();
|
|
2674
|
+
} catch (err) {
|
|
2675
|
+
authSpinner.stop();
|
|
2676
|
+
return `Error checking permissions: ${err.message}`;
|
|
2677
|
+
}
|
|
2678
|
+
if (!allowed) {
|
|
2679
|
+
return `Permission denied: only repository collaborators can update the project wiki.`;
|
|
2680
|
+
}
|
|
2681
|
+
const genSpinner = ora10("Analyzing project and generating overview\u2026").start();
|
|
2682
|
+
let content;
|
|
2683
|
+
try {
|
|
2684
|
+
content = await generateWiki(config);
|
|
2685
|
+
genSpinner.stop();
|
|
2686
|
+
} catch (err) {
|
|
2687
|
+
genSpinner.stop();
|
|
2688
|
+
return `Error generating wiki: ${err.message}`;
|
|
2689
|
+
}
|
|
2690
|
+
printWiki(content);
|
|
2691
|
+
let confirm;
|
|
2692
|
+
try {
|
|
2693
|
+
confirm = await select9({
|
|
2694
|
+
message: `Publish to repository as ${WIKI_PATH}?`,
|
|
2695
|
+
choices: [
|
|
2696
|
+
{ name: "Yes, commit to repo", value: "publish" },
|
|
2697
|
+
{ name: "Cancel", value: "cancel" }
|
|
2698
|
+
]
|
|
2699
|
+
});
|
|
2700
|
+
} catch {
|
|
2701
|
+
return "Cancelled.";
|
|
2702
|
+
}
|
|
2703
|
+
if (confirm === "cancel") return "Cancelled.";
|
|
2704
|
+
const writeSpinner = ora10(`Writing ${WIKI_PATH}\u2026`).start();
|
|
2705
|
+
try {
|
|
2706
|
+
const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
|
|
2707
|
+
writeSpinner.succeed(`Written: ${url}`);
|
|
2708
|
+
console.log("");
|
|
2709
|
+
return `TECHUNTER.md updated \u2014 ${url}`;
|
|
2710
|
+
} catch (err) {
|
|
2711
|
+
writeSpinner.fail(`Failed: ${err.message}`);
|
|
2712
|
+
return `Error: ${err.message}`;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
async function execute12(_input, config) {
|
|
2716
|
+
const me = await getAuthenticatedUser(config);
|
|
2717
|
+
if (!await isCollaborator(config, me)) {
|
|
2718
|
+
return `Permission denied: only repository collaborators can update the project wiki.`;
|
|
2719
|
+
}
|
|
2720
|
+
const content = await generateWiki(config);
|
|
2721
|
+
try {
|
|
2722
|
+
const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
|
|
2723
|
+
return `TECHUNTER.md updated \u2014 ${url}`;
|
|
2724
|
+
} catch (err) {
|
|
2725
|
+
return `Error: ${err.message}`;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
var terminal12 = true;
|
|
2729
|
+
|
|
2407
2730
|
// src/tools/list-tasks/index.ts
|
|
2408
2731
|
var list_tasks_exports = {};
|
|
2409
2732
|
__export(list_tasks_exports, {
|
|
2410
|
-
definition: () =>
|
|
2411
|
-
execute: () =>
|
|
2733
|
+
definition: () => definition13,
|
|
2734
|
+
execute: () => execute13
|
|
2412
2735
|
});
|
|
2413
2736
|
init_github();
|
|
2414
|
-
var
|
|
2737
|
+
var definition13 = {
|
|
2415
2738
|
type: "function",
|
|
2416
2739
|
function: {
|
|
2417
2740
|
name: "list_tasks",
|
|
@@ -2423,7 +2746,7 @@ var definition12 = {
|
|
|
2423
2746
|
}
|
|
2424
2747
|
}
|
|
2425
2748
|
};
|
|
2426
|
-
async function
|
|
2749
|
+
async function execute13(_input, config) {
|
|
2427
2750
|
const tasks = await listTasks(config);
|
|
2428
2751
|
if (tasks.length === 0) return "No open tasks.";
|
|
2429
2752
|
return tasks.map((t) => {
|
|
@@ -2436,11 +2759,11 @@ async function execute12(_input, config) {
|
|
|
2436
2759
|
// src/tools/get-task/index.ts
|
|
2437
2760
|
var get_task_exports = {};
|
|
2438
2761
|
__export(get_task_exports, {
|
|
2439
|
-
definition: () =>
|
|
2440
|
-
execute: () =>
|
|
2762
|
+
definition: () => definition14,
|
|
2763
|
+
execute: () => execute14
|
|
2441
2764
|
});
|
|
2442
2765
|
init_github();
|
|
2443
|
-
var
|
|
2766
|
+
var definition14 = {
|
|
2444
2767
|
type: "function",
|
|
2445
2768
|
function: {
|
|
2446
2769
|
name: "get_task",
|
|
@@ -2454,7 +2777,7 @@ var definition13 = {
|
|
|
2454
2777
|
}
|
|
2455
2778
|
}
|
|
2456
2779
|
};
|
|
2457
|
-
async function
|
|
2780
|
+
async function execute14(input3, config) {
|
|
2458
2781
|
const issue = await getTask(config, input3["issue_number"]);
|
|
2459
2782
|
const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
|
|
2460
2783
|
const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
|
|
@@ -2471,12 +2794,12 @@ ${issue.body}`);
|
|
|
2471
2794
|
// src/tools/get-comments/index.ts
|
|
2472
2795
|
var get_comments_exports = {};
|
|
2473
2796
|
__export(get_comments_exports, {
|
|
2474
|
-
definition: () =>
|
|
2475
|
-
execute: () =>
|
|
2797
|
+
definition: () => definition15,
|
|
2798
|
+
execute: () => execute15
|
|
2476
2799
|
});
|
|
2477
2800
|
init_github();
|
|
2478
2801
|
import ora11 from "ora";
|
|
2479
|
-
var
|
|
2802
|
+
var definition15 = {
|
|
2480
2803
|
type: "function",
|
|
2481
2804
|
function: {
|
|
2482
2805
|
name: "get_comments",
|
|
@@ -2491,7 +2814,7 @@ var definition14 = {
|
|
|
2491
2814
|
}
|
|
2492
2815
|
}
|
|
2493
2816
|
};
|
|
2494
|
-
async function
|
|
2817
|
+
async function execute15(input3, config) {
|
|
2495
2818
|
const issueNumber = input3["issue_number"];
|
|
2496
2819
|
const limit = input3["limit"] ?? 5;
|
|
2497
2820
|
const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
|
|
@@ -2513,11 +2836,11 @@ ${lines.join("\n\n")}`;
|
|
|
2513
2836
|
// src/tools/get-diff/index.ts
|
|
2514
2837
|
var get_diff_exports = {};
|
|
2515
2838
|
__export(get_diff_exports, {
|
|
2516
|
-
definition: () =>
|
|
2517
|
-
execute: () =>
|
|
2839
|
+
definition: () => definition16,
|
|
2840
|
+
execute: () => execute16
|
|
2518
2841
|
});
|
|
2519
2842
|
import ora12 from "ora";
|
|
2520
|
-
var
|
|
2843
|
+
var definition16 = {
|
|
2521
2844
|
type: "function",
|
|
2522
2845
|
function: {
|
|
2523
2846
|
name: "get_diff",
|
|
@@ -2525,7 +2848,7 @@ var definition15 = {
|
|
|
2525
2848
|
parameters: { type: "object", properties: {}, required: [] }
|
|
2526
2849
|
}
|
|
2527
2850
|
};
|
|
2528
|
-
async function
|
|
2851
|
+
async function execute16(_input, _config) {
|
|
2529
2852
|
const spinner = ora12("Reading git diff...").start();
|
|
2530
2853
|
try {
|
|
2531
2854
|
const diff = await getDiff();
|
|
@@ -2540,14 +2863,14 @@ async function execute15(_input, _config) {
|
|
|
2540
2863
|
// src/tools/run-command/index.ts
|
|
2541
2864
|
var run_command_exports = {};
|
|
2542
2865
|
__export(run_command_exports, {
|
|
2543
|
-
definition: () =>
|
|
2544
|
-
execute: () =>
|
|
2866
|
+
definition: () => definition17,
|
|
2867
|
+
execute: () => execute17
|
|
2545
2868
|
});
|
|
2546
2869
|
import { exec } from "child_process";
|
|
2547
2870
|
import { promisify } from "util";
|
|
2548
2871
|
import ora13 from "ora";
|
|
2549
2872
|
var execAsync = promisify(exec);
|
|
2550
|
-
var
|
|
2873
|
+
var definition17 = {
|
|
2551
2874
|
type: "function",
|
|
2552
2875
|
function: {
|
|
2553
2876
|
name: "run_command",
|
|
@@ -2561,7 +2884,7 @@ var definition16 = {
|
|
|
2561
2884
|
}
|
|
2562
2885
|
}
|
|
2563
2886
|
};
|
|
2564
|
-
async function
|
|
2887
|
+
async function execute17(input3, _config) {
|
|
2565
2888
|
const command = input3["command"];
|
|
2566
2889
|
const cwd = process.cwd();
|
|
2567
2890
|
const spinner = ora13(`$ ${command}`).start();
|
|
@@ -2580,20 +2903,34 @@ ${out || e.message}`;
|
|
|
2580
2903
|
}
|
|
2581
2904
|
}
|
|
2582
2905
|
|
|
2583
|
-
// src/tools/
|
|
2584
|
-
var
|
|
2585
|
-
__export(
|
|
2586
|
-
definition: () =>
|
|
2587
|
-
execute: () =>
|
|
2906
|
+
// src/tools/list-files/index.ts
|
|
2907
|
+
var list_files_exports = {};
|
|
2908
|
+
__export(list_files_exports, {
|
|
2909
|
+
definition: () => definition18,
|
|
2910
|
+
execute: () => execute18
|
|
2588
2911
|
});
|
|
2589
|
-
import
|
|
2590
|
-
|
|
2591
|
-
// src/lib/project.ts
|
|
2592
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
2912
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2593
2913
|
import { existsSync } from "fs";
|
|
2594
2914
|
import path2 from "path";
|
|
2595
2915
|
import { globby } from "globby";
|
|
2596
2916
|
import ignore from "ignore";
|
|
2917
|
+
var definition18 = {
|
|
2918
|
+
type: "function",
|
|
2919
|
+
function: {
|
|
2920
|
+
name: "list_files",
|
|
2921
|
+
description: "List file paths in the project. Use this first to orient yourself before searching or reading.",
|
|
2922
|
+
parameters: {
|
|
2923
|
+
type: "object",
|
|
2924
|
+
properties: {
|
|
2925
|
+
glob: {
|
|
2926
|
+
type: "string",
|
|
2927
|
+
description: 'Glob pattern to filter results, e.g. "src/**/*.ts". Defaults to all text files.'
|
|
2928
|
+
}
|
|
2929
|
+
},
|
|
2930
|
+
required: []
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
};
|
|
2597
2934
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2598
2935
|
".png",
|
|
2599
2936
|
".jpg",
|
|
@@ -2606,297 +2943,668 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2606
2943
|
".zip",
|
|
2607
2944
|
".tar",
|
|
2608
2945
|
".gz",
|
|
2609
|
-
".bz2",
|
|
2610
|
-
".rar",
|
|
2611
2946
|
".exe",
|
|
2612
2947
|
".dll",
|
|
2613
|
-
".so",
|
|
2614
|
-
".dylib",
|
|
2615
2948
|
".woff",
|
|
2616
2949
|
".woff2",
|
|
2617
2950
|
".ttf",
|
|
2618
|
-
".otf",
|
|
2619
|
-
".eot",
|
|
2620
2951
|
".mp3",
|
|
2621
2952
|
".mp4",
|
|
2622
|
-
".wav",
|
|
2623
|
-
".avi",
|
|
2624
|
-
".mov",
|
|
2625
2953
|
".db",
|
|
2626
2954
|
".sqlite",
|
|
2627
2955
|
".lock"
|
|
2628
2956
|
]);
|
|
2629
|
-
|
|
2630
|
-
"
|
|
2631
|
-
|
|
2632
|
-
"README",
|
|
2633
|
-
"package.json",
|
|
2634
|
-
"pyproject.toml",
|
|
2635
|
-
"go.mod",
|
|
2636
|
-
"Cargo.toml",
|
|
2637
|
-
"tsconfig.json",
|
|
2638
|
-
"vite.config.ts",
|
|
2639
|
-
"vite.config.js",
|
|
2640
|
-
"webpack.config.js",
|
|
2641
|
-
"rollup.config.js",
|
|
2642
|
-
".env.example",
|
|
2643
|
-
"docker-compose.yml",
|
|
2644
|
-
"Dockerfile"
|
|
2645
|
-
];
|
|
2646
|
-
var MAX_TOTAL_BYTES = 8e4;
|
|
2647
|
-
var MAX_FILE_BYTES = 15e3;
|
|
2648
|
-
async function buildIgnoreFilter(cwd) {
|
|
2957
|
+
async function execute18(input3, _config) {
|
|
2958
|
+
const glob = input3["glob"] ?? "**/*";
|
|
2959
|
+
const cwd = process.cwd();
|
|
2649
2960
|
const ig = ignore();
|
|
2650
2961
|
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
2651
2962
|
if (existsSync(gitignorePath)) {
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
const content = await readFile2(filePath, "utf-8");
|
|
2661
|
-
if (content.length > maxBytes) {
|
|
2662
|
-
return content.slice(0, maxBytes) + `
|
|
2663
|
-
... (truncated at ${maxBytes} chars)`;
|
|
2664
|
-
}
|
|
2665
|
-
return content;
|
|
2666
|
-
} catch {
|
|
2667
|
-
return null;
|
|
2668
|
-
}
|
|
2963
|
+
ig.add(await readFile3(gitignorePath, "utf-8"));
|
|
2964
|
+
}
|
|
2965
|
+
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
|
|
2966
|
+
const files = await globby(glob, { cwd, dot: false, onlyFiles: true, gitignore: false });
|
|
2967
|
+
const filtered = files.filter((f) => !ig.ignores(f) && !BINARY_EXTENSIONS.has(path2.extname(f).toLowerCase()));
|
|
2968
|
+
if (filtered.length === 0) return `No files matched: ${glob}`;
|
|
2969
|
+
return `${filtered.length} file(s):
|
|
2970
|
+
${filtered.join("\n")}`;
|
|
2669
2971
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
for (const f of rootFiles) lines.push(f);
|
|
2684
|
-
const dirs = Object.keys(tree).filter((d) => d !== ".").sort();
|
|
2685
|
-
for (const dir of dirs) {
|
|
2686
|
-
lines.push(`${dir}/`);
|
|
2687
|
-
for (const f of tree[dir]) {
|
|
2688
|
-
lines.push(` ${f}`);
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
return lines.join("\n");
|
|
2692
|
-
}
|
|
2693
|
-
function scoreRelevance(filePath, keywords) {
|
|
2694
|
-
const lower = filePath.toLowerCase();
|
|
2695
|
-
let score = 0;
|
|
2696
|
-
for (const kw of keywords) {
|
|
2697
|
-
if (lower.includes(kw)) score += 1;
|
|
2698
|
-
}
|
|
2699
|
-
return score;
|
|
2700
|
-
}
|
|
2701
|
-
async function buildProjectContext(cwd, issueTitle, issueBody) {
|
|
2702
|
-
const ig = await buildIgnoreFilter(cwd);
|
|
2703
|
-
const allFiles = await globby("**/*", {
|
|
2704
|
-
cwd,
|
|
2705
|
-
gitignore: false,
|
|
2706
|
-
// We handle ignore ourselves
|
|
2707
|
-
dot: false,
|
|
2708
|
-
onlyFiles: true
|
|
2709
|
-
});
|
|
2710
|
-
const filtered = allFiles.filter((f) => !ig.ignores(f) && !isBinaryFile(f));
|
|
2711
|
-
const fileTree = buildFileTree(filtered);
|
|
2712
|
-
const issueText = `${issueTitle} ${issueBody}`.toLowerCase();
|
|
2713
|
-
const keywords = issueText.replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
2714
|
-
const keyFiles = {};
|
|
2715
|
-
let totalBytes = 0;
|
|
2716
|
-
for (const always of ALWAYS_READ) {
|
|
2717
|
-
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
2718
|
-
const fullPath = path2.join(cwd, always);
|
|
2719
|
-
if (!existsSync(fullPath)) continue;
|
|
2720
|
-
const content = await safeReadFile(fullPath);
|
|
2721
|
-
if (content !== null) {
|
|
2722
|
-
keyFiles[always] = content;
|
|
2723
|
-
totalBytes += content.length;
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
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);
|
|
2727
|
-
for (const { file } of scored) {
|
|
2728
|
-
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
2729
|
-
const fullPath = path2.join(cwd, file);
|
|
2730
|
-
const content = await safeReadFile(fullPath);
|
|
2731
|
-
if (content !== null) {
|
|
2732
|
-
keyFiles[file] = content;
|
|
2733
|
-
totalBytes += content.length;
|
|
2734
|
-
}
|
|
2735
|
-
}
|
|
2736
|
-
return { fileTree, keyFiles };
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
// src/tools/scan-project/index.ts
|
|
2740
|
-
var definition17 = {
|
|
2972
|
+
|
|
2973
|
+
// src/tools/grep-code/index.ts
|
|
2974
|
+
var grep_code_exports = {};
|
|
2975
|
+
__export(grep_code_exports, {
|
|
2976
|
+
definition: () => definition19,
|
|
2977
|
+
execute: () => execute19
|
|
2978
|
+
});
|
|
2979
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2980
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2981
|
+
import path3 from "path";
|
|
2982
|
+
import { globby as globby2 } from "globby";
|
|
2983
|
+
import ignore2 from "ignore";
|
|
2984
|
+
var definition19 = {
|
|
2741
2985
|
type: "function",
|
|
2742
2986
|
function: {
|
|
2743
|
-
name: "
|
|
2744
|
-
description: "
|
|
2987
|
+
name: "grep_code",
|
|
2988
|
+
description: "Search for a pattern across files, or read a specific line range from a file.\n- Search mode: provide `pattern` \u2014 returns matching lines with context.\n- Read-range mode: provide `file_glob` (single file) + `start_line` + `end_line`, no `pattern` \u2014 read an exact section. Use after grep has given you line numbers.",
|
|
2745
2989
|
parameters: {
|
|
2746
2990
|
type: "object",
|
|
2747
2991
|
properties: {
|
|
2748
|
-
|
|
2992
|
+
pattern: {
|
|
2993
|
+
type: "string",
|
|
2994
|
+
description: "Regex or plain text to search for (case-insensitive). Omit for read-range mode."
|
|
2995
|
+
},
|
|
2996
|
+
file_glob: {
|
|
2749
2997
|
type: "string",
|
|
2750
|
-
description:
|
|
2998
|
+
description: 'Glob to restrict which files to search or read, e.g. "src/**/*.ts" or "src/lib/agent.ts". Defaults to all text files.'
|
|
2999
|
+
},
|
|
3000
|
+
context_lines: {
|
|
3001
|
+
type: "number",
|
|
3002
|
+
description: "Lines of context before/after each match (search mode only). Default: 2."
|
|
3003
|
+
},
|
|
3004
|
+
max_results: {
|
|
3005
|
+
type: "number",
|
|
3006
|
+
description: "Max matches to return (search mode only). Default: 50."
|
|
3007
|
+
},
|
|
3008
|
+
start_line: {
|
|
3009
|
+
type: "number",
|
|
3010
|
+
description: "First line to read, 1-based (read-range mode). Requires file_glob pointing to a single file."
|
|
3011
|
+
},
|
|
3012
|
+
end_line: {
|
|
3013
|
+
type: "number",
|
|
3014
|
+
description: "Last line to read, 1-based (read-range mode)."
|
|
2751
3015
|
}
|
|
2752
3016
|
},
|
|
2753
3017
|
required: []
|
|
2754
3018
|
}
|
|
2755
3019
|
}
|
|
2756
3020
|
};
|
|
2757
|
-
async function
|
|
2758
|
-
const
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
3021
|
+
async function buildIgnore(cwd) {
|
|
3022
|
+
const ig = ignore2();
|
|
3023
|
+
const gitignorePath = path3.join(cwd, ".gitignore");
|
|
3024
|
+
if (existsSync2(gitignorePath)) {
|
|
3025
|
+
ig.add(await readFile4(gitignorePath, "utf-8"));
|
|
3026
|
+
}
|
|
3027
|
+
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
|
|
3028
|
+
return ig;
|
|
3029
|
+
}
|
|
3030
|
+
var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
3031
|
+
".png",
|
|
3032
|
+
".jpg",
|
|
3033
|
+
".jpeg",
|
|
3034
|
+
".gif",
|
|
3035
|
+
".svg",
|
|
3036
|
+
".ico",
|
|
3037
|
+
".webp",
|
|
3038
|
+
".pdf",
|
|
3039
|
+
".zip",
|
|
3040
|
+
".tar",
|
|
3041
|
+
".gz",
|
|
3042
|
+
".exe",
|
|
3043
|
+
".dll",
|
|
3044
|
+
".woff",
|
|
3045
|
+
".woff2",
|
|
3046
|
+
".ttf",
|
|
3047
|
+
".mp3",
|
|
3048
|
+
".mp4",
|
|
3049
|
+
".db",
|
|
3050
|
+
".sqlite",
|
|
3051
|
+
".lock"
|
|
3052
|
+
]);
|
|
3053
|
+
function isText(f) {
|
|
3054
|
+
return !BINARY_EXTENSIONS2.has(path3.extname(f).toLowerCase());
|
|
3055
|
+
}
|
|
3056
|
+
var MAX_RANGE_LINES = 300;
|
|
3057
|
+
async function execute19(input3, _config) {
|
|
3058
|
+
const pattern = input3["pattern"] ?? "";
|
|
3059
|
+
const fileGlob = input3["file_glob"] ?? "**/*";
|
|
3060
|
+
const contextLines = Math.min(input3["context_lines"] ?? 2, 5);
|
|
3061
|
+
const maxResults = Math.min(input3["max_results"] ?? 50, 200);
|
|
3062
|
+
const startLine = input3["start_line"];
|
|
3063
|
+
const endLine = input3["end_line"];
|
|
3064
|
+
const cwd = process.cwd();
|
|
3065
|
+
if (!pattern && startLine != null && endLine != null) {
|
|
3066
|
+
const files = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
|
|
3067
|
+
if (files.length === 0) return `No file matched: ${fileGlob}`;
|
|
3068
|
+
if (files.length > 1) return `file_glob matched ${files.length} files \u2014 narrow it to a single file for read-range mode.`;
|
|
3069
|
+
const raw = await readFile4(path3.join(cwd, files[0]), "utf-8");
|
|
3070
|
+
const lines = raw.split("\n");
|
|
3071
|
+
const total = lines.length;
|
|
3072
|
+
const from = Math.max(1, startLine);
|
|
3073
|
+
const to = Math.min(total, endLine, from + MAX_RANGE_LINES - 1);
|
|
3074
|
+
const numbered = lines.slice(from - 1, to).map((l, i) => `${String(from + i).padStart(5)}: ${l}`).join("\n");
|
|
3075
|
+
const truncNote = to < Math.min(total, endLine) ? `
|
|
3076
|
+
\u2026 (use start_line=${to + 1} to continue)` : "";
|
|
3077
|
+
return `${files[0]} \u2014 lines ${from}\u2013${to} of ${total}:
|
|
2769
3078
|
\`\`\`
|
|
2770
|
-
${
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
3079
|
+
${numbered}
|
|
3080
|
+
\`\`\`${truncNote}`;
|
|
3081
|
+
}
|
|
3082
|
+
if (!pattern) return "Provide a `pattern` to search, or `start_line` + `end_line` for read-range mode. Use list_files to browse file paths.";
|
|
3083
|
+
const ig = await buildIgnore(cwd);
|
|
3084
|
+
let regex;
|
|
3085
|
+
try {
|
|
3086
|
+
regex = new RegExp(pattern, "i");
|
|
3087
|
+
} catch {
|
|
3088
|
+
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
3089
|
+
}
|
|
3090
|
+
const allFiles = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
|
|
3091
|
+
const filtered = allFiles.filter((f) => !ig.ignores(f) && isText(f));
|
|
3092
|
+
const matches = [];
|
|
3093
|
+
let totalMatches = 0;
|
|
3094
|
+
for (const file of filtered) {
|
|
3095
|
+
if (totalMatches >= maxResults) break;
|
|
3096
|
+
let content;
|
|
3097
|
+
try {
|
|
3098
|
+
content = await readFile4(path3.join(cwd, file), "utf-8");
|
|
3099
|
+
} catch {
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
const lines = content.split("\n");
|
|
3103
|
+
const hitLines = [];
|
|
3104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3105
|
+
if (regex.test(lines[i])) hitLines.push(i);
|
|
3106
|
+
}
|
|
3107
|
+
if (hitLines.length === 0) continue;
|
|
3108
|
+
const ranges = [];
|
|
3109
|
+
for (const hit of hitLines) {
|
|
3110
|
+
const s = Math.max(0, hit - contextLines);
|
|
3111
|
+
const e = Math.min(lines.length - 1, hit + contextLines);
|
|
3112
|
+
if (ranges.length > 0 && s <= ranges[ranges.length - 1][1] + 1) {
|
|
3113
|
+
ranges[ranges.length - 1][1] = e;
|
|
3114
|
+
} else {
|
|
3115
|
+
ranges.push([s, e]);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
const snippets = [];
|
|
3119
|
+
for (const [s, e] of ranges) {
|
|
3120
|
+
if (totalMatches >= maxResults) break;
|
|
3121
|
+
snippets.push(
|
|
3122
|
+
lines.slice(s, e + 1).map((l, i) => {
|
|
3123
|
+
const n = s + i + 1;
|
|
3124
|
+
return `${regex.test(l) ? ">" : " "} ${String(n).padStart(4)}: ${l}`;
|
|
3125
|
+
}).join("\n")
|
|
3126
|
+
);
|
|
3127
|
+
totalMatches += hitLines.filter((h) => h >= s && h <= e).length;
|
|
3128
|
+
}
|
|
3129
|
+
if (snippets.length > 0) {
|
|
3130
|
+
matches.push(`## ${file}
|
|
2774
3131
|
\`\`\`
|
|
2775
|
-
${
|
|
3132
|
+
${snippets.join("\n---\n")}
|
|
2776
3133
|
\`\`\``);
|
|
2777
3134
|
}
|
|
2778
|
-
return parts.join("\n\n");
|
|
2779
|
-
} catch (err) {
|
|
2780
|
-
spinner.stop();
|
|
2781
|
-
throw err;
|
|
2782
3135
|
}
|
|
3136
|
+
if (matches.length === 0) return `No matches found for: ${pattern}`;
|
|
3137
|
+
const header = `Found matches in ${matches.length} file(s) (${totalMatches} match${totalMatches === 1 ? "" : "es"})${totalMatches >= maxResults ? " \u2014 limit reached" : ""}:`;
|
|
3138
|
+
return [header, ...matches].join("\n\n");
|
|
2783
3139
|
}
|
|
2784
3140
|
|
|
2785
|
-
// src/tools/
|
|
2786
|
-
var
|
|
2787
|
-
__export(
|
|
2788
|
-
definition: () =>
|
|
2789
|
-
execute: () =>
|
|
3141
|
+
// src/tools/ask-user/index.ts
|
|
3142
|
+
var ask_user_exports = {};
|
|
3143
|
+
__export(ask_user_exports, {
|
|
3144
|
+
definition: () => definition20,
|
|
3145
|
+
execute: () => execute20
|
|
2790
3146
|
});
|
|
2791
|
-
import
|
|
2792
|
-
import
|
|
2793
|
-
var
|
|
3147
|
+
import chalk11 from "chalk";
|
|
3148
|
+
import { select as select10, input as promptInput5 } from "@inquirer/prompts";
|
|
3149
|
+
var definition20 = {
|
|
2794
3150
|
type: "function",
|
|
2795
3151
|
function: {
|
|
2796
|
-
name: "
|
|
2797
|
-
description: "
|
|
3152
|
+
name: "ask_user",
|
|
3153
|
+
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.",
|
|
2798
3154
|
parameters: {
|
|
2799
3155
|
type: "object",
|
|
2800
3156
|
properties: {
|
|
2801
|
-
|
|
3157
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
3158
|
+
options: {
|
|
3159
|
+
type: "array",
|
|
3160
|
+
items: { type: "string" },
|
|
3161
|
+
description: "2\u20134 concrete answer choices"
|
|
3162
|
+
}
|
|
2802
3163
|
},
|
|
2803
|
-
required: ["
|
|
3164
|
+
required: ["question", "options"]
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
};
|
|
3168
|
+
async function execute20(input3, _config) {
|
|
3169
|
+
const question = input3["question"];
|
|
3170
|
+
const options = input3["options"];
|
|
3171
|
+
const OTHER = "__other__";
|
|
3172
|
+
console.log("");
|
|
3173
|
+
console.log(chalk11.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
|
|
3174
|
+
console.log(chalk11.dim(" \u2502"));
|
|
3175
|
+
for (const line of question.split("\n")) {
|
|
3176
|
+
console.log(chalk11.dim(" \u2502 ") + line);
|
|
3177
|
+
}
|
|
3178
|
+
console.log(chalk11.dim(" \u2514" + "\u2500".repeat(67)));
|
|
3179
|
+
let answer;
|
|
3180
|
+
try {
|
|
3181
|
+
const chosen = await select10({
|
|
3182
|
+
message: " ",
|
|
3183
|
+
choices: [
|
|
3184
|
+
...options.map((o) => ({ name: o, value: o })),
|
|
3185
|
+
{ name: chalk11.dim("Other (describe below)"), value: OTHER }
|
|
3186
|
+
]
|
|
3187
|
+
});
|
|
3188
|
+
answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
|
|
3189
|
+
} catch {
|
|
3190
|
+
answer = "User skipped this question \u2014 use your best judgement.";
|
|
3191
|
+
}
|
|
3192
|
+
console.log("");
|
|
3193
|
+
return answer;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// src/tools/registry.ts
|
|
3197
|
+
var toolModules = [
|
|
3198
|
+
// Command tools
|
|
3199
|
+
pick_exports,
|
|
3200
|
+
new_task_exports,
|
|
3201
|
+
close_exports,
|
|
3202
|
+
submit_exports,
|
|
3203
|
+
my_status_exports,
|
|
3204
|
+
review_exports,
|
|
3205
|
+
refresh_exports,
|
|
3206
|
+
open_code_exports,
|
|
3207
|
+
reject_exports,
|
|
3208
|
+
accept_exports,
|
|
3209
|
+
edit_task_exports,
|
|
3210
|
+
wiki_exports,
|
|
3211
|
+
// Low-level tools
|
|
3212
|
+
list_tasks_exports,
|
|
3213
|
+
get_task_exports,
|
|
3214
|
+
get_comments_exports,
|
|
3215
|
+
get_diff_exports,
|
|
3216
|
+
run_command_exports,
|
|
3217
|
+
list_files_exports,
|
|
3218
|
+
grep_code_exports,
|
|
3219
|
+
ask_user_exports
|
|
3220
|
+
];
|
|
3221
|
+
|
|
3222
|
+
// src/lib/agent-ui.ts
|
|
3223
|
+
import chalk12 from "chalk";
|
|
3224
|
+
function formatInput(input3) {
|
|
3225
|
+
return Object.entries(input3).map(([k, v]) => {
|
|
3226
|
+
if (typeof v === "number") return `${k}=${v}`;
|
|
3227
|
+
if (typeof v === "string") {
|
|
3228
|
+
if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
|
|
3229
|
+
return `${k}="${v}"`;
|
|
3230
|
+
}
|
|
3231
|
+
return `${k}=${JSON.stringify(v)}`;
|
|
3232
|
+
}).join(" ");
|
|
3233
|
+
}
|
|
3234
|
+
function summarize(result) {
|
|
3235
|
+
const first = result.split("\n").find((l) => l.trim()) ?? result;
|
|
3236
|
+
return first.length > 100 ? first.slice(0, 97) + "..." : first;
|
|
3237
|
+
}
|
|
3238
|
+
function printToolCall(name, input3) {
|
|
3239
|
+
const params = formatInput(input3);
|
|
3240
|
+
console.log(` ${chalk12.cyan("\u2192")} ${chalk12.bold(name)}${params ? " " + chalk12.dim(params) : ""}`);
|
|
3241
|
+
}
|
|
3242
|
+
function printToolResult(result) {
|
|
3243
|
+
const ok = !result.startsWith("Error:");
|
|
3244
|
+
const icon = ok ? chalk12.green("\u2713") : chalk12.red("\u2717");
|
|
3245
|
+
console.log(` ${icon} ${chalk12.dim(summarize(result))}`);
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
// src/lib/sub-agent.ts
|
|
3249
|
+
async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
|
|
3250
|
+
const client = createClient(config);
|
|
3251
|
+
const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
|
|
3252
|
+
const tools2 = selected.map((m) => m.definition);
|
|
3253
|
+
const messages = [
|
|
3254
|
+
{ role: "system", content: systemPrompt },
|
|
3255
|
+
{ role: "user", content: userMessage }
|
|
3256
|
+
];
|
|
3257
|
+
const MAX_ITERATIONS = 100;
|
|
3258
|
+
let iterations = 0;
|
|
3259
|
+
for (; ; ) {
|
|
3260
|
+
if (++iterations > MAX_ITERATIONS) {
|
|
3261
|
+
throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
3262
|
+
}
|
|
3263
|
+
const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
|
|
3264
|
+
const choice = res.choices[0];
|
|
3265
|
+
messages.push({
|
|
3266
|
+
role: "assistant",
|
|
3267
|
+
content: choice.message.content ?? null,
|
|
3268
|
+
...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
|
|
3269
|
+
});
|
|
3270
|
+
if (choice.finish_reason === "stop") {
|
|
3271
|
+
return choice.message.content ?? "";
|
|
3272
|
+
}
|
|
3273
|
+
if (choice.finish_reason === "tool_calls") {
|
|
3274
|
+
for (const tc of choice.message.tool_calls ?? []) {
|
|
3275
|
+
let input3;
|
|
3276
|
+
try {
|
|
3277
|
+
input3 = JSON.parse(tc.function.arguments);
|
|
3278
|
+
} catch {
|
|
3279
|
+
input3 = {};
|
|
3280
|
+
}
|
|
3281
|
+
printToolCall(tc.function.name, input3);
|
|
3282
|
+
const mod = selected.find((m) => m.definition.function.name === tc.function.name);
|
|
3283
|
+
const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
|
|
3284
|
+
printToolResult(result);
|
|
3285
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
// src/tools/wiki/prompts.ts
|
|
3292
|
+
var WIKI_FORMAT = `
|
|
3293
|
+
The document you produce must be valid Markdown with these exact sections:
|
|
3294
|
+
|
|
3295
|
+
# [Project Name]
|
|
3296
|
+
|
|
3297
|
+
> One-sentence description of what this project does.
|
|
3298
|
+
|
|
3299
|
+
## What Is This?
|
|
3300
|
+
|
|
3301
|
+
2-4 paragraphs covering:
|
|
3302
|
+
- The problem this project solves
|
|
3303
|
+
- Who uses it and in what context
|
|
3304
|
+
- Core capabilities / key features
|
|
3305
|
+
|
|
3306
|
+
## Quick Start
|
|
3307
|
+
|
|
3308
|
+
Numbered steps for a brand-new developer to install, configure, and run the project for the first time.
|
|
3309
|
+
|
|
3310
|
+
## Architecture
|
|
3311
|
+
|
|
3312
|
+
High-level explanation of how the system is structured:
|
|
3313
|
+
- Key components / layers and their responsibilities
|
|
3314
|
+
- Request or data flow (prose or ASCII diagram)
|
|
3315
|
+
- Noteworthy design decisions
|
|
3316
|
+
|
|
3317
|
+
## Key Files
|
|
3318
|
+
|
|
3319
|
+
| File / Directory | Purpose |
|
|
3320
|
+
|---|---|
|
|
3321
|
+
| ... | ... |
|
|
3322
|
+
|
|
3323
|
+
(List the 8-15 most important files.)
|
|
3324
|
+
|
|
3325
|
+
## Development Workflow
|
|
3326
|
+
|
|
3327
|
+
Common day-to-day tasks a contributor will need:
|
|
3328
|
+
- How to build / run locally
|
|
3329
|
+
- How to add a new feature (brief steps)
|
|
3330
|
+
- Any testing or linting commands
|
|
3331
|
+
|
|
3332
|
+
---
|
|
3333
|
+
*Maintained by Techunter \u2014 run \`tch wiki\` to regenerate*
|
|
3334
|
+
`;
|
|
3335
|
+
|
|
3336
|
+
// src/tools/wiki/wiki-generator.ts
|
|
3337
|
+
async function generateWiki(config) {
|
|
3338
|
+
return runSubAgentLoop(
|
|
3339
|
+
config,
|
|
3340
|
+
"You are a senior engineer writing a project overview document for new team members. Use list_files to understand the project structure, then grep_code and run_command to read key files (e.g. package.json, README, entry points, config files). Be concrete and specific \u2014 reference real file names, commands, and concepts from this codebase. Avoid vague filler. When you have enough context, write the document.\n\n" + WIKI_FORMAT,
|
|
3341
|
+
"Analyze this project thoroughly and produce a comprehensive TECHUNTER.md overview document for new team members.",
|
|
3342
|
+
["list_files", "grep_code", "run_command"]
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/commands/init.ts
|
|
3347
|
+
async function getGitHubTokenViaPAT() {
|
|
3348
|
+
console.log(chalk13.dim("\n Create a token at: https://github.com/settings/tokens/new"));
|
|
3349
|
+
console.log(chalk13.dim(" Required scopes: repo, read:user\n"));
|
|
3350
|
+
const token = await password({
|
|
3351
|
+
message: "GitHub Personal Access Token:",
|
|
3352
|
+
mask: "*"
|
|
3353
|
+
});
|
|
3354
|
+
return { token: token.trim() };
|
|
3355
|
+
}
|
|
3356
|
+
var OAUTH_CLIENT_ID = "Ov23liW4zJ4r2RdZOsCJ";
|
|
3357
|
+
async function getGitHubTokenViaDeviceFlow() {
|
|
3358
|
+
let verificationUri = "";
|
|
3359
|
+
let userCode = "";
|
|
3360
|
+
const auth = createOAuthDeviceAuth({
|
|
3361
|
+
clientType: "oauth-app",
|
|
3362
|
+
clientId: OAUTH_CLIENT_ID,
|
|
3363
|
+
scopes: ["repo"],
|
|
3364
|
+
onVerification(verification) {
|
|
3365
|
+
verificationUri = verification.verification_uri;
|
|
3366
|
+
userCode = verification.user_code;
|
|
3367
|
+
console.log("");
|
|
3368
|
+
console.log(chalk13.bold(" 1. Open this URL in your browser:"));
|
|
3369
|
+
console.log(" " + chalk13.cyan(verificationUri));
|
|
3370
|
+
console.log("");
|
|
3371
|
+
console.log(chalk13.bold(" 2. Enter this code:"));
|
|
3372
|
+
console.log(" " + chalk13.yellow.bold(userCode));
|
|
3373
|
+
console.log("");
|
|
3374
|
+
open2(verificationUri).catch(() => {
|
|
3375
|
+
});
|
|
2804
3376
|
}
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
const filePath = input3["path"];
|
|
3377
|
+
});
|
|
3378
|
+
const spinner = ora14("Waiting for authorization in browser...").start();
|
|
3379
|
+
let token;
|
|
2809
3380
|
try {
|
|
2810
|
-
const
|
|
2811
|
-
|
|
2812
|
-
|
|
3381
|
+
const result = await auth({ type: "oauth" });
|
|
3382
|
+
token = result.token;
|
|
3383
|
+
spinner.succeed("Authorized!");
|
|
2813
3384
|
} catch (err) {
|
|
2814
|
-
|
|
3385
|
+
spinner.fail("Authorization failed");
|
|
3386
|
+
throw err;
|
|
2815
3387
|
}
|
|
3388
|
+
return { token, clientId: OAUTH_CLIENT_ID };
|
|
2816
3389
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
name: "ask_user",
|
|
2830
|
-
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.",
|
|
2831
|
-
parameters: {
|
|
2832
|
-
type: "object",
|
|
2833
|
-
properties: {
|
|
2834
|
-
question: { type: "string", description: "The question to ask the user" },
|
|
2835
|
-
options: {
|
|
2836
|
-
type: "array",
|
|
2837
|
-
items: { type: "string" },
|
|
2838
|
-
description: "2\u20134 concrete answer choices"
|
|
2839
|
-
}
|
|
2840
|
-
},
|
|
2841
|
-
required: ["question", "options"]
|
|
3390
|
+
async function initCommand() {
|
|
3391
|
+
console.log(chalk13.bold.cyan("\nTechunter \u2014 Initial Setup\n"));
|
|
3392
|
+
let detectedOwner = "";
|
|
3393
|
+
let detectedRepo = "";
|
|
3394
|
+
const remoteUrl = await getRemoteUrl();
|
|
3395
|
+
if (remoteUrl) {
|
|
3396
|
+
const parsed = parseOwnerRepo(remoteUrl);
|
|
3397
|
+
if (parsed) {
|
|
3398
|
+
detectedOwner = parsed.owner;
|
|
3399
|
+
detectedRepo = parsed.repo;
|
|
3400
|
+
console.log(chalk13.dim(`Detected GitHub repo: ${detectedOwner}/${detectedRepo}
|
|
3401
|
+
`));
|
|
2842
3402
|
}
|
|
2843
3403
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
3404
|
+
const authMethod = await select11({
|
|
3405
|
+
message: "How would you like to authenticate with GitHub?",
|
|
3406
|
+
choices: [
|
|
3407
|
+
{
|
|
3408
|
+
name: "Browser login (OAuth) \u2014 open a URL and click Authorize",
|
|
3409
|
+
value: "device"
|
|
3410
|
+
},
|
|
3411
|
+
{
|
|
3412
|
+
name: "Personal Access Token (PAT) \u2014 paste a token from github.com/settings/tokens",
|
|
3413
|
+
value: "pat"
|
|
3414
|
+
}
|
|
3415
|
+
]
|
|
3416
|
+
});
|
|
3417
|
+
let githubToken;
|
|
3418
|
+
let githubClientId;
|
|
3419
|
+
if (authMethod === "device") {
|
|
3420
|
+
const result = await getGitHubTokenViaDeviceFlow();
|
|
3421
|
+
githubToken = result.token;
|
|
3422
|
+
githubClientId = result.clientId;
|
|
3423
|
+
} else {
|
|
3424
|
+
const result = await getGitHubTokenViaPAT();
|
|
3425
|
+
githubToken = result.token;
|
|
2854
3426
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
3427
|
+
const providerChoice = await select11({
|
|
3428
|
+
message: "AI provider:",
|
|
3429
|
+
choices: [
|
|
3430
|
+
{ name: `OpenRouter (recommended) ${chalk13.dim(`${DEFAULT_BASE_URL} \xB7 ${DEFAULT_MODEL}`)}`, value: "openrouter" },
|
|
3431
|
+
{ name: "Custom (specify base URL and model)", value: "custom" }
|
|
3432
|
+
]
|
|
3433
|
+
});
|
|
3434
|
+
let aiBaseUrl;
|
|
3435
|
+
let aiModel;
|
|
3436
|
+
if (providerChoice === "custom") {
|
|
3437
|
+
aiBaseUrl = (await input({ message: "API base URL:", default: DEFAULT_BASE_URL })).trim();
|
|
3438
|
+
aiModel = (await input({ message: "Model name:", default: DEFAULT_MODEL })).trim();
|
|
3439
|
+
}
|
|
3440
|
+
const apiKeyHint = providerChoice === "openrouter" ? chalk13.dim(" Get a key at: https://openrouter.ai/settings/keys\n") : chalk13.dim(" API key for your provider\n");
|
|
3441
|
+
console.log(apiKeyHint);
|
|
3442
|
+
const aiApiKey = await password({
|
|
3443
|
+
message: "API Key:",
|
|
3444
|
+
mask: "*"
|
|
3445
|
+
});
|
|
3446
|
+
let owner = detectedOwner;
|
|
3447
|
+
let repo = detectedRepo;
|
|
3448
|
+
if (!owner || !repo) {
|
|
3449
|
+
owner = await input({
|
|
3450
|
+
message: "GitHub repo owner (user or org):",
|
|
3451
|
+
required: true
|
|
3452
|
+
});
|
|
3453
|
+
repo = await input({
|
|
3454
|
+
message: "GitHub repo name:",
|
|
3455
|
+
required: true
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
const config = {
|
|
3459
|
+
githubToken,
|
|
3460
|
+
githubClientId,
|
|
3461
|
+
aiApiKey: aiApiKey.trim(),
|
|
3462
|
+
...aiBaseUrl ? { aiBaseUrl } : {},
|
|
3463
|
+
...aiModel ? { aiModel } : {},
|
|
3464
|
+
github: {
|
|
3465
|
+
owner: owner.trim(),
|
|
3466
|
+
repo: repo.trim()
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
setConfig(config);
|
|
3470
|
+
const spinner = ora14("Setting up GitHub labels...").start();
|
|
2857
3471
|
try {
|
|
2858
|
-
|
|
2859
|
-
|
|
3472
|
+
await ensureLabels(config);
|
|
3473
|
+
spinner.succeed("GitHub labels created");
|
|
3474
|
+
} catch (err) {
|
|
3475
|
+
spinner.fail("Failed to create labels (check token permissions)");
|
|
3476
|
+
console.error(chalk13.red(String(err)));
|
|
3477
|
+
}
|
|
3478
|
+
console.log(chalk13.green("\nSetup complete!"));
|
|
3479
|
+
console.log(chalk13.dim(`Config saved to: ${getConfigPath()}
|
|
3480
|
+
`));
|
|
3481
|
+
let genWiki = false;
|
|
3482
|
+
try {
|
|
3483
|
+
genWiki = await select11({
|
|
3484
|
+
message: "Generate TECHUNTER.md project overview for new team members?",
|
|
2860
3485
|
choices: [
|
|
2861
|
-
|
|
2862
|
-
{ name:
|
|
3486
|
+
{ name: "Yes, generate now", value: true },
|
|
3487
|
+
{ name: "No, skip (run /wiki later)", value: false }
|
|
2863
3488
|
]
|
|
2864
3489
|
});
|
|
2865
|
-
answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
|
|
2866
3490
|
} catch {
|
|
2867
|
-
answer = "User skipped this question \u2014 use your best judgement.";
|
|
2868
3491
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
3492
|
+
if (genWiki) {
|
|
3493
|
+
const wikiSpinner = ora14("Analyzing project and generating TECHUNTER.md\u2026").start();
|
|
3494
|
+
try {
|
|
3495
|
+
const content = await generateWiki(config);
|
|
3496
|
+
await upsertRepoFile(config, "TECHUNTER.md", content, "docs: add TECHUNTER.md project overview");
|
|
3497
|
+
wikiSpinner.succeed("TECHUNTER.md created");
|
|
3498
|
+
} catch (err) {
|
|
3499
|
+
wikiSpinner.fail(`Could not generate wiki: ${err.message}`);
|
|
3500
|
+
}
|
|
3501
|
+
console.log("");
|
|
3502
|
+
}
|
|
2871
3503
|
}
|
|
2872
3504
|
|
|
2873
|
-
// src/
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3505
|
+
// src/commands/config.ts
|
|
3506
|
+
import { input as input2, password as password2, select as select12 } from "@inquirer/prompts";
|
|
3507
|
+
import chalk14 from "chalk";
|
|
3508
|
+
async function configCommand() {
|
|
3509
|
+
let config;
|
|
3510
|
+
try {
|
|
3511
|
+
config = getConfig();
|
|
3512
|
+
} catch {
|
|
3513
|
+
console.error(chalk14.red("No config found. Run `tch init` first."));
|
|
3514
|
+
process.exit(1);
|
|
3515
|
+
}
|
|
3516
|
+
console.log(chalk14.bold.cyan("\nTechunter \u2014 Settings\n"));
|
|
3517
|
+
console.log(chalk14.dim(`Config file: ${getConfigPath()}
|
|
3518
|
+
`));
|
|
3519
|
+
const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
|
|
3520
|
+
const currentModel = config.aiModel ?? DEFAULT_MODEL;
|
|
3521
|
+
const currentBaseBranch = config.baseBranch ?? "main";
|
|
3522
|
+
const field = await select12({
|
|
3523
|
+
message: "Which setting to change?",
|
|
3524
|
+
choices: [
|
|
3525
|
+
{ name: `GitHub repo ${chalk14.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
|
|
3526
|
+
{ name: `Base branch ${chalk14.dim(currentBaseBranch)}`, value: "baseBranch" },
|
|
3527
|
+
{ name: `AI base URL ${chalk14.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
|
|
3528
|
+
{ name: `AI model ${chalk14.dim(currentModel)}`, value: "aiModel" },
|
|
3529
|
+
{ name: `AI API Key ${chalk14.dim("(hidden)")}`, value: "aiApiKey" },
|
|
3530
|
+
{ name: `GitHub Token ${chalk14.dim("(hidden)")}`, value: "githubToken" },
|
|
3531
|
+
{ name: "Cancel", value: "cancel" }
|
|
3532
|
+
]
|
|
3533
|
+
});
|
|
3534
|
+
if (field === "cancel") return;
|
|
3535
|
+
if (field === "baseBranch") {
|
|
3536
|
+
const val = await input2({ message: "Base branch name:", default: currentBaseBranch });
|
|
3537
|
+
if (val.trim()) {
|
|
3538
|
+
setConfig({ baseBranch: val.trim() });
|
|
3539
|
+
console.log(chalk14.green(`
|
|
3540
|
+
Base branch set to: ${val.trim()}
|
|
3541
|
+
`));
|
|
3542
|
+
}
|
|
3543
|
+
} else if (field === "repo") {
|
|
3544
|
+
const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
|
|
3545
|
+
const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
|
|
3546
|
+
setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
|
|
3547
|
+
console.log(chalk14.green(`
|
|
3548
|
+
Repo set to: ${owner.trim()}/${repo.trim()}
|
|
3549
|
+
`));
|
|
3550
|
+
} else if (field === "aiBaseUrl") {
|
|
3551
|
+
const val = await input2({ message: "AI base URL:", default: currentBaseUrl });
|
|
3552
|
+
if (val.trim()) {
|
|
3553
|
+
setConfig({ aiBaseUrl: val.trim() });
|
|
3554
|
+
console.log(chalk14.green(`
|
|
3555
|
+
AI base URL set to: ${val.trim()}
|
|
3556
|
+
`));
|
|
3557
|
+
}
|
|
3558
|
+
} else if (field === "aiModel") {
|
|
3559
|
+
const val = await input2({ message: "AI model name:", default: currentModel });
|
|
3560
|
+
if (val.trim()) {
|
|
3561
|
+
setConfig({ aiModel: val.trim() });
|
|
3562
|
+
console.log(chalk14.green(`
|
|
3563
|
+
AI model set to: ${val.trim()}
|
|
3564
|
+
`));
|
|
3565
|
+
}
|
|
3566
|
+
} else if (field === "aiApiKey") {
|
|
3567
|
+
const val = await password2({ message: "New AI API Key:", mask: "*" });
|
|
3568
|
+
if (val.trim()) {
|
|
3569
|
+
setConfig({ aiApiKey: val.trim() });
|
|
3570
|
+
console.log(chalk14.green("\nAI API Key updated.\n"));
|
|
3571
|
+
}
|
|
3572
|
+
} else if (field === "githubToken") {
|
|
3573
|
+
const val = await password2({ message: "New GitHub Token:", mask: "*" });
|
|
3574
|
+
if (val.trim()) {
|
|
3575
|
+
setConfig({ githubToken: val.trim() });
|
|
3576
|
+
console.log(chalk14.green("\nGitHub Token updated.\n"));
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
// src/index.ts
|
|
3582
|
+
init_github();
|
|
2897
3583
|
|
|
2898
3584
|
// src/lib/agent.ts
|
|
3585
|
+
import ora15 from "ora";
|
|
3586
|
+
import chalk15 from "chalk";
|
|
2899
3587
|
var tools = toolModules.map((m) => m.definition);
|
|
3588
|
+
var HISTORY_KEEP_TURNS = 8;
|
|
3589
|
+
function trimHistory(messages) {
|
|
3590
|
+
const userIndices = messages.map((m, i) => m.role === "user" ? i : -1).filter((i) => i !== -1);
|
|
3591
|
+
if (userIndices.length <= HISTORY_KEEP_TURNS) return;
|
|
3592
|
+
const compressBefore = userIndices[userIndices.length - HISTORY_KEEP_TURNS];
|
|
3593
|
+
const compressed = [];
|
|
3594
|
+
for (let t = 0; t < userIndices.length - HISTORY_KEEP_TURNS; t++) {
|
|
3595
|
+
const start = userIndices[t];
|
|
3596
|
+
const end = t + 1 < userIndices.length ? userIndices[t + 1] : compressBefore;
|
|
3597
|
+
const turnMessages = messages.slice(start, end);
|
|
3598
|
+
compressed.push(turnMessages[0]);
|
|
3599
|
+
const lastAssistant = [...turnMessages].reverse().find(
|
|
3600
|
+
(m) => m.role === "assistant" && typeof m.content === "string" && !!m.content
|
|
3601
|
+
);
|
|
3602
|
+
if (lastAssistant) {
|
|
3603
|
+
compressed.push({ role: "assistant", content: lastAssistant.content });
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
messages.splice(0, compressBefore, ...compressed);
|
|
3607
|
+
}
|
|
2900
3608
|
async function executeTool(name, input3, config) {
|
|
2901
3609
|
const mod = toolModules.find((m) => m.definition.function.name === name);
|
|
2902
3610
|
if (!mod) return `Unknown tool: ${name}`;
|
|
@@ -2922,9 +3630,14 @@ async function runAgentLoop(config, messages) {
|
|
|
2922
3630
|
"## Tool philosophy",
|
|
2923
3631
|
"Command tools (pick, new_task, close, submit, my_status, review, refresh, open_code) run",
|
|
2924
3632
|
"hardcoded interactive flows \u2014 always use these for user-facing actions.",
|
|
2925
|
-
"Low-level tools are for reasoning: run_command,
|
|
3633
|
+
"Low-level tools are for reasoning: run_command, grep_code, ask_user,",
|
|
2926
3634
|
"get_task, get_comments, get_diff.",
|
|
2927
3635
|
"",
|
|
3636
|
+
"## Exploring the codebase",
|
|
3637
|
+
"1. list_files \u2014 see all files or filter by glob to orient yourself.",
|
|
3638
|
+
"2. grep_code(pattern) \u2014 find where functions/variables appear.",
|
|
3639
|
+
"3. grep_code(file_glob, start_line, end_line) \u2014 read a specific section after grep gives you line numbers.",
|
|
3640
|
+
"",
|
|
2928
3641
|
"## Creating a task",
|
|
2929
3642
|
"If the task description is vague, call ask_user to clarify (max 3 times).",
|
|
2930
3643
|
"Then call new_task with the title \u2014 the tool scans the project and generates the guide automatically.",
|
|
@@ -2955,7 +3668,8 @@ async function runAgentLoop(config, messages) {
|
|
|
2955
3668
|
if (++iterations > MAX_ITERATIONS) {
|
|
2956
3669
|
throw new Error(`Agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
2957
3670
|
}
|
|
2958
|
-
|
|
3671
|
+
trimHistory(messages);
|
|
3672
|
+
const spinner = ora15({ text: chalk15.dim("Thinking\u2026"), color: "cyan" }).start();
|
|
2959
3673
|
let response;
|
|
2960
3674
|
try {
|
|
2961
3675
|
response = await client.chat.completions.create({
|
|
@@ -3000,7 +3714,7 @@ async function runAgentLoop(config, messages) {
|
|
|
3000
3714
|
return executeTool(tc.function.name, parsed, config);
|
|
3001
3715
|
})
|
|
3002
3716
|
);
|
|
3003
|
-
let
|
|
3717
|
+
let terminal13 = false;
|
|
3004
3718
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
3005
3719
|
printToolResult(results[i]);
|
|
3006
3720
|
messages.push({
|
|
@@ -3009,16 +3723,93 @@ async function runAgentLoop(config, messages) {
|
|
|
3009
3723
|
content: results[i]
|
|
3010
3724
|
});
|
|
3011
3725
|
if (toolModules.find((m) => m.definition.function.name === toolCalls[i].function.name)?.terminal) {
|
|
3012
|
-
|
|
3726
|
+
terminal13 = true;
|
|
3013
3727
|
}
|
|
3014
3728
|
}
|
|
3015
|
-
if (
|
|
3729
|
+
if (terminal13) return results[results.length - 1];
|
|
3016
3730
|
} else {
|
|
3017
3731
|
return choice.message.content ?? "";
|
|
3018
3732
|
}
|
|
3019
3733
|
}
|
|
3020
3734
|
}
|
|
3021
3735
|
|
|
3736
|
+
// src/lib/update-check.ts
|
|
3737
|
+
import Conf2 from "conf";
|
|
3738
|
+
import chalk16 from "chalk";
|
|
3739
|
+
import { execFile } from "child_process";
|
|
3740
|
+
var PACKAGE_NAME = "techunter";
|
|
3741
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3742
|
+
var cache = new Conf2({
|
|
3743
|
+
projectName: "techunter-update-cache",
|
|
3744
|
+
defaults: { lastChecked: 0, latestVersion: "" }
|
|
3745
|
+
});
|
|
3746
|
+
async function fetchLatestVersion() {
|
|
3747
|
+
try {
|
|
3748
|
+
const { fetch } = await import("undici");
|
|
3749
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
3750
|
+
signal: AbortSignal.timeout(5e3)
|
|
3751
|
+
});
|
|
3752
|
+
if (!res.ok) return null;
|
|
3753
|
+
const data = await res.json();
|
|
3754
|
+
return data.version ?? null;
|
|
3755
|
+
} catch {
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
function isNewer(latest, current) {
|
|
3760
|
+
const parse = (v) => v.split(".").map(Number);
|
|
3761
|
+
const [la, lb, lc] = parse(latest);
|
|
3762
|
+
const [ca, cb, cc] = parse(current);
|
|
3763
|
+
if (la !== ca) return la > ca;
|
|
3764
|
+
if (lb !== cb) return lb > cb;
|
|
3765
|
+
return lc > cc;
|
|
3766
|
+
}
|
|
3767
|
+
async function getAvailableUpdate(currentVersion) {
|
|
3768
|
+
const now = Date.now();
|
|
3769
|
+
const lastChecked = cache.get("lastChecked");
|
|
3770
|
+
let latest = cache.get("latestVersion");
|
|
3771
|
+
if (!latest || now - lastChecked > CHECK_INTERVAL_MS) {
|
|
3772
|
+
const fetched = await fetchLatestVersion();
|
|
3773
|
+
if (fetched) {
|
|
3774
|
+
latest = fetched;
|
|
3775
|
+
cache.set("latestVersion", fetched);
|
|
3776
|
+
cache.set("lastChecked", now);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
return latest && isNewer(latest, currentVersion) ? latest : null;
|
|
3780
|
+
}
|
|
3781
|
+
function installUpdate() {
|
|
3782
|
+
return new Promise((resolve, reject) => {
|
|
3783
|
+
execFile("npm", ["install", "-g", PACKAGE_NAME], { shell: true }, (err, _stdout, stderr) => {
|
|
3784
|
+
if (err) {
|
|
3785
|
+
reject(new Error(stderr.trim() || err.message));
|
|
3786
|
+
} else {
|
|
3787
|
+
resolve(cache.get("latestVersion"));
|
|
3788
|
+
}
|
|
3789
|
+
});
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
async function startAutoUpdate(currentVersion) {
|
|
3793
|
+
const latest = await Promise.race([
|
|
3794
|
+
getAvailableUpdate(currentVersion),
|
|
3795
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 2e3))
|
|
3796
|
+
]);
|
|
3797
|
+
if (!latest) return;
|
|
3798
|
+
console.log(
|
|
3799
|
+
chalk16.cyan("\n \u2191 Auto-updating to v" + latest + "\u2026") + chalk16.dim(" (running in background)\n")
|
|
3800
|
+
);
|
|
3801
|
+
installUpdate().then((installedVersion) => {
|
|
3802
|
+
console.log(
|
|
3803
|
+
"\n" + chalk16.green(" \u2714 Updated to v" + (installedVersion || latest)) + chalk16.dim(" \u2014 restart tch to use the new version\n") + chalk16.cyan(" You \u203A ")
|
|
3804
|
+
// redraw the prompt hint
|
|
3805
|
+
);
|
|
3806
|
+
}).catch((err) => {
|
|
3807
|
+
console.log(
|
|
3808
|
+
"\n" + chalk16.red(" \u2718 Auto-update failed: ") + chalk16.dim(err.message) + "\n" + chalk16.dim(" Run manually: npm install -g techunter\n") + chalk16.cyan(" You \u203A ")
|
|
3809
|
+
);
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3022
3813
|
// src/index.ts
|
|
3023
3814
|
var _require = createRequire(import.meta.url);
|
|
3024
3815
|
var { version } = _require("../package.json");
|
|
@@ -3045,6 +3836,8 @@ var SLASH_NAMES = [
|
|
|
3045
3836
|
"/me",
|
|
3046
3837
|
"/code",
|
|
3047
3838
|
"/c",
|
|
3839
|
+
"/wiki",
|
|
3840
|
+
"/w",
|
|
3048
3841
|
"/config",
|
|
3049
3842
|
"/cfg",
|
|
3050
3843
|
"/init"
|
|
@@ -3061,7 +3854,7 @@ function promptUser() {
|
|
|
3061
3854
|
return new Promise((resolve) => {
|
|
3062
3855
|
if (process.stdin.isPaused()) process.stdin.resume();
|
|
3063
3856
|
_rl.resume();
|
|
3064
|
-
_rl.question(
|
|
3857
|
+
_rl.question(chalk17.cyan("You") + chalk17.dim(" \u203A "), resolve);
|
|
3065
3858
|
});
|
|
3066
3859
|
}
|
|
3067
3860
|
var COMMANDS = [
|
|
@@ -3077,37 +3870,38 @@ var COMMANDS = [
|
|
|
3077
3870
|
{ cmd: "/config", alias: "/cfg", desc: "Change settings (branch, repo, API keys)" },
|
|
3078
3871
|
{ cmd: "/init", desc: "Re-run setup wizard for this repo" },
|
|
3079
3872
|
{ cmd: "/status", alias: "/me", desc: "Show tasks assigned to you" },
|
|
3080
|
-
{ cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" }
|
|
3873
|
+
{ cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" },
|
|
3874
|
+
{ cmd: "/wiki", alias: "/w", desc: "Generate or refresh TECHUNTER.md project overview" }
|
|
3081
3875
|
];
|
|
3082
3876
|
function cmdHelp() {
|
|
3083
3877
|
console.log("");
|
|
3084
|
-
console.log(
|
|
3085
|
-
console.log(
|
|
3878
|
+
console.log(chalk17.bold(" Commands"));
|
|
3879
|
+
console.log(chalk17.dim(" \u2500".repeat(35)));
|
|
3086
3880
|
for (const { cmd, alias, desc } of COMMANDS) {
|
|
3087
|
-
const left = (cmd + (alias ? ` ${
|
|
3088
|
-
console.log(` ${
|
|
3881
|
+
const left = (cmd + (alias ? ` ${chalk17.dim(alias)}` : "")).padEnd(22);
|
|
3882
|
+
console.log(` ${chalk17.cyan(cmd)}${alias ? " " + chalk17.dim(alias) : ""}`.padEnd(30) + chalk17.dim(desc));
|
|
3089
3883
|
}
|
|
3090
|
-
console.log(
|
|
3884
|
+
console.log(chalk17.dim("\n Anything else is sent to the AI agent.\n"));
|
|
3091
3885
|
}
|
|
3092
3886
|
function printBanner(config) {
|
|
3093
3887
|
const { owner, repo } = config.github;
|
|
3094
|
-
const g =
|
|
3095
|
-
const b =
|
|
3096
|
-
const p =
|
|
3888
|
+
const g = chalk17.cyan;
|
|
3889
|
+
const b = chalk17.bold.white;
|
|
3890
|
+
const p = chalk17.yellow.bold;
|
|
3097
3891
|
console.log("");
|
|
3098
3892
|
console.log(" " + g("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
3099
3893
|
console.log(p("\u25C6") + b("\u2550\u2550\u2550") + g("\u256C") + b(" TECHUNTER ") + g("\u256C") + b("\u2550\u2550\u2550\u25B6"));
|
|
3100
3894
|
console.log(" " + g("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
3101
3895
|
console.log("");
|
|
3102
3896
|
console.log(
|
|
3103
|
-
" " +
|
|
3897
|
+
" " + chalk17.bold("Techunter") + chalk17.dim(` v${version}`) + chalk17.dim(" \xB7 ") + chalk17.cyan(getModel(config)) + chalk17.dim(" \xB7 ") + chalk17.dim(`${owner}/${repo}`)
|
|
3104
3898
|
);
|
|
3105
3899
|
console.log("");
|
|
3106
3900
|
}
|
|
3107
3901
|
async function initNewRepo(config, owner, repo) {
|
|
3108
3902
|
console.log("");
|
|
3109
|
-
console.log(
|
|
3110
|
-
console.log(
|
|
3903
|
+
console.log(chalk17.bold.cyan(` New repo detected: ${owner}/${repo}`));
|
|
3904
|
+
console.log(chalk17.dim(" Setting up Techunter for this repository...\n"));
|
|
3111
3905
|
const newConfig = {
|
|
3112
3906
|
...config,
|
|
3113
3907
|
github: { owner, repo }
|
|
@@ -3130,7 +3924,7 @@ async function main() {
|
|
|
3130
3924
|
try {
|
|
3131
3925
|
await configCommand();
|
|
3132
3926
|
} catch (err) {
|
|
3133
|
-
console.error(
|
|
3927
|
+
console.error(chalk17.red(`
|
|
3134
3928
|
Error: ${err.message}`));
|
|
3135
3929
|
process.exit(1);
|
|
3136
3930
|
}
|
|
@@ -3144,7 +3938,7 @@ Error: ${err.message}`));
|
|
|
3144
3938
|
await initCommand();
|
|
3145
3939
|
config = getConfig();
|
|
3146
3940
|
} catch (err) {
|
|
3147
|
-
console.error(
|
|
3941
|
+
console.error(chalk17.red(`
|
|
3148
3942
|
Setup failed: ${err.message}`));
|
|
3149
3943
|
process.exit(1);
|
|
3150
3944
|
return;
|
|
@@ -3162,11 +3956,13 @@ Setup failed: ${err.message}`));
|
|
|
3162
3956
|
}
|
|
3163
3957
|
}
|
|
3164
3958
|
} else if (!config.github.owner) {
|
|
3165
|
-
console.error(
|
|
3959
|
+
console.error(chalk17.red("\nNo git remote found and no repo configured. Run tch init."));
|
|
3166
3960
|
process.exit(1);
|
|
3167
3961
|
}
|
|
3168
3962
|
printBanner(config);
|
|
3169
|
-
console.log(
|
|
3963
|
+
console.log(chalk17.dim(" Type /help for commands, or describe what you want.\n"));
|
|
3964
|
+
startAutoUpdate(version).catch(() => {
|
|
3965
|
+
});
|
|
3170
3966
|
await printTaskList(config);
|
|
3171
3967
|
await printMyTasks(config);
|
|
3172
3968
|
_rl = readline.createInterface({
|
|
@@ -3176,11 +3972,11 @@ Setup failed: ${err.message}`));
|
|
|
3176
3972
|
terminal: true
|
|
3177
3973
|
});
|
|
3178
3974
|
_rl.on("close", () => {
|
|
3179
|
-
console.log(
|
|
3975
|
+
console.log(chalk17.gray("\nGoodbye!"));
|
|
3180
3976
|
process.exit(0);
|
|
3181
3977
|
});
|
|
3182
3978
|
_rl.on("SIGINT", () => {
|
|
3183
|
-
console.log(
|
|
3979
|
+
console.log(chalk17.gray("\nGoodbye!"));
|
|
3184
3980
|
process.exit(0);
|
|
3185
3981
|
});
|
|
3186
3982
|
const messages = [];
|
|
@@ -3197,7 +3993,7 @@ Setup failed: ${err.message}`));
|
|
|
3197
3993
|
break;
|
|
3198
3994
|
case "/refresh":
|
|
3199
3995
|
case "/r":
|
|
3200
|
-
await
|
|
3996
|
+
await run9({}, config);
|
|
3201
3997
|
break;
|
|
3202
3998
|
case "/pick":
|
|
3203
3999
|
case "/p": {
|
|
@@ -3205,7 +4001,7 @@ Setup failed: ${err.message}`));
|
|
|
3205
4001
|
const preselected = arg ? parseInt(arg, 10) : void 0;
|
|
3206
4002
|
const result = await run3({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
|
|
3207
4003
|
if (result && result !== "Cancelled.") {
|
|
3208
|
-
console.log(
|
|
4004
|
+
console.log(chalk17.green(`
|
|
3209
4005
|
${result}
|
|
3210
4006
|
`));
|
|
3211
4007
|
}
|
|
@@ -3215,7 +4011,7 @@ Setup failed: ${err.message}`));
|
|
|
3215
4011
|
case "/new":
|
|
3216
4012
|
case "/n": {
|
|
3217
4013
|
const result = await run4({}, config);
|
|
3218
|
-
console.log(
|
|
4014
|
+
console.log(chalk17.green(`
|
|
3219
4015
|
${result}
|
|
3220
4016
|
`));
|
|
3221
4017
|
await printTaskList(config);
|
|
@@ -3233,7 +4029,7 @@ Setup failed: ${err.message}`));
|
|
|
3233
4029
|
const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
|
|
3234
4030
|
const preselected = arg ? parseInt(arg, 10) : void 0;
|
|
3235
4031
|
const result = await run11({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
|
|
3236
|
-
console.log(
|
|
4032
|
+
console.log(chalk17.green(`
|
|
3237
4033
|
${result}
|
|
3238
4034
|
`));
|
|
3239
4035
|
await printTaskList(config);
|
|
@@ -3242,7 +4038,7 @@ Setup failed: ${err.message}`));
|
|
|
3242
4038
|
case "/close":
|
|
3243
4039
|
case "/d": {
|
|
3244
4040
|
const result = await run2({}, config);
|
|
3245
|
-
console.log(
|
|
4041
|
+
console.log(chalk17.green(`
|
|
3246
4042
|
${result}
|
|
3247
4043
|
`));
|
|
3248
4044
|
await printTaskList(config);
|
|
@@ -3250,7 +4046,7 @@ Setup failed: ${err.message}`));
|
|
|
3250
4046
|
}
|
|
3251
4047
|
case "/review":
|
|
3252
4048
|
case "/rv": {
|
|
3253
|
-
const result = await
|
|
4049
|
+
const result = await run8({}, config);
|
|
3254
4050
|
console.log("\n" + renderMarkdown(result));
|
|
3255
4051
|
break;
|
|
3256
4052
|
}
|
|
@@ -3264,8 +4060,8 @@ Setup failed: ${err.message}`));
|
|
|
3264
4060
|
case "/ac": {
|
|
3265
4061
|
const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
|
|
3266
4062
|
const preselected = arg ? parseInt(arg, 10) : void 0;
|
|
3267
|
-
const result = await
|
|
3268
|
-
console.log(
|
|
4063
|
+
const result = await run6({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
|
|
4064
|
+
console.log(chalk17.green(`
|
|
3269
4065
|
${result}
|
|
3270
4066
|
`));
|
|
3271
4067
|
await printTaskList(config);
|
|
@@ -3281,17 +4077,25 @@ Setup failed: ${err.message}`));
|
|
|
3281
4077
|
config = getConfig();
|
|
3282
4078
|
await printTaskList(config);
|
|
3283
4079
|
} catch (err) {
|
|
3284
|
-
console.error(
|
|
4080
|
+
console.error(chalk17.red(`
|
|
3285
4081
|
Init failed: ${err.message}
|
|
3286
4082
|
`));
|
|
3287
4083
|
}
|
|
3288
4084
|
break;
|
|
3289
4085
|
case "/code":
|
|
3290
4086
|
case "/c":
|
|
3291
|
-
await
|
|
4087
|
+
await run10({}, config);
|
|
3292
4088
|
break;
|
|
4089
|
+
case "/wiki":
|
|
4090
|
+
case "/w": {
|
|
4091
|
+
const result = await run12({}, config);
|
|
4092
|
+
console.log(chalk17.green(`
|
|
4093
|
+
${result}
|
|
4094
|
+
`));
|
|
4095
|
+
break;
|
|
4096
|
+
}
|
|
3293
4097
|
default:
|
|
3294
|
-
console.log(
|
|
4098
|
+
console.log(chalk17.yellow(` Unknown command: ${cmd} (try /help)`));
|
|
3295
4099
|
}
|
|
3296
4100
|
continue;
|
|
3297
4101
|
}
|
|
@@ -3299,10 +4103,10 @@ Init failed: ${err.message}
|
|
|
3299
4103
|
messages.push({ role: "user", content: userInput });
|
|
3300
4104
|
try {
|
|
3301
4105
|
const response = await runAgentLoop(config, messages);
|
|
3302
|
-
console.log("\n" +
|
|
4106
|
+
console.log("\n" + chalk17.green("Techunter:") + "\n" + renderMarkdown(response));
|
|
3303
4107
|
} catch (err) {
|
|
3304
4108
|
messages.splice(prevLength);
|
|
3305
|
-
console.error(
|
|
4109
|
+
console.error(chalk17.red(`
|
|
3306
4110
|
Error: ${err.message}
|
|
3307
4111
|
`));
|
|
3308
4112
|
}
|
|
@@ -3310,6 +4114,6 @@ Error: ${err.message}
|
|
|
3310
4114
|
}
|
|
3311
4115
|
}
|
|
3312
4116
|
main().catch((err) => {
|
|
3313
|
-
console.error(
|
|
4117
|
+
console.error(chalk17.red(`Fatal: ${err.message}`));
|
|
3314
4118
|
process.exit(1);
|
|
3315
4119
|
});
|