deepdebug-local-agent 0.3.1 β†’ 0.3.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.
package/src/server.js CHANGED
@@ -734,6 +734,71 @@ app.post("/workspace/open", async (req, res) => {
734
734
  res.json({ ok: true, root: WORKSPACE_ROOT, workspaceId: wsId, meta, port });
735
735
  });
736
736
 
737
+ /**
738
+ * POST /workspace/clone
739
+ * Clones a git repository into the local filesystem, then opens it as workspace.
740
+ * Body: { gitUrl, targetPath, workspaceId? }
741
+ */
742
+ app.post("/workspace/clone", async (req, res) => {
743
+ const { gitUrl, targetPath, workspaceId } = req.body || {};
744
+
745
+ if (!gitUrl) return res.status(400).json({ ok: false, error: "gitUrl is required" });
746
+ if (!targetPath) return res.status(400).json({ ok: false, error: "targetPath is required" });
747
+
748
+ const absTarget = path.resolve(targetPath);
749
+ console.log(`πŸ“₯ Clone request: ${gitUrl} -> ${absTarget}`);
750
+
751
+ try {
752
+ // Ensure parent directory exists
753
+ const parentDir = path.dirname(absTarget);
754
+ await fsPromises.mkdir(parentDir, { recursive: true });
755
+
756
+ // If already cloned, do git pull instead
757
+ const gitDir = path.join(absTarget, '.git');
758
+ const alreadyCloned = await exists(gitDir);
759
+
760
+ if (alreadyCloned) {
761
+ console.log(`πŸ”„ Repo already exists at ${absTarget}, running git pull...`);
762
+ const { stdout } = await execAsync('git pull', { cwd: absTarget, timeout: 120000 });
763
+ console.log(`βœ… git pull: ${stdout.trim()}`);
764
+ } else {
765
+ console.log(`πŸ”½ Running: git clone "${gitUrl}" "${absTarget}"`);
766
+ await execAsync(`git clone "${gitUrl}" "${absTarget}"`, { timeout: 300000 });
767
+ console.log(`βœ… Clone complete: ${absTarget}`);
768
+ }
769
+
770
+ // Open the cloned workspace
771
+ WORKSPACE_ROOT = absTarget;
772
+ const wsId = workspaceId || "default";
773
+ try {
774
+ await wsManager.open(wsId, absTarget);
775
+ } catch (err) {
776
+ console.warn(`⚠️ WorkspaceManager.open failed (non-fatal): ${err.message}`);
777
+ }
778
+
779
+ const meta = await detectProject(absTarget);
780
+ const port = await detectPort(absTarget);
781
+
782
+ res.json({
783
+ ok: true,
784
+ path: absTarget,
785
+ workspaceId: wsId,
786
+ meta,
787
+ port,
788
+ message: alreadyCloned ? "Repository updated (git pull)" : "Repository cloned successfully"
789
+ });
790
+
791
+ } catch (err) {
792
+ console.error(`❌ Clone failed: ${err.message}`);
793
+ const hint = err.message.includes('Authentication') || err.message.includes('could not read')
794
+ ? "Authentication failed. Ensure the repo is public or GitHub integration is configured."
795
+ : err.message.includes('not found') || err.message.includes('does not exist')
796
+ ? "Repository not found. Check the URL is correct."
797
+ : "Clone failed. Ensure git is installed and the URL is accessible.";
798
+ res.status(500).json({ ok: false, error: err.message, hint });
799
+ }
800
+ });
801
+
737
802
  /** Info do workspace */
738
803
  app.get("/workspace/info", async (_req, res) => {
739
804
  if (!WORKSPACE_ROOT) return res.status(400).json({ error: "workspace not set" });
@@ -4220,7 +4285,9 @@ app.post("/workspace/git/create-fix-branch", async (req, res) => {
4220
4285
  .replace(/^https?:\/\/(.*@)?bitbucket\.org\//, '')
4221
4286
  .replace(/^git@bitbucket\.org:/, '')
4222
4287
  .replace(/\.git$/, '');
4223
- authenticatedUrl = `https://x-token-auth:${gitToken}@bitbucket.org/${repoPath}.git`;
4288
+ // βœ… FIXED: Bitbucket App Password auth β€” token jΓ‘ estΓ‘ no formato "username:appPassword"
4289
+ // x-token-auth nΓ£o funciona; Basic Auth via URL Γ© o mΓ©todo correcto
4290
+ authenticatedUrl = `https://${gitToken}@bitbucket.org/${repoPath}.git`;
4224
4291
  } else if (remoteUrlRaw) {
4225
4292
  // Unknown provider β€” try generic https with token
4226
4293
  const urlObj = new URL(remoteUrlRaw.replace(/^git@([^:]+):/, 'https://$1/'));
@@ -4348,52 +4415,140 @@ app.post("/workspace/git/create-fix-branch", async (req, res) => {
4348
4415
  } catch {}
4349
4416
 
4350
4417
  // 11. Auto-create Pull Request if pushed and token available
4418
+ // βœ… FIXED: suporta GitHub, GitLab e Bitbucket; branding Insptech AI
4351
4419
  let prUrl = '';
4352
- if (pushed && gitToken && remoteUrl && remoteUrl.includes('github.com')) {
4353
- try {
4354
- const repoPath = remoteUrl
4355
- .replace(/^https?:\/\/(.*@)?github\.com\//, '')
4356
- .replace(/^git@github\.com:/, '')
4357
- .replace(/\.git$/, '');
4358
-
4359
- const prBody = {
4360
- title: commitMessage,
4361
- head: finalBranchName,
4362
- base: targetBranch,
4363
- body: `## πŸ€– Auto-fix by DeepDebug AI\n\n` +
4364
- `**Branch:** \`${finalBranchName}\`\n` +
4365
- `**Files changed:** ${status.split('\n').length}\n` +
4366
- `**Commit:** ${commitSha.substring(0, 8)}\n\n` +
4367
- `This pull request was automatically created by DeepDebug AI after detecting and fixing a production error.\n\n` +
4368
- `### Changes\n` +
4369
- status.split('\n').map(l => `- \`${l.trim()}\``).join('\n') + '\n\n' +
4370
- `---\n*Generated by [DeepDebug AI](https://deepdebug.ai) β€” Autonomous Debugging Platform*`
4371
- };
4420
+ const prBodyText =
4421
+ `## πŸ€– Auto-fix by Insptech AI\n\n` +
4422
+ `**Branch:** \`${finalBranchName}\`\n` +
4423
+ `**Files changed:** ${status.split('\n').length}\n` +
4424
+ `**Commit:** ${commitSha.substring(0, 8)}\n\n` +
4425
+ `This pull request was automatically created by Insptech AI after detecting and fixing a production error.\n\n` +
4426
+ `### Changes\n` +
4427
+ status.split('\n').map(l => `- \`${l.trim()}\``).join('\n') + '\n\n' +
4428
+ `---\n*Generated by [Insptech AI](https://app.insptech.pt) β€” Autonomous Debugging Platform*`;
4429
+
4430
+ if (pushed && gitToken && remoteUrl) {
4431
+
4432
+ // ── GitHub ──────────────────────────────────────────────────────
4433
+ if (remoteUrl.includes('github.com')) {
4434
+ try {
4435
+ const repoPath = remoteUrl
4436
+ .replace(/^https?:\/\/(.*@)?github\.com\//, '')
4437
+ .replace(/^git@github\.com:/, '')
4438
+ .replace(/\.git$/, '');
4372
4439
 
4373
- const response = await fetch(`https://api.github.com/repos/${repoPath}/pulls`, {
4374
- method: 'POST',
4375
- headers: {
4376
- 'Authorization': `Bearer ${gitToken}`,
4377
- 'Accept': 'application/vnd.github+json',
4378
- 'Content-Type': 'application/json',
4379
- 'X-GitHub-Api-Version': '2022-11-28'
4380
- },
4381
- body: JSON.stringify(prBody)
4382
- });
4440
+ const response = await fetch(`https://api.github.com/repos/${repoPath}/pulls`, {
4441
+ method: 'POST',
4442
+ headers: {
4443
+ 'Authorization': `Bearer ${gitToken}`,
4444
+ 'Accept': 'application/vnd.github+json',
4445
+ 'Content-Type': 'application/json',
4446
+ 'X-GitHub-Api-Version': '2022-11-28'
4447
+ },
4448
+ body: JSON.stringify({
4449
+ title: commitMessage,
4450
+ head: finalBranchName,
4451
+ base: targetBranch,
4452
+ body: prBodyText
4453
+ })
4454
+ });
4383
4455
 
4384
- if (response.ok) {
4385
- const prData = await response.json();
4386
- prUrl = prData.html_url;
4387
- console.log(`βœ… Pull Request created: ${prUrl}`);
4388
- } else {
4389
- const errText = await response.text();
4390
- console.warn(`⚠️ PR creation failed (${response.status}): ${errText.substring(0, 200)}`);
4391
- // Still use the compare URL as fallback
4456
+ if (response.ok) {
4457
+ const prData = await response.json();
4458
+ prUrl = prData.html_url;
4459
+ console.log(`βœ… GitHub PR created: ${prUrl}`);
4460
+ } else {
4461
+ const errText = await response.text();
4462
+ console.warn(`⚠️ GitHub PR creation failed (${response.status}): ${errText.substring(0, 200)}`);
4463
+ prUrl = mrUrl;
4464
+ }
4465
+ } catch (prErr) {
4466
+ console.warn(`⚠️ GitHub PR error: ${prErr.message}`);
4467
+ prUrl = mrUrl;
4468
+ }
4469
+
4470
+ // ── GitLab ──────────────────────────────────────────────────────
4471
+ } else if (remoteUrl.includes('gitlab.com') || remoteUrl.includes('gitlab')) {
4472
+ try {
4473
+ const hostMatch = remoteUrl.match(/https?:\/\/([^\/]+)/);
4474
+ const host = hostMatch ? hostMatch[1] : 'gitlab.com';
4475
+ const repoPath = remoteUrl
4476
+ .replace(/^https?:\/\/(.*@)?[^\/]+\//, '')
4477
+ .replace(/^git@[^:]+:/, '')
4478
+ .replace(/\.git$/, '');
4479
+ // GitLab API requer o path encoded (ex: "mygroup/myrepo" β†’ "mygroup%2Fmyrepo")
4480
+ const encodedPath = encodeURIComponent(repoPath);
4481
+
4482
+ const response = await fetch(`https://${host}/api/v4/projects/${encodedPath}/merge_requests`, {
4483
+ method: 'POST',
4484
+ headers: {
4485
+ 'PRIVATE-TOKEN': gitToken,
4486
+ 'Content-Type': 'application/json'
4487
+ },
4488
+ body: JSON.stringify({
4489
+ title: commitMessage,
4490
+ description: prBodyText,
4491
+ source_branch: finalBranchName,
4492
+ target_branch: targetBranch,
4493
+ remove_source_branch: false
4494
+ })
4495
+ });
4496
+
4497
+ if (response.ok) {
4498
+ const mrData = await response.json();
4499
+ prUrl = mrData.web_url;
4500
+ console.log(`βœ… GitLab MR created: ${prUrl}`);
4501
+ } else {
4502
+ const errText = await response.text();
4503
+ console.warn(`⚠️ GitLab MR creation failed (${response.status}): ${errText.substring(0, 200)}`);
4504
+ prUrl = mrUrl;
4505
+ }
4506
+ } catch (glErr) {
4507
+ console.warn(`⚠️ GitLab MR error: ${glErr.message}`);
4508
+ prUrl = mrUrl;
4509
+ }
4510
+
4511
+ // ── Bitbucket ────────────────────────────────────────────────────
4512
+ } else if (remoteUrl.includes('bitbucket.org') || remoteUrl.includes('bitbucket')) {
4513
+ try {
4514
+ const repoPath = remoteUrl
4515
+ .replace(/^https?:\/\/(.*@)?bitbucket\.org\//, '')
4516
+ .replace(/^git@bitbucket\.org:/, '')
4517
+ .replace(/\.git$/, '');
4518
+ const [workspace, repoSlug] = repoPath.split('/');
4519
+
4520
+ // gitToken Γ© "username:appPassword" β€” codificar para Basic Auth
4521
+ const credentials = Buffer.from(gitToken).toString('base64');
4522
+
4523
+ const response = await fetch(
4524
+ `https://api.bitbucket.org/2.0/repositories/${workspace}/${repoSlug}/pullrequests`, {
4525
+ method: 'POST',
4526
+ headers: {
4527
+ 'Authorization': `Basic ${credentials}`,
4528
+ 'Content-Type': 'application/json'
4529
+ },
4530
+ body: JSON.stringify({
4531
+ title: commitMessage,
4532
+ description: prBodyText,
4533
+ source: { branch: { name: finalBranchName } },
4534
+ destination: { branch: { name: targetBranch } },
4535
+ close_source_branch: false
4536
+ })
4537
+ });
4538
+
4539
+ if (response.ok) {
4540
+ const prData = await response.json();
4541
+ prUrl = prData.links && prData.links.html ? prData.links.html.href : mrUrl;
4542
+ console.log(`βœ… Bitbucket PR created: ${prUrl}`);
4543
+ } else {
4544
+ const errText = await response.text();
4545
+ console.warn(`⚠️ Bitbucket PR creation failed (${response.status}): ${errText.substring(0, 200)}`);
4546
+ prUrl = mrUrl;
4547
+ }
4548
+ } catch (bbErr) {
4549
+ console.warn(`⚠️ Bitbucket PR error: ${bbErr.message}`);
4392
4550
  prUrl = mrUrl;
4393
4551
  }
4394
- } catch (prErr) {
4395
- console.warn(`⚠️ PR creation error: ${prErr.message}`);
4396
- prUrl = mrUrl;
4397
4552
  }
4398
4553
  }
4399
4554
 
@@ -4456,6 +4611,396 @@ app.get("/workspace/git/status", async (req, res) => {
4456
4611
  }
4457
4612
  });
4458
4613
 
4614
+ // ============================================
4615
+ // πŸ’¬ PULL REQUEST COMMENTS ENDPOINTS
4616
+ // Read, reply, and resolve PR comments via git providers
4617
+ // Used by the frontend "Code Review" tab in incident detail
4618
+ // ============================================
4619
+
4620
+ /**
4621
+ * GET /workspace/git/pr/comments
4622
+ *
4623
+ * List all comments for a pull request.
4624
+ * Detects provider (GitHub/Bitbucket) from remote URL.
4625
+ *
4626
+ * Query params:
4627
+ * - prNumber (required): PR/MR number
4628
+ * - filter: 'all' | 'unresolved' | 'review' (default: 'all')
4629
+ */
4630
+ app.get("/workspace/git/pr/comments", async (req, res) => {
4631
+ if (!WORKSPACE_ROOT) {
4632
+ return res.status(400).json({ error: "workspace not set" });
4633
+ }
4634
+
4635
+ const { prNumber, filter = 'all' } = req.query;
4636
+
4637
+ if (!prNumber) {
4638
+ return res.status(400).json({ error: "prNumber query param is required" });
4639
+ }
4640
+
4641
+ try {
4642
+ const provider = await _getGitProvider();
4643
+ if (!provider) {
4644
+ return res.status(400).json({ error: "No git provider configured. Check remote URL and token." });
4645
+ }
4646
+
4647
+ const { owner, repo } = provider.info;
4648
+ let comments;
4649
+
4650
+ if (filter === 'unresolved') {
4651
+ comments = await provider.instance.listUnresolvedPRComments(owner, repo, Number(prNumber));
4652
+ } else if (filter === 'review') {
4653
+ comments = await provider.instance.listPRReviewComments(owner, repo, Number(prNumber));
4654
+ } else {
4655
+ // Get both general and review comments, merge and sort by date
4656
+ const [general, review] = await Promise.all([
4657
+ provider.instance.listPRComments(owner, repo, Number(prNumber)).catch(() => []),
4658
+ provider.instance.listPRReviewComments(owner, repo, Number(prNumber)).catch(() => [])
4659
+ ]);
4660
+
4661
+ // Deduplicate by id
4662
+ const seen = new Set();
4663
+ comments = [...general, ...review].filter(c => {
4664
+ if (seen.has(c.id)) return false;
4665
+ seen.add(c.id);
4666
+ return true;
4667
+ });
4668
+
4669
+ // Sort by creation date
4670
+ comments.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
4671
+ }
4672
+
4673
+ res.json({
4674
+ ok: true,
4675
+ prNumber: Number(prNumber),
4676
+ filter,
4677
+ comments,
4678
+ total: comments.length,
4679
+ unresolvedCount: comments.filter(c => c.resolved === false || c.resolved === undefined).length
4680
+ });
4681
+ } catch (err) {
4682
+ console.error('❌ Failed to fetch PR comments:', err.message);
4683
+ res.status(500).json({ ok: false, error: err.message });
4684
+ }
4685
+ });
4686
+
4687
+ /**
4688
+ * GET /workspace/git/pr/comments/:commentId
4689
+ *
4690
+ * Get a specific comment by ID.
4691
+ *
4692
+ * Query params:
4693
+ * - prNumber (required): PR/MR number
4694
+ */
4695
+ app.get("/workspace/git/pr/comments/:commentId", async (req, res) => {
4696
+ if (!WORKSPACE_ROOT) {
4697
+ return res.status(400).json({ error: "workspace not set" });
4698
+ }
4699
+
4700
+ const { commentId } = req.params;
4701
+ const { prNumber } = req.query;
4702
+
4703
+ if (!prNumber) {
4704
+ return res.status(400).json({ error: "prNumber query param is required" });
4705
+ }
4706
+
4707
+ try {
4708
+ const provider = await _getGitProvider();
4709
+ if (!provider) {
4710
+ return res.status(400).json({ error: "No git provider configured" });
4711
+ }
4712
+
4713
+ const { owner, repo } = provider.info;
4714
+ const comment = await provider.instance.getPRComment(owner, repo, Number(prNumber), commentId);
4715
+
4716
+ res.json({ ok: true, comment });
4717
+ } catch (err) {
4718
+ console.error('❌ Failed to fetch PR comment:', err.message);
4719
+ res.status(500).json({ ok: false, error: err.message });
4720
+ }
4721
+ });
4722
+
4723
+ /**
4724
+ * POST /workspace/git/pr/comments
4725
+ *
4726
+ * Add a comment to a pull request.
4727
+ *
4728
+ * Body:
4729
+ * - prNumber (required): PR/MR number
4730
+ * - body (required): Comment text (markdown)
4731
+ * - type: 'general' | 'review' (default: 'general')
4732
+ * - path: File path (required for type='review')
4733
+ * - line: Line number (required for type='review')
4734
+ * - side: 'LEFT' | 'RIGHT' (default: 'RIGHT')
4735
+ * - commitId: Commit SHA (for review comments)
4736
+ * - inReplyToId: Parent comment ID (for thread replies)
4737
+ */
4738
+ app.post("/workspace/git/pr/comments", async (req, res) => {
4739
+ if (!WORKSPACE_ROOT) {
4740
+ return res.status(400).json({ error: "workspace not set" });
4741
+ }
4742
+
4743
+ const { prNumber, body, type = 'general', path: filePath, line, side, commitId, inReplyToId } = req.body;
4744
+
4745
+ if (!prNumber) {
4746
+ return res.status(400).json({ error: "prNumber is required" });
4747
+ }
4748
+ if (!body || !body.trim()) {
4749
+ return res.status(400).json({ error: "body is required" });
4750
+ }
4751
+
4752
+ try {
4753
+ const provider = await _getGitProvider();
4754
+ if (!provider) {
4755
+ return res.status(400).json({ error: "No git provider configured" });
4756
+ }
4757
+
4758
+ const { owner, repo } = provider.info;
4759
+ let comment;
4760
+
4761
+ if (inReplyToId) {
4762
+ // Reply to existing comment
4763
+ comment = await provider.instance.replyToPRComment(owner, repo, Number(prNumber), inReplyToId, body);
4764
+ } else if (type === 'review' && filePath) {
4765
+ // Inline code comment
4766
+ comment = await provider.instance.addPRReviewComment(owner, repo, Number(prNumber), {
4767
+ body,
4768
+ path: filePath,
4769
+ line: line ? Number(line) : null,
4770
+ side: side || 'RIGHT',
4771
+ commitId,
4772
+ inReplyToId
4773
+ });
4774
+ } else {
4775
+ // General PR comment
4776
+ comment = await provider.instance.addPRComment(owner, repo, Number(prNumber), body);
4777
+ }
4778
+
4779
+ console.log(`πŸ’¬ PR comment added to PR #${prNumber}: ${body.substring(0, 50)}...`);
4780
+ res.json({ ok: true, comment });
4781
+ } catch (err) {
4782
+ console.error('❌ Failed to add PR comment:', err.message);
4783
+ res.status(500).json({ ok: false, error: err.message });
4784
+ }
4785
+ });
4786
+
4787
+ /**
4788
+ * POST /workspace/git/pr/comments/:commentId/resolve
4789
+ *
4790
+ * Mark a comment/thread as resolved.
4791
+ *
4792
+ * Body:
4793
+ * - prNumber (required): PR/MR number
4794
+ */
4795
+ app.post("/workspace/git/pr/comments/:commentId/resolve", async (req, res) => {
4796
+ if (!WORKSPACE_ROOT) {
4797
+ return res.status(400).json({ error: "workspace not set" });
4798
+ }
4799
+
4800
+ const { commentId } = req.params;
4801
+ const { prNumber } = req.body;
4802
+
4803
+ if (!prNumber) {
4804
+ return res.status(400).json({ error: "prNumber is required" });
4805
+ }
4806
+
4807
+ try {
4808
+ const provider = await _getGitProvider();
4809
+ if (!provider) {
4810
+ return res.status(400).json({ error: "No git provider configured" });
4811
+ }
4812
+
4813
+ const { owner, repo } = provider.info;
4814
+ const result = await provider.instance.resolvePRComment(owner, repo, Number(prNumber), commentId);
4815
+
4816
+ console.log(`βœ… PR comment ${commentId} resolved on PR #${prNumber}`);
4817
+ res.json({ ok: true, commentId, ...result });
4818
+ } catch (err) {
4819
+ console.error('❌ Failed to resolve PR comment:', err.message);
4820
+ res.status(500).json({ ok: false, error: err.message });
4821
+ }
4822
+ });
4823
+
4824
+ /**
4825
+ * POST /workspace/git/pr/comments/:commentId/unresolve
4826
+ *
4827
+ * Mark a comment/thread as unresolved.
4828
+ *
4829
+ * Body:
4830
+ * - prNumber (required): PR/MR number
4831
+ */
4832
+ app.post("/workspace/git/pr/comments/:commentId/unresolve", async (req, res) => {
4833
+ if (!WORKSPACE_ROOT) {
4834
+ return res.status(400).json({ error: "workspace not set" });
4835
+ }
4836
+
4837
+ const { commentId } = req.params;
4838
+ const { prNumber } = req.body;
4839
+
4840
+ if (!prNumber) {
4841
+ return res.status(400).json({ error: "prNumber is required" });
4842
+ }
4843
+
4844
+ try {
4845
+ const provider = await _getGitProvider();
4846
+ if (!provider) {
4847
+ return res.status(400).json({ error: "No git provider configured" });
4848
+ }
4849
+
4850
+ const { owner, repo } = provider.info;
4851
+ const result = await provider.instance.unresolvePRComment(owner, repo, Number(prNumber), commentId);
4852
+
4853
+ console.log(`πŸ”„ PR comment ${commentId} unresolve on PR #${prNumber}`);
4854
+ res.json({ ok: true, commentId, ...result });
4855
+ } catch (err) {
4856
+ console.error('❌ Failed to unresolve PR comment:', err.message);
4857
+ res.status(500).json({ ok: false, error: err.message });
4858
+ }
4859
+ });
4860
+
4861
+ /**
4862
+ * POST /workspace/git/pr/comments/:commentId/fix-and-resolve
4863
+ *
4864
+ * AI-powered: Read the comment, understand the request, apply the code fix,
4865
+ * commit, reply with explanation, and resolve the thread.
4866
+ *
4867
+ * This is the "magic" endpoint that makes DeepDebug unique.
4868
+ *
4869
+ * Body:
4870
+ * - prNumber (required): PR/MR number
4871
+ * - branch: Branch to apply fix on (defaults to current branch)
4872
+ */
4873
+ app.post("/workspace/git/pr/comments/:commentId/fix-and-resolve", async (req, res) => {
4874
+ if (!WORKSPACE_ROOT) {
4875
+ return res.status(400).json({ error: "workspace not set" });
4876
+ }
4877
+
4878
+ const { commentId } = req.params;
4879
+ const { prNumber, branch } = req.body;
4880
+
4881
+ if (!prNumber) {
4882
+ return res.status(400).json({ error: "prNumber is required" });
4883
+ }
4884
+
4885
+ try {
4886
+ const provider = await _getGitProvider();
4887
+ if (!provider) {
4888
+ return res.status(400).json({ error: "No git provider configured" });
4889
+ }
4890
+
4891
+ const { owner, repo } = provider.info;
4892
+
4893
+ // 1. Fetch the comment
4894
+ const comment = await provider.instance.getPRComment(owner, repo, Number(prNumber), commentId);
4895
+
4896
+ if (!comment) {
4897
+ return res.status(404).json({ error: "Comment not found" });
4898
+ }
4899
+
4900
+ console.log(`πŸ€– AI Fix & Resolve: PR #${prNumber}, comment ${commentId}`);
4901
+ console.log(` File: ${comment.path || '(general)'}, Line: ${comment.line || 'N/A'}`);
4902
+ console.log(` Request: ${comment.body.substring(0, 100)}...`);
4903
+
4904
+ // 2. Return the comment info for the Gateway to orchestrate the AI fix
4905
+ // The actual AI analysis + patch is handled by the Gateway's AI service,
4906
+ // not the Local Agent. We provide the context here.
4907
+ res.json({
4908
+ ok: true,
4909
+ commentId,
4910
+ prNumber: Number(prNumber),
4911
+ comment: {
4912
+ id: comment.id,
4913
+ body: comment.body,
4914
+ path: comment.path,
4915
+ line: comment.line,
4916
+ side: comment.side,
4917
+ author: comment.authorUsername,
4918
+ type: comment.type,
4919
+ resolved: comment.resolved
4920
+ },
4921
+ context: {
4922
+ owner,
4923
+ repo,
4924
+ branch: branch || null,
4925
+ provider: provider.instance.getId()
4926
+ },
4927
+ // Action hint for the Gateway
4928
+ action: 'fix_and_resolve',
4929
+ message: 'Comment fetched. Gateway should now: 1) Read file, 2) AI analyze, 3) Apply patch, 4) Commit, 5) Reply, 6) Resolve'
4930
+ });
4931
+ } catch (err) {
4932
+ console.error('❌ Failed to prepare fix-and-resolve:', err.message);
4933
+ res.status(500).json({ ok: false, error: err.message });
4934
+ }
4935
+ });
4936
+
4937
+
4938
+ // ============================================
4939
+ // πŸ”§ GIT PROVIDER HELPER (internal)
4940
+ // Detects provider from remote URL and configures with token
4941
+ // ============================================
4942
+
4943
+ async function _getGitProvider() {
4944
+ if (!WORKSPACE_ROOT) return null;
4945
+
4946
+ try {
4947
+ const { execSync } = await import('child_process');
4948
+ const opts = { cwd: WORKSPACE_ROOT, encoding: 'utf8', timeout: 5000 };
4949
+
4950
+ // Get remote URL
4951
+ let remoteUrl;
4952
+ try {
4953
+ remoteUrl = execSync('git remote get-url origin', opts).trim();
4954
+ } catch {
4955
+ console.warn('⚠️ No git remote found');
4956
+ return null;
4957
+ }
4958
+
4959
+ // Detect provider from URL
4960
+ const { getGitProviderRegistry } = await import('./git/git-provider-registry.js');
4961
+ const registry = getGitProviderRegistry();
4962
+ const providerId = registry.detectProviderFromUrl(remoteUrl);
4963
+
4964
+ if (!providerId) {
4965
+ console.warn(`⚠️ Unknown git provider for URL: ${remoteUrl}`);
4966
+ return null;
4967
+ }
4968
+
4969
+ // Get token from environment
4970
+ let token = null;
4971
+ if (providerId === 'github') {
4972
+ token = process.env.GITHUB_TOKEN || process.env.GIT_TOKEN;
4973
+ } else if (providerId === 'bitbucket') {
4974
+ token = process.env.BITBUCKET_TOKEN || process.env.BITBUCKET_APP_PASSWORD || process.env.GIT_TOKEN;
4975
+ } else if (providerId === 'gitlab') {
4976
+ token = process.env.GITLAB_TOKEN || process.env.GIT_TOKEN;
4977
+ }
4978
+
4979
+ if (!token) {
4980
+ console.warn(`⚠️ No token found for ${providerId}. Set ${providerId.toUpperCase()}_TOKEN or GIT_TOKEN env var.`);
4981
+ return null;
4982
+ }
4983
+
4984
+ // Create provider instance
4985
+ const config = { accessToken: token };
4986
+
4987
+ // For Bitbucket App Password auth, add username
4988
+ if (providerId === 'bitbucket' && process.env.BITBUCKET_USERNAME) {
4989
+ config.username = process.env.BITBUCKET_USERNAME;
4990
+ config.authMode = 'app_password';
4991
+ }
4992
+
4993
+ const instance = registry.create(providerId, config);
4994
+
4995
+ // Parse owner/repo from URL
4996
+ const info = instance.parseRepositoryUrl(remoteUrl);
4997
+
4998
+ return { instance, info, providerId, remoteUrl };
4999
+ } catch (err) {
5000
+ console.error('❌ Failed to initialize git provider:', err.message);
5001
+ return null;
5002
+ }
5003
+ }
4459
5004
  // ============================================
4460
5005
  // πŸ“‚ MULTI-WORKSPACE ENDPOINTS
4461
5006
  // ============================================