@iloom/cli 0.9.2 → 0.10.1

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 (231) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +160 -41
  3. package/dist/{BranchNamingService-K6XNWQ6C.js → BranchNamingService-25KSZAEM.js} +2 -2
  4. package/dist/ClaudeContextManager-66GR4BGM.js +14 -0
  5. package/dist/ClaudeService-7KM5NA5Z.js +13 -0
  6. package/dist/{GitHubService-TGWJN4V4.js → GitHubService-MEHKHUQP.js} +4 -4
  7. package/dist/IssueTrackerFactory-NG53YX5S.js +14 -0
  8. package/dist/{LoomLauncher-73NXL2CL.js → LoomLauncher-TDLZSYG2.js} +9 -9
  9. package/dist/{MetadataManager-W3C54UYT.js → MetadataManager-5QZSTKNN.js} +2 -2
  10. package/dist/{ProjectCapabilityDetector-N5L7T4IY.js → ProjectCapabilityDetector-5KSYUTBJ.js} +3 -3
  11. package/dist/{PromptTemplateManager-36YLQRHP.js → PromptTemplateManager-YOE2SIPG.js} +2 -2
  12. package/dist/README.md +160 -41
  13. package/dist/{SettingsManager-AW3JTJHD.js → SettingsManager-FNKCOZMQ.js} +4 -2
  14. package/dist/agents/iloom-artifact-reviewer.md +11 -0
  15. package/dist/agents/iloom-code-reviewer.md +14 -0
  16. package/dist/agents/iloom-issue-analyze-and-plan.md +55 -12
  17. package/dist/agents/iloom-issue-analyzer.md +49 -6
  18. package/dist/agents/iloom-issue-complexity-evaluator.md +47 -6
  19. package/dist/agents/iloom-issue-enhancer.md +86 -7
  20. package/dist/agents/iloom-issue-implementer.md +48 -7
  21. package/dist/agents/iloom-issue-planner.md +115 -62
  22. package/dist/{build-THZI572G.js → build-VHGEMXBA.js} +9 -9
  23. package/dist/chunk-4232AHNQ.js +35 -0
  24. package/dist/chunk-4232AHNQ.js.map +1 -0
  25. package/dist/chunk-4E7LCFUG.js +24 -0
  26. package/dist/chunk-4E7LCFUG.js.map +1 -0
  27. package/dist/{chunk-AR5QKYNE.js → chunk-4FGEGQW4.js} +4 -4
  28. package/dist/{chunk-R4YWBGY6.js → chunk-5FJWO4IT.js} +67 -22
  29. package/dist/chunk-5FJWO4IT.js.map +1 -0
  30. package/dist/{chunk-VPTAX5TR.js → chunk-5RPBYK5Q.js} +35 -30
  31. package/dist/chunk-5RPBYK5Q.js.map +1 -0
  32. package/dist/{chunk-YKFCCV6S.js → chunk-63QWFWH3.js} +7 -7
  33. package/dist/chunk-63QWFWH3.js.map +1 -0
  34. package/dist/{chunk-RI2YL6TK.js → chunk-7VHJNVLF.js} +80 -23
  35. package/dist/chunk-7VHJNVLF.js.map +1 -0
  36. package/dist/{chunk-B7U6OKUR.js → chunk-C6HNNJIV.js} +11 -3
  37. package/dist/chunk-C6HNNJIV.js.map +1 -0
  38. package/dist/{chunk-A7NJF73J.js → chunk-CVCTIDDK.js} +4 -4
  39. package/dist/{chunk-Z2TWEXR7.js → chunk-E6KOWMKA.js} +6 -6
  40. package/dist/chunk-E6KOWMKA.js.map +1 -0
  41. package/dist/{chunk-3I4ONZRT.js → chunk-EVPZFV3K.js} +10 -10
  42. package/dist/chunk-EVPZFV3K.js.map +1 -0
  43. package/dist/{chunk-IZIYLYPK.js → chunk-G5V75JD5.js} +2 -2
  44. package/dist/chunk-GRISNU6G.js +651 -0
  45. package/dist/chunk-GRISNU6G.js.map +1 -0
  46. package/dist/chunk-HEXKPKCK.js +1396 -0
  47. package/dist/chunk-HEXKPKCK.js.map +1 -0
  48. package/dist/{chunk-TC7APDKU.js → chunk-I5T677EA.js} +2 -2
  49. package/dist/{chunk-KBEIQP4G.js → chunk-KB64WNBZ.js} +43 -3
  50. package/dist/chunk-KB64WNBZ.js.map +1 -0
  51. package/dist/{chunk-NWMORW3U.js → chunk-KIK2ZFAL.js} +2 -2
  52. package/dist/{chunk-CWRI4JC3.js → chunk-KKV5WH5M.js} +30 -31
  53. package/dist/chunk-KKV5WH5M.js.map +1 -0
  54. package/dist/{chunk-DGG2VY7B.js → chunk-KVHIAWVT.js} +9 -9
  55. package/dist/chunk-KVHIAWVT.js.map +1 -0
  56. package/dist/{chunk-OFDN5NKS.js → chunk-KXDRI47U.js} +69 -12
  57. package/dist/chunk-KXDRI47U.js.map +1 -0
  58. package/dist/{chunk-NUACL52E.js → chunk-LLHXQS3C.js} +2 -2
  59. package/dist/chunk-LUKXJSRI.js +73 -0
  60. package/dist/chunk-LUKXJSRI.js.map +1 -0
  61. package/dist/{chunk-TL72BGP6.js → chunk-MORRVYPT.js} +2 -2
  62. package/dist/chunk-OTGH2HRS.js +1427 -0
  63. package/dist/chunk-OTGH2HRS.js.map +1 -0
  64. package/dist/{chunk-7ZEHSSUP.js → chunk-P4O6EH46.js} +4 -4
  65. package/dist/{chunk-KAYXR544.js → chunk-QVLPWNE3.js} +2 -2
  66. package/dist/chunk-QZWEJVWV.js +207 -0
  67. package/dist/chunk-QZWEJVWV.js.map +1 -0
  68. package/dist/chunk-RJ3VBUFK.js +781 -0
  69. package/dist/chunk-RJ3VBUFK.js.map +1 -0
  70. package/dist/chunk-RSYT7MVI.js +202 -0
  71. package/dist/chunk-RSYT7MVI.js.map +1 -0
  72. package/dist/{chunk-6IIL5M2L.js → chunk-S7PZA6IV.js} +10 -8
  73. package/dist/{chunk-6IIL5M2L.js.map → chunk-S7PZA6IV.js.map} +1 -1
  74. package/dist/chunk-SKSYYBCU.js +229 -0
  75. package/dist/chunk-SKSYYBCU.js.map +1 -0
  76. package/dist/{chunk-ULSWCPQG.js → chunk-SWSJWA2S.js} +476 -5
  77. package/dist/chunk-SWSJWA2S.js.map +1 -0
  78. package/dist/{chunk-KXGQYLFZ.js → chunk-UKBAJ2QQ.js} +61 -7
  79. package/dist/chunk-UKBAJ2QQ.js.map +1 -0
  80. package/dist/{chunk-FO5GGFOV.js → chunk-UR5DGNUO.js} +71 -9
  81. package/dist/chunk-UR5DGNUO.js.map +1 -0
  82. package/dist/{chunk-QN47QVBX.js → chunk-UUEW5KWB.js} +1 -1
  83. package/dist/chunk-UUEW5KWB.js.map +1 -0
  84. package/dist/{chunk-4CO6KG5S.js → chunk-VG45TUYK.js} +53 -7
  85. package/dist/{chunk-4CO6KG5S.js.map → chunk-VG45TUYK.js.map} +1 -1
  86. package/dist/{chunk-4LKGCFGG.js → chunk-WWKOVDWC.js} +2 -2
  87. package/dist/{chunk-KJTVU3HZ.js → chunk-WXIM2WS7.js} +8 -8
  88. package/dist/chunk-WXIM2WS7.js.map +1 -0
  89. package/dist/{chunk-VOGGLPG5.js → chunk-YQ57ORTV.js} +14 -1
  90. package/dist/chunk-YQ57ORTV.js.map +1 -0
  91. package/dist/{chunk-SOSQILHO.js → chunk-ZNMPGMHY.js} +44 -797
  92. package/dist/chunk-ZNMPGMHY.js.map +1 -0
  93. package/dist/{claude-TP2QO3BU.js → claude-7GGEWVEM.js} +2 -2
  94. package/dist/{cleanup-PJRIFFU4.js → cleanup-6PVAC4NI.js} +85 -34
  95. package/dist/cleanup-6PVAC4NI.js.map +1 -0
  96. package/dist/cli.js +630 -801
  97. package/dist/cli.js.map +1 -1
  98. package/dist/{commit-IVP3M4HG.js → commit-FZR5XDQG.js} +26 -23
  99. package/dist/commit-FZR5XDQG.js.map +1 -0
  100. package/dist/{compile-R2J65HBQ.js → compile-7ALJHZ4N.js} +9 -9
  101. package/dist/{contribute-VDZXHK5Y.js → contribute-5GKLK3BQ.js} +14 -6
  102. package/dist/contribute-5GKLK3BQ.js.map +1 -0
  103. package/dist/{dev-server-7F622OEO.js → dev-server-7SMIB7OF.js} +29 -15
  104. package/dist/dev-server-7SMIB7OF.js.map +1 -0
  105. package/dist/{feedback-E7VET7CL.js → feedback-G2GJFN2F.js} +18 -16
  106. package/dist/{feedback-E7VET7CL.js.map → feedback-G2GJFN2F.js.map} +1 -1
  107. package/dist/{git-2QDQ2X2S.js → git-GTLKAZRJ.js} +4 -4
  108. package/dist/hooks/iloom-hook.js +15 -0
  109. package/dist/ignite-H2O5Y5A2.js +34 -0
  110. package/dist/ignite-H2O5Y5A2.js.map +1 -0
  111. package/dist/index.d.ts +482 -58
  112. package/dist/index.js +1340 -44
  113. package/dist/index.js.map +1 -1
  114. package/dist/{init-676DHF6R.js → init-32YOKXRL.js} +57 -21
  115. package/dist/init-32YOKXRL.js.map +1 -0
  116. package/dist/{issues-PJSOLOBJ.js → issues-4UUAQ5K6.js} +61 -20
  117. package/dist/issues-4UUAQ5K6.js.map +1 -0
  118. package/dist/{lint-CJM7BAIM.js → lint-AAN2NZWG.js} +9 -9
  119. package/dist/mcp/harness-server.js +140 -0
  120. package/dist/mcp/harness-server.js.map +1 -0
  121. package/dist/mcp/issue-management-server.js +2599 -262
  122. package/dist/mcp/issue-management-server.js.map +1 -1
  123. package/dist/mcp/recap-server.js +144 -21
  124. package/dist/mcp/recap-server.js.map +1 -1
  125. package/dist/{neon-helpers-VVFFTLXE.js → neon-helpers-CQN2PB4S.js} +3 -3
  126. package/dist/neon-helpers-CQN2PB4S.js.map +1 -0
  127. package/dist/{open-544H7JF5.js → open-FXWW3VI4.js} +15 -15
  128. package/dist/open-FXWW3VI4.js.map +1 -0
  129. package/dist/{plan-Q7ELXDLC.js → plan-RQ5FPIGF.js} +358 -40
  130. package/dist/plan-RQ5FPIGF.js.map +1 -0
  131. package/dist/{projects-LH362JZQ.js → projects-2UOXFLNZ.js} +4 -4
  132. package/dist/prompts/CLAUDE.md +62 -0
  133. package/dist/prompts/init-prompt.txt +430 -34
  134. package/dist/prompts/issue-prompt.txt +473 -54
  135. package/dist/prompts/plan-prompt.txt +140 -19
  136. package/dist/prompts/pr-prompt.txt +44 -1
  137. package/dist/prompts/regular-prompt.txt +42 -1
  138. package/dist/prompts/session-summary-prompt.txt +14 -0
  139. package/dist/prompts/swarm-orchestrator-prompt.txt +464 -0
  140. package/dist/{rebase-YND35CIE.js → rebase-6NVLX5V7.js} +21 -12
  141. package/dist/rebase-6NVLX5V7.js.map +1 -0
  142. package/dist/{recap-3W7COH7D.js → recap-OMBOKJST.js} +47 -19
  143. package/dist/recap-OMBOKJST.js.map +1 -0
  144. package/dist/{run-QUXJKDQQ.js → run-BBXLRIZB.js} +15 -15
  145. package/dist/run-BBXLRIZB.js.map +1 -0
  146. package/dist/schema/package-iloom.schema.json +58 -0
  147. package/dist/schema/settings.schema.json +149 -15
  148. package/dist/{shell-QGECBLST.js → shell-RF7LTND5.js} +14 -7
  149. package/dist/shell-RF7LTND5.js.map +1 -0
  150. package/dist/{summary-G2T4452H.js → summary-WTQZ7XG2.js} +27 -25
  151. package/dist/summary-WTQZ7XG2.js.map +1 -0
  152. package/dist/{test-EA5NQFDC.js → test-SGO6I5Z7.js} +9 -9
  153. package/dist/{test-git-M7LSLEFL.js → test-git-XM4TM65W.js} +4 -4
  154. package/dist/test-jira-LDTOYFSD.js +96 -0
  155. package/dist/test-jira-LDTOYFSD.js.map +1 -0
  156. package/dist/{test-prefix-64NAAUON.js → test-prefix-GBO37XCN.js} +4 -4
  157. package/dist/{test-webserver-OK6Z5FJM.js → test-webserver-NZ3JTVLL.js} +6 -6
  158. package/dist/{vscode-AR5NNXXI.js → vscode-6XUGHJKL.js} +7 -7
  159. package/package.json +5 -1
  160. package/dist/ClaudeContextManager-HR5JQKAI.js +0 -14
  161. package/dist/ClaudeService-TK7FMC2X.js +0 -13
  162. package/dist/chunk-3I4ONZRT.js.map +0 -1
  163. package/dist/chunk-B7U6OKUR.js.map +0 -1
  164. package/dist/chunk-CWRI4JC3.js.map +0 -1
  165. package/dist/chunk-DGG2VY7B.js.map +0 -1
  166. package/dist/chunk-FJDRTVJX.js +0 -520
  167. package/dist/chunk-FJDRTVJX.js.map +0 -1
  168. package/dist/chunk-FO5GGFOV.js.map +0 -1
  169. package/dist/chunk-KBEIQP4G.js.map +0 -1
  170. package/dist/chunk-KJTVU3HZ.js.map +0 -1
  171. package/dist/chunk-KXGQYLFZ.js.map +0 -1
  172. package/dist/chunk-OFDN5NKS.js.map +0 -1
  173. package/dist/chunk-QN47QVBX.js.map +0 -1
  174. package/dist/chunk-R4YWBGY6.js.map +0 -1
  175. package/dist/chunk-RI2YL6TK.js.map +0 -1
  176. package/dist/chunk-SOSQILHO.js.map +0 -1
  177. package/dist/chunk-ULSWCPQG.js.map +0 -1
  178. package/dist/chunk-VOGGLPG5.js.map +0 -1
  179. package/dist/chunk-VPTAX5TR.js.map +0 -1
  180. package/dist/chunk-W6DP5RVR.js +0 -101
  181. package/dist/chunk-W6DP5RVR.js.map +0 -1
  182. package/dist/chunk-WHI5KEOX.js +0 -121
  183. package/dist/chunk-WHI5KEOX.js.map +0 -1
  184. package/dist/chunk-YKFCCV6S.js.map +0 -1
  185. package/dist/chunk-Z2TWEXR7.js.map +0 -1
  186. package/dist/cleanup-PJRIFFU4.js.map +0 -1
  187. package/dist/commit-IVP3M4HG.js.map +0 -1
  188. package/dist/contribute-VDZXHK5Y.js.map +0 -1
  189. package/dist/dev-server-7F622OEO.js.map +0 -1
  190. package/dist/ignite-IW35CDBD.js +0 -784
  191. package/dist/ignite-IW35CDBD.js.map +0 -1
  192. package/dist/init-676DHF6R.js.map +0 -1
  193. package/dist/issues-PJSOLOBJ.js.map +0 -1
  194. package/dist/open-544H7JF5.js.map +0 -1
  195. package/dist/plan-Q7ELXDLC.js.map +0 -1
  196. package/dist/rebase-YND35CIE.js.map +0 -1
  197. package/dist/recap-3W7COH7D.js.map +0 -1
  198. package/dist/run-QUXJKDQQ.js.map +0 -1
  199. package/dist/shell-QGECBLST.js.map +0 -1
  200. package/dist/summary-G2T4452H.js.map +0 -1
  201. /package/dist/{BranchNamingService-K6XNWQ6C.js.map → BranchNamingService-25KSZAEM.js.map} +0 -0
  202. /package/dist/{ClaudeContextManager-HR5JQKAI.js.map → ClaudeContextManager-66GR4BGM.js.map} +0 -0
  203. /package/dist/{ClaudeService-TK7FMC2X.js.map → ClaudeService-7KM5NA5Z.js.map} +0 -0
  204. /package/dist/{GitHubService-TGWJN4V4.js.map → GitHubService-MEHKHUQP.js.map} +0 -0
  205. /package/dist/{MetadataManager-W3C54UYT.js.map → IssueTrackerFactory-NG53YX5S.js.map} +0 -0
  206. /package/dist/{LoomLauncher-73NXL2CL.js.map → LoomLauncher-TDLZSYG2.js.map} +0 -0
  207. /package/dist/{ProjectCapabilityDetector-N5L7T4IY.js.map → MetadataManager-5QZSTKNN.js.map} +0 -0
  208. /package/dist/{PromptTemplateManager-36YLQRHP.js.map → ProjectCapabilityDetector-5KSYUTBJ.js.map} +0 -0
  209. /package/dist/{SettingsManager-AW3JTJHD.js.map → PromptTemplateManager-YOE2SIPG.js.map} +0 -0
  210. /package/dist/{claude-TP2QO3BU.js.map → SettingsManager-FNKCOZMQ.js.map} +0 -0
  211. /package/dist/{build-THZI572G.js.map → build-VHGEMXBA.js.map} +0 -0
  212. /package/dist/{chunk-AR5QKYNE.js.map → chunk-4FGEGQW4.js.map} +0 -0
  213. /package/dist/{chunk-A7NJF73J.js.map → chunk-CVCTIDDK.js.map} +0 -0
  214. /package/dist/{chunk-IZIYLYPK.js.map → chunk-G5V75JD5.js.map} +0 -0
  215. /package/dist/{chunk-TC7APDKU.js.map → chunk-I5T677EA.js.map} +0 -0
  216. /package/dist/{chunk-NWMORW3U.js.map → chunk-KIK2ZFAL.js.map} +0 -0
  217. /package/dist/{chunk-NUACL52E.js.map → chunk-LLHXQS3C.js.map} +0 -0
  218. /package/dist/{chunk-TL72BGP6.js.map → chunk-MORRVYPT.js.map} +0 -0
  219. /package/dist/{chunk-7ZEHSSUP.js.map → chunk-P4O6EH46.js.map} +0 -0
  220. /package/dist/{chunk-KAYXR544.js.map → chunk-QVLPWNE3.js.map} +0 -0
  221. /package/dist/{chunk-4LKGCFGG.js.map → chunk-WWKOVDWC.js.map} +0 -0
  222. /package/dist/{git-2QDQ2X2S.js.map → claude-7GGEWVEM.js.map} +0 -0
  223. /package/dist/{compile-R2J65HBQ.js.map → compile-7ALJHZ4N.js.map} +0 -0
  224. /package/dist/{neon-helpers-VVFFTLXE.js.map → git-GTLKAZRJ.js.map} +0 -0
  225. /package/dist/{lint-CJM7BAIM.js.map → lint-AAN2NZWG.js.map} +0 -0
  226. /package/dist/{projects-LH362JZQ.js.map → projects-2UOXFLNZ.js.map} +0 -0
  227. /package/dist/{test-EA5NQFDC.js.map → test-SGO6I5Z7.js.map} +0 -0
  228. /package/dist/{test-git-M7LSLEFL.js.map → test-git-XM4TM65W.js.map} +0 -0
  229. /package/dist/{test-prefix-64NAAUON.js.map → test-prefix-GBO37XCN.js.map} +0 -0
  230. /package/dist/{test-webserver-OK6Z5FJM.js.map → test-webserver-NZ3JTVLL.js.map} +0 -0
  231. /package/dist/{vscode-AR5NNXXI.js.map → vscode-6XUGHJKL.js.map} +0 -0
@@ -3,7 +3,7 @@
3
3
  // src/mcp/issue-management-server.ts
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { z } from "zod";
6
+ import { z as z2 } from "zod";
7
7
 
8
8
  // src/utils/github.ts
9
9
  import { execa as execa2 } from "execa";
@@ -377,6 +377,41 @@ async function removeIssueDependency(blockedIssueNumber, blockingIssueDatabaseId
377
377
  apiPath
378
378
  ]);
379
379
  }
380
+ async function closeGhIssue(issueNumber, repo) {
381
+ logger.debug("Closing GitHub issue", { issueNumber, repo });
382
+ const args = ["issue", "close", String(issueNumber)];
383
+ if (repo) {
384
+ args.push("--repo", repo);
385
+ }
386
+ await executeGhCommand(args);
387
+ }
388
+ async function reopenGhIssue(issueNumber, repo) {
389
+ logger.debug("Reopening GitHub issue", { issueNumber, repo });
390
+ const args = ["issue", "reopen", String(issueNumber)];
391
+ if (repo) {
392
+ args.push("--repo", repo);
393
+ }
394
+ await executeGhCommand(args);
395
+ }
396
+ async function editGhIssue(issueNumber, options, repo) {
397
+ logger.debug("Editing GitHub issue", { issueNumber, options, repo });
398
+ const args = ["issue", "edit", String(issueNumber)];
399
+ if (options.title !== void 0) {
400
+ args.push("--title", options.title);
401
+ }
402
+ if (options.body !== void 0) {
403
+ args.push("--body", options.body);
404
+ }
405
+ if (options.labels) {
406
+ if (options.labels.length > 0) {
407
+ args.push("--add-label", options.labels.join(","));
408
+ }
409
+ }
410
+ if (repo) {
411
+ args.push("--repo", repo);
412
+ }
413
+ await executeGhCommand(args);
414
+ }
380
415
 
381
416
  // src/utils/image-processor.ts
382
417
  import { tmpdir } from "os";
@@ -776,6 +811,55 @@ var GitHubIssueManagementProvider = class {
776
811
  }
777
812
  return result;
778
813
  }
814
+ /**
815
+ * Fetch PR review comments (inline code comments on specific files/lines)
816
+ * Uses gh api with --paginate to handle PRs with many review comments
817
+ * Optionally filters by review ID
818
+ */
819
+ async getReviewComments(input) {
820
+ const { number, reviewId, repo } = input;
821
+ const prNumber = parseInt(number, 10);
822
+ if (isNaN(prNumber)) {
823
+ throw new Error(`Invalid GitHub PR number: ${number}. GitHub PR IDs must be numeric.`);
824
+ }
825
+ let numericReviewId;
826
+ if (reviewId) {
827
+ numericReviewId = parseInt(reviewId, 10);
828
+ if (isNaN(numericReviewId)) {
829
+ throw new Error(`Invalid review ID: ${reviewId}. Review IDs must be numeric.`);
830
+ }
831
+ }
832
+ const apiPath = repo ? `repos/${repo}/pulls/${prNumber}/comments` : `repos/:owner/:repo/pulls/${prNumber}/comments`;
833
+ const args = [
834
+ "api",
835
+ apiPath,
836
+ "--paginate",
837
+ "--jq",
838
+ "[.[] | {id: .id, body: .body, path: .path, line: .line, side: .side, user: .user, created_at: .created_at, updated_at: .updated_at, in_reply_to_id: .in_reply_to_id, pull_request_review_id: .pull_request_review_id}]"
839
+ ];
840
+ const raw = await executeGhCommand(args);
841
+ let comments = raw;
842
+ if (numericReviewId !== void 0) {
843
+ comments = comments.filter((c) => c.pull_request_review_id === numericReviewId);
844
+ }
845
+ const results = [];
846
+ for (const comment of comments) {
847
+ const processedBody = await processMarkdownImages(comment.body, "github");
848
+ results.push({
849
+ id: String(comment.id),
850
+ body: processedBody,
851
+ path: comment.path,
852
+ line: comment.line,
853
+ side: comment.side,
854
+ author: normalizeAuthor(comment.user),
855
+ createdAt: comment.created_at,
856
+ updatedAt: comment.updated_at ?? null,
857
+ inReplyToId: comment.in_reply_to_id ? String(comment.in_reply_to_id) : null,
858
+ pullRequestReviewId: comment.pull_request_review_id
859
+ });
860
+ }
861
+ return results;
862
+ }
779
863
  /**
780
864
  * Fetch a specific comment by ID using gh API
781
865
  * Normalizes author to FlexibleAuthor format
@@ -935,6 +1019,55 @@ var GitHubIssueManagementProvider = class {
935
1019
  }
936
1020
  return await getSubIssues(issueNumber, repo);
937
1021
  }
1022
+ /**
1023
+ * Close an issue
1024
+ */
1025
+ async closeIssue(input) {
1026
+ const { number, repo } = input;
1027
+ const issueNumber = parseInt(number, 10);
1028
+ if (isNaN(issueNumber)) {
1029
+ throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
1030
+ }
1031
+ await closeGhIssue(issueNumber, repo);
1032
+ }
1033
+ /**
1034
+ * Reopen a closed issue
1035
+ */
1036
+ async reopenIssue(input) {
1037
+ const { number, repo } = input;
1038
+ const issueNumber = parseInt(number, 10);
1039
+ if (isNaN(issueNumber)) {
1040
+ throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
1041
+ }
1042
+ await reopenGhIssue(issueNumber, repo);
1043
+ }
1044
+ /**
1045
+ * Edit an issue's properties
1046
+ * State changes are delegated to closeIssue/reopenIssue
1047
+ */
1048
+ async editIssue(input) {
1049
+ const { number, title, body, state, labels, repo } = input;
1050
+ const issueNumber = parseInt(number, 10);
1051
+ if (isNaN(issueNumber)) {
1052
+ throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
1053
+ }
1054
+ if (state === "closed") {
1055
+ await this.closeIssue({ number, repo });
1056
+ } else if (state === "open") {
1057
+ await this.reopenIssue({ number, repo });
1058
+ }
1059
+ if (title !== void 0 || body !== void 0 || labels !== void 0) {
1060
+ await editGhIssue(
1061
+ issueNumber,
1062
+ {
1063
+ ...title !== void 0 && { title },
1064
+ ...body !== void 0 && { body },
1065
+ ...labels !== void 0 && { labels }
1066
+ },
1067
+ repo
1068
+ );
1069
+ }
1070
+ }
938
1071
  };
939
1072
 
940
1073
  // src/utils/linear.ts
@@ -1141,6 +1274,61 @@ async function createLinearComment(identifier, body) {
1141
1274
  handleLinearError(error, "createLinearComment");
1142
1275
  }
1143
1276
  }
1277
+ async function updateLinearIssueState(identifier, stateName) {
1278
+ try {
1279
+ logger.debug(`Updating Linear issue ${identifier} state to: ${stateName}`);
1280
+ const client = createLinearClient();
1281
+ const issue = await client.issue(identifier);
1282
+ if (!issue) {
1283
+ throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
1284
+ }
1285
+ const team = await issue.team;
1286
+ if (!team) {
1287
+ throw new LinearServiceError("CLI_ERROR", "Issue has no team");
1288
+ }
1289
+ const states = await team.states();
1290
+ const state = states.nodes.find((s) => s.name === stateName);
1291
+ if (!state) {
1292
+ throw new LinearServiceError(
1293
+ "NOT_FOUND",
1294
+ `State "${stateName}" not found in team ${team.key}`
1295
+ );
1296
+ }
1297
+ await client.updateIssue(issue.id, {
1298
+ stateId: state.id
1299
+ });
1300
+ } catch (error) {
1301
+ if (error instanceof LinearServiceError) {
1302
+ throw error;
1303
+ }
1304
+ handleLinearError(error, "updateLinearIssueState");
1305
+ }
1306
+ }
1307
+ async function editLinearIssue(identifier, updates) {
1308
+ try {
1309
+ logger.debug(`Editing Linear issue ${identifier}`, { updates });
1310
+ const client = createLinearClient();
1311
+ const issue = await client.issue(identifier);
1312
+ if (!issue) {
1313
+ throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
1314
+ }
1315
+ const updatePayload = {};
1316
+ if (updates.title !== void 0) {
1317
+ updatePayload.title = updates.title;
1318
+ }
1319
+ if (updates.description !== void 0) {
1320
+ updatePayload.description = updates.description;
1321
+ }
1322
+ if (Object.keys(updatePayload).length > 0) {
1323
+ await client.updateIssue(issue.id, updatePayload);
1324
+ }
1325
+ } catch (error) {
1326
+ if (error instanceof LinearServiceError) {
1327
+ throw error;
1328
+ }
1329
+ handleLinearError(error, "editLinearIssue");
1330
+ }
1331
+ }
1144
1332
  async function getLinearComment(commentId) {
1145
1333
  try {
1146
1334
  logger.debug(`Fetching Linear comment: ${commentId}`);
@@ -1711,229 +1899,2210 @@ var LinearIssueManagementProvider = class {
1711
1899
  const { number } = input;
1712
1900
  return await getLinearChildIssues(number);
1713
1901
  }
1714
- };
1715
-
1716
- // src/mcp/IssueManagementProviderFactory.ts
1717
- var IssueManagementProviderFactory = class {
1718
1902
  /**
1719
- * Create an issue management provider based on the provider type
1903
+ * Close an issue by transitioning to "Done" state
1720
1904
  */
1721
- static create(provider) {
1722
- switch (provider) {
1723
- case "github":
1724
- return new GitHubIssueManagementProvider();
1725
- case "linear":
1726
- return new LinearIssueManagementProvider();
1727
- default:
1728
- throw new Error(`Unsupported issue management provider: ${provider}`);
1905
+ async closeIssue(input) {
1906
+ const { number } = input;
1907
+ await updateLinearIssueState(number, "Done");
1908
+ }
1909
+ /**
1910
+ * Reopen a closed issue by transitioning to "Todo" state
1911
+ */
1912
+ async reopenIssue(input) {
1913
+ const { number } = input;
1914
+ await updateLinearIssueState(number, "Todo");
1915
+ }
1916
+ /**
1917
+ * Edit an issue's properties
1918
+ * State changes are delegated to closeIssue/reopenIssue
1919
+ */
1920
+ async editIssue(input) {
1921
+ const { number, title, body, state } = input;
1922
+ if (state === "closed") {
1923
+ await this.closeIssue({ number });
1924
+ } else if (state === "open") {
1925
+ await this.reopenIssue({ number });
1926
+ }
1927
+ if (title !== void 0 || body !== void 0) {
1928
+ await editLinearIssue(number, {
1929
+ ...title !== void 0 && { title },
1930
+ ...body !== void 0 && { description: body }
1931
+ });
1729
1932
  }
1730
1933
  }
1731
1934
  };
1732
1935
 
1733
- // src/mcp/issue-management-server.ts
1734
- function validateEnvironment() {
1735
- const provider = process.env.ISSUE_PROVIDER;
1736
- if (!provider) {
1737
- console.error("Missing required environment variable: ISSUE_PROVIDER");
1738
- process.exit(1);
1936
+ // src/utils/jira.ts
1937
+ function escapeJql(value) {
1938
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1939
+ }
1940
+
1941
+ // src/lib/providers/jira/JiraApiClient.ts
1942
+ import https from "https";
1943
+
1944
+ // src/utils/logger-context.ts
1945
+ import { AsyncLocalStorage } from "async_hooks";
1946
+ var loggerStorage = new AsyncLocalStorage();
1947
+ function getLogger() {
1948
+ return loggerStorage.getStore() ?? logger;
1949
+ }
1950
+
1951
+ // src/lib/providers/jira/AdfMarkdownConverter.ts
1952
+ import { Parser } from "extended-markdown-adf-parser";
1953
+ var parser = new Parser();
1954
+ function sanitizeCodeMarks(node) {
1955
+ var _a;
1956
+ if ((_a = node.marks) == null ? void 0 : _a.some((mark) => mark.type === "code")) {
1957
+ node.marks = [{ type: "code" }];
1739
1958
  }
1740
- if (provider !== "github" && provider !== "linear") {
1741
- console.error(`Invalid ISSUE_PROVIDER: ${provider}. Must be 'github' or 'linear'`);
1742
- process.exit(1);
1959
+ if (node.content && Array.isArray(node.content)) {
1960
+ node.content = node.content.map((child) => sanitizeCodeMarks(child));
1743
1961
  }
1744
- if (provider === "github") {
1745
- const required = ["REPO_OWNER", "REPO_NAME"];
1746
- const missing = required.filter((key) => !process.env[key]);
1747
- if (missing.length > 0) {
1748
- console.error(
1749
- `Missing required environment variables for GitHub provider: ${missing.join(", ")}`
1750
- );
1751
- process.exit(1);
1752
- }
1962
+ return node;
1963
+ }
1964
+ var BLOCK_LEVEL_TYPES = /* @__PURE__ */ new Set([
1965
+ "paragraph",
1966
+ "bulletList",
1967
+ "orderedList",
1968
+ "codeBlock",
1969
+ "heading",
1970
+ "blockquote",
1971
+ "rule",
1972
+ "mediaGroup",
1973
+ "nestedExpand",
1974
+ "panel",
1975
+ "table",
1976
+ "taskList",
1977
+ "decisionList",
1978
+ "mediaSingle"
1979
+ ]);
1980
+ function wrapTableCellContent(node) {
1981
+ if (node.content && Array.isArray(node.content)) {
1982
+ node.content = node.content.map((child) => wrapTableCellContent(child));
1753
1983
  }
1754
- if (provider === "linear") {
1755
- if (!process.env.LINEAR_API_TOKEN) {
1756
- console.error("Missing required environment variable for Linear provider: LINEAR_API_TOKEN");
1757
- process.exit(1);
1758
- }
1984
+ if (node.type !== "tableCell" && node.type !== "tableHeader") {
1985
+ return node;
1759
1986
  }
1760
- return provider;
1761
- }
1762
- var server = new McpServer({
1763
- name: "issue-management-broker",
1764
- version: "0.1.0"
1765
- });
1766
- var flexibleAuthorSchema = z.object({
1767
- id: z.string(),
1768
- displayName: z.string()
1769
- }).passthrough();
1770
- server.registerTool(
1771
- "get_issue",
1772
- {
1773
- title: "Get Issue",
1774
- description: "Fetch issue details including body, title, comments, labels, assignees, and other metadata. Author fields vary by provider: GitHub uses { login }, Linear uses { name, displayName }, Jira uses { displayName, accountId }. All authors have normalized core fields: { id, displayName } plus provider-specific fields.",
1775
- inputSchema: {
1776
- number: z.string().describe("The issue identifier"),
1777
- includeComments: z.boolean().optional().describe("Whether to include comments (default: true)"),
1778
- repo: z.string().optional().describe(
1779
- 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
1780
- )
1781
- },
1782
- outputSchema: {
1783
- // Core validated fields
1784
- id: z.string().describe("Issue identifier"),
1785
- title: z.string().describe("Issue title"),
1786
- body: z.string().describe("Issue body/description"),
1787
- state: z.string().describe("Issue state (open, closed, etc.)"),
1788
- url: z.string().describe("Issue URL"),
1789
- provider: z.enum(["github", "linear"]).describe("Issue management provider"),
1790
- // Flexible author - core fields + passthrough
1791
- author: flexibleAuthorSchema.nullable().describe(
1792
- "Issue author with normalized { id, displayName } plus provider-specific fields"
1793
- ),
1794
- // Optional flexible arrays
1795
- assignees: z.array(flexibleAuthorSchema).optional().describe(
1796
- "Issue assignees with normalized { id, displayName } plus provider-specific fields"
1797
- ),
1798
- labels: z.array(
1799
- z.object({ name: z.string() }).passthrough()
1800
- ).optional().describe("Issue labels"),
1801
- // Comments with flexible author
1802
- comments: z.array(
1803
- z.object({
1804
- id: z.string(),
1805
- body: z.string(),
1806
- author: flexibleAuthorSchema.nullable(),
1807
- createdAt: z.string()
1808
- }).passthrough()
1809
- ).optional().describe("Issue comments with flexible author structure")
1987
+ if (!node.content || node.content.length === 0) {
1988
+ return node;
1989
+ }
1990
+ const allInline = node.content.every((child) => !BLOCK_LEVEL_TYPES.has(child.type));
1991
+ if (allInline) {
1992
+ node.content = [{ type: "paragraph", content: node.content }];
1993
+ } else {
1994
+ const newContent = [];
1995
+ let inlineRun = [];
1996
+ for (const child of node.content) {
1997
+ if (BLOCK_LEVEL_TYPES.has(child.type)) {
1998
+ if (inlineRun.length > 0) {
1999
+ newContent.push({ type: "paragraph", content: inlineRun });
2000
+ inlineRun = [];
2001
+ }
2002
+ newContent.push(child);
2003
+ } else {
2004
+ inlineRun.push(child);
2005
+ }
1810
2006
  }
1811
- },
1812
- async ({ number, includeComments, repo }) => {
1813
- console.error(`Fetching issue ${number}${repo ? ` from ${repo}` : ""}`);
1814
- try {
1815
- const provider = IssueManagementProviderFactory.create(
1816
- process.env.ISSUE_PROVIDER
1817
- );
1818
- const result = await provider.getIssue({ number, includeComments, repo });
1819
- console.error(`Issue fetched successfully: ${result.number} - ${result.title}`);
1820
- return {
1821
- content: [
1822
- {
1823
- type: "text",
1824
- text: JSON.stringify(result)
1825
- }
1826
- ],
1827
- structuredContent: result
1828
- };
1829
- } catch (error) {
1830
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1831
- console.error(`Failed to fetch issue: ${errorMessage}`);
1832
- throw new Error(`Failed to fetch issue: ${errorMessage}`);
2007
+ if (inlineRun.length > 0) {
2008
+ newContent.push({ type: "paragraph", content: inlineRun });
1833
2009
  }
2010
+ node.content = newContent;
1834
2011
  }
1835
- );
1836
- server.registerTool(
1837
- "get_pr",
1838
- {
1839
- title: "Get Pull Request",
1840
- description: "Fetch pull request details including title, body, comments, files, commits, and branch information. PRs only exist on GitHub, so this tool always uses GitHub regardless of configured issue tracker. Author fields have normalized core fields: { id, displayName } plus provider-specific fields.",
1841
- inputSchema: {
1842
- number: z.string().describe("The PR number"),
1843
- includeComments: z.boolean().optional().describe("Whether to include comments (default: true)"),
1844
- repo: z.string().optional().describe(
1845
- 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository.'
1846
- )
1847
- },
1848
- outputSchema: {
1849
- // Core validated fields
1850
- id: z.string().describe("PR identifier"),
1851
- number: z.number().describe("PR number"),
1852
- title: z.string().describe("PR title"),
1853
- body: z.string().describe("PR body/description"),
1854
- state: z.string().describe("PR state (OPEN, CLOSED, MERGED)"),
1855
- url: z.string().describe("PR URL"),
1856
- // Branch info
1857
- headRefName: z.string().describe("Source branch name"),
1858
- baseRefName: z.string().describe("Target branch name"),
1859
- // Flexible author - core fields + passthrough
1860
- author: flexibleAuthorSchema.nullable().describe(
1861
- "PR author with normalized { id, displayName } plus provider-specific fields"
1862
- ),
1863
- // Optional flexible arrays
1864
- files: z.array(
1865
- z.object({
1866
- path: z.string(),
1867
- additions: z.number(),
1868
- deletions: z.number()
1869
- }).passthrough()
1870
- ).optional().describe("Changed files in the PR"),
1871
- commits: z.array(
1872
- z.object({
1873
- oid: z.string(),
1874
- messageHeadline: z.string(),
1875
- author: flexibleAuthorSchema.nullable()
1876
- }).passthrough()
1877
- ).optional().describe("Commits in the PR"),
1878
- comments: z.array(
1879
- z.object({
1880
- id: z.string(),
1881
- body: z.string(),
1882
- author: flexibleAuthorSchema.nullable(),
1883
- createdAt: z.string()
1884
- }).passthrough()
1885
- ).optional().describe("PR comments")
2012
+ return node;
2013
+ }
2014
+ var taskIdCounter = 0;
2015
+ function getCanonicalPlainText(text) {
2016
+ const miniAdf = parser.markdownToAdf(text);
2017
+ return getPlainText(miniAdf);
2018
+ }
2019
+ function extractCheckboxBlocks(markdown) {
2020
+ var _a, _b;
2021
+ const lines = markdown.split("\n");
2022
+ const blocks = [];
2023
+ let i = 0;
2024
+ while (i < lines.length) {
2025
+ const bulletLines = [];
2026
+ let blockIndent = null;
2027
+ while (i < lines.length) {
2028
+ const line = lines[i] ?? "";
2029
+ const checkboxMatch = line.match(/^(\s*)[-*+] \[([ xX])\] (.*)$/);
2030
+ if (checkboxMatch) {
2031
+ const indent = ((_a = checkboxMatch[1]) == null ? void 0 : _a.length) ?? 0;
2032
+ if (blockIndent === null) {
2033
+ blockIndent = indent;
2034
+ } else if (indent !== blockIndent) {
2035
+ break;
2036
+ }
2037
+ const state = checkboxMatch[2] === " " ? "TODO" : "DONE";
2038
+ bulletLines.push({ isCheckbox: true, state, rawText: checkboxMatch[3] ?? "" });
2039
+ i++;
2040
+ } else if (line.match(/^\s*[-*+] /)) {
2041
+ const indentMatch = line.match(/^(\s*)/);
2042
+ const indent = ((_b = indentMatch == null ? void 0 : indentMatch[1]) == null ? void 0 : _b.length) ?? 0;
2043
+ if (blockIndent === null) {
2044
+ blockIndent = indent;
2045
+ } else if (indent !== blockIndent) {
2046
+ break;
2047
+ }
2048
+ bulletLines.push({ isCheckbox: false, state: null, rawText: "" });
2049
+ i++;
2050
+ } else if (bulletLines.length > 0 && line.match(/^\s/) && line.trim() !== "") {
2051
+ const lastItem = bulletLines[bulletLines.length - 1];
2052
+ if (lastItem) {
2053
+ lastItem.rawText += "\n" + line.trim();
2054
+ }
2055
+ i++;
2056
+ } else {
2057
+ break;
2058
+ }
1886
2059
  }
1887
- },
1888
- async ({ number, includeComments, repo }) => {
1889
- console.error(`Fetching PR ${number}${repo ? ` from ${repo}` : ""}`);
1890
- try {
1891
- const provider = new GitHubIssueManagementProvider();
1892
- const result = await provider.getPR({ number, includeComments, repo });
1893
- console.error(`PR fetched successfully: #${result.number} - ${result.title}`);
1894
- return {
1895
- content: [
1896
- {
1897
- type: "text",
1898
- text: JSON.stringify(result)
1899
- }
1900
- ],
1901
- structuredContent: result
1902
- };
1903
- } catch (error) {
1904
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1905
- console.error(`Failed to fetch PR: ${errorMessage}`);
1906
- throw new Error(`Failed to fetch PR: ${errorMessage}`);
2060
+ if (bulletLines.length > 0) {
2061
+ const allCheckboxes = bulletLines.every((l) => l.isCheckbox);
2062
+ if (allCheckboxes) {
2063
+ blocks.push({
2064
+ states: bulletLines.map((l) => l.state),
2065
+ texts: bulletLines.map((l) => getCanonicalPlainText(l.rawText))
2066
+ });
2067
+ }
2068
+ } else {
2069
+ i++;
1907
2070
  }
1908
2071
  }
1909
- );
1910
- server.registerTool(
2072
+ return blocks;
2073
+ }
2074
+ function getPlainText(node) {
2075
+ if (node.type === "text" && node.text !== void 0) return node.text;
2076
+ if (!node.content) return "";
2077
+ return node.content.map(getPlainText).join("");
2078
+ }
2079
+ function convertCheckboxesToTaskList(node, blocks) {
2080
+ const cursor = { index: 0 };
2081
+ return convertCheckboxesRecursive(node, blocks, cursor);
2082
+ }
2083
+ function convertCheckboxesRecursive(node, blocks, cursor) {
2084
+ var _a;
2085
+ if (node.type === "bulletList" && node.content && node.content.length > 0 && cursor.index < blocks.length) {
2086
+ const block = blocks[cursor.index];
2087
+ if (!block) return node;
2088
+ if (node.content.length === block.states.length) {
2089
+ const plaintexts = node.content.map((listItem) => getPlainText(listItem));
2090
+ const matches = plaintexts.every((text, i) => text === block.texts[i]);
2091
+ if (matches) {
2092
+ const allSimple = node.content.every((item) => {
2093
+ var _a2, _b;
2094
+ return ((_a2 = item.content) == null ? void 0 : _a2.length) === 1 && ((_b = item.content[0]) == null ? void 0 : _b.type) === "paragraph";
2095
+ });
2096
+ if (!allSimple) {
2097
+ cursor.index++;
2098
+ return node;
2099
+ }
2100
+ cursor.index++;
2101
+ node.type = "taskList";
2102
+ node.attrs = { localId: `tasklist-${++taskIdCounter}` };
2103
+ for (const [i, listItem] of node.content.entries()) {
2104
+ listItem.type = "taskItem";
2105
+ listItem.attrs = {
2106
+ localId: `task-${++taskIdCounter}`,
2107
+ state: block.states[i]
2108
+ };
2109
+ const firstChild = (_a = listItem.content) == null ? void 0 : _a[0];
2110
+ if ((firstChild == null ? void 0 : firstChild.type) === "paragraph" && firstChild.content) {
2111
+ listItem.content = firstChild.content;
2112
+ }
2113
+ }
2114
+ return node;
2115
+ }
2116
+ }
2117
+ }
2118
+ if (node.content && Array.isArray(node.content)) {
2119
+ node.content = node.content.map((child) => convertCheckboxesRecursive(child, blocks, cursor));
2120
+ }
2121
+ return node;
2122
+ }
2123
+ function convertDetailsToExpandSyntax(markdown) {
2124
+ if (!markdown) return markdown;
2125
+ let previousText = "";
2126
+ let currentText = markdown;
2127
+ while (previousText !== currentText) {
2128
+ previousText = currentText;
2129
+ currentText = currentText.replace(
2130
+ /<details[^>]*>\s*<summary[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi,
2131
+ (_match, summary, content) => {
2132
+ const cleanSummary = summary.trim().replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
2133
+ let cleanContent = content.trim();
2134
+ cleanContent = cleanContent.replace(/\n{3,}/g, "\n\n");
2135
+ if (cleanContent) {
2136
+ return `~~~expand title="${cleanSummary}"
2137
+ ${cleanContent}
2138
+ ~~~`;
2139
+ } else {
2140
+ return `~~~expand title="${cleanSummary}"
2141
+ ~~~`;
2142
+ }
2143
+ }
2144
+ );
2145
+ }
2146
+ return currentText;
2147
+ }
2148
+ function adfToMarkdown(adf) {
2149
+ if (!adf) return "";
2150
+ if (typeof adf === "string") return adf;
2151
+ return parser.adfToMarkdown(adf);
2152
+ }
2153
+ function markdownToAdf(markdown) {
2154
+ if (!markdown) {
2155
+ return { type: "doc", version: 1, content: [] };
2156
+ }
2157
+ taskIdCounter = 0;
2158
+ const checkboxBlocks = extractCheckboxBlocks(markdown);
2159
+ const preprocessed = convertDetailsToExpandSyntax(markdown);
2160
+ const adf = parser.markdownToAdf(preprocessed);
2161
+ let result = sanitizeCodeMarks(adf);
2162
+ result = wrapTableCellContent(result);
2163
+ result = convertCheckboxesToTaskList(result, checkboxBlocks);
2164
+ return result;
2165
+ }
2166
+
2167
+ // src/lib/providers/jira/JiraApiClient.ts
2168
+ var JiraApiClient = class {
2169
+ constructor(config) {
2170
+ this.baseUrl = `${config.host.replace(/\/$/, "")}/rest/api/3`;
2171
+ const credentials = Buffer.from(`${config.username}:${config.apiToken}`).toString("base64");
2172
+ this.authHeader = `Basic ${credentials}`;
2173
+ }
2174
+ /**
2175
+ * Make an HTTP request to Jira API
2176
+ */
2177
+ async request(method, endpoint, body) {
2178
+ const url = new URL(`${this.baseUrl}${endpoint}`);
2179
+ getLogger().debug(`Jira API ${method} request`, { url: url.toString() });
2180
+ if (body) {
2181
+ getLogger().debug("Jira API request body", JSON.stringify(body, null, 2));
2182
+ }
2183
+ return new Promise((resolve, reject) => {
2184
+ const options = {
2185
+ hostname: url.hostname,
2186
+ port: url.port || 443,
2187
+ path: url.pathname + url.search,
2188
+ method,
2189
+ headers: {
2190
+ "Authorization": this.authHeader,
2191
+ "Accept": "application/json",
2192
+ "Content-Type": "application/json"
2193
+ }
2194
+ };
2195
+ const req = https.request({ ...options, timeout: 3e4 }, (res) => {
2196
+ const chunks = [];
2197
+ res.on("data", (chunk) => {
2198
+ chunks.push(chunk);
2199
+ });
2200
+ res.on("end", () => {
2201
+ var _a;
2202
+ const data = Buffer.concat(chunks).toString("utf8");
2203
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
2204
+ let errorDetail = data;
2205
+ try {
2206
+ const parsed = JSON.parse(data);
2207
+ const parts = [];
2208
+ if ((_a = parsed.errorMessages) == null ? void 0 : _a.length) {
2209
+ parts.push(`messages: ${parsed.errorMessages.join(", ")}`);
2210
+ }
2211
+ if (parsed.errors && Object.keys(parsed.errors).length) {
2212
+ parts.push(`field errors: ${JSON.stringify(parsed.errors)}`);
2213
+ }
2214
+ if (parts.length) {
2215
+ errorDetail = parts.join("; ");
2216
+ }
2217
+ } catch {
2218
+ }
2219
+ reject(new Error(`Jira API error (${res.statusCode}): ${errorDetail}`));
2220
+ return;
2221
+ }
2222
+ if (res.statusCode === 204 || !data) {
2223
+ resolve({});
2224
+ return;
2225
+ }
2226
+ try {
2227
+ resolve(JSON.parse(data));
2228
+ } catch (error) {
2229
+ reject(new Error(`Failed to parse Jira API response: ${error}`));
2230
+ }
2231
+ });
2232
+ });
2233
+ req.on("timeout", () => {
2234
+ req.destroy();
2235
+ reject(new Error("Jira API request timed out after 30 seconds"));
2236
+ });
2237
+ req.on("error", (error) => {
2238
+ reject(new Error(`Jira API request failed: ${error.message}`));
2239
+ });
2240
+ if (body) {
2241
+ req.write(JSON.stringify(body));
2242
+ }
2243
+ req.end();
2244
+ });
2245
+ }
2246
+ /**
2247
+ * Make a GET request to Jira API
2248
+ */
2249
+ async get(endpoint) {
2250
+ return this.request("GET", endpoint);
2251
+ }
2252
+ /**
2253
+ * Make a POST request to Jira API
2254
+ */
2255
+ async post(endpoint, body) {
2256
+ return this.request("POST", endpoint, body);
2257
+ }
2258
+ /**
2259
+ * Make a PUT request to Jira API
2260
+ */
2261
+ async put(endpoint, body) {
2262
+ return this.request("PUT", endpoint, body);
2263
+ }
2264
+ /**
2265
+ * Make a DELETE request to Jira API
2266
+ */
2267
+ async delete(endpoint) {
2268
+ await this.request("DELETE", endpoint);
2269
+ }
2270
+ /**
2271
+ * Fetch an issue by key (e.g., "PROJ-123")
2272
+ */
2273
+ async getIssue(issueKey) {
2274
+ return this.get(`/issue/${issueKey}`);
2275
+ }
2276
+ /**
2277
+ * Add a comment to an issue
2278
+ * Accepts Markdown content which is converted to ADF for Jira
2279
+ */
2280
+ async addComment(issueKey, body) {
2281
+ const adfBody = markdownToAdf(body);
2282
+ getLogger().debug("Adding comment to Jira issue", { issueKey, bodyLength: body.length });
2283
+ return this.post(`/issue/${issueKey}/comment`, {
2284
+ body: adfBody
2285
+ });
2286
+ }
2287
+ /**
2288
+ * Get all comments for an issue
2289
+ */
2290
+ async getComments(issueKey) {
2291
+ const response = await this.get(`/issue/${issueKey}/comment?maxResults=5000`);
2292
+ if (response.total > response.comments.length) {
2293
+ getLogger().warn(`Comments truncated for issue ${issueKey}: returned ${response.comments.length} of ${response.total} total comments`);
2294
+ }
2295
+ return response.comments;
2296
+ }
2297
+ /**
2298
+ * Update a comment on an issue
2299
+ * Accepts Markdown content which is converted to ADF for Jira
2300
+ */
2301
+ async updateComment(issueKey, commentId, body) {
2302
+ return this.put(`/issue/${issueKey}/comment/${commentId}`, {
2303
+ body: markdownToAdf(body)
2304
+ });
2305
+ }
2306
+ /**
2307
+ * Get available transitions for an issue
2308
+ */
2309
+ async getTransitions(issueKey) {
2310
+ const response = await this.get(`/issue/${issueKey}/transitions`);
2311
+ return response.transitions;
2312
+ }
2313
+ /**
2314
+ * Transition an issue to a new state
2315
+ */
2316
+ async transitionIssue(issueKey, transitionId) {
2317
+ await this.post(`/issue/${issueKey}/transitions`, {
2318
+ transition: {
2319
+ id: transitionId
2320
+ }
2321
+ });
2322
+ }
2323
+ /**
2324
+ * Create a new issue
2325
+ * Accepts Markdown description which is converted to ADF for Jira
2326
+ */
2327
+ async createIssue(projectKey, summary, description, issueType = "Task") {
2328
+ return this.post("/issue", {
2329
+ fields: {
2330
+ project: {
2331
+ key: projectKey
2332
+ },
2333
+ summary,
2334
+ description: markdownToAdf(description),
2335
+ issuetype: {
2336
+ name: issueType
2337
+ }
2338
+ }
2339
+ });
2340
+ }
2341
+ /**
2342
+ * Update an issue's fields (summary, description)
2343
+ * @param issueKey - Jira issue key (e.g., "PROJ-123")
2344
+ * @param fields - Fields to update
2345
+ */
2346
+ async updateIssue(issueKey, fields) {
2347
+ const updateFields = {};
2348
+ if (fields.summary !== void 0) {
2349
+ updateFields.summary = fields.summary;
2350
+ }
2351
+ if (fields.description !== void 0) {
2352
+ updateFields.description = markdownToAdf(fields.description);
2353
+ }
2354
+ await this.put(`/issue/${issueKey}`, { fields: updateFields });
2355
+ }
2356
+ /**
2357
+ * Create an issue with a parent (subtask or child issue)
2358
+ * Accepts Markdown description which is converted to ADF for Jira
2359
+ */
2360
+ async createIssueWithParent(projectKey, summary, description, parentKey, issueType = "Subtask") {
2361
+ return this.post("/issue", {
2362
+ fields: {
2363
+ project: {
2364
+ key: projectKey
2365
+ },
2366
+ summary,
2367
+ description: markdownToAdf(description),
2368
+ issuetype: {
2369
+ name: issueType
2370
+ },
2371
+ parent: {
2372
+ key: parentKey
2373
+ }
2374
+ }
2375
+ });
2376
+ }
2377
+ /**
2378
+ * Create an issue link (dependency/relationship between issues)
2379
+ * @param inwardKey - The issue key for the inward side (e.g., the blocked issue)
2380
+ * @param outwardKey - The issue key for the outward side (e.g., the blocking issue)
2381
+ * @param linkType - The link type name (e.g., "Blocks")
2382
+ */
2383
+ async createIssueLink(inwardKey, outwardKey, linkType) {
2384
+ await this.post("/issueLink", {
2385
+ type: {
2386
+ name: linkType
2387
+ },
2388
+ inwardIssue: {
2389
+ key: inwardKey
2390
+ },
2391
+ outwardIssue: {
2392
+ key: outwardKey
2393
+ }
2394
+ });
2395
+ }
2396
+ /**
2397
+ * Delete an issue link by ID
2398
+ */
2399
+ async deleteIssueLink(linkId) {
2400
+ await this.delete(`/issueLink/${linkId}`);
2401
+ }
2402
+ /**
2403
+ * Search issues using JQL
2404
+ * Automatically paginates through all results up to MAX_SEARCH_RESULTS.
2405
+ */
2406
+ async searchIssues(jql) {
2407
+ const MAX_SEARCH_RESULTS = 5e3;
2408
+ const allIssues = [];
2409
+ let nextPageToken;
2410
+ const maxResults = 100;
2411
+ while (allIssues.length < MAX_SEARCH_RESULTS) {
2412
+ const body = {
2413
+ jql,
2414
+ maxResults,
2415
+ fields: [
2416
+ "summary",
2417
+ "description",
2418
+ "status",
2419
+ "issuetype",
2420
+ "project",
2421
+ "assignee",
2422
+ "reporter",
2423
+ "labels",
2424
+ "created",
2425
+ "updated",
2426
+ "issuelinks",
2427
+ "parent"
2428
+ ]
2429
+ };
2430
+ if (nextPageToken) {
2431
+ body.nextPageToken = nextPageToken;
2432
+ }
2433
+ const response = await this.post(
2434
+ "/search/jql",
2435
+ body
2436
+ );
2437
+ allIssues.push(...response.issues);
2438
+ if (!response.nextPageToken || response.issues.length === 0) {
2439
+ break;
2440
+ }
2441
+ nextPageToken = response.nextPageToken;
2442
+ }
2443
+ if (allIssues.length >= MAX_SEARCH_RESULTS) {
2444
+ getLogger().warn(`Search results truncated at ${MAX_SEARCH_RESULTS} issues. The query matched more results than the safety cap allows.`, { jql, returnedCount: allIssues.length });
2445
+ }
2446
+ return allIssues;
2447
+ }
2448
+ /**
2449
+ * Test connection to Jira API
2450
+ */
2451
+ async testConnection() {
2452
+ try {
2453
+ await this.get("/myself");
2454
+ return true;
2455
+ } catch (error) {
2456
+ const message = error instanceof Error ? error.message : String(error);
2457
+ if (message.includes("Jira API error (401)") || message.includes("Jira API error (403)")) {
2458
+ getLogger().error("Jira connection test failed: authentication error", { error });
2459
+ return false;
2460
+ }
2461
+ throw error;
2462
+ }
2463
+ }
2464
+ };
2465
+
2466
+ // src/utils/prompt.ts
2467
+ import * as readline from "readline";
2468
+
2469
+ // src/utils/notification.ts
2470
+ import net from "net";
2471
+ import fs from "fs";
2472
+ import path from "path";
2473
+ import os from "os";
2474
+ import { execSync } from "child_process";
2475
+ var DEBUG = process.env.ILOOM_NOTIF_DEBUG === "1";
2476
+
2477
+ // src/utils/prompt.ts
2478
+ async function promptConfirmation(message, defaultValue = false) {
2479
+ const suffix = defaultValue ? "[Y/n]" : "[y/N]";
2480
+ const fullMessage = `${message} ${suffix}: `;
2481
+ while (true) {
2482
+ const rl = readline.createInterface({
2483
+ input: process.stdin,
2484
+ output: process.stdout
2485
+ });
2486
+ const answer = await new Promise((resolve) => {
2487
+ rl.question(fullMessage, (ans) => {
2488
+ rl.close();
2489
+ resolve(ans);
2490
+ });
2491
+ });
2492
+ const normalized = answer.trim().toLowerCase();
2493
+ if (normalized === "") {
2494
+ return defaultValue;
2495
+ }
2496
+ if (normalized === "y" || normalized === "yes") {
2497
+ return true;
2498
+ }
2499
+ if (normalized === "n" || normalized === "no") {
2500
+ return false;
2501
+ }
2502
+ logger.warn("Invalid input. Please enter y/yes or n/no.");
2503
+ }
2504
+ }
2505
+
2506
+ // src/lib/providers/jira/JiraIssueTracker.ts
2507
+ var JiraIssueTracker = class {
2508
+ constructor(config, options) {
2509
+ this.providerName = "jira";
2510
+ this.supportsPullRequests = false;
2511
+ this.config = config;
2512
+ this.client = new JiraApiClient({
2513
+ host: config.host,
2514
+ username: config.username,
2515
+ apiToken: config.apiToken
2516
+ });
2517
+ this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
2518
+ }
2519
+ /**
2520
+ * Normalize identifier to canonical uppercase form
2521
+ * Jira issue keys are case-sensitive in the API (must be uppercase)
2522
+ */
2523
+ normalizeIdentifier(identifier) {
2524
+ return String(identifier).toUpperCase();
2525
+ }
2526
+ /**
2527
+ * Detect input type from user input
2528
+ * Jira issues follow pattern: PROJECTKEY-123 (case-insensitive)
2529
+ */
2530
+ async detectInputType(input) {
2531
+ const jiraPattern = /^([A-Z][A-Z0-9]+)-(\d+)$/i;
2532
+ const match = input.match(jiraPattern);
2533
+ if (!match) {
2534
+ return { type: "unknown", identifier: null, rawInput: input };
2535
+ }
2536
+ const issueKey = this.normalizeIdentifier(input);
2537
+ getLogger().debug("Checking if input is a Jira issue", { issueKey });
2538
+ try {
2539
+ await this.client.getIssue(issueKey);
2540
+ return { type: "issue", identifier: issueKey, rawInput: input };
2541
+ } catch (error) {
2542
+ if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
2543
+ getLogger().debug("Issue not found", { issueKey, error });
2544
+ return { type: "unknown", identifier: null, rawInput: input };
2545
+ }
2546
+ throw error;
2547
+ }
2548
+ }
2549
+ /**
2550
+ * Fetch issue details
2551
+ */
2552
+ async fetchIssue(identifier) {
2553
+ const issueKey = this.normalizeIdentifier(identifier);
2554
+ getLogger().debug("Fetching Jira issue", { issueKey });
2555
+ const jiraIssue = await this.client.getIssue(issueKey);
2556
+ return this.mapJiraIssueToIssue(jiraIssue);
2557
+ }
2558
+ /**
2559
+ * Check if issue exists (silent validation)
2560
+ */
2561
+ async isValidIssue(identifier) {
2562
+ try {
2563
+ return await this.fetchIssue(identifier);
2564
+ } catch (error) {
2565
+ if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
2566
+ getLogger().debug("Issue validation failed: not found", { identifier, error });
2567
+ return false;
2568
+ }
2569
+ throw error;
2570
+ }
2571
+ }
2572
+ /**
2573
+ * Validate issue state
2574
+ * Note: Jira doesn't have a simple "closed" state - depends on workflow
2575
+ */
2576
+ async validateIssueState(issue) {
2577
+ getLogger().debug("Jira issue state", { issueKey: issue.number, state: issue.state });
2578
+ if (issue.state === "closed") {
2579
+ const shouldContinue = await this.prompter(
2580
+ `Issue ${issue.number} is in a completed state. Continue anyway?`
2581
+ );
2582
+ if (!shouldContinue) {
2583
+ throw new Error("User cancelled due to completed issue");
2584
+ }
2585
+ }
2586
+ }
2587
+ /**
2588
+ * Create a new issue
2589
+ */
2590
+ async createIssue(title, body, _repository, _labels) {
2591
+ getLogger().debug("Creating Jira issue", { title, projectKey: this.config.projectKey });
2592
+ const jiraIssue = await this.client.createIssue(
2593
+ this.config.projectKey,
2594
+ title,
2595
+ body,
2596
+ this.config.defaultIssueType
2597
+ );
2598
+ return {
2599
+ number: jiraIssue.key,
2600
+ url: `${this.config.host}/browse/${jiraIssue.key}`
2601
+ };
2602
+ }
2603
+ /**
2604
+ * Get issue URL
2605
+ */
2606
+ async getIssueUrl(identifier) {
2607
+ const issueKey = this.normalizeIdentifier(identifier);
2608
+ return `${this.config.host}/browse/${issueKey}`;
2609
+ }
2610
+ /**
2611
+ * Move issue to "In Progress" state
2612
+ * Uses configured transition mapping or default transition name
2613
+ */
2614
+ async moveIssueToInProgress(identifier) {
2615
+ var _a;
2616
+ const issueKey = this.normalizeIdentifier(identifier);
2617
+ getLogger().debug("Moving Jira issue to In Progress", { issueKey });
2618
+ const transitions = await this.client.getTransitions(issueKey);
2619
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["In Progress"]) ?? this.findTransitionByName(transitions, ["In Progress", "Start Progress", "Start"]);
2620
+ if (!transitionName) {
2621
+ throw new Error(
2622
+ `Could not find "In Progress" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
2623
+ );
2624
+ }
2625
+ const transition = transitions.find((t) => t.name === transitionName);
2626
+ if (!transition) {
2627
+ throw new Error(`Transition "${transitionName}" not found`);
2628
+ }
2629
+ await this.client.transitionIssue(issueKey, transition.id);
2630
+ getLogger().info("Issue transitioned successfully", { issueKey, transition: transitionName });
2631
+ }
2632
+ /**
2633
+ * Move issue to "Ready for Review" state
2634
+ * Uses configured transition mapping or default transition name
2635
+ */
2636
+ async moveIssueToReadyForReview(identifier) {
2637
+ var _a;
2638
+ const issueKey = this.normalizeIdentifier(identifier);
2639
+ getLogger().debug("Moving Jira issue to Ready for Review", { issueKey });
2640
+ const transitions = await this.client.getTransitions(issueKey);
2641
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Ready for Review"]) ?? this.findTransitionByName(transitions, ["Ready for Review", "In Review", "Code Review", "Review"]);
2642
+ if (!transitionName) {
2643
+ throw new Error(
2644
+ `Could not find "Ready for Review" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
2645
+ );
2646
+ }
2647
+ const transition = transitions.find((t) => t.name === transitionName);
2648
+ if (!transition) {
2649
+ throw new Error(`Transition "${transitionName}" not found`);
2650
+ }
2651
+ await this.client.transitionIssue(issueKey, transition.id);
2652
+ getLogger().info("Issue transitioned to Ready for Review", { issueKey, transition: transitionName });
2653
+ }
2654
+ /**
2655
+ * Close an issue by transitioning to "Done" state
2656
+ * Uses configured transition mapping or default transition names
2657
+ */
2658
+ async closeIssue(identifier) {
2659
+ var _a;
2660
+ const issueKey = this.normalizeIdentifier(identifier);
2661
+ getLogger().debug("Closing Jira issue", { issueKey });
2662
+ const transitions = await this.client.getTransitions(issueKey);
2663
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Done"]) ?? this.findTransitionByName(transitions, ["Done", "Close", "Closed", "Resolve", "Resolved"]);
2664
+ if (!transitionName) {
2665
+ throw new Error(
2666
+ `Could not find "Done" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
2667
+ );
2668
+ }
2669
+ const transition = transitions.find((t) => t.name === transitionName);
2670
+ if (!transition) {
2671
+ throw new Error(`Transition "${transitionName}" not found`);
2672
+ }
2673
+ await this.client.transitionIssue(issueKey, transition.id);
2674
+ getLogger().info("Issue closed successfully", { issueKey, transition: transitionName });
2675
+ }
2676
+ /**
2677
+ * Reopen an issue by transitioning back to an open state
2678
+ * Uses configured transition mapping or default transition names
2679
+ */
2680
+ async reopenIssue(identifier) {
2681
+ var _a;
2682
+ const issueKey = this.normalizeIdentifier(identifier);
2683
+ getLogger().debug("Reopening Jira issue", { issueKey });
2684
+ const transitions = await this.client.getTransitions(issueKey);
2685
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Reopen"]) ?? this.findTransitionByName(transitions, ["Reopen", "To Do", "Open", "Backlog"]);
2686
+ if (!transitionName) {
2687
+ throw new Error(
2688
+ `Could not find "Reopen" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
2689
+ );
2690
+ }
2691
+ const transition = transitions.find((t) => t.name === transitionName);
2692
+ if (!transition) {
2693
+ throw new Error(`Transition "${transitionName}" not found`);
2694
+ }
2695
+ await this.client.transitionIssue(issueKey, transition.id);
2696
+ getLogger().info("Issue reopened successfully", { issueKey, transition: transitionName });
2697
+ }
2698
+ /**
2699
+ * Extract context from issue for AI prompts
2700
+ */
2701
+ extractContext(entity) {
2702
+ return `Issue: ${entity.number}
2703
+ Title: ${entity.title}
2704
+ Status: ${entity.state}
2705
+ URL: ${entity.url}
2706
+
2707
+ Description:
2708
+ ${entity.body}
2709
+
2710
+ ${entity.labels.length > 0 ? `Labels: ${entity.labels.join(", ")}` : ""}
2711
+ ${entity.assignees.length > 0 ? `Assignees: ${entity.assignees.join(", ")}` : ""}`;
2712
+ }
2713
+ /**
2714
+ * Fetch child issues of a Jira parent issue using JQL
2715
+ * @param parentIdentifier - Jira issue key (e.g., "PROJ-123")
2716
+ * @param _repo - Repository (unused for Jira)
2717
+ * @returns Array of child issues
2718
+ */
2719
+ async getChildIssues(parentIdentifier, _repo) {
2720
+ const parentKey = this.normalizeIdentifier(parentIdentifier);
2721
+ const jiraKeyPattern = /^[A-Z][A-Z0-9]+-\d+$/;
2722
+ if (!jiraKeyPattern.test(parentKey)) {
2723
+ getLogger().warn(`Invalid Jira issue key format: ${parentKey}`);
2724
+ return [];
2725
+ }
2726
+ const issues = await this.client.searchIssues(`parent = ${parentKey}`);
2727
+ return issues.map((issue) => ({
2728
+ id: issue.key,
2729
+ title: issue.fields.summary,
2730
+ url: `${this.config.host}/browse/${issue.key}`,
2731
+ state: issue.fields.status.name.toLowerCase()
2732
+ }));
2733
+ }
2734
+ /**
2735
+ * Get issue details (alias for fetchIssue for MCP compatibility)
2736
+ */
2737
+ async getIssue(identifier) {
2738
+ return this.fetchIssue(identifier);
2739
+ }
2740
+ /**
2741
+ * Get all comments for an issue
2742
+ */
2743
+ async getComments(identifier) {
2744
+ const issueKey = this.normalizeIdentifier(identifier);
2745
+ getLogger().debug("Fetching Jira comments", { issueKey });
2746
+ const comments = await this.client.getComments(issueKey);
2747
+ return comments.map((comment) => ({
2748
+ id: comment.id,
2749
+ body: adfToMarkdown(comment.body),
2750
+ author: comment.author,
2751
+ createdAt: comment.created,
2752
+ updatedAt: comment.updated
2753
+ }));
2754
+ }
2755
+ /**
2756
+ * Add a comment to an issue
2757
+ */
2758
+ async addComment(identifier, body) {
2759
+ const issueKey = this.normalizeIdentifier(identifier);
2760
+ getLogger().debug("Adding Jira comment", { issueKey });
2761
+ const comment = await this.client.addComment(issueKey, body);
2762
+ return { id: comment.id };
2763
+ }
2764
+ /**
2765
+ * Update an existing comment
2766
+ */
2767
+ async updateComment(identifier, commentId, body) {
2768
+ const issueKey = this.normalizeIdentifier(identifier);
2769
+ getLogger().debug("Updating Jira comment", { issueKey, commentId });
2770
+ await this.client.updateComment(issueKey, commentId, body);
2771
+ }
2772
+ /**
2773
+ * Get the underlying API client (for direct API access by MCP provider)
2774
+ */
2775
+ getApiClient() {
2776
+ return this.client;
2777
+ }
2778
+ /**
2779
+ * Get configuration (for MCP provider)
2780
+ */
2781
+ getConfig() {
2782
+ return this.config;
2783
+ }
2784
+ /**
2785
+ * Map Jira API issue to generic Issue type
2786
+ */
2787
+ mapJiraIssueToIssue(jiraIssue) {
2788
+ const description = adfToMarkdown(jiraIssue.fields.description);
2789
+ return {
2790
+ id: jiraIssue.id,
2791
+ key: jiraIssue.key,
2792
+ number: jiraIssue.key,
2793
+ title: jiraIssue.fields.summary,
2794
+ body: description,
2795
+ state: this.mapJiraStatusToState(jiraIssue.fields.status.name),
2796
+ labels: jiraIssue.fields.labels,
2797
+ assignees: jiraIssue.fields.assignee ? [jiraIssue.fields.assignee.displayName] : [],
2798
+ assignee: jiraIssue.fields.assignee,
2799
+ author: jiraIssue.fields.reporter,
2800
+ url: `${this.config.host}/browse/${jiraIssue.key}`,
2801
+ issueType: jiraIssue.fields.issuetype.name,
2802
+ status: jiraIssue.fields.status.name
2803
+ };
2804
+ }
2805
+ mapJiraStatusToState(statusName) {
2806
+ const normalized = statusName.toLowerCase();
2807
+ const closedStatuses = ["done", "closed", "resolved", "cancelled", "canceled"];
2808
+ return closedStatuses.includes(normalized) ? "closed" : "open";
2809
+ }
2810
+ /**
2811
+ * Find a transition by name, trying multiple possible names
2812
+ */
2813
+ findTransitionByName(transitions, names) {
2814
+ for (const name of names) {
2815
+ const transition = transitions.find(
2816
+ (t) => t.name.toLowerCase() === name.toLowerCase()
2817
+ );
2818
+ if (transition) {
2819
+ return transition.name;
2820
+ }
2821
+ }
2822
+ return null;
2823
+ }
2824
+ };
2825
+
2826
+ // src/lib/SettingsManager.ts
2827
+ import { readFile } from "fs/promises";
2828
+ import path2 from "path";
2829
+ import os2 from "os";
2830
+ import { z } from "zod";
2831
+ import deepmerge from "deepmerge";
2832
+ var BaseAgentSettingsSchema = z.object({
2833
+ model: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Claude model shorthand: sonnet, opus, or haiku"),
2834
+ swarmModel: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Model to use for this agent in swarm mode. Overrides the base model when running inside swarm workers."),
2835
+ enabled: z.boolean().optional().describe("Whether this agent is enabled. Defaults to true."),
2836
+ providers: z.record(
2837
+ z.enum(["claude", "gemini", "codex"]),
2838
+ z.string()
2839
+ ).optional().describe('Map of review providers to model names. Keys: claude, gemini, codex. Values: model name strings (e.g., "sonnet", "gemini-3-pro-preview", "gpt-5.2-codex")'),
2840
+ review: z.boolean().optional().describe("Whether artifacts from this agent should be reviewed before posting (defaults to false)")
2841
+ });
2842
+ var AgentSettingsSchema = BaseAgentSettingsSchema.extend({
2843
+ agents: z.record(z.string(), BaseAgentSettingsSchema).optional().describe("Nested per-agent settings. Only meaningful under the iloom-swarm-worker agent entry for sub-agent timeout configuration."),
2844
+ subAgentTimeout: z.number().min(1, "Sub-agent timeout must be at least 1 minute").max(120, "Sub-agent timeout cannot exceed 120 minutes").default(10).describe("Timeout in minutes for sub-agent claude -p invocations in swarm mode. Applies to each phase agent (evaluator, analyzer, planner, implementer) when invoked via the Bash tool. Default: 10 minutes. Only meaningful under the iloom-swarm-worker agent entry.")
2845
+ });
2846
+ var SpinAgentSettingsSchema = z.object({
2847
+ model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for spin orchestrator"),
2848
+ swarmModel: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Model for the spin orchestrator when running in swarm mode. Overrides spin.model for swarm workflows.")
2849
+ });
2850
+ var PlanCommandSettingsSchema = z.object({
2851
+ model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for plan command"),
2852
+ planner: z.enum(["claude", "gemini", "codex"]).default("claude").describe("AI provider for creating the plan"),
2853
+ reviewer: z.enum(["claude", "gemini", "codex", "none"]).default("none").describe("AI provider for reviewing the plan (none to skip review)")
2854
+ });
2855
+ var SummarySettingsSchema = z.object({
2856
+ model: z.enum(["sonnet", "opus", "haiku"]).default("sonnet").describe("Claude model shorthand for session summary generation")
2857
+ });
2858
+ var WorkflowPermissionSchema = z.object({
2859
+ permissionMode: z.enum(["plan", "acceptEdits", "bypassPermissions", "default"]).optional().describe("Permission mode for Claude CLI in this workflow type"),
2860
+ noVerify: z.boolean().optional().describe("Skip pre-commit hooks (--no-verify) when committing during commit and finish workflows"),
2861
+ startIde: z.boolean().default(true).describe("Launch IDE (code) when starting this workflow type"),
2862
+ startDevServer: z.boolean().default(true).describe("Launch development server when starting this workflow type"),
2863
+ startAiAgent: z.boolean().default(true).describe("Launch Claude Code agent when starting this workflow type"),
2864
+ startTerminal: z.boolean().default(false).describe("Launch terminal window without dev server when starting this workflow type"),
2865
+ generateSummary: z.boolean().default(true).describe("Generate and post Claude session summary when finishing this workflow type")
2866
+ });
2867
+ var WorkflowPermissionSchemaNoDefaults = z.object({
2868
+ permissionMode: z.enum(["plan", "acceptEdits", "bypassPermissions", "default"]).optional().describe("Permission mode for Claude CLI in this workflow type"),
2869
+ noVerify: z.boolean().optional().describe("Skip pre-commit hooks (--no-verify) when committing during commit and finish workflows"),
2870
+ startIde: z.boolean().optional().describe("Launch IDE (code) when starting this workflow type"),
2871
+ startDevServer: z.boolean().optional().describe("Launch development server when starting this workflow type"),
2872
+ startAiAgent: z.boolean().optional().describe("Launch Claude Code agent when starting this workflow type"),
2873
+ startTerminal: z.boolean().optional().describe("Launch terminal window without dev server when starting this workflow type"),
2874
+ generateSummary: z.boolean().optional().describe("Generate and post Claude session summary when finishing this workflow type")
2875
+ });
2876
+ var WorkflowsSettingsSchema = z.object({
2877
+ issue: WorkflowPermissionSchema.optional(),
2878
+ pr: WorkflowPermissionSchema.optional(),
2879
+ regular: WorkflowPermissionSchema.optional()
2880
+ }).optional();
2881
+ var WorkflowsSettingsSchemaNoDefaults = z.object({
2882
+ issue: WorkflowPermissionSchemaNoDefaults.optional(),
2883
+ pr: WorkflowPermissionSchemaNoDefaults.optional(),
2884
+ regular: WorkflowPermissionSchemaNoDefaults.optional()
2885
+ }).optional();
2886
+ var CapabilitiesSettingsSchema = z.object({
2887
+ web: z.object({
2888
+ basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
2889
+ }).optional().describe('Web dev server settings. To declare a project as a web project, add "web" to the capabilities array in .iloom/package.iloom.json or .iloom/package.iloom.local.json.'),
2890
+ database: z.object({
2891
+ databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().default("DATABASE_URL").describe("Name of environment variable for database connection URL")
2892
+ }).optional()
2893
+ }).optional();
2894
+ var CapabilitiesSettingsSchemaNoDefaults = z.object({
2895
+ web: z.object({
2896
+ basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
2897
+ }).optional().describe('Web dev server settings. To declare a project as a web project, add "web" to the capabilities array in .iloom/package.iloom.json or .iloom/package.iloom.local.json.'),
2898
+ database: z.object({
2899
+ databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().describe("Name of environment variable for database connection URL")
2900
+ }).optional()
2901
+ }).optional();
2902
+ var NeonSettingsSchema = z.object({
2903
+ projectId: z.string().min(1).regex(/^[a-zA-Z0-9-]+$/, "Neon project ID must contain only letters, numbers, and hyphens").describe('Neon project ID found in your project URL (e.g., "fantastic-fox-3566354")'),
2904
+ parentBranch: z.string().min(1).describe("Branch from which new database branches are created")
2905
+ });
2906
+ var DatabaseProvidersSettingsSchema = z.object({
2907
+ neon: NeonSettingsSchema.optional().describe(
2908
+ "Neon database configuration. Requires Neon CLI installed and authenticated for database branching."
2909
+ )
2910
+ }).optional();
2911
+ var IloomSettingsSchema = z.object({
2912
+ mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
2913
+ sourceEnvOnStart: z.boolean().default(false).describe(
2914
+ "Source all dotenv-flow files (.env, .env.local, .env.development, .env.development.local) when launching terminal processes (Claude, dev server, terminal). Files are sourced in precedence order so later files override earlier ones. NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env file compatibility. WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. Before enabling, verify ALL your .env.* files do not contain unquoted special characters (e.g., database URLs with ?, &, or other shell metacharacters). Shell compatibility issues may cause processes to fail or behave unexpectedly."
2915
+ ),
2916
+ worktreePrefix: z.string().optional().refine(
2917
+ (val) => {
2918
+ if (val === void 0) return true;
2919
+ if (val === "") return true;
2920
+ const allowedChars = /^[a-zA-Z0-9\-_/]+$/;
2921
+ if (!allowedChars.test(val)) return false;
2922
+ if (/^[-_/]+$/.test(val)) return false;
2923
+ const segments = val.split("/");
2924
+ for (const segment of segments) {
2925
+ if (segment && /^[-_]+$/.test(segment)) {
2926
+ return false;
2927
+ }
2928
+ }
2929
+ return true;
2930
+ },
2931
+ {
2932
+ message: "worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories."
2933
+ }
2934
+ ).describe(
2935
+ "Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set."
2936
+ ),
2937
+ protectedBranches: z.array(z.string().min(1, "Protected branch name cannot be empty")).optional().describe('List of branches that cannot be deleted (defaults to [mainBranch, "main", "master", "develop"])'),
2938
+ copyGitIgnoredPatterns: z.array(z.string().min(1, "Pattern cannot be empty")).optional().describe(`Glob patterns for gitignored files to copy to looms (e.g., ["*.db", "data/*.sqlite"]). Great for local dbs and large test data files that are too big to commit to git. Note: .env (dotenv-flow) files, iloom's and claude's local settings are automatically copied and do not need to be specified here.`),
2939
+ workflows: WorkflowsSettingsSchema.describe("Per-workflow-type permission configurations"),
2940
+ agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
2941
+ "Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting), iloom-swarm-worker (swarm worker agent, dynamically generated). Use swarmModel on any agent to override its model in swarm mode."
2942
+ ),
2943
+ spin: SpinAgentSettingsSchema.optional().describe(
2944
+ "Spin orchestrator configuration. Model defaults to opus when not configured."
2945
+ ),
2946
+ plan: PlanCommandSettingsSchema.optional().describe(
2947
+ "Plan command configuration. Model defaults to opus, planner to claude, reviewer to none when not configured."
2948
+ ),
2949
+ summary: SummarySettingsSchema.optional().describe(
2950
+ "Session summary generation configuration. Model defaults to sonnet when not configured."
2951
+ ),
2952
+ capabilities: CapabilitiesSettingsSchema.describe("Project capability configurations"),
2953
+ databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
2954
+ issueManagement: z.object({
2955
+ // SYNC: If this default changes, update displayDefaultsBox() in src/utils/first-run-setup.ts
2956
+ provider: z.enum(["github", "linear", "jira"]).optional().default("github").describe("Issue tracker provider (github, linear, jira)"),
2957
+ github: z.object({
2958
+ remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
2959
+ }).optional(),
2960
+ linear: z.object({
2961
+ teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
2962
+ branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
2963
+ apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
2964
+ }).optional(),
2965
+ jira: z.object({
2966
+ host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
2967
+ username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
2968
+ apiToken: z.string().optional().describe("Jira API token. SECURITY: Store in settings.local.json only, never commit to source control. Generate at: https://id.atlassian.com/manage-profile/security/api-tokens"),
2969
+ projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
2970
+ boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
2971
+ transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
2972
+ defaultIssueType: z.string().min(1).optional().default("Task").describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
2973
+ defaultSubtaskType: z.string().min(1).optional().default("Subtask").describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
2974
+ doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
2975
+ }).optional()
2976
+ }).optional().describe("Issue management configuration"),
2977
+ mergeBehavior: z.object({
2978
+ // SYNC: If this default changes, update displayDefaultsBox() in src/utils/first-run-setup.ts
2979
+ mode: z.enum(["local", "github-pr", "github-draft-pr"]).default("local"),
2980
+ remote: z.string().optional(),
2981
+ autoCommitPush: z.boolean().optional().describe(
2982
+ "Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
2983
+ ),
2984
+ openBrowserOnFinish: z.boolean().default(true).describe(
2985
+ "Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
2986
+ )
2987
+ }).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
2988
+ ide: z.object({
2989
+ // SYNC: If this default changes, update displayDefaultsBox() in src/utils/first-run-setup.ts
2990
+ type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf", "antigravity"]).default("vscode").describe(
2991
+ "IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor), antigravity (Antigravity IDE)."
2992
+ )
2993
+ }).optional().describe(
2994
+ "IDE configuration for workspace launches. Controls which editor opens when you start a loom. Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, Windsurf, and Antigravity. Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf, antigravity)."
2995
+ ),
2996
+ colors: z.object({
2997
+ terminal: z.boolean().default(true).describe("Apply terminal background colors based on branch name (macOS only)"),
2998
+ vscode: z.boolean().default(false).describe(
2999
+ "Apply VSCode/Cursor title bar colors based on branch name. Note: This modifies .vscode/settings.json which may be in source control. Default is false for safety; enable via init or explicitly if .vscode is gitignored."
3000
+ )
3001
+ }).optional().describe("Color synchronization settings for workspace identification"),
3002
+ attribution: z.enum(["off", "upstreamOnly", "on"]).default("upstreamOnly").describe(
3003
+ 'Controls when iloom attribution appears in session summaries. "off" - never show attribution. "upstreamOnly" - only show for contributions to external repositories (e.g., open source). "on" - always show attribution.'
3004
+ ),
3005
+ git: z.object({
3006
+ commitTimeout: z.number().min(1e3, "Commit timeout must be at least 1000ms").max(6e5, "Commit timeout cannot exceed 600000ms (10 minutes)").default(6e4).describe("Timeout in milliseconds for git commit operations. Increase for long-running pre-commit hooks.")
3007
+ }).default({}).describe("Git operation settings")
3008
+ });
3009
+ var IloomSettingsSchemaNoDefaults = z.object({
3010
+ mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
3011
+ sourceEnvOnStart: z.boolean().optional().describe(
3012
+ "Source all dotenv-flow files (.env, .env.local, .env.development, .env.development.local) when launching terminal processes (Claude, dev server, terminal). Files are sourced in precedence order so later files override earlier ones. NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env compatibility. WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. Before enabling, verify ALL your .env.* files do not contain unquoted special characters (e.g., database URLs with ?, &, or other shell metacharacters). Shell compatibility issues may cause processes to fail or behave unexpectedly."
3013
+ ),
3014
+ worktreePrefix: z.string().optional().refine(
3015
+ (val) => {
3016
+ if (val === void 0) return true;
3017
+ if (val === "") return true;
3018
+ const allowedChars = /^[a-zA-Z0-9\-_/]+$/;
3019
+ if (!allowedChars.test(val)) return false;
3020
+ if (/^[-_/]+$/.test(val)) return false;
3021
+ const segments = val.split("/");
3022
+ for (const segment of segments) {
3023
+ if (segment && /^[-_]+$/.test(segment)) {
3024
+ return false;
3025
+ }
3026
+ }
3027
+ return true;
3028
+ },
3029
+ {
3030
+ message: "worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories."
3031
+ }
3032
+ ).describe(
3033
+ "Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set."
3034
+ ),
3035
+ protectedBranches: z.array(z.string().min(1, "Protected branch name cannot be empty")).optional().describe('List of branches that cannot be deleted (defaults to [mainBranch, "main", "master", "develop"])'),
3036
+ copyGitIgnoredPatterns: z.array(z.string().min(1, "Pattern cannot be empty")).optional().describe(`Glob patterns for gitignored files to copy to looms (e.g., ["*.db", "data/*.sqlite"]). Great for local dbs and large test data files that are too big to commit to git. Note: .env (dotenv-flow) files, iloom's and claude's local settings are automatically copied and do not need to be specified here.`),
3037
+ workflows: WorkflowsSettingsSchemaNoDefaults.describe("Per-workflow-type permission configurations"),
3038
+ agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
3039
+ "Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting), iloom-swarm-worker (swarm worker agent, dynamically generated). Use swarmModel on any agent to override its model in swarm mode."
3040
+ ),
3041
+ spin: z.object({
3042
+ model: z.enum(["sonnet", "opus", "haiku"]).optional(),
3043
+ swarmModel: z.enum(["sonnet", "opus", "haiku"]).optional()
3044
+ }).optional().describe("Spin orchestrator configuration"),
3045
+ plan: z.object({
3046
+ model: z.enum(["sonnet", "opus", "haiku"]).optional(),
3047
+ planner: z.enum(["claude", "gemini", "codex"]).optional(),
3048
+ reviewer: z.enum(["claude", "gemini", "codex", "none"]).optional()
3049
+ }).optional().describe("Plan command configuration"),
3050
+ summary: z.object({
3051
+ model: z.enum(["sonnet", "opus", "haiku"]).optional()
3052
+ }).optional().describe("Session summary generation configuration"),
3053
+ capabilities: CapabilitiesSettingsSchemaNoDefaults.describe("Project capability configurations"),
3054
+ databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
3055
+ issueManagement: z.object({
3056
+ provider: z.enum(["github", "linear", "jira"]).optional().describe("Issue tracker provider (github, linear, jira)"),
3057
+ github: z.object({
3058
+ remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
3059
+ }).optional(),
3060
+ linear: z.object({
3061
+ teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
3062
+ branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
3063
+ apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
3064
+ }).optional(),
3065
+ jira: z.object({
3066
+ host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
3067
+ username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
3068
+ apiToken: z.string().optional().describe("Jira API token. SECURITY: Store in settings.local.json only, never commit to source control. Generate at: https://id.atlassian.com/manage-profile/security/api-tokens"),
3069
+ projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
3070
+ boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
3071
+ transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
3072
+ defaultIssueType: z.string().min(1).optional().describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
3073
+ defaultSubtaskType: z.string().min(1).optional().describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
3074
+ doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
3075
+ }).optional()
3076
+ }).optional().describe("Issue management configuration"),
3077
+ mergeBehavior: z.object({
3078
+ mode: z.enum(["local", "github-pr", "github-draft-pr"]).optional(),
3079
+ remote: z.string().optional(),
3080
+ autoCommitPush: z.boolean().optional().describe(
3081
+ "Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
3082
+ ),
3083
+ openBrowserOnFinish: z.boolean().optional().describe(
3084
+ "Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
3085
+ )
3086
+ }).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
3087
+ ide: z.object({
3088
+ type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf", "antigravity"]).optional().describe(
3089
+ "IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor), antigravity (Antigravity IDE)."
3090
+ )
3091
+ }).optional().describe(
3092
+ "IDE configuration for workspace launches. Controls which editor opens when you start a loom. Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, Windsurf, and Antigravity. Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf, antigravity)."
3093
+ ),
3094
+ colors: z.object({
3095
+ terminal: z.boolean().optional().describe("Apply terminal background colors based on branch name (macOS only)"),
3096
+ vscode: z.boolean().optional().describe(
3097
+ "Apply VSCode/Cursor title bar colors based on branch name. Note: This modifies .vscode/settings.json which may be in source control."
3098
+ )
3099
+ }).optional().describe("Color synchronization settings for workspace identification"),
3100
+ attribution: z.enum(["off", "upstreamOnly", "on"]).optional().describe(
3101
+ 'Controls when iloom attribution appears in session summaries. "off" - never show attribution. "upstreamOnly" - only show for contributions to external repositories (e.g., open source). "on" - always show attribution.'
3102
+ ),
3103
+ git: z.object({
3104
+ commitTimeout: z.number().min(1e3, "Commit timeout must be at least 1000ms").max(6e5, "Commit timeout cannot exceed 600000ms (10 minutes)").optional().describe("Timeout in milliseconds for git commit operations. Increase for long-running pre-commit hooks.")
3105
+ }).optional().describe("Git operation settings")
3106
+ });
3107
+ function redactSensitiveFields(obj) {
3108
+ if (obj === null || obj === void 0) return obj;
3109
+ if (typeof obj !== "object") return obj;
3110
+ if (Array.isArray(obj)) return obj.map(redactSensitiveFields);
3111
+ const sensitiveKeys = ["apitoken", "token", "secret", "password"];
3112
+ const result = {};
3113
+ for (const [key, value] of Object.entries(obj)) {
3114
+ const lowerKey = key.toLowerCase();
3115
+ if (sensitiveKeys.some((s) => lowerKey.includes(s)) && typeof value === "string") {
3116
+ result[key] = "[REDACTED]";
3117
+ } else if (typeof value === "object" && value !== null) {
3118
+ result[key] = redactSensitiveFields(value);
3119
+ } else {
3120
+ result[key] = value;
3121
+ }
3122
+ }
3123
+ return result;
3124
+ }
3125
+ var SettingsManager = class {
3126
+ /**
3127
+ * Load settings from global, project, and local sources with proper precedence
3128
+ * Merge hierarchy (lowest to highest priority):
3129
+ * 1. Global settings (~/.config/iloom-ai/settings.json)
3130
+ * 2. Project settings (<PROJECT_ROOT>/.iloom/settings.json)
3131
+ * 3. Local settings (<PROJECT_ROOT>/.iloom/settings.local.json)
3132
+ * 4. CLI overrides (--set flags)
3133
+ * Returns empty object if all files don't exist (not an error)
3134
+ */
3135
+ async loadSettings(projectRoot, cliOverrides) {
3136
+ const root = this.getProjectRoot(projectRoot);
3137
+ const globalSettings = await this.loadGlobalSettingsFile();
3138
+ const globalSettingsPath = this.getGlobalSettingsPath();
3139
+ logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(redactSensitiveFields(globalSettings), null, 2));
3140
+ const baseSettings = await this.loadSettingsFile(root, "settings.json");
3141
+ const baseSettingsPath = path2.join(root, ".iloom", "settings.json");
3142
+ logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(redactSensitiveFields(baseSettings), null, 2));
3143
+ const localSettings = await this.loadSettingsFile(root, "settings.local.json");
3144
+ const localSettingsPath = path2.join(root, ".iloom", "settings.local.json");
3145
+ logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(redactSensitiveFields(localSettings), null, 2));
3146
+ let merged = this.mergeSettings(this.mergeSettings(globalSettings, baseSettings), localSettings);
3147
+ logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(redactSensitiveFields(merged), null, 2));
3148
+ if (cliOverrides && Object.keys(cliOverrides).length > 0) {
3149
+ logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(redactSensitiveFields(cliOverrides), null, 2));
3150
+ merged = this.mergeSettings(merged, cliOverrides);
3151
+ logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(redactSensitiveFields(merged), null, 2));
3152
+ }
3153
+ try {
3154
+ const finalSettings = IloomSettingsSchema.parse(merged);
3155
+ this.logFinalConfiguration(finalSettings);
3156
+ return finalSettings;
3157
+ } catch (error) {
3158
+ if (error instanceof z.ZodError) {
3159
+ const errorMsg = this.formatAllZodErrors(error, "<merged settings>");
3160
+ if (cliOverrides && Object.keys(cliOverrides).length > 0) {
3161
+ throw new Error(`${errorMsg.message}
3162
+
3163
+ Note: CLI overrides were applied. Check your --set arguments.`);
3164
+ }
3165
+ throw errorMsg;
3166
+ }
3167
+ throw error;
3168
+ }
3169
+ }
3170
+ /**
3171
+ * Log the final merged configuration for debugging
3172
+ */
3173
+ logFinalConfiguration(settings2) {
3174
+ logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(redactSensitiveFields(settings2), null, 2));
3175
+ }
3176
+ /**
3177
+ * Load and parse a single settings file
3178
+ * Returns empty object if file doesn't exist (not an error)
3179
+ * Uses non-defaulting schema to prevent polluting partial settings with defaults before merge
3180
+ */
3181
+ async loadSettingsFile(projectRoot, filename) {
3182
+ const settingsPath = path2.join(projectRoot, ".iloom", filename);
3183
+ try {
3184
+ const content = await readFile(settingsPath, "utf-8");
3185
+ let parsed;
3186
+ try {
3187
+ parsed = JSON.parse(content);
3188
+ } catch (error) {
3189
+ throw new Error(
3190
+ `Failed to parse settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}`
3191
+ );
3192
+ }
3193
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3194
+ throw new Error(
3195
+ `Settings validation failed at ${filename}:
3196
+ - root: Expected object, received ${typeof parsed}`
3197
+ );
3198
+ }
3199
+ return parsed;
3200
+ } catch (error) {
3201
+ if (error.code === "ENOENT") {
3202
+ logger.debug(`No settings file found at ${settingsPath}, using defaults`);
3203
+ return {};
3204
+ }
3205
+ throw error;
3206
+ }
3207
+ }
3208
+ /**
3209
+ * Deep merge two settings objects with priority to override
3210
+ * Uses deepmerge library with array replacement strategy
3211
+ */
3212
+ mergeSettings(base, override) {
3213
+ return deepmerge(base, override, {
3214
+ // Replace arrays instead of concatenating them
3215
+ arrayMerge: (_destinationArray, sourceArray) => sourceArray
3216
+ });
3217
+ }
3218
+ /**
3219
+ * Format all Zod validation errors into a single error message
3220
+ */
3221
+ formatAllZodErrors(error, settingsPath) {
3222
+ const errorMessages = error.issues.map((issue) => {
3223
+ const path3 = issue.path.length > 0 ? issue.path.join(".") : "root";
3224
+ return ` - ${path3}: ${issue.message}`;
3225
+ });
3226
+ return new Error(
3227
+ `Settings validation failed at ${settingsPath}:
3228
+ ${errorMessages.join("\n")}`
3229
+ );
3230
+ }
3231
+ /**
3232
+ * Validate settings structure and model names using Zod schema
3233
+ * This method is kept for testing purposes but uses Zod internally
3234
+ * @internal - Only used in tests via bracket notation
3235
+ */
3236
+ // @ts-expect-error - Used in tests via bracket notation, TypeScript can't detect this usage
3237
+ validateSettings(settings2) {
3238
+ try {
3239
+ IloomSettingsSchema.parse(settings2);
3240
+ } catch (error) {
3241
+ if (error instanceof z.ZodError) {
3242
+ throw this.formatAllZodErrors(error, "<validation>");
3243
+ }
3244
+ throw error;
3245
+ }
3246
+ }
3247
+ /**
3248
+ * Get project root (defaults to process.cwd())
3249
+ */
3250
+ getProjectRoot(projectRoot) {
3251
+ return projectRoot ?? process.cwd();
3252
+ }
3253
+ /**
3254
+ * Get global config directory path (~/.config/iloom-ai)
3255
+ */
3256
+ getGlobalConfigDir() {
3257
+ return path2.join(os2.homedir(), ".config", "iloom-ai");
3258
+ }
3259
+ /**
3260
+ * Get global settings file path (~/.config/iloom-ai/settings.json)
3261
+ */
3262
+ getGlobalSettingsPath() {
3263
+ return path2.join(this.getGlobalConfigDir(), "settings.json");
3264
+ }
3265
+ /**
3266
+ * Load and parse global settings file
3267
+ * Returns empty object if file doesn't exist (not an error)
3268
+ * Warns but returns empty object on validation/parse errors (graceful degradation)
3269
+ */
3270
+ async loadGlobalSettingsFile() {
3271
+ const settingsPath = this.getGlobalSettingsPath();
3272
+ try {
3273
+ const content = await readFile(settingsPath, "utf-8");
3274
+ let parsed;
3275
+ try {
3276
+ parsed = JSON.parse(content);
3277
+ } catch (error) {
3278
+ logger.warn(
3279
+ `Failed to parse global settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}. Ignoring global settings.`
3280
+ );
3281
+ return {};
3282
+ }
3283
+ try {
3284
+ const validated = IloomSettingsSchemaNoDefaults.strict().parse(parsed);
3285
+ return validated;
3286
+ } catch (error) {
3287
+ if (error instanceof z.ZodError) {
3288
+ const errorMsg = this.formatAllZodErrors(error, "global settings");
3289
+ logger.warn(`${errorMsg.message}. Ignoring global settings.`);
3290
+ } else {
3291
+ logger.warn(`Validation error in global settings: ${error instanceof Error ? error.message : "Unknown error"}. Ignoring global settings.`);
3292
+ }
3293
+ return {};
3294
+ }
3295
+ } catch (error) {
3296
+ if (error.code === "ENOENT") {
3297
+ logger.debug(`No global settings file found at ${settingsPath}`);
3298
+ return {};
3299
+ }
3300
+ logger.warn(`Error reading global settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Unknown error"}. Ignoring global settings.`);
3301
+ return {};
3302
+ }
3303
+ }
3304
+ /**
3305
+ * Get effective protected branches list with mainBranch always included
3306
+ *
3307
+ * This method provides a single source of truth for protected branches logic:
3308
+ * 1. Use configured protectedBranches if provided
3309
+ * 2. Otherwise use defaults: [mainBranch, 'main', 'master', 'develop']
3310
+ * 3. ALWAYS ensure mainBranch is included even if user configured custom list
3311
+ *
3312
+ * @param projectRoot - Optional project root directory (defaults to process.cwd())
3313
+ * @returns Array of protected branch names with mainBranch guaranteed to be included
3314
+ */
3315
+ async getProtectedBranches(projectRoot) {
3316
+ const settings2 = await this.loadSettings(projectRoot);
3317
+ const mainBranch = settings2.mainBranch ?? "main";
3318
+ let protectedBranches;
3319
+ if (settings2.protectedBranches) {
3320
+ protectedBranches = settings2.protectedBranches.includes(mainBranch) ? settings2.protectedBranches : [mainBranch, ...settings2.protectedBranches];
3321
+ } else {
3322
+ protectedBranches = [mainBranch, "main", "master", "develop"];
3323
+ }
3324
+ return protectedBranches;
3325
+ }
3326
+ /**
3327
+ * Get the spin orchestrator model with default applied
3328
+ * Default is defined in SpinAgentSettingsSchema
3329
+ *
3330
+ * @param settings - Pre-loaded settings object
3331
+ * @returns Model shorthand ('opus', 'sonnet', or 'haiku')
3332
+ */
3333
+ getSpinModel(settings2, mode) {
3334
+ var _a, _b;
3335
+ if (mode === "swarm") {
3336
+ if ((_a = settings2 == null ? void 0 : settings2.spin) == null ? void 0 : _a.swarmModel) {
3337
+ return settings2.spin.swarmModel;
3338
+ }
3339
+ return "opus";
3340
+ }
3341
+ return ((_b = settings2 == null ? void 0 : settings2.spin) == null ? void 0 : _b.model) ?? SpinAgentSettingsSchema.parse({}).model;
3342
+ }
3343
+ /**
3344
+ * Get the plan command model with default applied
3345
+ * Default is defined in PlanCommandSettingsSchema
3346
+ *
3347
+ * @param settings - Pre-loaded settings object
3348
+ * @returns Model shorthand ('opus', 'sonnet', or 'haiku')
3349
+ */
3350
+ getPlanModel(settings2) {
3351
+ var _a;
3352
+ return ((_a = settings2 == null ? void 0 : settings2.plan) == null ? void 0 : _a.model) ?? PlanCommandSettingsSchema.parse({}).model;
3353
+ }
3354
+ /**
3355
+ * Get the plan command planner with default applied
3356
+ * Default is 'claude'
3357
+ *
3358
+ * @param settings - Pre-loaded settings object
3359
+ * @returns Planner provider ('claude', 'gemini', or 'codex')
3360
+ */
3361
+ getPlanPlanner(settings2) {
3362
+ var _a;
3363
+ return ((_a = settings2 == null ? void 0 : settings2.plan) == null ? void 0 : _a.planner) ?? "claude";
3364
+ }
3365
+ /**
3366
+ * Get the plan command reviewer with default applied
3367
+ * Default is 'none' (no review step)
3368
+ *
3369
+ * @param settings - Pre-loaded settings object
3370
+ * @returns Reviewer provider ('claude', 'gemini', 'codex', or 'none')
3371
+ */
3372
+ getPlanReviewer(settings2) {
3373
+ var _a;
3374
+ return ((_a = settings2 == null ? void 0 : settings2.plan) == null ? void 0 : _a.reviewer) ?? "none";
3375
+ }
3376
+ /**
3377
+ * Get the session summary model with default applied
3378
+ * Default is defined in SummarySettingsSchema
3379
+ *
3380
+ * @param settings - Pre-loaded settings object
3381
+ * @returns Model shorthand ('opus', 'sonnet', or 'haiku')
3382
+ */
3383
+ getSummaryModel(settings2) {
3384
+ var _a;
3385
+ return ((_a = settings2 == null ? void 0 : settings2.summary) == null ? void 0 : _a.model) ?? SummarySettingsSchema.parse({}).model;
3386
+ }
3387
+ };
3388
+
3389
+ // src/mcp/JiraIssueManagementProvider.ts
3390
+ function normalizeAuthor2(author) {
3391
+ if (!author) return null;
3392
+ return {
3393
+ id: author.accountId ?? author.emailAddress ?? "unknown",
3394
+ displayName: author.displayName ?? author.emailAddress ?? "Unknown",
3395
+ ...author.emailAddress && { email: author.emailAddress },
3396
+ ...author.accountId && { accountId: author.accountId }
3397
+ };
3398
+ }
3399
+ var getJiraTrackerConfig = (settings2) => {
3400
+ var _a;
3401
+ const jiraSettings = (_a = settings2.issueManagement) == null ? void 0 : _a.jira;
3402
+ if ((jiraSettings == null ? void 0 : jiraSettings.host) && (jiraSettings == null ? void 0 : jiraSettings.username) && (jiraSettings == null ? void 0 : jiraSettings.apiToken) && (jiraSettings == null ? void 0 : jiraSettings.projectKey)) {
3403
+ const config = {
3404
+ host: jiraSettings.host,
3405
+ username: jiraSettings.username,
3406
+ apiToken: jiraSettings.apiToken,
3407
+ projectKey: jiraSettings.projectKey
3408
+ };
3409
+ if (jiraSettings.transitionMappings) {
3410
+ config.transitionMappings = jiraSettings.transitionMappings;
3411
+ }
3412
+ if (jiraSettings.defaultIssueType) {
3413
+ config.defaultIssueType = jiraSettings.defaultIssueType;
3414
+ }
3415
+ if (jiraSettings.defaultSubtaskType) {
3416
+ config.defaultSubtaskType = jiraSettings.defaultSubtaskType;
3417
+ }
3418
+ return config;
3419
+ }
3420
+ if (process.env.JIRA_HOST && process.env.JIRA_USERNAME && process.env.JIRA_API_TOKEN && process.env.JIRA_PROJECT_KEY) {
3421
+ const config = {
3422
+ host: process.env.JIRA_HOST,
3423
+ username: process.env.JIRA_USERNAME,
3424
+ apiToken: process.env.JIRA_API_TOKEN,
3425
+ projectKey: process.env.JIRA_PROJECT_KEY
3426
+ };
3427
+ if (process.env.JIRA_TRANSITION_MAPPINGS) {
3428
+ try {
3429
+ config.transitionMappings = JSON.parse(process.env.JIRA_TRANSITION_MAPPINGS);
3430
+ } catch {
3431
+ throw new Error("Invalid JSON in JIRA_TRANSITION_MAPPINGS environment variable");
3432
+ }
3433
+ }
3434
+ if (process.env.JIRA_DEFAULT_ISSUE_TYPE) {
3435
+ config.defaultIssueType = process.env.JIRA_DEFAULT_ISSUE_TYPE;
3436
+ }
3437
+ if (process.env.JIRA_DEFAULT_SUBTASK_TYPE) {
3438
+ config.defaultSubtaskType = process.env.JIRA_DEFAULT_SUBTASK_TYPE;
3439
+ }
3440
+ return config;
3441
+ }
3442
+ throw new Error(
3443
+ "Missing required Jira settings: issueManagement.jira.{host, username, apiToken, projectKey} or corresponding environment variables"
3444
+ );
3445
+ };
3446
+ var JiraIssueManagementProvider = class _JiraIssueManagementProvider {
3447
+ constructor(settings2) {
3448
+ this.providerName = "jira";
3449
+ this.issuePrefix = "";
3450
+ const config = getJiraTrackerConfig(settings2);
3451
+ this.tracker = new JiraIssueTracker(config);
3452
+ this.projectKey = config.projectKey;
3453
+ }
3454
+ /**
3455
+ * Static factory for convenience when settings aren't pre-loaded
3456
+ */
3457
+ static async create() {
3458
+ const settingsManager = new SettingsManager();
3459
+ const settings2 = await settingsManager.loadSettings();
3460
+ return new _JiraIssueManagementProvider(settings2);
3461
+ }
3462
+ /**
3463
+ * Fetch issue details using JiraIssueTracker
3464
+ */
3465
+ async getIssue(input) {
3466
+ const { number, includeComments = true } = input;
3467
+ const issue = await this.tracker.getIssue(number);
3468
+ const issueExt = issue;
3469
+ const result = {
3470
+ id: issueExt.id ?? String(issue.number),
3471
+ title: issue.title,
3472
+ body: issue.body,
3473
+ state: issue.state,
3474
+ url: issue.url,
3475
+ provider: "jira",
3476
+ author: normalizeAuthor2(issueExt.author),
3477
+ number: issue.number,
3478
+ key: issueExt.key,
3479
+ // Preserve Jira-specific fields
3480
+ ...issueExt.issueType && { issueType: issueExt.issueType },
3481
+ ...issueExt.priority && { priority: issueExt.priority },
3482
+ ...issueExt.status && { status: issueExt.status }
3483
+ };
3484
+ if (issue.labels && issue.labels.length > 0) {
3485
+ result.labels = issue.labels.map((label) => ({ name: label }));
3486
+ }
3487
+ if (issue.assignees && issue.assignees.length > 0) {
3488
+ result.assignees = issue.assignees.map((name) => ({
3489
+ id: name,
3490
+ displayName: name
3491
+ }));
3492
+ }
3493
+ if (includeComments) {
3494
+ const comments = await this.tracker.getComments(number);
3495
+ result.comments = comments.map((comment) => ({
3496
+ id: comment.id,
3497
+ body: comment.body,
3498
+ author: normalizeAuthor2(comment.author),
3499
+ createdAt: comment.createdAt,
3500
+ updatedAt: comment.updatedAt
3501
+ }));
3502
+ }
3503
+ return result;
3504
+ }
3505
+ /**
3506
+ * Fetch a specific comment by ID
3507
+ */
3508
+ async getComment(input) {
3509
+ const { commentId, number } = input;
3510
+ const comments = await this.tracker.getComments(number);
3511
+ const comment = comments.find((c) => c.id === commentId);
3512
+ if (!comment) {
3513
+ throw new Error(`Comment ${commentId} not found on issue ${number}`);
3514
+ }
3515
+ return {
3516
+ id: comment.id,
3517
+ body: comment.body,
3518
+ author: normalizeAuthor2(comment.author),
3519
+ created_at: comment.createdAt,
3520
+ updated_at: comment.updatedAt
3521
+ };
3522
+ }
3523
+ /**
3524
+ * Create a new comment on an issue
3525
+ */
3526
+ async createComment(input) {
3527
+ const { number, body } = input;
3528
+ const normalizedKey = this.tracker.normalizeIdentifier(number);
3529
+ const comment = await this.tracker.addComment(normalizedKey, body);
3530
+ return {
3531
+ id: comment.id,
3532
+ url: `${this.tracker.getConfig().host}/browse/${normalizedKey}?focusedCommentId=${comment.id}`,
3533
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3534
+ };
3535
+ }
3536
+ /**
3537
+ * Update an existing comment
3538
+ */
3539
+ async updateComment(input) {
3540
+ const { commentId, number, body } = input;
3541
+ const normalizedKey = this.tracker.normalizeIdentifier(number);
3542
+ await this.tracker.updateComment(normalizedKey, commentId, body);
3543
+ return {
3544
+ id: commentId,
3545
+ url: `${this.tracker.getConfig().host}/browse/${normalizedKey}?focusedCommentId=${commentId}`,
3546
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
3547
+ };
3548
+ }
3549
+ /**
3550
+ * Create a new issue
3551
+ */
3552
+ async createIssue(input) {
3553
+ const { title, body } = input;
3554
+ const issue = await this.tracker.createIssue(title, body);
3555
+ const result = {
3556
+ id: String(issue.number),
3557
+ url: issue.url
3558
+ };
3559
+ if (typeof issue.number === "number") {
3560
+ result.number = issue.number;
3561
+ }
3562
+ return result;
3563
+ }
3564
+ /**
3565
+ * Fetch pull request details
3566
+ * Jira does not have pull requests - throw like Linear does
3567
+ */
3568
+ async getPR(_input) {
3569
+ throw new Error(
3570
+ "Jira does not support pull requests. PRs exist only on GitHub. Use the GitHub provider for PR operations."
3571
+ );
3572
+ }
3573
+ /**
3574
+ * Create a child issue linked to a parent issue
3575
+ * Uses Jira's parent field to create a subtask
3576
+ */
3577
+ async createChildIssue(input) {
3578
+ const { parentId, title, body } = input;
3579
+ const parentKey = this.tracker.normalizeIdentifier(parentId);
3580
+ const jiraIssue = await this.tracker.getApiClient().createIssueWithParent(
3581
+ this.projectKey,
3582
+ title,
3583
+ body,
3584
+ parentKey,
3585
+ this.tracker.getConfig().defaultSubtaskType
3586
+ );
3587
+ return {
3588
+ id: jiraIssue.key,
3589
+ url: `${this.tracker.getConfig().host}/browse/${jiraIssue.key}`
3590
+ };
3591
+ }
3592
+ /**
3593
+ * Create a blocking dependency between two issues
3594
+ * Uses Jira issue links with "Blocks" link type
3595
+ */
3596
+ async createDependency(input) {
3597
+ const blockingKey = this.tracker.normalizeIdentifier(input.blockingIssue);
3598
+ const blockedKey = this.tracker.normalizeIdentifier(input.blockedIssue);
3599
+ await this.tracker.getApiClient().createIssueLink(blockingKey, blockedKey, "Blocks");
3600
+ }
3601
+ /**
3602
+ * Get dependencies for an issue
3603
+ * Parses issue links of type "Blocks"
3604
+ */
3605
+ async getDependencies(input) {
3606
+ const issueKey = this.tracker.normalizeIdentifier(input.number);
3607
+ const host = this.tracker.getConfig().host;
3608
+ const issue = await this.tracker.getApiClient().getIssue(issueKey);
3609
+ const links = issue.fields.issuelinks ?? [];
3610
+ const blocking = [];
3611
+ const blockedBy = [];
3612
+ for (const link of links) {
3613
+ if (link.type.name !== "Blocks") continue;
3614
+ if (link.inwardIssue) {
3615
+ blockedBy.push({
3616
+ id: link.inwardIssue.key,
3617
+ title: link.inwardIssue.fields.summary,
3618
+ url: `${host}/browse/${link.inwardIssue.key}`,
3619
+ state: link.inwardIssue.fields.status.name.toLowerCase()
3620
+ });
3621
+ }
3622
+ if (link.outwardIssue) {
3623
+ blocking.push({
3624
+ id: link.outwardIssue.key,
3625
+ title: link.outwardIssue.fields.summary,
3626
+ url: `${host}/browse/${link.outwardIssue.key}`,
3627
+ state: link.outwardIssue.fields.status.name.toLowerCase()
3628
+ });
3629
+ }
3630
+ }
3631
+ if (input.direction === "blocking") {
3632
+ return { blocking, blockedBy: [] };
3633
+ }
3634
+ if (input.direction === "blocked_by") {
3635
+ return { blocking: [], blockedBy };
3636
+ }
3637
+ return { blocking, blockedBy };
3638
+ }
3639
+ /**
3640
+ * Remove a blocking dependency between two issues
3641
+ * Finds the matching "Blocks" link and deletes it
3642
+ */
3643
+ async removeDependency(input) {
3644
+ const blockingKey = this.tracker.normalizeIdentifier(input.blockingIssue);
3645
+ const blockedKey = this.tracker.normalizeIdentifier(input.blockedIssue);
3646
+ const issue = await this.tracker.getApiClient().getIssue(blockedKey);
3647
+ const links = issue.fields.issuelinks ?? [];
3648
+ const matchingLink = links.find(
3649
+ (link) => {
3650
+ var _a;
3651
+ return link.type.name === "Blocks" && ((_a = link.inwardIssue) == null ? void 0 : _a.key) === blockingKey;
3652
+ }
3653
+ );
3654
+ if (!matchingLink) {
3655
+ throw new Error(
3656
+ `No "Blocks" dependency found from ${blockingKey} to ${blockedKey}`
3657
+ );
3658
+ }
3659
+ await this.tracker.getApiClient().deleteIssueLink(matchingLink.id);
3660
+ }
3661
+ /**
3662
+ * Get child issues of a parent issue
3663
+ * Uses JQL search: parent = KEY
3664
+ */
3665
+ async getChildIssues(input) {
3666
+ const parentKey = this.tracker.normalizeIdentifier(input.number);
3667
+ const host = this.tracker.getConfig().host;
3668
+ const issues = await this.tracker.getApiClient().searchIssues(`parent = "${escapeJql(parentKey)}"`);
3669
+ return issues.map((issue) => ({
3670
+ id: issue.key,
3671
+ title: issue.fields.summary,
3672
+ url: `${host}/browse/${issue.key}`,
3673
+ state: issue.fields.status.name.toLowerCase()
3674
+ }));
3675
+ }
3676
+ /**
3677
+ * Close an issue by transitioning to "Done" state
3678
+ */
3679
+ async closeIssue(input) {
3680
+ const issueKey = this.tracker.normalizeIdentifier(input.number);
3681
+ await this.tracker.closeIssue(issueKey);
3682
+ }
3683
+ /**
3684
+ * Reopen a closed issue
3685
+ */
3686
+ async reopenIssue(input) {
3687
+ const issueKey = this.tracker.normalizeIdentifier(input.number);
3688
+ await this.tracker.reopenIssue(issueKey);
3689
+ }
3690
+ /**
3691
+ * Edit an issue's properties
3692
+ * State changes are delegated to closeIssue/reopenIssue
3693
+ */
3694
+ async editIssue(input) {
3695
+ const { number, title, body, state } = input;
3696
+ if (state === "closed") {
3697
+ await this.closeIssue({ number });
3698
+ } else if (state === "open") {
3699
+ await this.reopenIssue({ number });
3700
+ }
3701
+ if (title !== void 0 || body !== void 0) {
3702
+ const issueKey = this.tracker.normalizeIdentifier(number);
3703
+ await this.tracker.getApiClient().updateIssue(issueKey, {
3704
+ ...title !== void 0 && { summary: title },
3705
+ ...body !== void 0 && { description: body }
3706
+ });
3707
+ }
3708
+ }
3709
+ };
3710
+
3711
+ // src/mcp/IssueManagementProviderFactory.ts
3712
+ var IssueManagementProviderFactory = class {
3713
+ /**
3714
+ * Create an issue management provider based on the provider type
3715
+ * @param provider - The provider type (github, linear, jira)
3716
+ * @param settings - Required for Jira provider, optional for others
3717
+ */
3718
+ static create(provider, settings2) {
3719
+ switch (provider) {
3720
+ case "github":
3721
+ return new GitHubIssueManagementProvider();
3722
+ case "linear":
3723
+ return new LinearIssueManagementProvider();
3724
+ case "jira":
3725
+ if (!settings2) {
3726
+ throw new Error("Settings required for Jira provider");
3727
+ }
3728
+ return new JiraIssueManagementProvider(settings2);
3729
+ default:
3730
+ throw new Error(`Unsupported issue management provider: ${provider}`);
3731
+ }
3732
+ }
3733
+ };
3734
+
3735
+ // src/utils/jira-wiki-sanitizer.ts
3736
+ var JiraWikiSanitizer = class {
3737
+ /**
3738
+ * Sanitize body text by converting unambiguous Jira Wiki patterns to Markdown.
3739
+ * Preserves content inside backtick-fenced code blocks.
3740
+ * Returns text unchanged if no Wiki patterns detected.
3741
+ */
3742
+ static sanitize(text) {
3743
+ if (!text) {
3744
+ return "";
3745
+ }
3746
+ const segments = this.splitByCodeBlocks(text);
3747
+ const converted = segments.map((segment) => {
3748
+ if (segment.isCode) {
3749
+ return segment.text;
3750
+ }
3751
+ return this.convertSegment(segment.text);
3752
+ });
3753
+ return converted.join("");
3754
+ }
3755
+ /**
3756
+ * Check if text contains unambiguous Jira Wiki patterns.
3757
+ * Only checks for patterns that are safe to convert.
3758
+ */
3759
+ static hasJiraWikiPatterns(text) {
3760
+ if (!text) {
3761
+ return false;
3762
+ }
3763
+ if (/^h[1-6]\.\s+/m.test(text)) {
3764
+ return true;
3765
+ }
3766
+ if (/\{code(?::[^}]*)?\}/i.test(text)) {
3767
+ return true;
3768
+ }
3769
+ if (/\{quote\}/i.test(text)) {
3770
+ return true;
3771
+ }
3772
+ if (/\[[^\]|]+\|https?:\/\/[^\]]+\]/.test(text)) {
3773
+ return true;
3774
+ }
3775
+ return false;
3776
+ }
3777
+ /**
3778
+ * Split text into segments, separating existing Markdown fenced code blocks
3779
+ * from the rest of the content. This ensures we don't modify content inside
3780
+ * code blocks (e.g., Jira Wiki examples shown in a Markdown code block).
3781
+ */
3782
+ static splitByCodeBlocks(text) {
3783
+ const segments = [];
3784
+ const codeBlockRegex = /^(`{3,})[^\n]*\n[\s\S]*?^\1\s*$/gm;
3785
+ let lastIndex = 0;
3786
+ for (const match of text.matchAll(codeBlockRegex)) {
3787
+ const matchStart = match.index ?? 0;
3788
+ if (matchStart > lastIndex) {
3789
+ segments.push({ text: text.slice(lastIndex, matchStart), isCode: false });
3790
+ }
3791
+ segments.push({ text: match[0], isCode: true });
3792
+ lastIndex = matchStart + match[0].length;
3793
+ }
3794
+ if (lastIndex < text.length) {
3795
+ segments.push({ text: text.slice(lastIndex), isCode: false });
3796
+ }
3797
+ return segments;
3798
+ }
3799
+ /**
3800
+ * Apply all safe Jira Wiki -> Markdown conversions to a text segment.
3801
+ */
3802
+ static convertSegment(text) {
3803
+ let result = text;
3804
+ result = result.replace(/^h([1-6])\.\s+(.*?)$/gm, (_match, level, content) => {
3805
+ const hashes = "#".repeat(parseInt(level, 10));
3806
+ return `${hashes} ${content}`;
3807
+ });
3808
+ result = result.replace(
3809
+ /\{code:([^}]+)\}\s*\n([\s\S]*?)\n?\s*\{code\}/gi,
3810
+ (_match, lang, content) => {
3811
+ return "```" + lang.trim() + "\n" + content + "\n```";
3812
+ }
3813
+ );
3814
+ result = result.replace(
3815
+ /\{code\}\s*\n([\s\S]*?)\n?\s*\{code\}/gi,
3816
+ (_match, content) => {
3817
+ return "```\n" + content + "\n```";
3818
+ }
3819
+ );
3820
+ result = result.replace(
3821
+ /\{quote\}\s*\n([\s\S]*?)\n?\s*\{quote\}/gi,
3822
+ (_match, content) => {
3823
+ const lines = content.split("\n");
3824
+ return lines.map((line) => `> ${line}`).join("\n");
3825
+ }
3826
+ );
3827
+ result = result.replace(
3828
+ /\[([^\]|]+)\|(https?:\/\/[^\]]+)\]/g,
3829
+ (_match, linkText, url) => {
3830
+ return `[${linkText}](${url})`;
3831
+ }
3832
+ );
3833
+ return result;
3834
+ }
3835
+ };
3836
+
3837
+ // src/mcp/issue-management-server.ts
3838
+ var settings;
3839
+ function validateEnvironment() {
3840
+ const provider = process.env.ISSUE_PROVIDER;
3841
+ if (!provider) {
3842
+ console.error("Missing required environment variable: ISSUE_PROVIDER");
3843
+ process.exit(1);
3844
+ }
3845
+ if (provider !== "github" && provider !== "linear" && provider !== "jira") {
3846
+ console.error(`Invalid ISSUE_PROVIDER: ${provider}. Must be 'github', 'linear', or 'jira'`);
3847
+ process.exit(1);
3848
+ }
3849
+ if (provider === "github") {
3850
+ const required = ["REPO_OWNER", "REPO_NAME"];
3851
+ const missing = required.filter((key) => !process.env[key]);
3852
+ if (missing.length > 0) {
3853
+ console.error(
3854
+ `Missing required environment variables for GitHub provider: ${missing.join(", ")}`
3855
+ );
3856
+ process.exit(1);
3857
+ }
3858
+ }
3859
+ if (provider === "linear") {
3860
+ if (!process.env.LINEAR_API_TOKEN) {
3861
+ console.error("Missing required environment variable for Linear provider: LINEAR_API_TOKEN");
3862
+ process.exit(1);
3863
+ }
3864
+ }
3865
+ if (provider === "jira") {
3866
+ const required = ["JIRA_HOST", "JIRA_USERNAME", "JIRA_API_TOKEN", "JIRA_PROJECT_KEY"];
3867
+ const missing = required.filter((key) => !process.env[key]);
3868
+ if (missing.length > 0) {
3869
+ console.error(
3870
+ `Missing required environment variables for Jira provider: ${missing.join(", ")}`
3871
+ );
3872
+ process.exit(1);
3873
+ }
3874
+ }
3875
+ return provider;
3876
+ }
3877
+ var server = new McpServer({
3878
+ name: "issue-management-broker",
3879
+ version: "0.1.0"
3880
+ });
3881
+ var flexibleAuthorSchema = z2.object({
3882
+ id: z2.string(),
3883
+ displayName: z2.string()
3884
+ }).passthrough();
3885
+ server.registerTool(
3886
+ "get_issue",
3887
+ {
3888
+ title: "Get Issue",
3889
+ description: "Fetch issue details including body, title, comments, labels, assignees, and other metadata. Author fields vary by provider: GitHub uses { login }, Linear uses { name, displayName }, Jira uses { displayName, accountId }. All authors have normalized core fields: { id, displayName } plus provider-specific fields.",
3890
+ inputSchema: {
3891
+ number: z2.string().describe("The issue identifier"),
3892
+ includeComments: z2.boolean().optional().describe("Whether to include comments (default: true)"),
3893
+ repo: z2.string().optional().describe(
3894
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
3895
+ )
3896
+ },
3897
+ outputSchema: {
3898
+ // Core validated fields
3899
+ id: z2.string().describe("Issue identifier"),
3900
+ title: z2.string().describe("Issue title"),
3901
+ body: z2.string().describe("Issue body/description"),
3902
+ state: z2.string().describe("Issue state (open, closed, etc.)"),
3903
+ url: z2.string().describe("Issue URL"),
3904
+ provider: z2.enum(["github", "linear", "jira"]).describe("Issue management provider"),
3905
+ // Flexible author - core fields + passthrough
3906
+ author: flexibleAuthorSchema.nullable().describe(
3907
+ "Issue author with normalized { id, displayName } plus provider-specific fields"
3908
+ ),
3909
+ // Optional flexible arrays
3910
+ assignees: z2.array(flexibleAuthorSchema).optional().describe(
3911
+ "Issue assignees with normalized { id, displayName } plus provider-specific fields"
3912
+ ),
3913
+ labels: z2.array(
3914
+ z2.object({ name: z2.string() }).passthrough()
3915
+ ).optional().describe("Issue labels"),
3916
+ // Comments with flexible author
3917
+ comments: z2.array(
3918
+ z2.object({
3919
+ id: z2.string(),
3920
+ body: z2.string(),
3921
+ author: flexibleAuthorSchema.nullable(),
3922
+ createdAt: z2.string()
3923
+ }).passthrough()
3924
+ ).optional().describe("Issue comments with flexible author structure")
3925
+ }
3926
+ },
3927
+ async ({ number, includeComments, repo }) => {
3928
+ console.error(`Fetching issue ${number}${repo ? ` from ${repo}` : ""}`);
3929
+ try {
3930
+ const provider = IssueManagementProviderFactory.create(
3931
+ process.env.ISSUE_PROVIDER,
3932
+ settings
3933
+ );
3934
+ const result = await provider.getIssue({ number, includeComments, repo });
3935
+ console.error(`Issue fetched successfully: ${result.number} - ${result.title}`);
3936
+ return {
3937
+ content: [
3938
+ {
3939
+ type: "text",
3940
+ text: JSON.stringify(result)
3941
+ }
3942
+ ],
3943
+ structuredContent: result
3944
+ };
3945
+ } catch (error) {
3946
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
3947
+ console.error(`Failed to fetch issue: ${errorMessage}`);
3948
+ throw new Error(`Failed to fetch issue: ${errorMessage}`);
3949
+ }
3950
+ }
3951
+ );
3952
+ server.registerTool(
3953
+ "get_pr",
3954
+ {
3955
+ title: "Get Pull Request",
3956
+ description: "Fetch pull request details including title, body, comments, files, commits, and branch information. PRs only exist on GitHub, so this tool always uses GitHub regardless of configured issue tracker. Author fields have normalized core fields: { id, displayName } plus provider-specific fields.",
3957
+ inputSchema: {
3958
+ number: z2.string().describe("The PR number"),
3959
+ includeComments: z2.boolean().optional().describe("Whether to include comments (default: true)"),
3960
+ repo: z2.string().optional().describe(
3961
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository.'
3962
+ )
3963
+ },
3964
+ outputSchema: {
3965
+ // Core validated fields
3966
+ id: z2.string().describe("PR identifier"),
3967
+ number: z2.number().describe("PR number"),
3968
+ title: z2.string().describe("PR title"),
3969
+ body: z2.string().describe("PR body/description"),
3970
+ state: z2.string().describe("PR state (OPEN, CLOSED, MERGED)"),
3971
+ url: z2.string().describe("PR URL"),
3972
+ // Branch info
3973
+ headRefName: z2.string().describe("Source branch name"),
3974
+ baseRefName: z2.string().describe("Target branch name"),
3975
+ // Flexible author - core fields + passthrough
3976
+ author: flexibleAuthorSchema.nullable().describe(
3977
+ "PR author with normalized { id, displayName } plus provider-specific fields"
3978
+ ),
3979
+ // Optional flexible arrays
3980
+ files: z2.array(
3981
+ z2.object({
3982
+ path: z2.string(),
3983
+ additions: z2.number(),
3984
+ deletions: z2.number()
3985
+ }).passthrough()
3986
+ ).optional().describe("Changed files in the PR"),
3987
+ commits: z2.array(
3988
+ z2.object({
3989
+ oid: z2.string(),
3990
+ messageHeadline: z2.string(),
3991
+ author: flexibleAuthorSchema.nullable()
3992
+ }).passthrough()
3993
+ ).optional().describe("Commits in the PR"),
3994
+ comments: z2.array(
3995
+ z2.object({
3996
+ id: z2.string(),
3997
+ body: z2.string(),
3998
+ author: flexibleAuthorSchema.nullable(),
3999
+ createdAt: z2.string()
4000
+ }).passthrough()
4001
+ ).optional().describe("PR comments")
4002
+ }
4003
+ },
4004
+ async ({ number, includeComments, repo }) => {
4005
+ console.error(`Fetching PR ${number}${repo ? ` from ${repo}` : ""}`);
4006
+ try {
4007
+ const provider = new GitHubIssueManagementProvider();
4008
+ const result = await provider.getPR({ number, includeComments, repo });
4009
+ console.error(`PR fetched successfully: #${result.number} - ${result.title}`);
4010
+ return {
4011
+ content: [
4012
+ {
4013
+ type: "text",
4014
+ text: JSON.stringify(result)
4015
+ }
4016
+ ],
4017
+ structuredContent: result
4018
+ };
4019
+ } catch (error) {
4020
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4021
+ console.error(`Failed to fetch PR: ${errorMessage}`);
4022
+ throw new Error(`Failed to fetch PR: ${errorMessage}`);
4023
+ }
4024
+ }
4025
+ );
4026
+ server.registerTool(
4027
+ "get_review_comments",
4028
+ {
4029
+ title: "Get PR Review Comments",
4030
+ description: "Fetch inline code review comments on a pull request (comments on specific files and lines). Returns comments with file path, line number, diff side, author, and reply threading. Optionally filter by review ID. PRs only exist on GitHub, so this tool always uses GitHub.",
4031
+ inputSchema: {
4032
+ number: z2.string().describe("The PR number"),
4033
+ reviewId: z2.string().optional().describe("Optional review ID to filter comments by a specific review"),
4034
+ repo: z2.string().optional().describe(
4035
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository.'
4036
+ )
4037
+ },
4038
+ outputSchema: {
4039
+ comments: z2.array(
4040
+ z2.object({
4041
+ id: z2.string().describe("Review comment ID"),
4042
+ body: z2.string().describe("Comment body content"),
4043
+ path: z2.string().describe("File path the comment is on"),
4044
+ line: z2.number().nullable().describe("Line number in the diff"),
4045
+ side: z2.string().nullable().describe("Side of the diff (LEFT or RIGHT)"),
4046
+ author: flexibleAuthorSchema.nullable().describe("Comment author"),
4047
+ createdAt: z2.string().describe("Comment creation timestamp"),
4048
+ updatedAt: z2.string().nullable().describe("Comment last updated timestamp"),
4049
+ inReplyToId: z2.string().nullable().describe("ID of the comment this replies to"),
4050
+ pullRequestReviewId: z2.number().nullable().describe("The review this comment belongs to")
4051
+ })
4052
+ ).describe("Inline review comments on the PR")
4053
+ }
4054
+ },
4055
+ async ({ number, reviewId, repo }) => {
4056
+ console.error(`Fetching review comments for PR ${number}${reviewId ? ` (review ${reviewId})` : ""}${repo ? ` from ${repo}` : ""}`);
4057
+ try {
4058
+ const provider = new GitHubIssueManagementProvider();
4059
+ const comments = await provider.getReviewComments({ number, reviewId, repo });
4060
+ console.error(`Review comments fetched successfully: ${comments.length} comments`);
4061
+ const result = { comments };
4062
+ return {
4063
+ content: [
4064
+ {
4065
+ type: "text",
4066
+ text: JSON.stringify(result)
4067
+ }
4068
+ ],
4069
+ structuredContent: result
4070
+ };
4071
+ } catch (error) {
4072
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4073
+ console.error(`Failed to fetch review comments: ${errorMessage}`);
4074
+ throw new Error(`Failed to fetch review comments: ${errorMessage}`);
4075
+ }
4076
+ }
4077
+ );
4078
+ server.registerTool(
1911
4079
  "get_comment",
1912
4080
  {
1913
4081
  title: "Get Comment",
1914
4082
  description: "Fetch a specific comment by ID. Author has normalized core fields { id, displayName } plus provider-specific fields.",
1915
4083
  inputSchema: {
1916
- commentId: z.string().describe("The comment identifier to fetch"),
1917
- number: z.string().describe("The issue or PR identifier (context for providers that need it)"),
1918
- repo: z.string().optional().describe(
4084
+ commentId: z2.string().describe("The comment identifier to fetch"),
4085
+ number: z2.string().describe("The issue or PR identifier (context for providers that need it)"),
4086
+ repo: z2.string().optional().describe(
1919
4087
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
1920
4088
  )
1921
4089
  },
1922
4090
  outputSchema: {
1923
- id: z.string().describe("Comment identifier"),
1924
- body: z.string().describe("Comment body content"),
4091
+ id: z2.string().describe("Comment identifier"),
4092
+ body: z2.string().describe("Comment body content"),
1925
4093
  author: flexibleAuthorSchema.nullable().describe(
1926
4094
  "Comment author with normalized { id, displayName } plus provider-specific fields"
1927
4095
  ),
1928
- created_at: z.string().describe("Comment creation timestamp"),
1929
- updated_at: z.string().optional().describe("Comment last updated timestamp")
4096
+ created_at: z2.string().describe("Comment creation timestamp"),
4097
+ updated_at: z2.string().optional().describe("Comment last updated timestamp")
1930
4098
  }
1931
4099
  },
1932
4100
  async ({ commentId, number, repo }) => {
1933
4101
  console.error(`Fetching comment ${commentId} from issue ${number}${repo ? ` in ${repo}` : ""}`);
1934
4102
  try {
1935
4103
  const provider = IssueManagementProviderFactory.create(
1936
- process.env.ISSUE_PROVIDER
4104
+ process.env.ISSUE_PROVIDER,
4105
+ settings
1937
4106
  );
1938
4107
  const result = await provider.getComment({ commentId, number, repo });
1939
4108
  console.error(`Comment fetched successfully: ${result.id}`);
@@ -1959,22 +4128,24 @@ server.registerTool(
1959
4128
  title: "Create Comment",
1960
4129
  description: "Create a new comment on an issue or pull request. Use this to start tracking a workflow phase.",
1961
4130
  inputSchema: {
1962
- number: z.string().describe("The issue or PR identifier"),
1963
- body: z.string().describe("The comment body (markdown supported)"),
1964
- type: z.enum(["issue", "pr"]).describe("Type of entity to comment on (issue or pr)")
4131
+ number: z2.string().describe("The issue or PR identifier"),
4132
+ body: z2.string().describe("The comment body (markdown supported)"),
4133
+ type: z2.enum(["issue", "pr"]).describe("Type of entity to comment on (issue or pr)"),
4134
+ markupLanguage: z2.literal("GFM").describe("The markup language for the body content. Must be GitHub Flavored Markdown (GFM).")
1965
4135
  },
1966
4136
  outputSchema: {
1967
- id: z.string(),
1968
- url: z.string(),
1969
- created_at: z.string().optional()
4137
+ id: z2.string(),
4138
+ url: z2.string(),
4139
+ created_at: z2.string().optional()
1970
4140
  }
1971
4141
  },
1972
4142
  async ({ number, body, type }) => {
1973
4143
  console.error(`Creating ${type} comment on ${number}`);
1974
4144
  try {
4145
+ const sanitizedBody = JiraWikiSanitizer.sanitize(body);
1975
4146
  const providerType = type === "pr" ? "github" : process.env.ISSUE_PROVIDER;
1976
- const provider = IssueManagementProviderFactory.create(providerType);
1977
- const result = await provider.createComment({ number, body, type });
4147
+ const provider = IssueManagementProviderFactory.create(providerType, settings);
4148
+ const result = await provider.createComment({ number, body: sanitizedBody, type });
1978
4149
  console.error(
1979
4150
  `Comment created successfully: ${result.id} at ${result.url}`
1980
4151
  );
@@ -2000,23 +4171,25 @@ server.registerTool(
2000
4171
  title: "Update Comment",
2001
4172
  description: "Update an existing comment. Use this to update progress during a workflow phase.",
2002
4173
  inputSchema: {
2003
- commentId: z.string().describe("The comment identifier to update"),
2004
- number: z.string().describe("The issue or PR identifier (context for providers that need it)"),
2005
- body: z.string().describe("The updated comment body (markdown supported)"),
2006
- type: z.enum(["issue", "pr"]).optional().describe("Optional type to route PR comments to GitHub regardless of configured provider")
4174
+ commentId: z2.string().describe("The comment identifier to update"),
4175
+ number: z2.string().describe("The issue or PR identifier (context for providers that need it)"),
4176
+ body: z2.string().describe("The updated comment body (markdown supported)"),
4177
+ type: z2.enum(["issue", "pr"]).optional().describe("Optional type to route PR comments to GitHub regardless of configured provider"),
4178
+ markupLanguage: z2.literal("GFM").describe("The markup language for the body content. Must be GitHub Flavored Markdown (GFM).")
2007
4179
  },
2008
4180
  outputSchema: {
2009
- id: z.string(),
2010
- url: z.string(),
2011
- updated_at: z.string().optional()
4181
+ id: z2.string(),
4182
+ url: z2.string(),
4183
+ updated_at: z2.string().optional()
2012
4184
  }
2013
4185
  },
2014
4186
  async ({ commentId, number, body, type }) => {
2015
4187
  console.error(`Updating comment ${commentId} on ${type === "pr" ? "PR" : "issue"} ${number}`);
2016
4188
  try {
4189
+ const sanitizedBody = JiraWikiSanitizer.sanitize(body);
2017
4190
  const providerType = type === "pr" ? "github" : process.env.ISSUE_PROVIDER;
2018
- const provider = IssueManagementProviderFactory.create(providerType);
2019
- const result = await provider.updateComment({ commentId, number, body });
4191
+ const provider = IssueManagementProviderFactory.create(providerType, settings);
4192
+ const result = await provider.updateComment({ commentId, number, body: sanitizedBody });
2020
4193
  console.error(
2021
4194
  `Comment updated successfully: ${result.id} at ${result.url}`
2022
4195
  );
@@ -2042,27 +4215,30 @@ server.registerTool(
2042
4215
  title: "Create Issue",
2043
4216
  description: 'Create a new issue in the configured issue tracker. For GitHub: creates issue in the configured repository. For Linear: requires teamKey parameter (e.g., "ENG", "PLAT"), or configure issueManagement.linear.teamId in settings, or call get_issue first to auto-detect the team.',
2044
4217
  inputSchema: {
2045
- title: z.string().describe("The issue title"),
2046
- body: z.string().describe("The issue body/description (markdown supported)"),
2047
- labels: z.array(z.string()).optional().describe("Optional labels to apply to the issue"),
2048
- teamKey: z.string().optional().describe('Team key for Linear (e.g., "ENG"). Falls back to settings or team extracted from previous get_issue call. Ignored for GitHub.'),
2049
- repo: z.string().optional().describe(
4218
+ title: z2.string().describe("The issue title"),
4219
+ body: z2.string().describe("The issue body/description (markdown supported)"),
4220
+ labels: z2.array(z2.string()).optional().describe("Optional labels to apply to the issue"),
4221
+ teamKey: z2.string().optional().describe('Team key for Linear (e.g., "ENG"). Falls back to settings or team extracted from previous get_issue call. Ignored for GitHub.'),
4222
+ repo: z2.string().optional().describe(
2050
4223
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2051
- )
4224
+ ),
4225
+ markupLanguage: z2.literal("GFM").describe("The markup language for the body content. Must be GitHub Flavored Markdown (GFM).")
2052
4226
  },
2053
4227
  outputSchema: {
2054
- id: z.string().describe("Issue identifier"),
2055
- url: z.string().describe("Issue URL"),
2056
- number: z.number().optional().describe("Issue number (GitHub only)")
4228
+ id: z2.string().describe("Issue identifier"),
4229
+ url: z2.string().describe("Issue URL"),
4230
+ number: z2.number().optional().describe("Issue number (GitHub only)")
2057
4231
  }
2058
4232
  },
2059
4233
  async ({ title, body, labels, teamKey, repo }) => {
2060
4234
  console.error(`Creating issue: ${title}${repo ? ` in ${repo}` : ""}`);
2061
4235
  try {
4236
+ const sanitizedBody = JiraWikiSanitizer.sanitize(body);
2062
4237
  const provider = IssueManagementProviderFactory.create(
2063
- process.env.ISSUE_PROVIDER
4238
+ process.env.ISSUE_PROVIDER,
4239
+ settings
2064
4240
  );
2065
- const result = await provider.createIssue({ title, body, labels, teamKey, repo });
4241
+ const result = await provider.createIssue({ title, body: sanitizedBody, labels, teamKey, repo });
2066
4242
  console.error(`Issue created successfully: ${result.id} at ${result.url}`);
2067
4243
  return {
2068
4244
  content: [
@@ -2086,28 +4262,31 @@ server.registerTool(
2086
4262
  title: "Create Child Issue",
2087
4263
  description: 'Create a new child issue linked to a parent issue. For GitHub: creates issue and links via sub-issue API (requires two API calls). For Linear: creates issue atomically with parent relationship. The parentId should be the parent issue identifier (GitHub issue number or Linear identifier like "ENG-123").',
2088
4264
  inputSchema: {
2089
- parentId: z.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
2090
- title: z.string().describe("The child issue title"),
2091
- body: z.string().describe("The child issue body/description (markdown supported)"),
2092
- labels: z.array(z.string()).optional().describe("Optional labels to apply to the child issue"),
2093
- teamKey: z.string().optional().describe('Team key for Linear (e.g., "ENG"). Falls back to parent team. Ignored for GitHub.'),
2094
- repo: z.string().optional().describe(
4265
+ parentId: z2.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
4266
+ title: z2.string().describe("The child issue title"),
4267
+ body: z2.string().describe("The child issue body/description (markdown supported)"),
4268
+ labels: z2.array(z2.string()).optional().describe("Optional labels to apply to the child issue"),
4269
+ teamKey: z2.string().optional().describe('Team key for Linear (e.g., "ENG"). Falls back to parent team. Ignored for GitHub.'),
4270
+ repo: z2.string().optional().describe(
2095
4271
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2096
- )
4272
+ ),
4273
+ markupLanguage: z2.literal("GFM").describe("The markup language for the body content. Must be GitHub Flavored Markdown (GFM).")
2097
4274
  },
2098
4275
  outputSchema: {
2099
- id: z.string().describe("Issue identifier"),
2100
- url: z.string().describe("Issue URL"),
2101
- number: z.number().optional().describe("Issue number (GitHub only)")
4276
+ id: z2.string().describe("Issue identifier"),
4277
+ url: z2.string().describe("Issue URL"),
4278
+ number: z2.number().optional().describe("Issue number (GitHub only)")
2102
4279
  }
2103
4280
  },
2104
4281
  async ({ parentId, title, body, labels, teamKey, repo }) => {
2105
4282
  console.error(`Creating child issue for parent ${parentId}: ${title}${repo ? ` in ${repo}` : ""}`);
2106
4283
  try {
4284
+ const sanitizedBody = JiraWikiSanitizer.sanitize(body);
2107
4285
  const provider = IssueManagementProviderFactory.create(
2108
- process.env.ISSUE_PROVIDER
4286
+ process.env.ISSUE_PROVIDER,
4287
+ settings
2109
4288
  );
2110
- const result = await provider.createChildIssue({ parentId, title, body, labels, teamKey, repo });
4289
+ const result = await provider.createChildIssue({ parentId, title, body: sanitizedBody, labels, teamKey, repo });
2111
4290
  console.error(`Child issue created successfully: ${result.id} at ${result.url}`);
2112
4291
  return {
2113
4292
  content: [
@@ -2125,11 +4304,11 @@ server.registerTool(
2125
4304
  }
2126
4305
  }
2127
4306
  );
2128
- var dependencyResultSchema = z.object({
2129
- id: z.string().describe("Issue identifier"),
2130
- title: z.string().describe("Issue title"),
2131
- url: z.string().describe("Issue URL"),
2132
- state: z.string().describe("Issue state")
4307
+ var dependencyResultSchema = z2.object({
4308
+ id: z2.string().describe("Issue identifier"),
4309
+ title: z2.string().describe("Issue title"),
4310
+ url: z2.string().describe("Issue URL"),
4311
+ state: z2.string().describe("Issue state")
2133
4312
  });
2134
4313
  server.registerTool(
2135
4314
  "create_dependency",
@@ -2137,21 +4316,22 @@ server.registerTool(
2137
4316
  title: "Create Dependency",
2138
4317
  description: 'Create a blocking dependency between two issues. The blockingIssue will block the blockedIssue. For GitHub: uses the sub-issue API. For Linear: creates a "blocks" relation.',
2139
4318
  inputSchema: {
2140
- blockingIssue: z.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
2141
- blockedIssue: z.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
2142
- repo: z.string().optional().describe(
4319
+ blockingIssue: z2.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
4320
+ blockedIssue: z2.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
4321
+ repo: z2.string().optional().describe(
2143
4322
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2144
4323
  )
2145
4324
  },
2146
4325
  outputSchema: {
2147
- success: z.boolean().describe("Whether the dependency was created successfully")
4326
+ success: z2.boolean().describe("Whether the dependency was created successfully")
2148
4327
  }
2149
4328
  },
2150
4329
  async ({ blockingIssue, blockedIssue, repo }) => {
2151
4330
  console.error(`Creating dependency: ${blockingIssue} blocks ${blockedIssue}${repo ? ` in ${repo}` : ""}`);
2152
4331
  try {
2153
4332
  const provider = IssueManagementProviderFactory.create(
2154
- process.env.ISSUE_PROVIDER
4333
+ process.env.ISSUE_PROVIDER,
4334
+ settings
2155
4335
  );
2156
4336
  await provider.createDependency({ blockingIssue, blockedIssue, repo });
2157
4337
  console.error(`Dependency created successfully: ${blockingIssue} -> ${blockedIssue}`);
@@ -2177,22 +4357,23 @@ server.registerTool(
2177
4357
  title: "Get Dependencies",
2178
4358
  description: "Get blocking/blocked_by dependencies for an issue. Returns lists of issues that this issue blocks and/or is blocked by.",
2179
4359
  inputSchema: {
2180
- number: z.string().describe('Issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
2181
- direction: z.enum(["blocking", "blocked_by", "both"]).describe('Which dependencies to fetch: "blocking" for issues this blocks, "blocked_by" for issues blocking this, "both" for all'),
2182
- repo: z.string().optional().describe(
4360
+ number: z2.string().describe('Issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
4361
+ direction: z2.enum(["blocking", "blocked_by", "both"]).describe('Which dependencies to fetch: "blocking" for issues this blocks, "blocked_by" for issues blocking this, "both" for all'),
4362
+ repo: z2.string().optional().describe(
2183
4363
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2184
4364
  )
2185
4365
  },
2186
4366
  outputSchema: {
2187
- blocking: z.array(dependencyResultSchema).describe("Issues that this issue blocks"),
2188
- blockedBy: z.array(dependencyResultSchema).describe("Issues that block this issue")
4367
+ blocking: z2.array(dependencyResultSchema).describe("Issues that this issue blocks"),
4368
+ blockedBy: z2.array(dependencyResultSchema).describe("Issues that block this issue")
2189
4369
  }
2190
4370
  },
2191
4371
  async ({ number, direction, repo }) => {
2192
4372
  console.error(`Getting dependencies for ${number} (direction: ${direction})${repo ? ` in ${repo}` : ""}`);
2193
4373
  try {
2194
4374
  const provider = IssueManagementProviderFactory.create(
2195
- process.env.ISSUE_PROVIDER
4375
+ process.env.ISSUE_PROVIDER,
4376
+ settings
2196
4377
  );
2197
4378
  const result = await provider.getDependencies({ number, direction, repo });
2198
4379
  console.error(`Dependencies fetched: ${result.blocking.length} blocking, ${result.blockedBy.length} blocked_by`);
@@ -2218,21 +4399,22 @@ server.registerTool(
2218
4399
  title: "Remove Dependency",
2219
4400
  description: "Remove a blocking dependency between two issues. The blockingIssue will no longer block the blockedIssue.",
2220
4401
  inputSchema: {
2221
- blockingIssue: z.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
2222
- blockedIssue: z.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
2223
- repo: z.string().optional().describe(
4402
+ blockingIssue: z2.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
4403
+ blockedIssue: z2.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
4404
+ repo: z2.string().optional().describe(
2224
4405
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2225
4406
  )
2226
4407
  },
2227
4408
  outputSchema: {
2228
- success: z.boolean().describe("Whether the dependency was removed successfully")
4409
+ success: z2.boolean().describe("Whether the dependency was removed successfully")
2229
4410
  }
2230
4411
  },
2231
4412
  async ({ blockingIssue, blockedIssue, repo }) => {
2232
4413
  console.error(`Removing dependency: ${blockingIssue} blocks ${blockedIssue}${repo ? ` in ${repo}` : ""}`);
2233
4414
  try {
2234
4415
  const provider = IssueManagementProviderFactory.create(
2235
- process.env.ISSUE_PROVIDER
4416
+ process.env.ISSUE_PROVIDER,
4417
+ settings
2236
4418
  );
2237
4419
  await provider.removeDependency({ blockingIssue, blockedIssue, repo });
2238
4420
  console.error(`Dependency removed successfully: ${blockingIssue} -> ${blockedIssue}`);
@@ -2252,11 +4434,11 @@ server.registerTool(
2252
4434
  }
2253
4435
  }
2254
4436
  );
2255
- var childIssueResultSchema = z.object({
2256
- id: z.string().describe("Issue identifier"),
2257
- title: z.string().describe("Issue title"),
2258
- url: z.string().describe("Issue URL"),
2259
- state: z.string().describe("Issue state")
4437
+ var childIssueResultSchema = z2.object({
4438
+ id: z2.string().describe("Issue identifier"),
4439
+ title: z2.string().describe("Issue title"),
4440
+ url: z2.string().describe("Issue URL"),
4441
+ state: z2.string().describe("Issue state")
2260
4442
  });
2261
4443
  server.registerTool(
2262
4444
  "get_child_issues",
@@ -2264,20 +4446,21 @@ server.registerTool(
2264
4446
  title: "Get Child Issues",
2265
4447
  description: "Get child issues (sub-issues) of a parent issue. Returns a list of issues that are children of the specified parent.",
2266
4448
  inputSchema: {
2267
- number: z.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
2268
- repo: z.string().optional().describe(
4449
+ number: z2.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
4450
+ repo: z2.string().optional().describe(
2269
4451
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2270
4452
  )
2271
4453
  },
2272
4454
  outputSchema: {
2273
- children: z.array(childIssueResultSchema).describe("Child issues of the parent")
4455
+ children: z2.array(childIssueResultSchema).describe("Child issues of the parent")
2274
4456
  }
2275
4457
  },
2276
4458
  async ({ number, repo }) => {
2277
4459
  console.error(`Getting child issues for ${number}${repo ? ` in ${repo}` : ""}`);
2278
4460
  try {
2279
4461
  const provider = IssueManagementProviderFactory.create(
2280
- process.env.ISSUE_PROVIDER
4462
+ process.env.ISSUE_PROVIDER,
4463
+ settings
2281
4464
  );
2282
4465
  const result = await provider.getChildIssues({ number, repo });
2283
4466
  console.error(`Child issues fetched: ${result.length} children`);
@@ -2297,8 +4480,162 @@ server.registerTool(
2297
4480
  }
2298
4481
  }
2299
4482
  );
4483
+ server.registerTool(
4484
+ "close_issue",
4485
+ {
4486
+ title: "Close Issue",
4487
+ description: 'Close an issue in the configured issue tracker. For GitHub: uses `gh issue close`. For Linear: transitions issue to "Done" state. For Jira: transitions issue to "Done" state.',
4488
+ inputSchema: {
4489
+ number: z2.string().describe("The issue identifier"),
4490
+ repo: z2.string().optional().describe(
4491
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
4492
+ )
4493
+ },
4494
+ outputSchema: {
4495
+ success: z2.boolean().describe("Whether the issue was closed successfully")
4496
+ }
4497
+ },
4498
+ async ({ number, repo }) => {
4499
+ console.error(`Closing issue ${number}${repo ? ` in ${repo}` : ""}`);
4500
+ try {
4501
+ const provider = IssueManagementProviderFactory.create(
4502
+ process.env.ISSUE_PROVIDER,
4503
+ settings
4504
+ );
4505
+ await provider.closeIssue({ number, repo });
4506
+ console.error(`Issue closed successfully: ${number}`);
4507
+ return {
4508
+ content: [
4509
+ {
4510
+ type: "text",
4511
+ text: JSON.stringify({ success: true })
4512
+ }
4513
+ ],
4514
+ structuredContent: { success: true }
4515
+ };
4516
+ } catch (error) {
4517
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4518
+ console.error(`Failed to close issue: ${errorMessage}`);
4519
+ throw new Error(`Failed to close issue: ${errorMessage}`);
4520
+ }
4521
+ }
4522
+ );
4523
+ server.registerTool(
4524
+ "reopen_issue",
4525
+ {
4526
+ title: "Reopen Issue",
4527
+ description: 'Reopen a closed issue in the configured issue tracker. For GitHub: uses `gh issue reopen`. For Linear: transitions issue to "Todo" state. For Jira: transitions issue to "Reopen" or "To Do" state.',
4528
+ inputSchema: {
4529
+ number: z2.string().describe("The issue identifier"),
4530
+ repo: z2.string().optional().describe(
4531
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
4532
+ )
4533
+ },
4534
+ outputSchema: {
4535
+ success: z2.boolean().describe("Whether the issue was reopened successfully")
4536
+ }
4537
+ },
4538
+ async ({ number, repo }) => {
4539
+ console.error(`Reopening issue ${number}${repo ? ` in ${repo}` : ""}`);
4540
+ try {
4541
+ const provider = IssueManagementProviderFactory.create(
4542
+ process.env.ISSUE_PROVIDER,
4543
+ settings
4544
+ );
4545
+ await provider.reopenIssue({ number, repo });
4546
+ console.error(`Issue reopened successfully: ${number}`);
4547
+ return {
4548
+ content: [
4549
+ {
4550
+ type: "text",
4551
+ text: JSON.stringify({ success: true })
4552
+ }
4553
+ ],
4554
+ structuredContent: { success: true }
4555
+ };
4556
+ } catch (error) {
4557
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4558
+ console.error(`Failed to reopen issue: ${errorMessage}`);
4559
+ throw new Error(`Failed to reopen issue: ${errorMessage}`);
4560
+ }
4561
+ }
4562
+ );
4563
+ server.registerTool(
4564
+ "edit_issue",
4565
+ {
4566
+ title: "Edit Issue",
4567
+ description: "Edit an issue's properties (title, body, state, labels) in the configured issue tracker. State changes use close/reopen internally. For GitHub: uses `gh issue edit` for field updates and `gh issue close/reopen` for state. For Linear: uses Linear SDK to update fields and state transitions. For Jira: uses REST API to update fields and transitions for state.",
4568
+ inputSchema: {
4569
+ number: z2.string().describe("The issue identifier"),
4570
+ title: z2.string().optional().describe("New issue title"),
4571
+ body: z2.string().optional().describe("New issue body/description"),
4572
+ state: z2.enum(["open", "closed"]).optional().describe("New issue state"),
4573
+ labels: z2.array(z2.string()).optional().describe("Labels to add to the issue"),
4574
+ repo: z2.string().optional().describe(
4575
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
4576
+ ),
4577
+ markupLanguage: z2.literal("GFM").optional().describe("The markup language for the body content. Must be GitHub Flavored Markdown (GFM).")
4578
+ },
4579
+ outputSchema: {
4580
+ success: z2.boolean().describe("Whether the issue was edited successfully")
4581
+ }
4582
+ },
4583
+ async ({ number, title, body, state, labels, repo }) => {
4584
+ console.error(`Editing issue ${number}${repo ? ` in ${repo}` : ""}`);
4585
+ try {
4586
+ const sanitizedBody = body ? JiraWikiSanitizer.sanitize(body) : void 0;
4587
+ const provider = IssueManagementProviderFactory.create(
4588
+ process.env.ISSUE_PROVIDER,
4589
+ settings
4590
+ );
4591
+ await provider.editIssue({ number, title, body: sanitizedBody, state, labels, repo });
4592
+ console.error(`Issue edited successfully: ${number}`);
4593
+ return {
4594
+ content: [
4595
+ {
4596
+ type: "text",
4597
+ text: JSON.stringify({ success: true })
4598
+ }
4599
+ ],
4600
+ structuredContent: { success: true }
4601
+ };
4602
+ } catch (error) {
4603
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4604
+ console.error(`Failed to edit issue: ${errorMessage}`);
4605
+ throw new Error(`Failed to edit issue: ${errorMessage}`);
4606
+ }
4607
+ }
4608
+ );
2300
4609
  async function main() {
2301
- console.error("Starting Issue Management MCP Server...");
4610
+ console.error("=== Issue Management MCP Server Starting ===");
4611
+ console.error(`PID: ${process.pid}`);
4612
+ console.error(`Node version: ${process.version}`);
4613
+ console.error(`CWD: ${process.cwd()}`);
4614
+ console.error(`Script: ${new URL(import.meta.url).pathname}`);
4615
+ const relevantEnvKeys = [
4616
+ "ISSUE_PROVIDER",
4617
+ "REPO_OWNER",
4618
+ "REPO_NAME",
4619
+ "GITHUB_API_URL",
4620
+ "GITHUB_EVENT_NAME",
4621
+ "DRAFT_PR_NUMBER",
4622
+ "LINEAR_API_TOKEN",
4623
+ "LINEAR_TEAM_KEY",
4624
+ "JIRA_HOST",
4625
+ "JIRA_USERNAME",
4626
+ "JIRA_API_TOKEN",
4627
+ "JIRA_PROJECT_KEY"
4628
+ ];
4629
+ console.error("Environment variables:");
4630
+ for (const key of relevantEnvKeys) {
4631
+ const val = process.env[key];
4632
+ if (val !== void 0) {
4633
+ console.error(` ${key}=${val}`);
4634
+ }
4635
+ }
4636
+ const settingsManager = new SettingsManager();
4637
+ settings = await settingsManager.loadSettings();
4638
+ console.error("Settings loaded");
2302
4639
  const provider = validateEnvironment();
2303
4640
  console.error("Environment validated");
2304
4641
  console.error(`Issue management provider: ${provider}`);
@@ -2308,7 +4645,7 @@ async function main() {
2308
4645
  }
2309
4646
  const transport = new StdioServerTransport();
2310
4647
  await server.connect(transport);
2311
- console.error("Issue Management MCP Server running on stdio transport");
4648
+ console.error("=== Issue Management MCP Server READY (stdio transport) ===");
2312
4649
  }
2313
4650
  main().catch((error) => {
2314
4651
  console.error("Fatal error starting MCP server:", error);