techunter 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +23 -11
  2. package/dist/index.js +308 -159
  3. package/dist/mcp.js +299 -104
  4. package/package.json +1 -1
package/dist/mcp.js CHANGED
@@ -21,9 +21,9 @@ __export(github_exports, {
21
21
  ensureLabels: () => ensureLabels,
22
22
  formatGuideAsMarkdown: () => formatGuideAsMarkdown,
23
23
  getAuthenticatedUser: () => getAuthenticatedUser,
24
- getBaseBranch: () => getBaseBranch,
25
24
  getDefaultBranch: () => getDefaultBranch,
26
25
  getTask: () => getTask,
26
+ isCollaborator: () => isCollaborator,
27
27
  listComments: () => listComments,
28
28
  listMyTasks: () => listMyTasks,
29
29
  listTasks: () => listTasks,
@@ -50,6 +50,7 @@ function parseIssue(issue) {
50
50
  title: issue.title,
51
51
  body: issue.body ?? null,
52
52
  state: issue.state,
53
+ author: issue.user?.login ?? null,
53
54
  assignee: issue.assignee?.login ?? null,
54
55
  labels: (issue.labels ?? []).map(
55
56
  (l) => typeof l === "string" ? l : l.name ?? ""
@@ -226,6 +227,16 @@ async function getAuthenticatedUser(config) {
226
227
  const { data } = await octokit.users.getAuthenticated();
227
228
  return data.login;
228
229
  }
230
+ async function isCollaborator(config, username) {
231
+ const octokit = createOctokit(config.githubToken);
232
+ const { owner, repo } = config.github;
233
+ try {
234
+ const { data } = await octokit.repos.getCollaboratorPermissionLevel({ owner, repo, username });
235
+ return data.permission === "admin" || data.permission === "write" || data.permission === "maintain";
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
229
240
  async function listMyTasks(config, username) {
230
241
  const octokit = createOctokit(config.githubToken);
231
242
  const { owner, repo } = config.github;
@@ -297,10 +308,6 @@ async function getDefaultBranch(config) {
297
308
  const { data } = await octokit.repos.get({ owner, repo });
298
309
  return data.default_branch;
299
310
  }
300
- function getBaseBranch(config) {
301
- if (config.github.baseBranch) return Promise.resolve(config.github.baseBranch);
302
- return getDefaultBranch(config);
303
- }
304
311
  async function acceptTask(config, issueNumber) {
305
312
  const octokit = createOctokit(config.githubToken);
306
313
  const { owner, repo } = config.github;
@@ -360,9 +367,6 @@ async function getCurrentBranch() {
360
367
  const summary = await git.branch();
361
368
  return summary.current;
362
369
  }
363
- async function createAndSwitchBranch(name) {
364
- await git.checkoutLocalBranch(name);
365
- }
366
370
  async function pushBranch(name) {
367
371
  await git.push("origin", name, ["--set-upstream"]);
368
372
  }
@@ -370,6 +374,61 @@ function makeBranchName(issueNumber, username) {
370
374
  const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
371
375
  return `task-${issueNumber}-${slug}`;
372
376
  }
377
+ function makeWorkerBranchName(username) {
378
+ const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
379
+ return `worker-${slug}`;
380
+ }
381
+ async function getCurrentCommit() {
382
+ return (await git.revparse(["HEAD"])).trim();
383
+ }
384
+ async function switchToBranchOrCreate(name) {
385
+ try {
386
+ const branches = await git.branch(["-a"]);
387
+ const exists = Object.keys(branches.branches).some(
388
+ (b) => b === name || b === `remotes/origin/${name}`
389
+ );
390
+ if (exists) {
391
+ await git.checkout(name);
392
+ return false;
393
+ }
394
+ await git.checkoutLocalBranch(name);
395
+ return true;
396
+ } catch {
397
+ await git.checkoutLocalBranch(name);
398
+ return true;
399
+ }
400
+ }
401
+ async function getDiffFromCommit(baseCommit) {
402
+ const status = await git.status();
403
+ const parts = [];
404
+ const fileLines = [
405
+ ...status.modified.map((f) => ` M ${f}`),
406
+ ...status.created.map((f) => ` A ${f}`),
407
+ ...status.deleted.map((f) => ` D ${f}`),
408
+ ...status.renamed.map((f) => ` R ${f.from} \u2192 ${f.to}`),
409
+ ...status.not_added.map((f) => ` ? ${f}`)
410
+ ];
411
+ if (fileLines.length > 0) {
412
+ parts.push("## Uncommitted changes\n" + fileLines.join("\n"));
413
+ const uncommitted = await git.diff(["HEAD"]);
414
+ if (uncommitted) {
415
+ const capped = uncommitted.length > 8e3 ? uncommitted.slice(0, 8e3) + "\n... (truncated)" : uncommitted;
416
+ parts.push("## Uncommitted diff\n```diff\n" + capped + "\n```");
417
+ }
418
+ }
419
+ const log = await git.log({ from: baseCommit, to: "HEAD" });
420
+ if (log.total > 0) {
421
+ const logLines = log.all.map((c) => ` ${c.hash.slice(0, 7)} ${c.message}`);
422
+ parts.push(`## Commits since task claimed (${log.total} total)
423
+ ` + logLines.join("\n"));
424
+ const branchDiff = await git.diff([baseCommit, "HEAD"]);
425
+ if (branchDiff) {
426
+ const capped = branchDiff.length > 12e3 ? branchDiff.slice(0, 12e3) + "\n... (truncated)" : branchDiff;
427
+ parts.push("## Full diff since task claimed\n```diff\n" + capped + "\n```");
428
+ }
429
+ }
430
+ return parts.length > 0 ? parts.join("\n\n") : "No changes since task was claimed.";
431
+ }
373
432
  async function findMergeBase(configuredBase) {
374
433
  const candidates = configuredBase ? [`origin/${configuredBase}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
375
434
  const unique = [...new Set(candidates)];
@@ -428,6 +487,68 @@ async function stageAllAndCommit(message) {
428
487
  await git.push("origin", branch, ["--set-upstream"]);
429
488
  }
430
489
 
490
+ // src/lib/config.ts
491
+ import Conf from "conf";
492
+ import { z } from "zod";
493
+ var configSchema = z.object({
494
+ aiApiKey: z.string().min(1),
495
+ aiBaseUrl: z.string().optional(),
496
+ aiModel: z.string().optional(),
497
+ githubToken: z.string().min(1),
498
+ githubClientId: z.string().optional(),
499
+ github: z.object({
500
+ owner: z.string().min(1),
501
+ repo: z.string().min(1)
502
+ }),
503
+ taskState: z.object({
504
+ activeIssueNumber: z.number().optional(),
505
+ baseCommit: z.string().optional()
506
+ }).optional()
507
+ });
508
+ var store = new Conf({
509
+ projectName: "techunter",
510
+ defaults: {}
511
+ });
512
+ function getConfig() {
513
+ const raw = store.store;
514
+ const result = configSchema.safeParse(raw);
515
+ if (!result.success) {
516
+ throw new Error("Configuration is missing or invalid.");
517
+ }
518
+ return result.data;
519
+ }
520
+ function setConfig(partial) {
521
+ const current = store.store;
522
+ if (partial.github) {
523
+ current["github"] = {
524
+ ...current["github"] ?? {},
525
+ ...partial.github
526
+ };
527
+ }
528
+ if (partial.aiApiKey !== void 0) {
529
+ current["aiApiKey"] = partial.aiApiKey;
530
+ }
531
+ if (partial.aiBaseUrl !== void 0) {
532
+ current["aiBaseUrl"] = partial.aiBaseUrl;
533
+ }
534
+ if (partial.aiModel !== void 0) {
535
+ current["aiModel"] = partial.aiModel;
536
+ }
537
+ if (partial.githubToken !== void 0) {
538
+ current["githubToken"] = partial.githubToken;
539
+ }
540
+ if (partial.githubClientId !== void 0) {
541
+ current["githubClientId"] = partial.githubClientId;
542
+ }
543
+ if (partial.taskState !== void 0) {
544
+ current["taskState"] = {
545
+ ...current["taskState"] ?? {},
546
+ ...partial.taskState
547
+ };
548
+ }
549
+ store.store = current;
550
+ }
551
+
431
552
  // src/lib/markdown.ts
432
553
  import { marked } from "marked";
433
554
  import { markedTerminal } from "marked-terminal";
@@ -635,7 +756,7 @@ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
635
756
  }
636
757
 
637
758
  // src/tools/submit/prompts.ts
638
- var REVIEWER_SYSTEM_PROMPT = "You are a concise code reviewer. Use run_command to run tests/lint if needed, and read_file to inspect specific files. Then output your review: for each acceptance criterion mark \u2705 met or \u274C not met with a one-line reason. End with an overall verdict line: Ready to submit / Not ready. Reply in the same language as the task.";
759
+ 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.";
639
760
 
640
761
  // src/tools/submit/reviewer.ts
641
762
  async function reviewChanges(config, issueNumber, issue, diff) {
@@ -669,32 +790,41 @@ var definition = {
669
790
  }
670
791
  };
671
792
  async function run(_input, config) {
672
- const branch = await getCurrentBranch();
673
- const match = branch.match(/^task-(\d+)-/);
674
- if (!match) {
675
- return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
793
+ const taskState = getConfig().taskState;
794
+ const issueNumber = taskState?.activeIssueNumber;
795
+ if (!issueNumber) {
796
+ return "No active task found. Claim a task first with /pick.";
676
797
  }
677
- const issueNumber = parseInt(match[1], 10);
678
798
  let spinner = ora("Loading task and diff\u2026").start();
679
- const [issue, defaultBranch, diff] = await Promise.all([
799
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
800
+ const [issue, diff, me] = await Promise.all([
680
801
  getTask(config, issueNumber),
681
- getBaseBranch(config),
682
- getDiff(config.github.baseBranch)
802
+ diffPromise,
803
+ getAuthenticatedUser(config)
683
804
  ]);
684
805
  spinner.stop();
685
- const reviewSpinner = ora("Reviewing changes\u2026").start();
806
+ const workerBranch = makeWorkerBranchName(issue.author ?? me);
807
+ const branch = await getCurrentBranch();
808
+ const isSelfSubmit = issue.author !== null && issue.author === me;
686
809
  let review = "";
687
- try {
688
- review = await reviewChanges(config, issueNumber, issue, diff);
689
- } catch (err) {
690
- review = `(Review failed: ${err.message})`;
810
+ if (!isSelfSubmit) {
811
+ const reviewSpinner = ora("Reviewing changes\u2026").start();
812
+ try {
813
+ review = await reviewChanges(config, issueNumber, issue, diff);
814
+ } catch (err) {
815
+ review = `(Review failed: ${err.message})`;
816
+ }
817
+ reviewSpinner.stop();
691
818
  }
692
- reviewSpinner.stop();
693
819
  const divider = chalk5.dim("\u2500".repeat(70));
694
820
  console.log("\n" + divider);
695
- console.log(chalk5.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
696
- console.log(divider);
697
- console.log(renderMarkdown(review));
821
+ if (isSelfSubmit) {
822
+ console.log(chalk5.yellow(` Self-submit detected \u2014 AI review skipped.`));
823
+ } else {
824
+ console.log(chalk5.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
825
+ console.log(divider);
826
+ console.log(renderMarkdown(review));
827
+ }
698
828
  console.log(divider + "\n");
699
829
  let shouldProceed;
700
830
  try {
@@ -730,15 +860,15 @@ async function run(_input, config) {
730
860
  spinner = ora("Creating pull request\u2026").start();
731
861
  let prUrl;
732
862
  try {
733
- prUrl = await createPR(
734
- config,
735
- issue.title,
736
- `Closes #${issueNumber}
737
-
738
- ${issue.body ?? ""}`.trim(),
739
- branch,
740
- defaultBranch
741
- );
863
+ const prBody = [
864
+ `Closes #${issueNumber}`,
865
+ issue.body ? `
866
+ ${issue.body}` : "",
867
+ review ? `
868
+ ## AI Review
869
+ ${review}` : ""
870
+ ].join("\n").trim();
871
+ prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
742
872
  spinner.stop();
743
873
  } catch (err) {
744
874
  spinner.stop();
@@ -752,25 +882,31 @@ ${issue.body ?? ""}`.trim(),
752
882
  spinner.stop();
753
883
  return `PR created (${prUrl}) but failed to update label: ${err.message}`;
754
884
  }
885
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
755
886
  return `Task #${issueNumber} submitted.
756
887
  Commit: "${commitMessage.trim()}"
757
888
  PR: ${prUrl}`;
758
889
  }
759
890
  async function execute(input, config) {
760
- const branch = await getCurrentBranch();
761
- const match = branch.match(/^task-(\d+)-/);
762
- if (!match) return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
763
- const issueNumber = parseInt(match[1], 10);
764
- const [issue, defaultBranch, diff] = await Promise.all([
891
+ const taskState = getConfig().taskState;
892
+ const issueNumber = taskState?.activeIssueNumber;
893
+ if (!issueNumber) return "No active task found. Claim a task first.";
894
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
895
+ const [issue, diff, branch, me] = await Promise.all([
765
896
  getTask(config, issueNumber),
766
- getBaseBranch(config),
767
- getDiff(config.github.baseBranch)
897
+ diffPromise,
898
+ getCurrentBranch(),
899
+ getAuthenticatedUser(config)
768
900
  ]);
901
+ const workerBranch = makeWorkerBranchName(issue.author ?? me);
902
+ const isSelfSubmit = issue.author !== null && issue.author === me;
769
903
  let review = "";
770
- try {
771
- review = await reviewChanges(config, issueNumber, issue, diff);
772
- } catch (err) {
773
- review = `(Review failed: ${err.message})`;
904
+ if (!isSelfSubmit) {
905
+ try {
906
+ review = await reviewChanges(config, issueNumber, issue, diff);
907
+ } catch (err) {
908
+ review = `(Review failed: ${err.message})`;
909
+ }
774
910
  }
775
911
  const commitMessage = input["commit_message"]?.trim() || `complete: ${issue.title}`;
776
912
  try {
@@ -780,15 +916,15 @@ async function execute(input, config) {
780
916
  }
781
917
  let prUrl;
782
918
  try {
783
- prUrl = await createPR(
784
- config,
785
- issue.title,
786
- `Closes #${issueNumber}
787
-
788
- ${issue.body ?? ""}`.trim(),
789
- branch,
790
- defaultBranch
791
- );
919
+ const prBody = [
920
+ `Closes #${issueNumber}`,
921
+ issue.body ? `
922
+ ${issue.body}` : "",
923
+ review ? `
924
+ ## AI Review
925
+ ${review}` : ""
926
+ ].join("\n").trim();
927
+ prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
792
928
  } catch (err) {
793
929
  return `Committed but PR creation failed: ${err.message}`;
794
930
  }
@@ -982,23 +1118,42 @@ Finish or submit it before claiming a new one.`;
982
1118
  let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
983
1119
  await claimTask(config, issue.number, me);
984
1120
  spinner.stop();
985
- const branch = makeBranchName(issue.number, me);
986
- spinner = ora3(`Creating branch ${branch}\u2026`).start();
1121
+ const workerBranch = makeWorkerBranchName(me);
1122
+ spinner = ora3(`Switching to ${workerBranch}\u2026`).start();
987
1123
  try {
988
- await createAndSwitchBranch(branch);
1124
+ const isNewWorker = await switchToBranchOrCreate(workerBranch);
989
1125
  spinner.stop();
1126
+ if (isNewWorker) {
1127
+ spinner = ora3("Pushing worker branch\u2026").start();
1128
+ try {
1129
+ await pushBranch(workerBranch);
1130
+ spinner.stop();
1131
+ } catch {
1132
+ spinner.warn("Could not push worker branch");
1133
+ }
1134
+ }
990
1135
  } catch {
991
- spinner.warn(`Could not create branch ${branch}`);
1136
+ spinner.warn(`Could not switch to ${workerBranch}`);
992
1137
  }
993
- spinner = ora3("Pushing branch\u2026").start();
1138
+ const branch = makeBranchName(issue.number, me);
1139
+ spinner = ora3(`Creating task branch ${branch}\u2026`).start();
994
1140
  try {
995
- await pushBranch(branch);
1141
+ await switchToBranchOrCreate(branch);
996
1142
  spinner.stop();
1143
+ spinner = ora3("Pushing task branch\u2026").start();
1144
+ try {
1145
+ await pushBranch(branch);
1146
+ spinner.stop();
1147
+ } catch {
1148
+ spinner.warn("Could not push task branch");
1149
+ }
997
1150
  } catch {
998
- spinner.warn("Could not push branch");
1151
+ spinner.warn(`Could not create branch ${branch}`);
999
1152
  }
1153
+ const baseCommit = await getCurrentCommit();
1154
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
1000
1155
  console.log(chalk6.green(`
1001
- Claimed! Branch: ${branch}
1156
+ Claimed! Branch: ${branch} (base: ${baseCommit.slice(0, 7)})
1002
1157
  `));
1003
1158
  let openClaude;
1004
1159
  try {
@@ -1051,16 +1206,29 @@ async function execute3(input, config) {
1051
1206
  } catch (err) {
1052
1207
  return `Error claiming task: ${err.message}`;
1053
1208
  }
1209
+ const workerBranch = makeWorkerBranchName(me);
1210
+ try {
1211
+ const isNewWorker = await switchToBranchOrCreate(workerBranch);
1212
+ if (isNewWorker) {
1213
+ try {
1214
+ await pushBranch(workerBranch);
1215
+ } catch {
1216
+ }
1217
+ }
1218
+ } catch {
1219
+ }
1054
1220
  const branch = makeBranchName(issueNumber, me);
1055
1221
  try {
1056
- await createAndSwitchBranch(branch);
1222
+ await switchToBranchOrCreate(branch);
1057
1223
  } catch {
1058
1224
  }
1059
1225
  try {
1060
1226
  await pushBranch(branch);
1061
1227
  } catch {
1062
1228
  }
1063
- return `Task #${issueNumber} claimed. Branch: ${branch}`;
1229
+ const baseCommit = await getCurrentCommit();
1230
+ setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
1231
+ return `Task #${issueNumber} claimed. Branch: ${branch} (base commit: ${baseCommit.slice(0, 7)})`;
1064
1232
  }
1065
1233
  return `Unknown action: ${action}`;
1066
1234
  }
@@ -1151,6 +1319,20 @@ var definition4 = {
1151
1319
  }
1152
1320
  };
1153
1321
  async function run4(input, config) {
1322
+ const authSpinner = ora4("Checking permissions\u2026").start();
1323
+ let me;
1324
+ let allowed;
1325
+ try {
1326
+ me = await getAuthenticatedUser(config);
1327
+ allowed = await isCollaborator(config, me);
1328
+ authSpinner.stop();
1329
+ } catch (err) {
1330
+ authSpinner.stop();
1331
+ return `Error checking permissions: ${err.message}`;
1332
+ }
1333
+ if (!allowed) {
1334
+ return `Permission denied: only repository collaborators can create tasks.`;
1335
+ }
1154
1336
  let title = input["title"]?.trim();
1155
1337
  if (!title) {
1156
1338
  try {
@@ -1248,6 +1430,10 @@ async function run4(input, config) {
1248
1430
  return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
1249
1431
  }
1250
1432
  async function execute4(input, config) {
1433
+ const me = await getAuthenticatedUser(config);
1434
+ if (!await isCollaborator(config, me)) {
1435
+ return `Permission denied: only repository collaborators can create tasks.`;
1436
+ }
1251
1437
  const title = input["title"].trim();
1252
1438
  const feedback = input["feedback"];
1253
1439
  let guide = await generateGuide(config, title);
@@ -1466,6 +1652,13 @@ var definition9 = {
1466
1652
  };
1467
1653
  async function run9(input, config) {
1468
1654
  const issueNumber = input["issue_number"];
1655
+ const [me, issue] = await Promise.all([
1656
+ getAuthenticatedUser(config),
1657
+ getTask(config, issueNumber)
1658
+ ]);
1659
+ if (issue.author && issue.author !== me) {
1660
+ return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
1661
+ }
1469
1662
  let feedback;
1470
1663
  try {
1471
1664
  feedback = await promptInput3({
@@ -1535,6 +1728,13 @@ async function run9(input, config) {
1535
1728
  async function execute9(input, config) {
1536
1729
  const issueNumber = input["issue_number"];
1537
1730
  const feedback = input["feedback"];
1731
+ const [me, issue] = await Promise.all([
1732
+ getAuthenticatedUser(config),
1733
+ getTask(config, issueNumber)
1734
+ ]);
1735
+ if (issue.author && issue.author !== me) {
1736
+ return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
1737
+ }
1538
1738
  let comment;
1539
1739
  try {
1540
1740
  comment = await generateRejectionComment(config, issueNumber, feedback);
@@ -1574,7 +1774,7 @@ var definition10 = {
1574
1774
  type: "function",
1575
1775
  function: {
1576
1776
  name: "accept",
1577
- description: "Accept an in-review task: merges the PR into the configured base branch and closes the issue.",
1777
+ description: "Accept an in-review task: merges the PR into your worker branch and closes the issue.",
1578
1778
  parameters: {
1579
1779
  type: "object",
1580
1780
  properties: {
@@ -1587,15 +1787,15 @@ var definition10 = {
1587
1787
  async function run10(input, config) {
1588
1788
  let issueNumber = input["issue_number"];
1589
1789
  if (!issueNumber) {
1590
- const spinner2 = ora8("Loading tasks for review\u2026").start();
1790
+ const spinner3 = ora8("Loading tasks for review\u2026").start();
1591
1791
  let tasks;
1592
1792
  let me;
1593
1793
  try {
1594
1794
  me = await getAuthenticatedUser(config);
1595
1795
  tasks = await listTasksForReview(config, me);
1596
- spinner2.stop();
1796
+ spinner3.stop();
1597
1797
  } catch (err) {
1598
- spinner2.stop();
1798
+ spinner3.stop();
1599
1799
  return `Error: ${err.message}`;
1600
1800
  }
1601
1801
  if (tasks.length === 0) return "No tasks pending review.";
@@ -1611,11 +1811,27 @@ async function run10(input, config) {
1611
1811
  return "Cancelled.";
1612
1812
  }
1613
1813
  }
1614
- const baseBranch = config.github.baseBranch ?? "main";
1814
+ const spinner2 = ora8("Verifying permissions\u2026").start();
1815
+ let me2;
1816
+ let issue;
1817
+ try {
1818
+ [me2, issue] = await Promise.all([
1819
+ getAuthenticatedUser(config),
1820
+ getTask(config, issueNumber)
1821
+ ]);
1822
+ spinner2.stop();
1823
+ } catch (err) {
1824
+ spinner2.stop();
1825
+ return `Error: ${err.message}`;
1826
+ }
1827
+ if (issue.author && issue.author !== me2) {
1828
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
1829
+ }
1830
+ const targetBranch = makeWorkerBranchName(me2);
1615
1831
  let confirmed;
1616
1832
  try {
1617
1833
  confirmed = await select6({
1618
- message: `Merge PR for #${issueNumber} into ${chalk9.cyan(baseBranch)} and close issue?`,
1834
+ message: `Merge PR for #${issueNumber} into ${chalk9.cyan(targetBranch)} and close issue?`,
1619
1835
  choices: [
1620
1836
  { name: "Yes, accept", value: true },
1621
1837
  { name: "Cancel", value: false }
@@ -1628,9 +1844,9 @@ async function run10(input, config) {
1628
1844
  const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
1629
1845
  try {
1630
1846
  const result = await acceptTask(config, issueNumber);
1631
- spinner.succeed(`PR #${result.prNumber} merged into ${baseBranch}`);
1847
+ spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
1632
1848
  return `Task #${issueNumber} accepted.
1633
- PR #${result.prNumber} merged \u2192 ${baseBranch}
1849
+ PR #${result.prNumber} merged \u2192 ${targetBranch}
1634
1850
  Issue closed.`;
1635
1851
  } catch (err) {
1636
1852
  spinner.fail("Failed");
@@ -1639,13 +1855,20 @@ Issue closed.`;
1639
1855
  }
1640
1856
  async function execute10(input, config) {
1641
1857
  const issueNumber = input["issue_number"];
1858
+ const [me, issue] = await Promise.all([
1859
+ getAuthenticatedUser(config),
1860
+ getTask(config, issueNumber)
1861
+ ]);
1862
+ if (issue.author && issue.author !== me) {
1863
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
1864
+ }
1865
+ const targetBranch = makeWorkerBranchName(me);
1642
1866
  const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
1643
1867
  try {
1644
1868
  const result = await acceptTask(config, issueNumber);
1645
1869
  spinner.stop();
1646
- const baseBranch = config.github.baseBranch ?? "main";
1647
1870
  return `Task #${issueNumber} accepted.
1648
- PR #${result.prNumber} merged \u2192 ${baseBranch}
1871
+ PR #${result.prNumber} merged \u2192 ${targetBranch}
1649
1872
  Issue closed.`;
1650
1873
  } catch (err) {
1651
1874
  spinner.stop();
@@ -1873,7 +2096,7 @@ var definition15 = {
1873
2096
  async function execute15(_input, _config) {
1874
2097
  const spinner = ora11("Reading git diff...").start();
1875
2098
  try {
1876
- const diff = await getDiff(_config.github.baseBranch);
2099
+ const diff = await getDiff();
1877
2100
  spinner.stop();
1878
2101
  return diff;
1879
2102
  } catch (err) {
@@ -2240,34 +2463,6 @@ var toolModules = [
2240
2463
  ask_user_exports
2241
2464
  ];
2242
2465
 
2243
- // src/lib/config.ts
2244
- import Conf from "conf";
2245
- import { z } from "zod";
2246
- var configSchema = z.object({
2247
- aiApiKey: z.string().min(1),
2248
- aiBaseUrl: z.string().optional(),
2249
- aiModel: z.string().optional(),
2250
- githubToken: z.string().min(1),
2251
- githubClientId: z.string().optional(),
2252
- github: z.object({
2253
- owner: z.string().min(1),
2254
- repo: z.string().min(1),
2255
- baseBranch: z.string().optional()
2256
- })
2257
- });
2258
- var store = new Conf({
2259
- projectName: "techunter",
2260
- defaults: {}
2261
- });
2262
- function getConfig() {
2263
- const raw = store.store;
2264
- const result = configSchema.safeParse(raw);
2265
- if (!result.success) {
2266
- throw new Error("Configuration is missing or invalid.");
2267
- }
2268
- return result.data;
2269
- }
2270
-
2271
2466
  // src/mcp.ts
2272
2467
  var tools = toolModules.filter((m) => m.definition.function.name !== "ask_user");
2273
2468
  var server = new Server(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "techunter",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "AI-powered task distribution CLI for development teams",
5
5
  "author": "Techunter Contributors",
6
6
  "license": "MIT",