@iloom/cli 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +159 -40
- package/dist/{BranchNamingService-K6XNWQ6C.js → BranchNamingService-ECJHBB67.js} +2 -2
- package/dist/ClaudeContextManager-QXX6ZFST.js +14 -0
- package/dist/ClaudeService-NJNK2SUH.js +13 -0
- package/dist/{GitHubService-TGWJN4V4.js → GitHubService-MEHKHUQP.js} +4 -4
- package/dist/IssueTrackerFactory-NG53YX5S.js +14 -0
- package/dist/{LoomLauncher-73NXL2CL.js → LoomLauncher-L64HHS3T.js} +9 -9
- package/dist/{MetadataManager-W3C54UYT.js → MetadataManager-5QZSTKNN.js} +2 -2
- package/dist/{ProjectCapabilityDetector-N5L7T4IY.js → ProjectCapabilityDetector-5KSYUTBJ.js} +3 -3
- package/dist/{PromptTemplateManager-36YLQRHP.js → PromptTemplateManager-DULSVRRE.js} +2 -2
- package/dist/README.md +159 -40
- package/dist/{SettingsManager-AW3JTJHD.js → SettingsManager-BQDQA3FK.js} +4 -2
- package/dist/agents/iloom-artifact-reviewer.md +11 -0
- package/dist/agents/iloom-code-reviewer.md +14 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +55 -12
- package/dist/agents/iloom-issue-analyzer.md +49 -6
- package/dist/agents/iloom-issue-complexity-evaluator.md +47 -6
- package/dist/agents/iloom-issue-enhancer.md +86 -7
- package/dist/agents/iloom-issue-implementer.md +48 -7
- package/dist/agents/iloom-issue-planner.md +115 -62
- package/dist/{build-THZI572G.js → build-5GO3XW26.js} +9 -9
- package/dist/{chunk-NUACL52E.js → chunk-3D7WQM7I.js} +2 -2
- package/dist/chunk-4232AHNQ.js +35 -0
- package/dist/chunk-4232AHNQ.js.map +1 -0
- package/dist/{chunk-QN47QVBX.js → chunk-4WJNIR5O.js} +1 -1
- package/dist/chunk-4WJNIR5O.js.map +1 -0
- package/dist/{chunk-A7NJF73J.js → chunk-5MWV33NN.js} +4 -4
- package/dist/{chunk-3I4ONZRT.js → chunk-6EU6TCF6.js} +10 -10
- package/dist/chunk-6EU6TCF6.js.map +1 -0
- package/dist/{chunk-CWRI4JC3.js → chunk-FB47TIJG.js} +29 -11
- package/dist/chunk-FB47TIJG.js.map +1 -0
- package/dist/chunk-HEXKPKCK.js +1396 -0
- package/dist/chunk-HEXKPKCK.js.map +1 -0
- package/dist/{chunk-KAYXR544.js → chunk-J5S7DFYC.js} +2 -2
- package/dist/{chunk-ULSWCPQG.js → chunk-JO2LZ6EQ.js} +476 -5
- package/dist/chunk-JO2LZ6EQ.js.map +1 -0
- package/dist/{chunk-KBEIQP4G.js → chunk-KB64WNBZ.js} +43 -3
- package/dist/chunk-KB64WNBZ.js.map +1 -0
- package/dist/{chunk-OFDN5NKS.js → chunk-KXDRI47U.js} +69 -12
- package/dist/chunk-KXDRI47U.js.map +1 -0
- package/dist/{chunk-R4YWBGY6.js → chunk-LXLMMXXY.js} +54 -14
- package/dist/chunk-LXLMMXXY.js.map +1 -0
- package/dist/{chunk-AR5QKYNE.js → chunk-MNHZB4Z2.js} +4 -4
- package/dist/{chunk-TL72BGP6.js → chunk-MORRVYPT.js} +2 -2
- package/dist/{chunk-KJTVU3HZ.js → chunk-NRSWLOAZ.js} +8 -8
- package/dist/chunk-NRSWLOAZ.js.map +1 -0
- package/dist/{chunk-FO5GGFOV.js → chunk-ONQYPICO.js} +13 -5
- package/dist/chunk-ONQYPICO.js.map +1 -0
- package/dist/{chunk-7ZEHSSUP.js → chunk-P4O6EH46.js} +4 -4
- package/dist/chunk-QZWEJVWV.js +207 -0
- package/dist/chunk-QZWEJVWV.js.map +1 -0
- package/dist/chunk-RSYT7MVI.js +202 -0
- package/dist/chunk-RSYT7MVI.js.map +1 -0
- package/dist/{chunk-Z2TWEXR7.js → chunk-RYWFS37M.js} +6 -6
- package/dist/chunk-RYWFS37M.js.map +1 -0
- package/dist/{chunk-B7U6OKUR.js → chunk-SF2P22EE.js} +11 -3
- package/dist/chunk-SF2P22EE.js.map +1 -0
- package/dist/{chunk-6IIL5M2L.js → chunk-SN3SQCFK.js} +10 -8
- package/dist/{chunk-6IIL5M2L.js.map → chunk-SN3SQCFK.js.map} +1 -1
- package/dist/{chunk-SOSQILHO.js → chunk-UD3WJDIV.js} +92 -82
- package/dist/chunk-UD3WJDIV.js.map +1 -0
- package/dist/{chunk-KXGQYLFZ.js → chunk-UKBAJ2QQ.js} +61 -7
- package/dist/chunk-UKBAJ2QQ.js.map +1 -0
- package/dist/{chunk-W6DP5RVR.js → chunk-UVD4CZKS.js} +3 -3
- package/dist/chunk-UWGVCXRF.js +207 -0
- package/dist/chunk-UWGVCXRF.js.map +1 -0
- package/dist/{chunk-NWMORW3U.js → chunk-VECNX6VX.js} +2 -2
- package/dist/{chunk-4CO6KG5S.js → chunk-VG45TUYK.js} +53 -7
- package/dist/{chunk-4CO6KG5S.js.map → chunk-VG45TUYK.js.map} +1 -1
- package/dist/{chunk-TC7APDKU.js → chunk-VGGST52X.js} +2 -2
- package/dist/{chunk-4LKGCFGG.js → chunk-WWKOVDWC.js} +2 -2
- package/dist/{chunk-YKFCCV6S.js → chunk-WY4QBK43.js} +7 -7
- package/dist/chunk-WY4QBK43.js.map +1 -0
- package/dist/chunk-Y4YZTHZE.js +73 -0
- package/dist/chunk-Y4YZTHZE.js.map +1 -0
- package/dist/{chunk-VOGGLPG5.js → chunk-YQ57ORTV.js} +14 -1
- package/dist/chunk-YQ57ORTV.js.map +1 -0
- package/dist/{chunk-RI2YL6TK.js → chunk-YYAKPQBT.js} +65 -18
- package/dist/chunk-YYAKPQBT.js.map +1 -0
- package/dist/{chunk-IZIYLYPK.js → chunk-ZEWU5PZK.js} +2 -2
- package/dist/{chunk-VPTAX5TR.js → chunk-ZHPNZC75.js} +12 -12
- package/dist/chunk-ZHPNZC75.js.map +1 -0
- package/dist/{chunk-DGG2VY7B.js → chunk-ZW2LKWWE.js} +9 -9
- package/dist/chunk-ZW2LKWWE.js.map +1 -0
- package/dist/{claude-TP2QO3BU.js → claude-P3NQR6IJ.js} +2 -2
- package/dist/{cleanup-PJRIFFU4.js → cleanup-6UCPVMFG.js} +81 -32
- package/dist/cleanup-6UCPVMFG.js.map +1 -0
- package/dist/cli.js +638 -349
- package/dist/cli.js.map +1 -1
- package/dist/{commit-IVP3M4HG.js → commit-L3EPY5QG.js} +21 -20
- package/dist/commit-L3EPY5QG.js.map +1 -0
- package/dist/{compile-R2J65HBQ.js → compile-ZS4HYRX5.js} +9 -9
- package/dist/{contribute-VDZXHK5Y.js → contribute-ORDDQGSL.js} +14 -6
- package/dist/contribute-ORDDQGSL.js.map +1 -0
- package/dist/{dev-server-7F622OEO.js → dev-server-FYZ2AQIH.js} +29 -15
- package/dist/dev-server-FYZ2AQIH.js.map +1 -0
- package/dist/{feedback-E7VET7CL.js → feedback-TMBXSCM5.js} +15 -15
- package/dist/{git-2QDQ2X2S.js → git-ET64COO3.js} +4 -4
- package/dist/hooks/iloom-hook.js +15 -0
- package/dist/ignite-CGOV3TD4.js +1393 -0
- package/dist/ignite-CGOV3TD4.js.map +1 -0
- package/dist/index.d.ts +382 -53
- package/dist/index.js +1167 -36
- package/dist/index.js.map +1 -1
- package/dist/{init-676DHF6R.js → init-GFQ5W7GK.js} +57 -21
- package/dist/init-GFQ5W7GK.js.map +1 -0
- package/dist/{issues-PJSOLOBJ.js → issues-T4ZZSPEG.js} +61 -20
- package/dist/issues-T4ZZSPEG.js.map +1 -0
- package/dist/{lint-CJM7BAIM.js → lint-6TQXDZ3T.js} +9 -9
- package/dist/mcp/issue-management-server.js +2471 -256
- package/dist/mcp/issue-management-server.js.map +1 -1
- package/dist/mcp/recap-server.js +144 -21
- package/dist/mcp/recap-server.js.map +1 -1
- package/dist/{neon-helpers-VVFFTLXE.js → neon-helpers-CQN2PB4S.js} +3 -3
- package/dist/neon-helpers-CQN2PB4S.js.map +1 -0
- package/dist/{open-544H7JF5.js → open-5QZGXQRF.js} +15 -15
- package/dist/open-5QZGXQRF.js.map +1 -0
- package/dist/{plan-Q7ELXDLC.js → plan-U7ZQWLFY.js} +41 -25
- package/dist/plan-U7ZQWLFY.js.map +1 -0
- package/dist/{projects-LH362JZQ.js → projects-2UOXFLNZ.js} +4 -4
- package/dist/prompts/CLAUDE.md +62 -0
- package/dist/prompts/init-prompt.txt +347 -26
- package/dist/prompts/issue-prompt.txt +427 -54
- package/dist/prompts/plan-prompt.txt +97 -16
- package/dist/prompts/pr-prompt.txt +44 -1
- package/dist/prompts/regular-prompt.txt +42 -1
- package/dist/prompts/session-summary-prompt.txt +14 -0
- package/dist/prompts/swarm-orchestrator-prompt.txt +437 -0
- package/dist/{rebase-YND35CIE.js → rebase-DWIB77KV.js} +10 -10
- package/dist/{recap-3W7COH7D.js → recap-MX63HAKV.js} +47 -19
- package/dist/recap-MX63HAKV.js.map +1 -0
- package/dist/{run-QUXJKDQQ.js → run-O3TFNQFC.js} +15 -15
- package/dist/run-O3TFNQFC.js.map +1 -0
- package/dist/schema/package-iloom.schema.json +58 -0
- package/dist/schema/settings.schema.json +115 -15
- package/dist/{shell-QGECBLST.js → shell-G6VC2CYR.js} +14 -7
- package/dist/shell-G6VC2CYR.js.map +1 -0
- package/dist/{summary-G2T4452H.js → summary-FWHAX55O.js} +27 -25
- package/dist/summary-FWHAX55O.js.map +1 -0
- package/dist/{test-EA5NQFDC.js → test-F7JNJZYP.js} +9 -9
- package/dist/{test-git-M7LSLEFL.js → test-git-BTAOIUE2.js} +4 -4
- package/dist/test-jira-CHYNV33F.js +96 -0
- package/dist/test-jira-CHYNV33F.js.map +1 -0
- package/dist/{test-prefix-64NAAUON.js → test-prefix-Q6TFSU6F.js} +4 -4
- package/dist/{test-webserver-OK6Z5FJM.js → test-webserver-EONCG7E7.js} +6 -6
- package/dist/{vscode-AR5NNXXI.js → vscode-VA5X4P25.js} +7 -7
- package/package.json +5 -1
- package/dist/ClaudeContextManager-HR5JQKAI.js +0 -14
- package/dist/ClaudeService-TK7FMC2X.js +0 -13
- package/dist/chunk-3I4ONZRT.js.map +0 -1
- package/dist/chunk-B7U6OKUR.js.map +0 -1
- package/dist/chunk-CWRI4JC3.js.map +0 -1
- package/dist/chunk-DGG2VY7B.js.map +0 -1
- package/dist/chunk-FJDRTVJX.js +0 -520
- package/dist/chunk-FJDRTVJX.js.map +0 -1
- package/dist/chunk-FO5GGFOV.js.map +0 -1
- package/dist/chunk-KBEIQP4G.js.map +0 -1
- package/dist/chunk-KJTVU3HZ.js.map +0 -1
- package/dist/chunk-KXGQYLFZ.js.map +0 -1
- package/dist/chunk-OFDN5NKS.js.map +0 -1
- package/dist/chunk-QN47QVBX.js.map +0 -1
- package/dist/chunk-R4YWBGY6.js.map +0 -1
- package/dist/chunk-RI2YL6TK.js.map +0 -1
- package/dist/chunk-SOSQILHO.js.map +0 -1
- package/dist/chunk-ULSWCPQG.js.map +0 -1
- package/dist/chunk-VOGGLPG5.js.map +0 -1
- package/dist/chunk-VPTAX5TR.js.map +0 -1
- package/dist/chunk-WHI5KEOX.js +0 -121
- package/dist/chunk-WHI5KEOX.js.map +0 -1
- package/dist/chunk-YKFCCV6S.js.map +0 -1
- package/dist/chunk-Z2TWEXR7.js.map +0 -1
- package/dist/cleanup-PJRIFFU4.js.map +0 -1
- package/dist/commit-IVP3M4HG.js.map +0 -1
- package/dist/contribute-VDZXHK5Y.js.map +0 -1
- package/dist/dev-server-7F622OEO.js.map +0 -1
- package/dist/ignite-IW35CDBD.js +0 -784
- package/dist/ignite-IW35CDBD.js.map +0 -1
- package/dist/init-676DHF6R.js.map +0 -1
- package/dist/issues-PJSOLOBJ.js.map +0 -1
- package/dist/open-544H7JF5.js.map +0 -1
- package/dist/plan-Q7ELXDLC.js.map +0 -1
- package/dist/recap-3W7COH7D.js.map +0 -1
- package/dist/run-QUXJKDQQ.js.map +0 -1
- package/dist/shell-QGECBLST.js.map +0 -1
- package/dist/summary-G2T4452H.js.map +0 -1
- /package/dist/{BranchNamingService-K6XNWQ6C.js.map → BranchNamingService-ECJHBB67.js.map} +0 -0
- /package/dist/{ClaudeContextManager-HR5JQKAI.js.map → ClaudeContextManager-QXX6ZFST.js.map} +0 -0
- /package/dist/{ClaudeService-TK7FMC2X.js.map → ClaudeService-NJNK2SUH.js.map} +0 -0
- /package/dist/{GitHubService-TGWJN4V4.js.map → GitHubService-MEHKHUQP.js.map} +0 -0
- /package/dist/{MetadataManager-W3C54UYT.js.map → IssueTrackerFactory-NG53YX5S.js.map} +0 -0
- /package/dist/{LoomLauncher-73NXL2CL.js.map → LoomLauncher-L64HHS3T.js.map} +0 -0
- /package/dist/{ProjectCapabilityDetector-N5L7T4IY.js.map → MetadataManager-5QZSTKNN.js.map} +0 -0
- /package/dist/{PromptTemplateManager-36YLQRHP.js.map → ProjectCapabilityDetector-5KSYUTBJ.js.map} +0 -0
- /package/dist/{SettingsManager-AW3JTJHD.js.map → PromptTemplateManager-DULSVRRE.js.map} +0 -0
- /package/dist/{claude-TP2QO3BU.js.map → SettingsManager-BQDQA3FK.js.map} +0 -0
- /package/dist/{build-THZI572G.js.map → build-5GO3XW26.js.map} +0 -0
- /package/dist/{chunk-NUACL52E.js.map → chunk-3D7WQM7I.js.map} +0 -0
- /package/dist/{chunk-A7NJF73J.js.map → chunk-5MWV33NN.js.map} +0 -0
- /package/dist/{chunk-KAYXR544.js.map → chunk-J5S7DFYC.js.map} +0 -0
- /package/dist/{chunk-AR5QKYNE.js.map → chunk-MNHZB4Z2.js.map} +0 -0
- /package/dist/{chunk-TL72BGP6.js.map → chunk-MORRVYPT.js.map} +0 -0
- /package/dist/{chunk-7ZEHSSUP.js.map → chunk-P4O6EH46.js.map} +0 -0
- /package/dist/{chunk-W6DP5RVR.js.map → chunk-UVD4CZKS.js.map} +0 -0
- /package/dist/{chunk-NWMORW3U.js.map → chunk-VECNX6VX.js.map} +0 -0
- /package/dist/{chunk-TC7APDKU.js.map → chunk-VGGST52X.js.map} +0 -0
- /package/dist/{chunk-4LKGCFGG.js.map → chunk-WWKOVDWC.js.map} +0 -0
- /package/dist/{chunk-IZIYLYPK.js.map → chunk-ZEWU5PZK.js.map} +0 -0
- /package/dist/{git-2QDQ2X2S.js.map → claude-P3NQR6IJ.js.map} +0 -0
- /package/dist/{compile-R2J65HBQ.js.map → compile-ZS4HYRX5.js.map} +0 -0
- /package/dist/{feedback-E7VET7CL.js.map → feedback-TMBXSCM5.js.map} +0 -0
- /package/dist/{neon-helpers-VVFFTLXE.js.map → git-ET64COO3.js.map} +0 -0
- /package/dist/{lint-CJM7BAIM.js.map → lint-6TQXDZ3T.js.map} +0 -0
- /package/dist/{projects-LH362JZQ.js.map → projects-2UOXFLNZ.js.map} +0 -0
- /package/dist/{rebase-YND35CIE.js.map → rebase-DWIB77KV.js.map} +0 -0
- /package/dist/{test-EA5NQFDC.js.map → test-F7JNJZYP.js.map} +0 -0
- /package/dist/{test-git-M7LSLEFL.js.map → test-git-BTAOIUE2.js.map} +0 -0
- /package/dist/{test-prefix-64NAAUON.js.map → test-prefix-Q6TFSU6F.js.map} +0 -0
- /package/dist/{test-webserver-OK6Z5FJM.js.map → test-webserver-EONCG7E7.js.map} +0 -0
- /package/dist/{vscode-AR5NNXXI.js.map → vscode-VA5X4P25.js.map} +0 -0
|
@@ -0,0 +1,1396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getLogger
|
|
4
|
+
} from "./chunk-6MLEBAYZ.js";
|
|
5
|
+
import {
|
|
6
|
+
promptConfirmation
|
|
7
|
+
} from "./chunk-7JDMYTFZ.js";
|
|
8
|
+
import {
|
|
9
|
+
logger
|
|
10
|
+
} from "./chunk-VT4PDUYT.js";
|
|
11
|
+
|
|
12
|
+
// src/utils/linear.ts
|
|
13
|
+
import { LinearClient, IssueRelationType } from "@linear/sdk";
|
|
14
|
+
|
|
15
|
+
// src/types/linear.ts
|
|
16
|
+
var LinearServiceError = class _LinearServiceError extends Error {
|
|
17
|
+
constructor(code, message, details) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.details = details;
|
|
21
|
+
this.name = "LinearServiceError";
|
|
22
|
+
if (Error.captureStackTrace) {
|
|
23
|
+
Error.captureStackTrace(this, _LinearServiceError);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/utils/linear.ts
|
|
29
|
+
function slugifyTitle(title, maxLength = 50) {
|
|
30
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31
|
+
if (slug.length <= maxLength) {
|
|
32
|
+
return slug;
|
|
33
|
+
}
|
|
34
|
+
const parts = slug.split("-");
|
|
35
|
+
let result = "";
|
|
36
|
+
for (const part of parts) {
|
|
37
|
+
const candidate = result ? `${result}-${part}` : part;
|
|
38
|
+
if (candidate.length > maxLength) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
result = candidate;
|
|
42
|
+
}
|
|
43
|
+
return result || slug.slice(0, maxLength);
|
|
44
|
+
}
|
|
45
|
+
function buildLinearIssueUrl(identifier, title) {
|
|
46
|
+
const base = `https://linear.app/issue/${identifier}`;
|
|
47
|
+
if (title) {
|
|
48
|
+
const slug = slugifyTitle(title);
|
|
49
|
+
return slug ? `${base}/${slug}` : base;
|
|
50
|
+
}
|
|
51
|
+
return base;
|
|
52
|
+
}
|
|
53
|
+
function getLinearApiToken() {
|
|
54
|
+
const token = process.env.LINEAR_API_TOKEN;
|
|
55
|
+
if (!token) {
|
|
56
|
+
throw new LinearServiceError(
|
|
57
|
+
"UNAUTHORIZED",
|
|
58
|
+
"LINEAR_API_TOKEN not set. Configure in settings.local.json or set environment variable."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return token;
|
|
62
|
+
}
|
|
63
|
+
function createLinearClient(apiToken) {
|
|
64
|
+
const token = apiToken ?? getLinearApiToken();
|
|
65
|
+
return new LinearClient({ apiKey: token });
|
|
66
|
+
}
|
|
67
|
+
function handleLinearError(error, context) {
|
|
68
|
+
logger.debug(`${context}: Handling error`, { error });
|
|
69
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
70
|
+
if (errorMessage.includes("not found") || errorMessage.includes("Not found")) {
|
|
71
|
+
throw new LinearServiceError("NOT_FOUND", "Linear issue or resource not found", { error });
|
|
72
|
+
}
|
|
73
|
+
if (errorMessage.includes("unauthorized") || errorMessage.includes("Unauthorized") || errorMessage.includes("Invalid API key")) {
|
|
74
|
+
throw new LinearServiceError(
|
|
75
|
+
"UNAUTHORIZED",
|
|
76
|
+
"Linear authentication failed. Check LINEAR_API_TOKEN.",
|
|
77
|
+
{ error }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (errorMessage.includes("rate limit")) {
|
|
81
|
+
throw new LinearServiceError("RATE_LIMITED", "Linear API rate limit exceeded", { error });
|
|
82
|
+
}
|
|
83
|
+
throw new LinearServiceError("CLI_ERROR", `Linear SDK error: ${errorMessage}`, { error });
|
|
84
|
+
}
|
|
85
|
+
async function fetchLinearIssue(identifier) {
|
|
86
|
+
try {
|
|
87
|
+
logger.debug(`Fetching Linear issue: ${identifier}`);
|
|
88
|
+
const client = createLinearClient();
|
|
89
|
+
const issue = await client.issue(identifier);
|
|
90
|
+
if (!issue) {
|
|
91
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
92
|
+
}
|
|
93
|
+
const result = {
|
|
94
|
+
id: issue.id,
|
|
95
|
+
identifier: issue.identifier,
|
|
96
|
+
title: issue.title,
|
|
97
|
+
url: issue.url,
|
|
98
|
+
createdAt: issue.createdAt.toISOString(),
|
|
99
|
+
updatedAt: issue.updatedAt.toISOString()
|
|
100
|
+
};
|
|
101
|
+
if (issue.description) {
|
|
102
|
+
result.description = issue.description;
|
|
103
|
+
}
|
|
104
|
+
if (issue.state) {
|
|
105
|
+
const state = await issue.state;
|
|
106
|
+
if (state == null ? void 0 : state.name) {
|
|
107
|
+
result.state = state.name;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof LinearServiceError) {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
handleLinearError(error, "fetchLinearIssue");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function createLinearIssue(title, body, teamKey, _labels) {
|
|
119
|
+
try {
|
|
120
|
+
logger.debug(`Creating Linear issue in team ${teamKey}: ${title}`);
|
|
121
|
+
const client = createLinearClient();
|
|
122
|
+
const teams = await client.teams();
|
|
123
|
+
const team = teams.nodes.find((t) => t.key === teamKey);
|
|
124
|
+
if (!team) {
|
|
125
|
+
throw new LinearServiceError("NOT_FOUND", `Linear team ${teamKey} not found`);
|
|
126
|
+
}
|
|
127
|
+
const issueInput = {
|
|
128
|
+
teamId: team.id,
|
|
129
|
+
title
|
|
130
|
+
};
|
|
131
|
+
if (body) {
|
|
132
|
+
issueInput.description = body;
|
|
133
|
+
}
|
|
134
|
+
const payload = await client.createIssue(issueInput);
|
|
135
|
+
const issue = await payload.issue;
|
|
136
|
+
if (!issue) {
|
|
137
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to create Linear issue");
|
|
138
|
+
}
|
|
139
|
+
const url = issue.url ?? buildLinearIssueUrl(issue.identifier, title);
|
|
140
|
+
return {
|
|
141
|
+
identifier: issue.identifier,
|
|
142
|
+
url
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error instanceof LinearServiceError) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
handleLinearError(error, "createLinearIssue");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function createLinearChildIssue(title, body, teamKey, parentId, _labels) {
|
|
152
|
+
try {
|
|
153
|
+
logger.debug(`Creating Linear child issue in team ${teamKey}: ${title}`);
|
|
154
|
+
const client = createLinearClient();
|
|
155
|
+
const teams = await client.teams();
|
|
156
|
+
const team = teams.nodes.find((t) => t.key === teamKey);
|
|
157
|
+
if (!team) {
|
|
158
|
+
throw new LinearServiceError("NOT_FOUND", `Linear team ${teamKey} not found`);
|
|
159
|
+
}
|
|
160
|
+
const issueInput = {
|
|
161
|
+
teamId: team.id,
|
|
162
|
+
title,
|
|
163
|
+
parentId
|
|
164
|
+
// UUID of parent issue
|
|
165
|
+
};
|
|
166
|
+
if (body) {
|
|
167
|
+
issueInput.description = body;
|
|
168
|
+
}
|
|
169
|
+
const payload = await client.createIssue(issueInput);
|
|
170
|
+
const issue = await payload.issue;
|
|
171
|
+
if (!issue) {
|
|
172
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to create Linear child issue");
|
|
173
|
+
}
|
|
174
|
+
const url = issue.url ?? buildLinearIssueUrl(issue.identifier, title);
|
|
175
|
+
return {
|
|
176
|
+
identifier: issue.identifier,
|
|
177
|
+
url
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof LinearServiceError) {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
handleLinearError(error, "createLinearChildIssue");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function createLinearComment(identifier, body) {
|
|
187
|
+
try {
|
|
188
|
+
logger.debug(`Creating comment on Linear issue ${identifier}`);
|
|
189
|
+
const client = createLinearClient();
|
|
190
|
+
const issue = await client.issue(identifier);
|
|
191
|
+
if (!issue) {
|
|
192
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
193
|
+
}
|
|
194
|
+
const payload = await client.createComment({
|
|
195
|
+
issueId: issue.id,
|
|
196
|
+
body
|
|
197
|
+
});
|
|
198
|
+
const comment = await payload.comment;
|
|
199
|
+
if (!comment) {
|
|
200
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to create Linear comment");
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
id: comment.id,
|
|
204
|
+
body: comment.body,
|
|
205
|
+
createdAt: comment.createdAt.toISOString(),
|
|
206
|
+
updatedAt: comment.updatedAt.toISOString(),
|
|
207
|
+
url: comment.url
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof LinearServiceError) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
handleLinearError(error, "createLinearComment");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function updateLinearIssueState(identifier, stateName) {
|
|
217
|
+
try {
|
|
218
|
+
logger.debug(`Updating Linear issue ${identifier} state to: ${stateName}`);
|
|
219
|
+
const client = createLinearClient();
|
|
220
|
+
const issue = await client.issue(identifier);
|
|
221
|
+
if (!issue) {
|
|
222
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
223
|
+
}
|
|
224
|
+
const team = await issue.team;
|
|
225
|
+
if (!team) {
|
|
226
|
+
throw new LinearServiceError("CLI_ERROR", "Issue has no team");
|
|
227
|
+
}
|
|
228
|
+
const states = await team.states();
|
|
229
|
+
const state = states.nodes.find((s) => s.name === stateName);
|
|
230
|
+
if (!state) {
|
|
231
|
+
throw new LinearServiceError(
|
|
232
|
+
"NOT_FOUND",
|
|
233
|
+
`State "${stateName}" not found in team ${team.key}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
await client.updateIssue(issue.id, {
|
|
237
|
+
stateId: state.id
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error instanceof LinearServiceError) {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
handleLinearError(error, "updateLinearIssueState");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async function editLinearIssue(identifier, updates) {
|
|
247
|
+
try {
|
|
248
|
+
logger.debug(`Editing Linear issue ${identifier}`, { updates });
|
|
249
|
+
const client = createLinearClient();
|
|
250
|
+
const issue = await client.issue(identifier);
|
|
251
|
+
if (!issue) {
|
|
252
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
253
|
+
}
|
|
254
|
+
const updatePayload = {};
|
|
255
|
+
if (updates.title !== void 0) {
|
|
256
|
+
updatePayload.title = updates.title;
|
|
257
|
+
}
|
|
258
|
+
if (updates.description !== void 0) {
|
|
259
|
+
updatePayload.description = updates.description;
|
|
260
|
+
}
|
|
261
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
262
|
+
await client.updateIssue(issue.id, updatePayload);
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof LinearServiceError) {
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
handleLinearError(error, "editLinearIssue");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function getLinearComment(commentId) {
|
|
272
|
+
try {
|
|
273
|
+
logger.debug(`Fetching Linear comment: ${commentId}`);
|
|
274
|
+
const client = createLinearClient();
|
|
275
|
+
const comment = await client.comment({ id: commentId });
|
|
276
|
+
if (!comment) {
|
|
277
|
+
throw new LinearServiceError("NOT_FOUND", `Linear comment ${commentId} not found`);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
id: comment.id,
|
|
281
|
+
body: comment.body,
|
|
282
|
+
createdAt: comment.createdAt.toISOString(),
|
|
283
|
+
updatedAt: comment.updatedAt.toISOString(),
|
|
284
|
+
url: comment.url
|
|
285
|
+
};
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof LinearServiceError) {
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
handleLinearError(error, "getLinearComment");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function updateLinearComment(commentId, body) {
|
|
294
|
+
try {
|
|
295
|
+
logger.debug(`Updating Linear comment: ${commentId}`);
|
|
296
|
+
const client = createLinearClient();
|
|
297
|
+
const payload = await client.updateComment(commentId, { body });
|
|
298
|
+
const comment = await payload.comment;
|
|
299
|
+
if (!comment) {
|
|
300
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to update Linear comment");
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
id: comment.id,
|
|
304
|
+
body: comment.body,
|
|
305
|
+
createdAt: comment.createdAt.toISOString(),
|
|
306
|
+
updatedAt: comment.updatedAt.toISOString(),
|
|
307
|
+
url: comment.url
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof LinearServiceError) {
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
handleLinearError(error, "updateLinearComment");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function fetchLinearIssueComments(identifier) {
|
|
317
|
+
try {
|
|
318
|
+
logger.debug(`Fetching comments for Linear issue: ${identifier}`);
|
|
319
|
+
const client = createLinearClient();
|
|
320
|
+
const issue = await client.issue(identifier);
|
|
321
|
+
if (!issue) {
|
|
322
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
323
|
+
}
|
|
324
|
+
const comments = await issue.comments({ first: 100 });
|
|
325
|
+
return comments.nodes.map((comment) => ({
|
|
326
|
+
id: comment.id,
|
|
327
|
+
body: comment.body,
|
|
328
|
+
createdAt: comment.createdAt.toISOString(),
|
|
329
|
+
updatedAt: comment.updatedAt.toISOString(),
|
|
330
|
+
url: comment.url
|
|
331
|
+
}));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error instanceof LinearServiceError) {
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
handleLinearError(error, "fetchLinearIssueComments");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function getLinearChildIssues(identifier, options) {
|
|
340
|
+
try {
|
|
341
|
+
logger.debug(`Fetching child issues for Linear issue: ${identifier}`);
|
|
342
|
+
const client = createLinearClient(options == null ? void 0 : options.apiToken);
|
|
343
|
+
const issue = await client.issue(identifier);
|
|
344
|
+
if (!issue) {
|
|
345
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
346
|
+
}
|
|
347
|
+
const children = await issue.children({ first: 100 });
|
|
348
|
+
const results = await Promise.all(
|
|
349
|
+
children.nodes.map(async (child) => {
|
|
350
|
+
const stateObj = await child.state;
|
|
351
|
+
const state = (stateObj == null ? void 0 : stateObj.name) ?? "unknown";
|
|
352
|
+
return {
|
|
353
|
+
id: child.identifier,
|
|
354
|
+
title: child.title,
|
|
355
|
+
url: child.url,
|
|
356
|
+
state
|
|
357
|
+
};
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
return results;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error instanceof LinearServiceError) {
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
handleLinearError(error, "getLinearChildIssues");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function createLinearIssueRelation(blockingIssueId, blockedIssueId) {
|
|
369
|
+
try {
|
|
370
|
+
logger.debug(`Creating Linear issue relation: ${blockingIssueId} blocks ${blockedIssueId}`);
|
|
371
|
+
const client = createLinearClient();
|
|
372
|
+
const payload = await client.createIssueRelation({
|
|
373
|
+
issueId: blockingIssueId,
|
|
374
|
+
relatedIssueId: blockedIssueId,
|
|
375
|
+
type: IssueRelationType.Blocks
|
|
376
|
+
});
|
|
377
|
+
if (!payload.success) {
|
|
378
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to create Linear issue relation");
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof LinearServiceError) {
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
handleLinearError(error, "createLinearIssueRelation");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function getLinearIssueDependencies(identifier, direction) {
|
|
388
|
+
try {
|
|
389
|
+
logger.debug(`Fetching Linear issue dependencies: ${identifier} (direction: ${direction})`);
|
|
390
|
+
const client = createLinearClient();
|
|
391
|
+
const issue = await client.issue(identifier);
|
|
392
|
+
if (!issue) {
|
|
393
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
394
|
+
}
|
|
395
|
+
const [relations, inverseRelations] = await Promise.all([
|
|
396
|
+
issue.relations(),
|
|
397
|
+
issue.inverseRelations()
|
|
398
|
+
]);
|
|
399
|
+
const blocking = [];
|
|
400
|
+
const blockedBy = [];
|
|
401
|
+
const buildDependencyResult = async (relatedIssue) => {
|
|
402
|
+
if (!relatedIssue) return null;
|
|
403
|
+
const stateObj = await relatedIssue.state;
|
|
404
|
+
const state = (stateObj == null ? void 0 : stateObj.name) ?? "unknown";
|
|
405
|
+
return {
|
|
406
|
+
id: relatedIssue.identifier,
|
|
407
|
+
title: relatedIssue.title,
|
|
408
|
+
url: relatedIssue.url,
|
|
409
|
+
state
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
if (direction === "blocking" || direction === "both") {
|
|
413
|
+
const blockingRelations = relations.nodes.filter(
|
|
414
|
+
(r) => r.type === IssueRelationType.Blocks
|
|
415
|
+
);
|
|
416
|
+
const relatedIssuePromises = blockingRelations.map((r) => r.relatedIssue).filter((p) => p !== void 0);
|
|
417
|
+
const relatedIssues = await Promise.all(relatedIssuePromises);
|
|
418
|
+
const blockingResults = await Promise.all(
|
|
419
|
+
relatedIssues.map((issue2) => buildDependencyResult(issue2))
|
|
420
|
+
);
|
|
421
|
+
blocking.push(...blockingResults.filter((r) => r !== null));
|
|
422
|
+
}
|
|
423
|
+
if (direction === "blocked_by" || direction === "both") {
|
|
424
|
+
const blockedByRelations = inverseRelations.nodes.filter(
|
|
425
|
+
(r) => r.type === IssueRelationType.Blocks
|
|
426
|
+
);
|
|
427
|
+
const sourceIssuePromises = blockedByRelations.map((r) => r.issue).filter((p) => p !== void 0);
|
|
428
|
+
const sourceIssues = await Promise.all(sourceIssuePromises);
|
|
429
|
+
const blockedByResults = await Promise.all(
|
|
430
|
+
sourceIssues.map((issue2) => buildDependencyResult(issue2))
|
|
431
|
+
);
|
|
432
|
+
blockedBy.push(...blockedByResults.filter((r) => r !== null));
|
|
433
|
+
}
|
|
434
|
+
return { blocking, blockedBy };
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (error instanceof LinearServiceError) {
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
handleLinearError(error, "getLinearIssueDependencies");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function deleteLinearIssueRelation(relationId) {
|
|
443
|
+
try {
|
|
444
|
+
logger.debug(`Deleting Linear issue relation: ${relationId}`);
|
|
445
|
+
const client = createLinearClient();
|
|
446
|
+
const payload = await client.deleteIssueRelation(relationId);
|
|
447
|
+
if (!payload.success) {
|
|
448
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to delete Linear issue relation");
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error instanceof LinearServiceError) {
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
handleLinearError(error, "deleteLinearIssueRelation");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function fetchLinearIssueList(teamKey, options) {
|
|
458
|
+
try {
|
|
459
|
+
const limit = (options == null ? void 0 : options.limit) ?? 100;
|
|
460
|
+
logger.debug(`Fetching Linear issue list for team ${teamKey}`, { limit, mine: options == null ? void 0 : options.mine });
|
|
461
|
+
const client = createLinearClient(options == null ? void 0 : options.apiToken);
|
|
462
|
+
const teams = await client.teams();
|
|
463
|
+
const team = teams.nodes.find((t) => t.key === teamKey);
|
|
464
|
+
if (!team) {
|
|
465
|
+
throw new LinearServiceError("NOT_FOUND", `Linear team ${teamKey} not found`);
|
|
466
|
+
}
|
|
467
|
+
const filter = {
|
|
468
|
+
state: {
|
|
469
|
+
type: {
|
|
470
|
+
nin: ["completed", "canceled"]
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
if (options == null ? void 0 : options.mine) {
|
|
475
|
+
filter.assignee = { isMe: { eq: true } };
|
|
476
|
+
}
|
|
477
|
+
const issues = await team.issues({
|
|
478
|
+
first: limit,
|
|
479
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- PaginationOrderBy is a const enum incompatible with isolatedModules
|
|
480
|
+
orderBy: "updatedAt",
|
|
481
|
+
filter
|
|
482
|
+
});
|
|
483
|
+
const results = await Promise.all(
|
|
484
|
+
issues.nodes.map(async (issue) => {
|
|
485
|
+
const stateObj = await issue.state;
|
|
486
|
+
const state = (stateObj == null ? void 0 : stateObj.name) ?? "unknown";
|
|
487
|
+
return {
|
|
488
|
+
id: issue.identifier,
|
|
489
|
+
title: issue.title,
|
|
490
|
+
updatedAt: issue.updatedAt.toISOString(),
|
|
491
|
+
url: issue.url,
|
|
492
|
+
state
|
|
493
|
+
};
|
|
494
|
+
})
|
|
495
|
+
);
|
|
496
|
+
return results;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (error instanceof LinearServiceError) {
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
handleLinearError(error, "fetchLinearIssueList");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function findLinearIssueRelation(blockingIdentifier, blockedIdentifier) {
|
|
505
|
+
try {
|
|
506
|
+
logger.debug(`Finding Linear issue relation: ${blockingIdentifier} blocks ${blockedIdentifier}`);
|
|
507
|
+
const client = createLinearClient();
|
|
508
|
+
const blockingIssue = await client.issue(blockingIdentifier);
|
|
509
|
+
if (!blockingIssue) {
|
|
510
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${blockingIdentifier} not found`);
|
|
511
|
+
}
|
|
512
|
+
const blockedIssue = await client.issue(blockedIdentifier);
|
|
513
|
+
if (!blockedIssue) {
|
|
514
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${blockedIdentifier} not found`);
|
|
515
|
+
}
|
|
516
|
+
const relations = await blockingIssue.relations();
|
|
517
|
+
const blockingRelations = relations.nodes.filter(
|
|
518
|
+
(r) => r.type === IssueRelationType.Blocks
|
|
519
|
+
);
|
|
520
|
+
const relationsWithIssues = await Promise.all(
|
|
521
|
+
blockingRelations.map(async (relation) => ({
|
|
522
|
+
relation,
|
|
523
|
+
relatedIssue: await relation.relatedIssue
|
|
524
|
+
}))
|
|
525
|
+
);
|
|
526
|
+
const matchingRelation = relationsWithIssues.find(
|
|
527
|
+
({ relatedIssue }) => (relatedIssue == null ? void 0 : relatedIssue.id) === blockedIssue.id
|
|
528
|
+
);
|
|
529
|
+
return (matchingRelation == null ? void 0 : matchingRelation.relation.id) ?? null;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (error instanceof LinearServiceError) {
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
handleLinearError(error, "findLinearIssueRelation");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/lib/providers/jira/JiraApiClient.ts
|
|
539
|
+
import https from "https";
|
|
540
|
+
|
|
541
|
+
// src/lib/providers/jira/AdfMarkdownConverter.ts
|
|
542
|
+
import { Parser } from "extended-markdown-adf-parser";
|
|
543
|
+
var parser = new Parser();
|
|
544
|
+
function sanitizeCodeMarks(node) {
|
|
545
|
+
var _a;
|
|
546
|
+
if ((_a = node.marks) == null ? void 0 : _a.some((mark) => mark.type === "code")) {
|
|
547
|
+
node.marks = [{ type: "code" }];
|
|
548
|
+
}
|
|
549
|
+
if (node.content && Array.isArray(node.content)) {
|
|
550
|
+
node.content = node.content.map((child) => sanitizeCodeMarks(child));
|
|
551
|
+
}
|
|
552
|
+
return node;
|
|
553
|
+
}
|
|
554
|
+
var BLOCK_LEVEL_TYPES = /* @__PURE__ */ new Set([
|
|
555
|
+
"paragraph",
|
|
556
|
+
"bulletList",
|
|
557
|
+
"orderedList",
|
|
558
|
+
"codeBlock",
|
|
559
|
+
"heading",
|
|
560
|
+
"blockquote",
|
|
561
|
+
"rule",
|
|
562
|
+
"mediaGroup",
|
|
563
|
+
"nestedExpand",
|
|
564
|
+
"panel",
|
|
565
|
+
"table",
|
|
566
|
+
"taskList",
|
|
567
|
+
"decisionList",
|
|
568
|
+
"mediaSingle"
|
|
569
|
+
]);
|
|
570
|
+
function wrapTableCellContent(node) {
|
|
571
|
+
if (node.content && Array.isArray(node.content)) {
|
|
572
|
+
node.content = node.content.map((child) => wrapTableCellContent(child));
|
|
573
|
+
}
|
|
574
|
+
if (node.type !== "tableCell" && node.type !== "tableHeader") {
|
|
575
|
+
return node;
|
|
576
|
+
}
|
|
577
|
+
if (!node.content || node.content.length === 0) {
|
|
578
|
+
return node;
|
|
579
|
+
}
|
|
580
|
+
const allInline = node.content.every((child) => !BLOCK_LEVEL_TYPES.has(child.type));
|
|
581
|
+
if (allInline) {
|
|
582
|
+
node.content = [{ type: "paragraph", content: node.content }];
|
|
583
|
+
} else {
|
|
584
|
+
const newContent = [];
|
|
585
|
+
let inlineRun = [];
|
|
586
|
+
for (const child of node.content) {
|
|
587
|
+
if (BLOCK_LEVEL_TYPES.has(child.type)) {
|
|
588
|
+
if (inlineRun.length > 0) {
|
|
589
|
+
newContent.push({ type: "paragraph", content: inlineRun });
|
|
590
|
+
inlineRun = [];
|
|
591
|
+
}
|
|
592
|
+
newContent.push(child);
|
|
593
|
+
} else {
|
|
594
|
+
inlineRun.push(child);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (inlineRun.length > 0) {
|
|
598
|
+
newContent.push({ type: "paragraph", content: inlineRun });
|
|
599
|
+
}
|
|
600
|
+
node.content = newContent;
|
|
601
|
+
}
|
|
602
|
+
return node;
|
|
603
|
+
}
|
|
604
|
+
var taskIdCounter = 0;
|
|
605
|
+
function getCanonicalPlainText(text) {
|
|
606
|
+
const miniAdf = parser.markdownToAdf(text);
|
|
607
|
+
return getPlainText(miniAdf);
|
|
608
|
+
}
|
|
609
|
+
function extractCheckboxBlocks(markdown) {
|
|
610
|
+
var _a, _b;
|
|
611
|
+
const lines = markdown.split("\n");
|
|
612
|
+
const blocks = [];
|
|
613
|
+
let i = 0;
|
|
614
|
+
while (i < lines.length) {
|
|
615
|
+
const bulletLines = [];
|
|
616
|
+
let blockIndent = null;
|
|
617
|
+
while (i < lines.length) {
|
|
618
|
+
const line = lines[i] ?? "";
|
|
619
|
+
const checkboxMatch = line.match(/^(\s*)[-*+] \[([ xX])\] (.*)$/);
|
|
620
|
+
if (checkboxMatch) {
|
|
621
|
+
const indent = ((_a = checkboxMatch[1]) == null ? void 0 : _a.length) ?? 0;
|
|
622
|
+
if (blockIndent === null) {
|
|
623
|
+
blockIndent = indent;
|
|
624
|
+
} else if (indent !== blockIndent) {
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
const state = checkboxMatch[2] === " " ? "TODO" : "DONE";
|
|
628
|
+
bulletLines.push({ isCheckbox: true, state, rawText: checkboxMatch[3] ?? "" });
|
|
629
|
+
i++;
|
|
630
|
+
} else if (line.match(/^\s*[-*+] /)) {
|
|
631
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
632
|
+
const indent = ((_b = indentMatch == null ? void 0 : indentMatch[1]) == null ? void 0 : _b.length) ?? 0;
|
|
633
|
+
if (blockIndent === null) {
|
|
634
|
+
blockIndent = indent;
|
|
635
|
+
} else if (indent !== blockIndent) {
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
bulletLines.push({ isCheckbox: false, state: null, rawText: "" });
|
|
639
|
+
i++;
|
|
640
|
+
} else if (bulletLines.length > 0 && line.match(/^\s/) && line.trim() !== "") {
|
|
641
|
+
const lastItem = bulletLines[bulletLines.length - 1];
|
|
642
|
+
if (lastItem) {
|
|
643
|
+
lastItem.rawText += "\n" + line.trim();
|
|
644
|
+
}
|
|
645
|
+
i++;
|
|
646
|
+
} else {
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (bulletLines.length > 0) {
|
|
651
|
+
const allCheckboxes = bulletLines.every((l) => l.isCheckbox);
|
|
652
|
+
if (allCheckboxes) {
|
|
653
|
+
blocks.push({
|
|
654
|
+
states: bulletLines.map((l) => l.state),
|
|
655
|
+
texts: bulletLines.map((l) => getCanonicalPlainText(l.rawText))
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
i++;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return blocks;
|
|
663
|
+
}
|
|
664
|
+
function getPlainText(node) {
|
|
665
|
+
if (node.type === "text" && node.text !== void 0) return node.text;
|
|
666
|
+
if (!node.content) return "";
|
|
667
|
+
return node.content.map(getPlainText).join("");
|
|
668
|
+
}
|
|
669
|
+
function convertCheckboxesToTaskList(node, blocks) {
|
|
670
|
+
const cursor = { index: 0 };
|
|
671
|
+
return convertCheckboxesRecursive(node, blocks, cursor);
|
|
672
|
+
}
|
|
673
|
+
function convertCheckboxesRecursive(node, blocks, cursor) {
|
|
674
|
+
var _a;
|
|
675
|
+
if (node.type === "bulletList" && node.content && node.content.length > 0 && cursor.index < blocks.length) {
|
|
676
|
+
const block = blocks[cursor.index];
|
|
677
|
+
if (!block) return node;
|
|
678
|
+
if (node.content.length === block.states.length) {
|
|
679
|
+
const plaintexts = node.content.map((listItem) => getPlainText(listItem));
|
|
680
|
+
const matches = plaintexts.every((text, i) => text === block.texts[i]);
|
|
681
|
+
if (matches) {
|
|
682
|
+
const allSimple = node.content.every((item) => {
|
|
683
|
+
var _a2, _b;
|
|
684
|
+
return ((_a2 = item.content) == null ? void 0 : _a2.length) === 1 && ((_b = item.content[0]) == null ? void 0 : _b.type) === "paragraph";
|
|
685
|
+
});
|
|
686
|
+
if (!allSimple) {
|
|
687
|
+
cursor.index++;
|
|
688
|
+
return node;
|
|
689
|
+
}
|
|
690
|
+
cursor.index++;
|
|
691
|
+
node.type = "taskList";
|
|
692
|
+
node.attrs = { localId: `tasklist-${++taskIdCounter}` };
|
|
693
|
+
for (const [i, listItem] of node.content.entries()) {
|
|
694
|
+
listItem.type = "taskItem";
|
|
695
|
+
listItem.attrs = {
|
|
696
|
+
localId: `task-${++taskIdCounter}`,
|
|
697
|
+
state: block.states[i]
|
|
698
|
+
};
|
|
699
|
+
const firstChild = (_a = listItem.content) == null ? void 0 : _a[0];
|
|
700
|
+
if ((firstChild == null ? void 0 : firstChild.type) === "paragraph" && firstChild.content) {
|
|
701
|
+
listItem.content = firstChild.content;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return node;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (node.content && Array.isArray(node.content)) {
|
|
709
|
+
node.content = node.content.map((child) => convertCheckboxesRecursive(child, blocks, cursor));
|
|
710
|
+
}
|
|
711
|
+
return node;
|
|
712
|
+
}
|
|
713
|
+
function convertDetailsToExpandSyntax(markdown) {
|
|
714
|
+
if (!markdown) return markdown;
|
|
715
|
+
let previousText = "";
|
|
716
|
+
let currentText = markdown;
|
|
717
|
+
while (previousText !== currentText) {
|
|
718
|
+
previousText = currentText;
|
|
719
|
+
currentText = currentText.replace(
|
|
720
|
+
/<details[^>]*>\s*<summary[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi,
|
|
721
|
+
(_match, summary, content) => {
|
|
722
|
+
const cleanSummary = summary.trim().replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
723
|
+
let cleanContent = content.trim();
|
|
724
|
+
cleanContent = cleanContent.replace(/\n{3,}/g, "\n\n");
|
|
725
|
+
if (cleanContent) {
|
|
726
|
+
return `~~~expand title="${cleanSummary}"
|
|
727
|
+
${cleanContent}
|
|
728
|
+
~~~`;
|
|
729
|
+
} else {
|
|
730
|
+
return `~~~expand title="${cleanSummary}"
|
|
731
|
+
~~~`;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
return currentText;
|
|
737
|
+
}
|
|
738
|
+
function adfToMarkdown(adf) {
|
|
739
|
+
if (!adf) return "";
|
|
740
|
+
if (typeof adf === "string") return adf;
|
|
741
|
+
return parser.adfToMarkdown(adf);
|
|
742
|
+
}
|
|
743
|
+
function markdownToAdf(markdown) {
|
|
744
|
+
if (!markdown) {
|
|
745
|
+
return { type: "doc", version: 1, content: [] };
|
|
746
|
+
}
|
|
747
|
+
taskIdCounter = 0;
|
|
748
|
+
const checkboxBlocks = extractCheckboxBlocks(markdown);
|
|
749
|
+
const preprocessed = convertDetailsToExpandSyntax(markdown);
|
|
750
|
+
const adf = parser.markdownToAdf(preprocessed);
|
|
751
|
+
let result = sanitizeCodeMarks(adf);
|
|
752
|
+
result = wrapTableCellContent(result);
|
|
753
|
+
result = convertCheckboxesToTaskList(result, checkboxBlocks);
|
|
754
|
+
return result;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/lib/providers/jira/JiraApiClient.ts
|
|
758
|
+
var JiraApiClient = class {
|
|
759
|
+
constructor(config) {
|
|
760
|
+
this.baseUrl = `${config.host.replace(/\/$/, "")}/rest/api/3`;
|
|
761
|
+
const credentials = Buffer.from(`${config.username}:${config.apiToken}`).toString("base64");
|
|
762
|
+
this.authHeader = `Basic ${credentials}`;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Make an HTTP request to Jira API
|
|
766
|
+
*/
|
|
767
|
+
async request(method, endpoint, body) {
|
|
768
|
+
const url = new URL(`${this.baseUrl}${endpoint}`);
|
|
769
|
+
getLogger().debug(`Jira API ${method} request`, { url: url.toString() });
|
|
770
|
+
if (body) {
|
|
771
|
+
getLogger().debug("Jira API request body", JSON.stringify(body, null, 2));
|
|
772
|
+
}
|
|
773
|
+
return new Promise((resolve, reject) => {
|
|
774
|
+
const options = {
|
|
775
|
+
hostname: url.hostname,
|
|
776
|
+
port: url.port || 443,
|
|
777
|
+
path: url.pathname + url.search,
|
|
778
|
+
method,
|
|
779
|
+
headers: {
|
|
780
|
+
"Authorization": this.authHeader,
|
|
781
|
+
"Accept": "application/json",
|
|
782
|
+
"Content-Type": "application/json"
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
const req = https.request({ ...options, timeout: 3e4 }, (res) => {
|
|
786
|
+
const chunks = [];
|
|
787
|
+
res.on("data", (chunk) => {
|
|
788
|
+
chunks.push(chunk);
|
|
789
|
+
});
|
|
790
|
+
res.on("end", () => {
|
|
791
|
+
var _a;
|
|
792
|
+
const data = Buffer.concat(chunks).toString("utf8");
|
|
793
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
794
|
+
let errorDetail = data;
|
|
795
|
+
try {
|
|
796
|
+
const parsed = JSON.parse(data);
|
|
797
|
+
const parts = [];
|
|
798
|
+
if ((_a = parsed.errorMessages) == null ? void 0 : _a.length) {
|
|
799
|
+
parts.push(`messages: ${parsed.errorMessages.join(", ")}`);
|
|
800
|
+
}
|
|
801
|
+
if (parsed.errors && Object.keys(parsed.errors).length) {
|
|
802
|
+
parts.push(`field errors: ${JSON.stringify(parsed.errors)}`);
|
|
803
|
+
}
|
|
804
|
+
if (parts.length) {
|
|
805
|
+
errorDetail = parts.join("; ");
|
|
806
|
+
}
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
reject(new Error(`Jira API error (${res.statusCode}): ${errorDetail}`));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (res.statusCode === 204 || !data) {
|
|
813
|
+
resolve({});
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
resolve(JSON.parse(data));
|
|
818
|
+
} catch (error) {
|
|
819
|
+
reject(new Error(`Failed to parse Jira API response: ${error}`));
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
req.on("timeout", () => {
|
|
824
|
+
req.destroy();
|
|
825
|
+
reject(new Error("Jira API request timed out after 30 seconds"));
|
|
826
|
+
});
|
|
827
|
+
req.on("error", (error) => {
|
|
828
|
+
reject(new Error(`Jira API request failed: ${error.message}`));
|
|
829
|
+
});
|
|
830
|
+
if (body) {
|
|
831
|
+
req.write(JSON.stringify(body));
|
|
832
|
+
}
|
|
833
|
+
req.end();
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Make a GET request to Jira API
|
|
838
|
+
*/
|
|
839
|
+
async get(endpoint) {
|
|
840
|
+
return this.request("GET", endpoint);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Make a POST request to Jira API
|
|
844
|
+
*/
|
|
845
|
+
async post(endpoint, body) {
|
|
846
|
+
return this.request("POST", endpoint, body);
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Make a PUT request to Jira API
|
|
850
|
+
*/
|
|
851
|
+
async put(endpoint, body) {
|
|
852
|
+
return this.request("PUT", endpoint, body);
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Make a DELETE request to Jira API
|
|
856
|
+
*/
|
|
857
|
+
async delete(endpoint) {
|
|
858
|
+
await this.request("DELETE", endpoint);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Fetch an issue by key (e.g., "PROJ-123")
|
|
862
|
+
*/
|
|
863
|
+
async getIssue(issueKey) {
|
|
864
|
+
return this.get(`/issue/${issueKey}`);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Add a comment to an issue
|
|
868
|
+
* Accepts Markdown content which is converted to ADF for Jira
|
|
869
|
+
*/
|
|
870
|
+
async addComment(issueKey, body) {
|
|
871
|
+
const adfBody = markdownToAdf(body);
|
|
872
|
+
getLogger().debug("Adding comment to Jira issue", { issueKey, bodyLength: body.length });
|
|
873
|
+
return this.post(`/issue/${issueKey}/comment`, {
|
|
874
|
+
body: adfBody
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Get all comments for an issue
|
|
879
|
+
*/
|
|
880
|
+
async getComments(issueKey) {
|
|
881
|
+
const response = await this.get(`/issue/${issueKey}/comment?maxResults=5000`);
|
|
882
|
+
if (response.total > response.comments.length) {
|
|
883
|
+
getLogger().warn(`Comments truncated for issue ${issueKey}: returned ${response.comments.length} of ${response.total} total comments`);
|
|
884
|
+
}
|
|
885
|
+
return response.comments;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Update a comment on an issue
|
|
889
|
+
* Accepts Markdown content which is converted to ADF for Jira
|
|
890
|
+
*/
|
|
891
|
+
async updateComment(issueKey, commentId, body) {
|
|
892
|
+
return this.put(`/issue/${issueKey}/comment/${commentId}`, {
|
|
893
|
+
body: markdownToAdf(body)
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Get available transitions for an issue
|
|
898
|
+
*/
|
|
899
|
+
async getTransitions(issueKey) {
|
|
900
|
+
const response = await this.get(`/issue/${issueKey}/transitions`);
|
|
901
|
+
return response.transitions;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Transition an issue to a new state
|
|
905
|
+
*/
|
|
906
|
+
async transitionIssue(issueKey, transitionId) {
|
|
907
|
+
await this.post(`/issue/${issueKey}/transitions`, {
|
|
908
|
+
transition: {
|
|
909
|
+
id: transitionId
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Create a new issue
|
|
915
|
+
* Accepts Markdown description which is converted to ADF for Jira
|
|
916
|
+
*/
|
|
917
|
+
async createIssue(projectKey, summary, description, issueType = "Task") {
|
|
918
|
+
return this.post("/issue", {
|
|
919
|
+
fields: {
|
|
920
|
+
project: {
|
|
921
|
+
key: projectKey
|
|
922
|
+
},
|
|
923
|
+
summary,
|
|
924
|
+
description: markdownToAdf(description),
|
|
925
|
+
issuetype: {
|
|
926
|
+
name: issueType
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Update an issue's fields (summary, description)
|
|
933
|
+
* @param issueKey - Jira issue key (e.g., "PROJ-123")
|
|
934
|
+
* @param fields - Fields to update
|
|
935
|
+
*/
|
|
936
|
+
async updateIssue(issueKey, fields) {
|
|
937
|
+
const updateFields = {};
|
|
938
|
+
if (fields.summary !== void 0) {
|
|
939
|
+
updateFields.summary = fields.summary;
|
|
940
|
+
}
|
|
941
|
+
if (fields.description !== void 0) {
|
|
942
|
+
updateFields.description = markdownToAdf(fields.description);
|
|
943
|
+
}
|
|
944
|
+
await this.put(`/issue/${issueKey}`, { fields: updateFields });
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Create an issue with a parent (subtask or child issue)
|
|
948
|
+
* Accepts Markdown description which is converted to ADF for Jira
|
|
949
|
+
*/
|
|
950
|
+
async createIssueWithParent(projectKey, summary, description, parentKey, issueType = "Subtask") {
|
|
951
|
+
return this.post("/issue", {
|
|
952
|
+
fields: {
|
|
953
|
+
project: {
|
|
954
|
+
key: projectKey
|
|
955
|
+
},
|
|
956
|
+
summary,
|
|
957
|
+
description: markdownToAdf(description),
|
|
958
|
+
issuetype: {
|
|
959
|
+
name: issueType
|
|
960
|
+
},
|
|
961
|
+
parent: {
|
|
962
|
+
key: parentKey
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Create an issue link (dependency/relationship between issues)
|
|
969
|
+
* @param inwardKey - The issue key for the inward side (e.g., the blocked issue)
|
|
970
|
+
* @param outwardKey - The issue key for the outward side (e.g., the blocking issue)
|
|
971
|
+
* @param linkType - The link type name (e.g., "Blocks")
|
|
972
|
+
*/
|
|
973
|
+
async createIssueLink(inwardKey, outwardKey, linkType) {
|
|
974
|
+
await this.post("/issueLink", {
|
|
975
|
+
type: {
|
|
976
|
+
name: linkType
|
|
977
|
+
},
|
|
978
|
+
inwardIssue: {
|
|
979
|
+
key: inwardKey
|
|
980
|
+
},
|
|
981
|
+
outwardIssue: {
|
|
982
|
+
key: outwardKey
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Delete an issue link by ID
|
|
988
|
+
*/
|
|
989
|
+
async deleteIssueLink(linkId) {
|
|
990
|
+
await this.delete(`/issueLink/${linkId}`);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Search issues using JQL
|
|
994
|
+
* Automatically paginates through all results up to MAX_SEARCH_RESULTS.
|
|
995
|
+
*/
|
|
996
|
+
async searchIssues(jql) {
|
|
997
|
+
const MAX_SEARCH_RESULTS = 5e3;
|
|
998
|
+
const allIssues = [];
|
|
999
|
+
let nextPageToken;
|
|
1000
|
+
const maxResults = 100;
|
|
1001
|
+
while (allIssues.length < MAX_SEARCH_RESULTS) {
|
|
1002
|
+
const body = {
|
|
1003
|
+
jql,
|
|
1004
|
+
maxResults,
|
|
1005
|
+
fields: [
|
|
1006
|
+
"summary",
|
|
1007
|
+
"description",
|
|
1008
|
+
"status",
|
|
1009
|
+
"issuetype",
|
|
1010
|
+
"project",
|
|
1011
|
+
"assignee",
|
|
1012
|
+
"reporter",
|
|
1013
|
+
"labels",
|
|
1014
|
+
"created",
|
|
1015
|
+
"updated",
|
|
1016
|
+
"issuelinks",
|
|
1017
|
+
"parent"
|
|
1018
|
+
]
|
|
1019
|
+
};
|
|
1020
|
+
if (nextPageToken) {
|
|
1021
|
+
body.nextPageToken = nextPageToken;
|
|
1022
|
+
}
|
|
1023
|
+
const response = await this.post(
|
|
1024
|
+
"/search/jql",
|
|
1025
|
+
body
|
|
1026
|
+
);
|
|
1027
|
+
allIssues.push(...response.issues);
|
|
1028
|
+
if (!response.nextPageToken || response.issues.length === 0) {
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
nextPageToken = response.nextPageToken;
|
|
1032
|
+
}
|
|
1033
|
+
if (allIssues.length >= MAX_SEARCH_RESULTS) {
|
|
1034
|
+
getLogger().warn(`Search results truncated at ${MAX_SEARCH_RESULTS} issues. The query matched more results than the safety cap allows.`, { jql, returnedCount: allIssues.length });
|
|
1035
|
+
}
|
|
1036
|
+
return allIssues;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Test connection to Jira API
|
|
1040
|
+
*/
|
|
1041
|
+
async testConnection() {
|
|
1042
|
+
try {
|
|
1043
|
+
await this.get("/myself");
|
|
1044
|
+
return true;
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1047
|
+
if (message.includes("Jira API error (401)") || message.includes("Jira API error (403)")) {
|
|
1048
|
+
getLogger().error("Jira connection test failed: authentication error", { error });
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
// src/lib/providers/jira/JiraIssueTracker.ts
|
|
1057
|
+
var JiraIssueTracker = class {
|
|
1058
|
+
constructor(config, options) {
|
|
1059
|
+
this.providerName = "jira";
|
|
1060
|
+
this.supportsPullRequests = false;
|
|
1061
|
+
this.config = config;
|
|
1062
|
+
this.client = new JiraApiClient({
|
|
1063
|
+
host: config.host,
|
|
1064
|
+
username: config.username,
|
|
1065
|
+
apiToken: config.apiToken
|
|
1066
|
+
});
|
|
1067
|
+
this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Normalize identifier to canonical uppercase form
|
|
1071
|
+
* Jira issue keys are case-sensitive in the API (must be uppercase)
|
|
1072
|
+
*/
|
|
1073
|
+
normalizeIdentifier(identifier) {
|
|
1074
|
+
return String(identifier).toUpperCase();
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Detect input type from user input
|
|
1078
|
+
* Jira issues follow pattern: PROJECTKEY-123 (case-insensitive)
|
|
1079
|
+
*/
|
|
1080
|
+
async detectInputType(input) {
|
|
1081
|
+
const jiraPattern = /^([A-Z][A-Z0-9]+)-(\d+)$/i;
|
|
1082
|
+
const match = input.match(jiraPattern);
|
|
1083
|
+
if (!match) {
|
|
1084
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
1085
|
+
}
|
|
1086
|
+
const issueKey = this.normalizeIdentifier(input);
|
|
1087
|
+
getLogger().debug("Checking if input is a Jira issue", { issueKey });
|
|
1088
|
+
try {
|
|
1089
|
+
await this.client.getIssue(issueKey);
|
|
1090
|
+
return { type: "issue", identifier: issueKey, rawInput: input };
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
|
|
1093
|
+
getLogger().debug("Issue not found", { issueKey, error });
|
|
1094
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
1095
|
+
}
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Fetch issue details
|
|
1101
|
+
*/
|
|
1102
|
+
async fetchIssue(identifier) {
|
|
1103
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1104
|
+
getLogger().debug("Fetching Jira issue", { issueKey });
|
|
1105
|
+
const jiraIssue = await this.client.getIssue(issueKey);
|
|
1106
|
+
return this.mapJiraIssueToIssue(jiraIssue);
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Check if issue exists (silent validation)
|
|
1110
|
+
*/
|
|
1111
|
+
async isValidIssue(identifier) {
|
|
1112
|
+
try {
|
|
1113
|
+
return await this.fetchIssue(identifier);
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
|
|
1116
|
+
getLogger().debug("Issue validation failed: not found", { identifier, error });
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
throw error;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Validate issue state
|
|
1124
|
+
* Note: Jira doesn't have a simple "closed" state - depends on workflow
|
|
1125
|
+
*/
|
|
1126
|
+
async validateIssueState(issue) {
|
|
1127
|
+
getLogger().debug("Jira issue state", { issueKey: issue.number, state: issue.state });
|
|
1128
|
+
if (issue.state === "closed") {
|
|
1129
|
+
const shouldContinue = await this.prompter(
|
|
1130
|
+
`Issue ${issue.number} is in a completed state. Continue anyway?`
|
|
1131
|
+
);
|
|
1132
|
+
if (!shouldContinue) {
|
|
1133
|
+
throw new Error("User cancelled due to completed issue");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Create a new issue
|
|
1139
|
+
*/
|
|
1140
|
+
async createIssue(title, body, _repository, _labels) {
|
|
1141
|
+
getLogger().debug("Creating Jira issue", { title, projectKey: this.config.projectKey });
|
|
1142
|
+
const jiraIssue = await this.client.createIssue(
|
|
1143
|
+
this.config.projectKey,
|
|
1144
|
+
title,
|
|
1145
|
+
body,
|
|
1146
|
+
this.config.defaultIssueType
|
|
1147
|
+
);
|
|
1148
|
+
return {
|
|
1149
|
+
number: jiraIssue.key,
|
|
1150
|
+
url: `${this.config.host}/browse/${jiraIssue.key}`
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Get issue URL
|
|
1155
|
+
*/
|
|
1156
|
+
async getIssueUrl(identifier) {
|
|
1157
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1158
|
+
return `${this.config.host}/browse/${issueKey}`;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Move issue to "In Progress" state
|
|
1162
|
+
* Uses configured transition mapping or default transition name
|
|
1163
|
+
*/
|
|
1164
|
+
async moveIssueToInProgress(identifier) {
|
|
1165
|
+
var _a;
|
|
1166
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1167
|
+
getLogger().debug("Moving Jira issue to In Progress", { issueKey });
|
|
1168
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
1169
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["In Progress"]) ?? this.findTransitionByName(transitions, ["In Progress", "Start Progress", "Start"]);
|
|
1170
|
+
if (!transitionName) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
`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`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
1176
|
+
if (!transition) {
|
|
1177
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
1178
|
+
}
|
|
1179
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
1180
|
+
getLogger().info("Issue transitioned successfully", { issueKey, transition: transitionName });
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Move issue to "Ready for Review" state
|
|
1184
|
+
* Uses configured transition mapping or default transition name
|
|
1185
|
+
*/
|
|
1186
|
+
async moveIssueToReadyForReview(identifier) {
|
|
1187
|
+
var _a;
|
|
1188
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1189
|
+
getLogger().debug("Moving Jira issue to Ready for Review", { issueKey });
|
|
1190
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
1191
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Ready for Review"]) ?? this.findTransitionByName(transitions, ["Ready for Review", "In Review", "Code Review", "Review"]);
|
|
1192
|
+
if (!transitionName) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`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`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
1198
|
+
if (!transition) {
|
|
1199
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
1200
|
+
}
|
|
1201
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
1202
|
+
getLogger().info("Issue transitioned to Ready for Review", { issueKey, transition: transitionName });
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Close an issue by transitioning to "Done" state
|
|
1206
|
+
* Uses configured transition mapping or default transition names
|
|
1207
|
+
*/
|
|
1208
|
+
async closeIssue(identifier) {
|
|
1209
|
+
var _a;
|
|
1210
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1211
|
+
getLogger().debug("Closing Jira issue", { issueKey });
|
|
1212
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
1213
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Done"]) ?? this.findTransitionByName(transitions, ["Done", "Close", "Closed", "Resolve", "Resolved"]);
|
|
1214
|
+
if (!transitionName) {
|
|
1215
|
+
throw new Error(
|
|
1216
|
+
`Could not find "Done" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
1220
|
+
if (!transition) {
|
|
1221
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
1222
|
+
}
|
|
1223
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
1224
|
+
getLogger().info("Issue closed successfully", { issueKey, transition: transitionName });
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Reopen an issue by transitioning back to an open state
|
|
1228
|
+
* Uses configured transition mapping or default transition names
|
|
1229
|
+
*/
|
|
1230
|
+
async reopenIssue(identifier) {
|
|
1231
|
+
var _a;
|
|
1232
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1233
|
+
getLogger().debug("Reopening Jira issue", { issueKey });
|
|
1234
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
1235
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Reopen"]) ?? this.findTransitionByName(transitions, ["Reopen", "To Do", "Open", "Backlog"]);
|
|
1236
|
+
if (!transitionName) {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`Could not find "Reopen" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
1242
|
+
if (!transition) {
|
|
1243
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
1244
|
+
}
|
|
1245
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
1246
|
+
getLogger().info("Issue reopened successfully", { issueKey, transition: transitionName });
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Extract context from issue for AI prompts
|
|
1250
|
+
*/
|
|
1251
|
+
extractContext(entity) {
|
|
1252
|
+
return `Issue: ${entity.number}
|
|
1253
|
+
Title: ${entity.title}
|
|
1254
|
+
Status: ${entity.state}
|
|
1255
|
+
URL: ${entity.url}
|
|
1256
|
+
|
|
1257
|
+
Description:
|
|
1258
|
+
${entity.body}
|
|
1259
|
+
|
|
1260
|
+
${entity.labels.length > 0 ? `Labels: ${entity.labels.join(", ")}` : ""}
|
|
1261
|
+
${entity.assignees.length > 0 ? `Assignees: ${entity.assignees.join(", ")}` : ""}`;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Fetch child issues of a Jira parent issue using JQL
|
|
1265
|
+
* @param parentIdentifier - Jira issue key (e.g., "PROJ-123")
|
|
1266
|
+
* @param _repo - Repository (unused for Jira)
|
|
1267
|
+
* @returns Array of child issues
|
|
1268
|
+
*/
|
|
1269
|
+
async getChildIssues(parentIdentifier, _repo) {
|
|
1270
|
+
const parentKey = this.normalizeIdentifier(parentIdentifier);
|
|
1271
|
+
const jiraKeyPattern = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
1272
|
+
if (!jiraKeyPattern.test(parentKey)) {
|
|
1273
|
+
getLogger().warn(`Invalid Jira issue key format: ${parentKey}`);
|
|
1274
|
+
return [];
|
|
1275
|
+
}
|
|
1276
|
+
const issues = await this.client.searchIssues(`parent = ${parentKey}`);
|
|
1277
|
+
return issues.map((issue) => ({
|
|
1278
|
+
id: issue.key,
|
|
1279
|
+
title: issue.fields.summary,
|
|
1280
|
+
url: `${this.config.host}/browse/${issue.key}`,
|
|
1281
|
+
state: issue.fields.status.name.toLowerCase()
|
|
1282
|
+
}));
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Get issue details (alias for fetchIssue for MCP compatibility)
|
|
1286
|
+
*/
|
|
1287
|
+
async getIssue(identifier) {
|
|
1288
|
+
return this.fetchIssue(identifier);
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Get all comments for an issue
|
|
1292
|
+
*/
|
|
1293
|
+
async getComments(identifier) {
|
|
1294
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1295
|
+
getLogger().debug("Fetching Jira comments", { issueKey });
|
|
1296
|
+
const comments = await this.client.getComments(issueKey);
|
|
1297
|
+
return comments.map((comment) => ({
|
|
1298
|
+
id: comment.id,
|
|
1299
|
+
body: adfToMarkdown(comment.body),
|
|
1300
|
+
author: comment.author,
|
|
1301
|
+
createdAt: comment.created,
|
|
1302
|
+
updatedAt: comment.updated
|
|
1303
|
+
}));
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Add a comment to an issue
|
|
1307
|
+
*/
|
|
1308
|
+
async addComment(identifier, body) {
|
|
1309
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1310
|
+
getLogger().debug("Adding Jira comment", { issueKey });
|
|
1311
|
+
const comment = await this.client.addComment(issueKey, body);
|
|
1312
|
+
return { id: comment.id };
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Update an existing comment
|
|
1316
|
+
*/
|
|
1317
|
+
async updateComment(identifier, commentId, body) {
|
|
1318
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
1319
|
+
getLogger().debug("Updating Jira comment", { issueKey, commentId });
|
|
1320
|
+
await this.client.updateComment(issueKey, commentId, body);
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Get the underlying API client (for direct API access by MCP provider)
|
|
1324
|
+
*/
|
|
1325
|
+
getApiClient() {
|
|
1326
|
+
return this.client;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Get configuration (for MCP provider)
|
|
1330
|
+
*/
|
|
1331
|
+
getConfig() {
|
|
1332
|
+
return this.config;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Map Jira API issue to generic Issue type
|
|
1336
|
+
*/
|
|
1337
|
+
mapJiraIssueToIssue(jiraIssue) {
|
|
1338
|
+
const description = adfToMarkdown(jiraIssue.fields.description);
|
|
1339
|
+
return {
|
|
1340
|
+
id: jiraIssue.id,
|
|
1341
|
+
key: jiraIssue.key,
|
|
1342
|
+
number: jiraIssue.key,
|
|
1343
|
+
title: jiraIssue.fields.summary,
|
|
1344
|
+
body: description,
|
|
1345
|
+
state: this.mapJiraStatusToState(jiraIssue.fields.status.name),
|
|
1346
|
+
labels: jiraIssue.fields.labels,
|
|
1347
|
+
assignees: jiraIssue.fields.assignee ? [jiraIssue.fields.assignee.displayName] : [],
|
|
1348
|
+
assignee: jiraIssue.fields.assignee,
|
|
1349
|
+
author: jiraIssue.fields.reporter,
|
|
1350
|
+
url: `${this.config.host}/browse/${jiraIssue.key}`,
|
|
1351
|
+
issueType: jiraIssue.fields.issuetype.name,
|
|
1352
|
+
status: jiraIssue.fields.status.name
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
mapJiraStatusToState(statusName) {
|
|
1356
|
+
const normalized = statusName.toLowerCase();
|
|
1357
|
+
const closedStatuses = ["done", "closed", "resolved", "cancelled", "canceled"];
|
|
1358
|
+
return closedStatuses.includes(normalized) ? "closed" : "open";
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Find a transition by name, trying multiple possible names
|
|
1362
|
+
*/
|
|
1363
|
+
findTransitionByName(transitions, names) {
|
|
1364
|
+
for (const name of names) {
|
|
1365
|
+
const transition = transitions.find(
|
|
1366
|
+
(t) => t.name.toLowerCase() === name.toLowerCase()
|
|
1367
|
+
);
|
|
1368
|
+
if (transition) {
|
|
1369
|
+
return transition.name;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
export {
|
|
1377
|
+
LinearServiceError,
|
|
1378
|
+
fetchLinearIssue,
|
|
1379
|
+
createLinearIssue,
|
|
1380
|
+
createLinearChildIssue,
|
|
1381
|
+
createLinearComment,
|
|
1382
|
+
updateLinearIssueState,
|
|
1383
|
+
editLinearIssue,
|
|
1384
|
+
getLinearComment,
|
|
1385
|
+
updateLinearComment,
|
|
1386
|
+
fetchLinearIssueComments,
|
|
1387
|
+
getLinearChildIssues,
|
|
1388
|
+
createLinearIssueRelation,
|
|
1389
|
+
getLinearIssueDependencies,
|
|
1390
|
+
deleteLinearIssueRelation,
|
|
1391
|
+
fetchLinearIssueList,
|
|
1392
|
+
findLinearIssueRelation,
|
|
1393
|
+
JiraApiClient,
|
|
1394
|
+
JiraIssueTracker
|
|
1395
|
+
};
|
|
1396
|
+
//# sourceMappingURL=chunk-HEXKPKCK.js.map
|