@iloom/cli 0.6.1 → 0.7.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 +35 -18
- package/dist/{BranchNamingService-B5PVRR7F.js → BranchNamingService-FLPUUFOB.js} +2 -2
- package/dist/ClaudeContextManager-KE5TBZVZ.js +14 -0
- package/dist/ClaudeService-CRSETT3A.js +13 -0
- package/dist/{GitHubService-S2OGUTDR.js → GitHubService-O7U4UQ7N.js} +3 -3
- package/dist/{LoomLauncher-5LFM4LXB.js → LoomLauncher-NL65LSKP.js} +6 -6
- package/dist/{MetadataManager-DFI73J3G.js → MetadataManager-XJ2YB762.js} +2 -2
- package/dist/PRManager-2ABCWXHW.js +16 -0
- package/dist/{ProjectCapabilityDetector-S5FLNCFI.js → ProjectCapabilityDetector-UZYW32SY.js} +3 -3
- package/dist/{PromptTemplateManager-C3DK6XZL.js → PromptTemplateManager-7L3HJQQU.js} +2 -2
- package/dist/README.md +35 -18
- package/dist/{SettingsManager-35F5RUJH.js → SettingsManager-YU4VYPTW.js} +2 -2
- package/dist/agents/iloom-issue-analyze-and-plan.md +42 -17
- package/dist/agents/iloom-issue-analyzer.md +14 -14
- package/dist/agents/iloom-issue-complexity-evaluator.md +38 -15
- package/dist/agents/iloom-issue-enhancer.md +15 -15
- package/dist/agents/iloom-issue-implementer.md +44 -15
- package/dist/agents/iloom-issue-planner.md +121 -17
- package/dist/agents/iloom-issue-reviewer.md +15 -15
- package/dist/{build-FJVYP7EV.js → build-O2EJHDEW.js} +9 -9
- package/dist/{chunk-ZPSTA5PR.js → chunk-3CDWFEGL.js} +2 -2
- package/dist/{chunk-VU3QMIP2.js → chunk-453NC377.js} +91 -15
- package/dist/chunk-453NC377.js.map +1 -0
- package/dist/{chunk-UQIXZ3BA.js → chunk-5V74K5ZA.js} +2 -2
- package/dist/{chunk-7WANFUIK.js → chunk-6TL3BYH6.js} +2 -2
- package/dist/{chunk-5TXLVEXT.js → chunk-C3AKFAIR.js} +2 -2
- package/dist/{chunk-K7SEEHKO.js → chunk-CNSTXBJ3.js} +7 -419
- package/dist/chunk-CNSTXBJ3.js.map +1 -0
- package/dist/{chunk-VDA5JMB4.js → chunk-EPPPDVHD.js} +21 -8
- package/dist/chunk-EPPPDVHD.js.map +1 -0
- package/dist/{chunk-LVBRMTE6.js → chunk-FEAJR6PN.js} +6 -6
- package/dist/{chunk-6YSFTPKW.js → chunk-FM4KBPVA.js} +18 -13
- package/dist/chunk-FM4KBPVA.js.map +1 -0
- package/dist/{chunk-AEIMYF4P.js → chunk-FP7G7DG3.js} +6 -2
- package/dist/chunk-FP7G7DG3.js.map +1 -0
- package/dist/{chunk-LT3SGBR7.js → chunk-GCPAZSGV.js} +36 -2
- package/dist/{chunk-LT3SGBR7.js.map → chunk-GCPAZSGV.js.map} +1 -1
- package/dist/chunk-GJMEKEI5.js +517 -0
- package/dist/chunk-GJMEKEI5.js.map +1 -0
- package/dist/{chunk-64O2UIWO.js → chunk-GV5X6XUE.js} +4 -4
- package/dist/{chunk-7Q66W4OH.js → chunk-HBJITKSZ.js} +37 -1
- package/dist/chunk-HBJITKSZ.js.map +1 -0
- package/dist/{chunk-7HIRPCKU.js → chunk-HVQNVRAF.js} +2 -2
- package/dist/{chunk-BXCPJJYM.js → chunk-ITN64ENQ.js} +1 -1
- package/dist/chunk-ITN64ENQ.js.map +1 -0
- package/dist/{chunk-6U6VI4SZ.js → chunk-KVS4XGBQ.js} +4 -4
- package/dist/{chunk-AXX3QIKK.js → chunk-LLWX3PCW.js} +2 -2
- package/dist/{chunk-2A7WQKBE.js → chunk-LQBLDI47.js} +96 -6
- package/dist/chunk-LQBLDI47.js.map +1 -0
- package/dist/{chunk-SN3Z6EZO.js → chunk-N7FVXZNI.js} +2 -2
- package/dist/chunk-NTIZLX42.js +822 -0
- package/dist/chunk-NTIZLX42.js.map +1 -0
- package/dist/{chunk-I75JMBNB.js → chunk-S7YMZQUD.js} +31 -43
- package/dist/chunk-S7YMZQUD.js.map +1 -0
- package/dist/chunk-TIYJEEVO.js +79 -0
- package/dist/chunk-TIYJEEVO.js.map +1 -0
- package/dist/{chunk-EK3XCAAS.js → chunk-UDRZY65Y.js} +2 -2
- package/dist/{chunk-3PT7RKL5.js → chunk-USJSNHGG.js} +2 -2
- package/dist/{chunk-CFUWQHCJ.js → chunk-VWGKGNJP.js} +114 -35
- package/dist/chunk-VWGKGNJP.js.map +1 -0
- package/dist/{chunk-F6WVM437.js → chunk-WFQ5CLTR.js} +6 -3
- package/dist/chunk-WFQ5CLTR.js.map +1 -0
- package/dist/{chunk-TRQ76ISK.js → chunk-Z6BO53V7.js} +9 -9
- package/dist/{chunk-GEXP5IOF.js → chunk-ZA575VLF.js} +21 -8
- package/dist/chunk-ZA575VLF.js.map +1 -0
- package/dist/{claude-H33OQMXO.js → claude-6H36IBHO.js} +4 -2
- package/dist/{cleanup-BRUAINKE.js → cleanup-ZPOMRSNN.js} +20 -16
- package/dist/cleanup-ZPOMRSNN.js.map +1 -0
- package/dist/cli.js +341 -954
- package/dist/cli.js.map +1 -1
- package/dist/commit-6S2RIA2K.js +237 -0
- package/dist/commit-6S2RIA2K.js.map +1 -0
- package/dist/{compile-ULNO5F7Q.js → compile-LRMAADUT.js} +9 -9
- package/dist/{contribute-Q6GX6AXK.js → contribute-GXKOIA42.js} +5 -5
- package/dist/{dev-server-4RCDJ5MU.js → dev-server-GREJUEKW.js} +22 -74
- package/dist/dev-server-GREJUEKW.js.map +1 -0
- package/dist/{feedback-O4Q55SVS.js → feedback-G7G5QCY4.js} +10 -10
- package/dist/{git-FVMGBHC2.js → git-ENLT2VNI.js} +6 -4
- package/dist/hooks/iloom-hook.js +30 -2
- package/dist/{ignite-VHV65WEZ.js → ignite-YUAOJ5PP.js} +20 -20
- package/dist/ignite-YUAOJ5PP.js.map +1 -0
- package/dist/index.d.ts +71 -27
- package/dist/index.js +196 -266
- package/dist/index.js.map +1 -1
- package/dist/init-XQQMFDM6.js +21 -0
- package/dist/{lint-5JMCWE4Y.js → lint-OFVN7FT6.js} +9 -9
- package/dist/mcp/issue-management-server.js +359 -13
- package/dist/mcp/issue-management-server.js.map +1 -1
- package/dist/mcp/recap-server.js +13 -4
- package/dist/mcp/recap-server.js.map +1 -1
- package/dist/{open-WHVUYGPY.js → open-MCWQAPSZ.js} +25 -76
- package/dist/open-MCWQAPSZ.js.map +1 -0
- package/dist/{projects-SA76I4TZ.js → projects-PQOTWUII.js} +11 -4
- package/dist/projects-PQOTWUII.js.map +1 -0
- package/dist/prompts/init-prompt.txt +62 -51
- package/dist/prompts/issue-prompt.txt +132 -63
- package/dist/prompts/pr-prompt.txt +3 -3
- package/dist/prompts/regular-prompt.txt +16 -18
- package/dist/prompts/session-summary-prompt.txt +13 -13
- package/dist/{rebase-Y4AS6LQW.js → rebase-RKQED567.js} +53 -8
- package/dist/rebase-RKQED567.js.map +1 -0
- package/dist/{recap-VOOUXOGP.js → recap-ZKGHZCX6.js} +6 -6
- package/dist/{run-NCRK5NPR.js → run-CCG24PBC.js} +25 -76
- package/dist/run-CCG24PBC.js.map +1 -0
- package/dist/schema/settings.schema.json +14 -3
- package/dist/{shell-SBLXVOVJ.js → shell-2NNSIU34.js} +6 -6
- package/dist/{summary-CVFAMDOJ.js → summary-G6L3VAKK.js} +11 -10
- package/dist/{summary-CVFAMDOJ.js.map → summary-G6L3VAKK.js.map} +1 -1
- package/dist/{test-3KIVXI6J.js → test-QZDOEUIO.js} +9 -9
- package/dist/{test-git-ZB6AGGRW.js → test-git-E2BLXR6M.js} +4 -4
- package/dist/{test-prefix-FBGXKMPA.js → test-prefix-A7JGGYAA.js} +4 -4
- package/dist/{test-webserver-YVQD42W6.js → test-webserver-NRMGT2HB.js} +29 -8
- package/dist/test-webserver-NRMGT2HB.js.map +1 -0
- package/package.json +3 -1
- package/dist/ClaudeContextManager-6J2EB4QU.js +0 -14
- package/dist/ClaudeService-O2PB22GX.js +0 -13
- package/dist/PRManager-GB3FOJ2W.js +0 -14
- package/dist/chunk-2A7WQKBE.js.map +0 -1
- package/dist/chunk-6YSFTPKW.js.map +0 -1
- package/dist/chunk-7Q66W4OH.js.map +0 -1
- package/dist/chunk-AEIMYF4P.js.map +0 -1
- package/dist/chunk-BXCPJJYM.js.map +0 -1
- package/dist/chunk-CFUWQHCJ.js.map +0 -1
- package/dist/chunk-F6WVM437.js.map +0 -1
- package/dist/chunk-GEXP5IOF.js.map +0 -1
- package/dist/chunk-I75JMBNB.js.map +0 -1
- package/dist/chunk-K7SEEHKO.js.map +0 -1
- package/dist/chunk-VDA5JMB4.js.map +0 -1
- package/dist/chunk-VU3QMIP2.js.map +0 -1
- package/dist/chunk-W6WVRHJ6.js +0 -251
- package/dist/chunk-W6WVRHJ6.js.map +0 -1
- package/dist/cleanup-BRUAINKE.js.map +0 -1
- package/dist/dev-server-4RCDJ5MU.js.map +0 -1
- package/dist/ignite-VHV65WEZ.js.map +0 -1
- package/dist/init-UTYRHNJJ.js +0 -21
- package/dist/open-WHVUYGPY.js.map +0 -1
- package/dist/projects-SA76I4TZ.js.map +0 -1
- package/dist/rebase-Y4AS6LQW.js.map +0 -1
- package/dist/run-NCRK5NPR.js.map +0 -1
- package/dist/test-webserver-YVQD42W6.js.map +0 -1
- /package/dist/{BranchNamingService-B5PVRR7F.js.map → BranchNamingService-FLPUUFOB.js.map} +0 -0
- /package/dist/{ClaudeContextManager-6J2EB4QU.js.map → ClaudeContextManager-KE5TBZVZ.js.map} +0 -0
- /package/dist/{ClaudeService-O2PB22GX.js.map → ClaudeService-CRSETT3A.js.map} +0 -0
- /package/dist/{GitHubService-S2OGUTDR.js.map → GitHubService-O7U4UQ7N.js.map} +0 -0
- /package/dist/{LoomLauncher-5LFM4LXB.js.map → LoomLauncher-NL65LSKP.js.map} +0 -0
- /package/dist/{MetadataManager-DFI73J3G.js.map → MetadataManager-XJ2YB762.js.map} +0 -0
- /package/dist/{PRManager-GB3FOJ2W.js.map → PRManager-2ABCWXHW.js.map} +0 -0
- /package/dist/{ProjectCapabilityDetector-S5FLNCFI.js.map → ProjectCapabilityDetector-UZYW32SY.js.map} +0 -0
- /package/dist/{PromptTemplateManager-C3DK6XZL.js.map → PromptTemplateManager-7L3HJQQU.js.map} +0 -0
- /package/dist/{SettingsManager-35F5RUJH.js.map → SettingsManager-YU4VYPTW.js.map} +0 -0
- /package/dist/{build-FJVYP7EV.js.map → build-O2EJHDEW.js.map} +0 -0
- /package/dist/{chunk-ZPSTA5PR.js.map → chunk-3CDWFEGL.js.map} +0 -0
- /package/dist/{chunk-UQIXZ3BA.js.map → chunk-5V74K5ZA.js.map} +0 -0
- /package/dist/{chunk-7WANFUIK.js.map → chunk-6TL3BYH6.js.map} +0 -0
- /package/dist/{chunk-5TXLVEXT.js.map → chunk-C3AKFAIR.js.map} +0 -0
- /package/dist/{chunk-LVBRMTE6.js.map → chunk-FEAJR6PN.js.map} +0 -0
- /package/dist/{chunk-64O2UIWO.js.map → chunk-GV5X6XUE.js.map} +0 -0
- /package/dist/{chunk-7HIRPCKU.js.map → chunk-HVQNVRAF.js.map} +0 -0
- /package/dist/{chunk-6U6VI4SZ.js.map → chunk-KVS4XGBQ.js.map} +0 -0
- /package/dist/{chunk-AXX3QIKK.js.map → chunk-LLWX3PCW.js.map} +0 -0
- /package/dist/{chunk-SN3Z6EZO.js.map → chunk-N7FVXZNI.js.map} +0 -0
- /package/dist/{chunk-EK3XCAAS.js.map → chunk-UDRZY65Y.js.map} +0 -0
- /package/dist/{chunk-3PT7RKL5.js.map → chunk-USJSNHGG.js.map} +0 -0
- /package/dist/{chunk-TRQ76ISK.js.map → chunk-Z6BO53V7.js.map} +0 -0
- /package/dist/{claude-H33OQMXO.js.map → claude-6H36IBHO.js.map} +0 -0
- /package/dist/{compile-ULNO5F7Q.js.map → compile-LRMAADUT.js.map} +0 -0
- /package/dist/{contribute-Q6GX6AXK.js.map → contribute-GXKOIA42.js.map} +0 -0
- /package/dist/{feedback-O4Q55SVS.js.map → feedback-G7G5QCY4.js.map} +0 -0
- /package/dist/{git-FVMGBHC2.js.map → git-ENLT2VNI.js.map} +0 -0
- /package/dist/{init-UTYRHNJJ.js.map → init-XQQMFDM6.js.map} +0 -0
- /package/dist/{lint-5JMCWE4Y.js.map → lint-OFVN7FT6.js.map} +0 -0
- /package/dist/{recap-VOOUXOGP.js.map → recap-ZKGHZCX6.js.map} +0 -0
- /package/dist/{shell-SBLXVOVJ.js.map → shell-2NNSIU34.js.map} +0 -0
- /package/dist/{test-3KIVXI6J.js.map → test-QZDOEUIO.js.map} +0 -0
- /package/dist/{test-git-ZB6AGGRW.js.map → test-git-E2BLXR6M.js.map} +0 -0
- /package/dist/{test-prefix-FBGXKMPA.js.map → test-prefix-A7JGGYAA.js.map} +0 -0
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
extractIssueNumber
|
|
4
|
+
} from "./chunk-ZA575VLF.js";
|
|
5
|
+
import {
|
|
6
|
+
extractPort,
|
|
7
|
+
findEnvFileContainingVariable,
|
|
8
|
+
logger,
|
|
9
|
+
parseEnvFile
|
|
10
|
+
} from "./chunk-VT4PDUYT.js";
|
|
2
11
|
|
|
3
12
|
// src/utils/port.ts
|
|
4
13
|
import { createHash } from "crypto";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import fs from "fs-extra";
|
|
16
|
+
function wrapPort(rawPort, basePort) {
|
|
17
|
+
if (rawPort <= 65535) return rawPort;
|
|
18
|
+
const range = 65535 - basePort;
|
|
19
|
+
return (rawPort - basePort - 1) % range + basePort + 1;
|
|
20
|
+
}
|
|
21
|
+
function extractNumericSuffix(issueId) {
|
|
22
|
+
const match = issueId.match(/[-_]?(\d+)$/);
|
|
23
|
+
const digits = match == null ? void 0 : match[1];
|
|
24
|
+
if (digits === void 0) return null;
|
|
25
|
+
return parseInt(digits, 10);
|
|
26
|
+
}
|
|
5
27
|
function generatePortOffsetFromBranchName(branchName) {
|
|
6
28
|
if (!branchName || branchName.trim().length === 0) {
|
|
7
29
|
throw new Error("Branch name cannot be empty");
|
|
@@ -15,11 +37,69 @@ function generatePortOffsetFromBranchName(branchName) {
|
|
|
15
37
|
function calculatePortForBranch(branchName, basePort = 3e3) {
|
|
16
38
|
const offset = generatePortOffsetFromBranchName(branchName);
|
|
17
39
|
const port = basePort + offset;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
return wrapPort(port, basePort);
|
|
41
|
+
}
|
|
42
|
+
function calculatePortFromIdentifier(identifier, basePort = 3e3) {
|
|
43
|
+
if (typeof identifier === "number") {
|
|
44
|
+
return wrapPort(basePort + identifier, basePort);
|
|
45
|
+
}
|
|
46
|
+
const numericValue = parseInt(identifier, 10);
|
|
47
|
+
if (!isNaN(numericValue) && String(numericValue) === identifier) {
|
|
48
|
+
return wrapPort(basePort + numericValue, basePort);
|
|
49
|
+
}
|
|
50
|
+
const numericSuffix = extractNumericSuffix(identifier);
|
|
51
|
+
if (numericSuffix !== null) {
|
|
52
|
+
return wrapPort(basePort + numericSuffix, basePort);
|
|
53
|
+
}
|
|
54
|
+
return calculatePortForBranch(`issue-${identifier}`, basePort);
|
|
55
|
+
}
|
|
56
|
+
async function getWorkspacePort(options, dependencies) {
|
|
57
|
+
const basePort = options.basePort ?? 3e3;
|
|
58
|
+
const checkEnvFile = options.checkEnvFile ?? false;
|
|
59
|
+
if (checkEnvFile) {
|
|
60
|
+
const deps = {
|
|
61
|
+
fileExists: (dependencies == null ? void 0 : dependencies.fileExists) ?? ((p) => fs.pathExists(p)),
|
|
62
|
+
readFile: (dependencies == null ? void 0 : dependencies.readFile) ?? ((p) => fs.readFile(p, "utf8"))
|
|
63
|
+
};
|
|
64
|
+
const envFile = await findEnvFileContainingVariable(
|
|
65
|
+
options.worktreePath,
|
|
66
|
+
"PORT",
|
|
67
|
+
async (p) => deps.fileExists(p),
|
|
68
|
+
async (p, varName) => {
|
|
69
|
+
const content = await deps.readFile(p);
|
|
70
|
+
const envMap = parseEnvFile(content);
|
|
71
|
+
return envMap.get(varName) ?? null;
|
|
72
|
+
}
|
|
21
73
|
);
|
|
74
|
+
if (envFile) {
|
|
75
|
+
const envPath = path.join(options.worktreePath, envFile);
|
|
76
|
+
const envContent = await deps.readFile(envPath);
|
|
77
|
+
const envMap = parseEnvFile(envContent);
|
|
78
|
+
const port2 = extractPort(envMap);
|
|
79
|
+
if (port2) {
|
|
80
|
+
logger.debug(`Using PORT from ${envFile}: ${port2}`);
|
|
81
|
+
return port2;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
logger.debug("PORT not found in any dotenv-flow file, calculating from workspace identifier");
|
|
85
|
+
}
|
|
86
|
+
const dirName = path.basename(options.worktreePath);
|
|
87
|
+
const prPattern = /_pr_(\d+)$/;
|
|
88
|
+
const prMatch = dirName.match(prPattern);
|
|
89
|
+
if (prMatch == null ? void 0 : prMatch[1]) {
|
|
90
|
+
const prNumber = parseInt(prMatch[1], 10);
|
|
91
|
+
const port2 = calculatePortFromIdentifier(prNumber, basePort);
|
|
92
|
+
logger.debug(`Calculated PORT for PR #${prNumber}: ${port2}`);
|
|
93
|
+
return port2;
|
|
22
94
|
}
|
|
95
|
+
const issueId = extractIssueNumber(dirName) ?? extractIssueNumber(options.worktreeBranch);
|
|
96
|
+
if (issueId !== null) {
|
|
97
|
+
const port2 = calculatePortFromIdentifier(issueId, basePort);
|
|
98
|
+
logger.debug(`Calculated PORT for issue ${issueId}: ${port2}`);
|
|
99
|
+
return port2;
|
|
100
|
+
}
|
|
101
|
+
const port = calculatePortForBranch(options.worktreeBranch, basePort);
|
|
102
|
+
logger.debug(`Calculated PORT for branch "${options.worktreeBranch}": ${port}`);
|
|
23
103
|
return port;
|
|
24
104
|
}
|
|
25
105
|
|
|
@@ -27,7 +107,8 @@ function calculatePortForBranch(branchName, basePort = 3e3) {
|
|
|
27
107
|
import { execa } from "execa";
|
|
28
108
|
import { setTimeout } from "timers/promises";
|
|
29
109
|
var ProcessManager = class {
|
|
30
|
-
constructor() {
|
|
110
|
+
constructor(basePort = 3e3) {
|
|
111
|
+
this.basePort = basePort;
|
|
31
112
|
this.platform = this.detectPlatform();
|
|
32
113
|
}
|
|
33
114
|
/**
|
|
@@ -188,22 +269,17 @@ var ProcessManager = class {
|
|
|
188
269
|
/**
|
|
189
270
|
* Calculate dev server port from issue/PR number
|
|
190
271
|
* Ports logic from merge-and-clean.sh lines 1093-1098
|
|
191
|
-
*
|
|
272
|
+
* Delegates to calculatePortFromIdentifier for the actual calculation.
|
|
192
273
|
*/
|
|
193
|
-
calculatePort(
|
|
194
|
-
|
|
195
|
-
return 3e3 + number;
|
|
196
|
-
}
|
|
197
|
-
const numericValue = Number(number);
|
|
198
|
-
if (!isNaN(numericValue) && isFinite(numericValue)) {
|
|
199
|
-
return 3e3 + numericValue;
|
|
200
|
-
}
|
|
201
|
-
return calculatePortForBranch(`issue-${number}`, 3e3);
|
|
274
|
+
calculatePort(identifier) {
|
|
275
|
+
return calculatePortFromIdentifier(identifier, this.basePort);
|
|
202
276
|
}
|
|
203
277
|
};
|
|
204
278
|
|
|
205
279
|
export {
|
|
206
280
|
calculatePortForBranch,
|
|
281
|
+
calculatePortFromIdentifier,
|
|
282
|
+
getWorkspacePort,
|
|
207
283
|
ProcessManager
|
|
208
284
|
};
|
|
209
|
-
//# sourceMappingURL=chunk-
|
|
285
|
+
//# sourceMappingURL=chunk-453NC377.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/port.ts","../src/lib/process/ProcessManager.ts"],"sourcesContent":["import { createHash } from 'crypto'\nimport path from 'path'\nimport fs from 'fs-extra'\nimport { parseEnvFile, extractPort, findEnvFileContainingVariable } from './env.js'\nimport { extractIssueNumber } from './git.js'\nimport { logger } from './logger.js'\n\n/**\n * Wrap a raw port that exceeds 65535 into the valid port range.\n * Uses modulo arithmetic to wrap back into [basePort+1, 65535].\n *\n * @param rawPort - The calculated port (basePort + issueNumber)\n * @param basePort - The base port (default: 3000)\n * @returns Port in valid range [basePort+1, 65535]\n */\nexport function wrapPort(rawPort: number, basePort: number): number {\n\tif (rawPort <= 65535) return rawPort\n\tconst range = 65535 - basePort\n\treturn ((rawPort - basePort - 1) % range) + basePort + 1\n}\n\n/**\n * Extract numeric suffix from alphanumeric issue ID (e.g., MARK-324 -> 324)\n * @returns The numeric part or null if no trailing number found\n */\nexport function extractNumericSuffix(issueId: string): number | null {\n\t// Match trailing digits after optional separator (-, _)\n\tconst match = issueId.match(/[-_]?(\\d+)$/)\n\tconst digits = match?.[1]\n\tif (digits === undefined) return null\n\treturn parseInt(digits, 10)\n}\n\n/**\n * Generate deterministic port offset from branch name using SHA256 hash\n * Range: 1-999 (matches existing random range for branches)\n *\n * @param branchName - Branch name to generate port offset from\n * @returns Port offset in range [1, 999]\n * @throws Error if branchName is empty\n */\nexport function generatePortOffsetFromBranchName(branchName: string): number {\n\t// Validate input\n\tif (!branchName || branchName.trim().length === 0) {\n\t\tthrow new Error('Branch name cannot be empty')\n\t}\n\n\t// Generate SHA256 hash of branch name (same pattern as color.ts)\n\tconst hash = createHash('sha256').update(branchName).digest('hex')\n\n\t// Take first 8 hex characters and convert to port offset (1-999)\n\tconst hashPrefix = hash.slice(0, 8)\n\tconst hashAsInt = parseInt(hashPrefix, 16)\n\tconst portOffset = (hashAsInt % 999) + 1 // +1 ensures range is 1-999, not 0-998\n\n\treturn portOffset\n}\n\n/**\n * Calculate deterministic port for branch-based workspace\n *\n * @param branchName - Branch name\n * @param basePort - Base port (default: 3000)\n * @returns Port number\n * @throws Error if branchName is empty\n */\nexport function calculatePortForBranch(branchName: string, basePort: number = 3000): number {\n\tconst offset = generatePortOffsetFromBranchName(branchName)\n\tconst port = basePort + offset\n\n\t// Use wrap-around for port overflow\n\treturn wrapPort(port, basePort)\n}\n\n/**\n * Calculate port from an identifier (issue number, PR number, or string).\n * This is the single source of truth for port calculation logic.\n *\n * Algorithm:\n * 1. Numeric identifiers: basePort + number (with wrapPort for overflow)\n * 2. String numeric (e.g., \"42\"): parse and same as above\n * 3. Alphanumeric with suffix (e.g., \"MARK-324\"): extract suffix and same as above\n * 4. Pure strings without numeric suffix: hash-based calculation via calculatePortForBranch\n *\n * @param identifier - The identifier (issue number, PR number, or string)\n * @param basePort - Base port (default: 3000)\n * @returns Port number in valid range\n */\nexport function calculatePortFromIdentifier(\n\tidentifier: string | number,\n\tbasePort: number = 3000\n): number {\n\t// Handle numeric identifiers directly\n\tif (typeof identifier === 'number') {\n\t\treturn wrapPort(basePort + identifier, basePort)\n\t}\n\n\t// Handle string identifiers\n\t// First, try to parse as pure numeric string\n\tconst numericValue = parseInt(identifier, 10)\n\tif (!isNaN(numericValue) && String(numericValue) === identifier) {\n\t\treturn wrapPort(basePort + numericValue, basePort)\n\t}\n\n\t// Try extracting numeric suffix from alphanumeric identifiers (e.g., MARK-324 -> 324)\n\tconst numericSuffix = extractNumericSuffix(identifier)\n\tif (numericSuffix !== null) {\n\t\treturn wrapPort(basePort + numericSuffix, basePort)\n\t}\n\n\t// For non-numeric strings without numeric suffix, use hash-based calculation\n\treturn calculatePortForBranch(`issue-${identifier}`, basePort)\n}\n\nexport interface GetWorkspacePortOptions {\n\tbasePort?: number | undefined\n\tworktreePath: string\n\tworktreeBranch: string\n\t/** If true, check .env files for PORT override before calculating. Defaults to false. */\n\tcheckEnvFile?: boolean\n}\n\nexport interface GetWorkspacePortDependencies {\n\tfileExists?: (path: string) => Promise<boolean>\n\treadFile?: (path: string) => Promise<string>\n\tlistWorktrees?: () => Promise<Array<{ path: string; branch: string }>>\n}\n\n/**\n * Get port for workspace - calculates based on workspace type, optionally checking .env files first.\n * Consolidates logic previously duplicated across dev-server, run, open commands.\n *\n * Priority (when checkEnvFile is true):\n * 1. Read PORT from dotenv-flow files (if present)\n * 2. Calculate from PR pattern (_pr_N suffix in directory name)\n * 3. Calculate from issue pattern (issue-N or alphanumeric like MARK-324)\n * 4. Calculate from branch name using deterministic hash\n *\n * When checkEnvFile is false (default), skips step 1 and only calculates.\n */\nexport async function getWorkspacePort(\n\toptions: GetWorkspacePortOptions,\n\tdependencies?: GetWorkspacePortDependencies\n): Promise<number> {\n\tconst basePort = options.basePort ?? 3000\n\tconst checkEnvFile = options.checkEnvFile ?? false\n\n\t// Only check .env files if explicitly requested\n\tif (checkEnvFile) {\n\t\tconst deps = {\n\t\t\tfileExists:\n\t\t\t\tdependencies?.fileExists ?? ((p: string): Promise<boolean> => fs.pathExists(p)),\n\t\t\treadFile:\n\t\t\t\tdependencies?.readFile ?? ((p: string): Promise<string> => fs.readFile(p, 'utf8')),\n\t\t}\n\n\t\t// Try to read PORT from any dotenv-flow file (as override)\n\t\tconst envFile = await findEnvFileContainingVariable(\n\t\t\toptions.worktreePath,\n\t\t\t'PORT',\n\t\t\tasync (p) => deps.fileExists(p),\n\t\t\tasync (p, varName) => {\n\t\t\t\tconst content = await deps.readFile(p)\n\t\t\t\tconst envMap = parseEnvFile(content)\n\t\t\t\treturn envMap.get(varName) ?? null\n\t\t\t}\n\t\t)\n\n\t\tif (envFile) {\n\t\t\tconst envPath = path.join(options.worktreePath, envFile)\n\t\t\tconst envContent = await deps.readFile(envPath)\n\t\t\tconst envMap = parseEnvFile(envContent)\n\t\t\tconst port = extractPort(envMap)\n\n\t\t\tif (port) {\n\t\t\t\tlogger.debug(`Using PORT from ${envFile}: ${port}`)\n\t\t\t\treturn port\n\t\t\t}\n\t\t}\n\n\t\tlogger.debug('PORT not found in any dotenv-flow file, calculating from workspace identifier')\n\t}\n\n\t// Calculate based on workspace identifier\n\n\t// Extract identifier from worktree path/branch\n\tconst dirName = path.basename(options.worktreePath)\n\n\t// Check for PR pattern: _pr_N\n\tconst prPattern = /_pr_(\\d+)$/\n\tconst prMatch = dirName.match(prPattern)\n\tif (prMatch?.[1]) {\n\t\tconst prNumber = parseInt(prMatch[1], 10)\n\t\tconst port = calculatePortFromIdentifier(prNumber, basePort)\n\t\tlogger.debug(`Calculated PORT for PR #${prNumber}: ${port}`)\n\t\treturn port\n\t}\n\n\t// Check for issue pattern: issue-N or alphanumeric like MARK-324\n\tconst issueId = extractIssueNumber(dirName) ?? extractIssueNumber(options.worktreeBranch)\n\tif (issueId !== null) {\n\t\tconst port = calculatePortFromIdentifier(issueId, basePort)\n\t\tlogger.debug(`Calculated PORT for issue ${issueId}: ${port}`)\n\t\treturn port\n\t}\n\n\t// Branch-based workspace - use deterministic hash\n\tconst port = calculatePortForBranch(options.worktreeBranch, basePort)\n\tlogger.debug(`Calculated PORT for branch \"${options.worktreeBranch}\": ${port}`)\n\treturn port\n}\n","import { execa } from 'execa'\nimport { setTimeout } from 'timers/promises'\nimport type { ProcessInfo, Platform } from '../../types/process.js'\nimport { calculatePortFromIdentifier } from '../../utils/port.js'\n\n/**\n * Manages process detection and termination across platforms\n * Ports dev server termination logic from bash/merge-and-clean.sh lines 1092-1148\n */\nexport class ProcessManager {\n\tprivate readonly platform: Platform\n\tprivate readonly basePort: number\n\n\tconstructor(basePort: number = 3000) {\n\t\tthis.basePort = basePort\n\t\tthis.platform = this.detectPlatform()\n\t}\n\n\t/**\n\t * Detect current platform\n\t */\n\tprivate detectPlatform(): Platform {\n\t\tswitch (process.platform) {\n\t\t\tcase 'darwin':\n\t\t\t\treturn 'darwin'\n\t\t\tcase 'linux':\n\t\t\t\treturn 'linux'\n\t\t\tcase 'win32':\n\t\t\t\treturn 'win32'\n\t\t\tdefault:\n\t\t\t\treturn 'unsupported'\n\t\t}\n\t}\n\n\t/**\n\t * Detect if a dev server is running on the specified port\n\t * Ports logic from merge-and-clean.sh lines 1107-1123\n\t */\n\tasync detectDevServer(port: number): Promise<ProcessInfo | null> {\n\t\tif (this.platform === 'unsupported') {\n\t\t\tthrow new Error('Process detection not supported on this platform')\n\t\t}\n\n\t\t// Use platform-specific detection\n\t\tif (this.platform === 'win32') {\n\t\t\treturn await this.detectOnPortWindows(port)\n\t\t} else {\n\t\t\treturn await this.detectOnPortUnix(port)\n\t\t}\n\t}\n\n\t/**\n\t * Unix/macOS implementation using lsof\n\t * Ports bash lines 1107-1123\n\t */\n\tprivate async detectOnPortUnix(port: number): Promise<ProcessInfo | null> {\n\t\ttry {\n\t\t\t// Run lsof to find process listening on port (LISTEN only)\n\t\t\tconst result = await execa('lsof', ['-i', `:${port}`, '-P'], {\n\t\t\t\treject: false,\n\t\t\t})\n\n\t\t\t// Filter for LISTEN state only\n\t\t\tconst lines = result.stdout.split('\\n').filter(line => line.includes('LISTEN'))\n\n\t\t\tif (lines.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Parse first LISTEN line\n\t\t\tconst firstLine = lines[0]\n\t\t\tif (!firstLine) return null\n\n\t\t\tconst parts = firstLine.split(/\\s+/)\n\t\t\tif (parts.length < 2) return null\n\n\t\t\tconst processName = parts[0] ?? ''\n\t\t\tconst pid = parseInt(parts[1] ?? '', 10)\n\n\t\t\tif (isNaN(pid)) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Get full command line using ps\n\t\t\tconst psResult = await execa('ps', ['-p', pid.toString(), '-o', 'command='], {\n\t\t\t\treject: false,\n\t\t\t})\n\t\t\tconst fullCommand = psResult.stdout.trim()\n\n\t\t\t// Validate if this is a dev server\n\t\t\tconst isDevServer = this.isDevServerProcess(processName, fullCommand)\n\n\t\t\treturn {\n\t\t\t\tpid,\n\t\t\t\tname: processName,\n\t\t\t\tcommand: fullCommand,\n\t\t\t\tport,\n\t\t\t\tisDevServer,\n\t\t\t}\n\t\t} catch {\n\t\t\t// If lsof fails, assume no process on port\n\t\t\treturn null\n\t\t}\n\t}\n\n\t/**\n\t * Windows implementation using netstat and tasklist\n\t */\n\tprivate async detectOnPortWindows(port: number): Promise<ProcessInfo | null> {\n\t\ttry {\n\t\t\t// Use netstat to find PID listening on port\n\t\t\tconst result = await execa('netstat', ['-ano'], { reject: false })\n\t\t\tconst lines = result.stdout.split('\\n')\n\n\t\t\t// Find line with our port and LISTENING state\n\t\t\tconst portLine = lines.find(\n\t\t\t\tline => line.includes(`:${port}`) && line.includes('LISTENING')\n\t\t\t)\n\n\t\t\tif (!portLine) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Extract PID (last column)\n\t\t\tconst parts = portLine.trim().split(/\\s+/)\n\t\t\tconst lastPart = parts[parts.length - 1]\n\t\t\tif (!lastPart) return null\n\n\t\t\tconst pid = parseInt(lastPart, 10)\n\n\t\t\tif (isNaN(pid)) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Get process info using tasklist\n\t\t\tconst taskResult = await execa(\n\t\t\t\t'tasklist',\n\t\t\t\t['/FI', `PID eq ${pid}`, '/FO', 'CSV'],\n\t\t\t\t{\n\t\t\t\t\treject: false,\n\t\t\t\t}\n\t\t\t)\n\n\t\t\t// Parse CSV output\n\t\t\tconst lines2 = taskResult.stdout.split('\\n')\n\t\t\tif (lines2.length < 2) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst secondLine = lines2[1]\n\t\t\tif (!secondLine) return null\n\n\t\t\tconst parts2 = secondLine.split(',')\n\t\t\tconst processName = (parts2[0] ?? '').replace(/\"/g, '')\n\n\t\t\t// TODO: Get full command line on Windows (more complex)\n\t\t\tconst fullCommand = processName\n\n\t\t\tconst isDevServer = this.isDevServerProcess(processName, fullCommand)\n\n\t\t\treturn {\n\t\t\t\tpid,\n\t\t\t\tname: processName,\n\t\t\t\tcommand: fullCommand,\n\t\t\t\tport,\n\t\t\t\tisDevServer,\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null\n\t\t}\n\t}\n\n\t/**\n\t * Validate if process is a dev server\n\t * Ports logic from merge-and-clean.sh lines 1121-1123\n\t */\n\tprivate isDevServerProcess(processName: string, command: string): boolean {\n\t\t// Check process name patterns\n\t\tconst devServerNames = /^(node|npm|pnpm|yarn|next|next-server|vite|webpack|dev-server)$/i\n\t\tif (devServerNames.test(processName)) {\n\t\t\t// Additional validation via command line\n\t\t\tconst devServerCommands =\n\t\t\t\t/(next dev|next-server|npm.*dev|pnpm.*dev|yarn.*dev|vite|webpack.*serve|turbo.*dev|dev.*server)/i\n\t\t\treturn devServerCommands.test(command)\n\t\t}\n\n\t\t// Check command line alone\n\t\tconst devServerCommands =\n\t\t\t/(next dev|next-server|npm.*dev|pnpm.*dev|yarn.*dev|vite|webpack.*serve|turbo.*dev|dev.*server)/i\n\t\treturn devServerCommands.test(command)\n\t}\n\n\t/**\n\t * Terminate a process by PID\n\t * Ports logic from merge-and-clean.sh lines 1126-1139\n\t */\n\tasync terminateProcess(pid: number): Promise<boolean> {\n\t\ttry {\n\t\t\tif (this.platform === 'win32') {\n\t\t\t\t// Windows: use taskkill\n\t\t\t\tawait execa('taskkill', ['/PID', pid.toString(), '/F'], { reject: true })\n\t\t\t} else {\n\t\t\t\t// Unix/macOS: use kill -9\n\t\t\t\tprocess.kill(pid, 'SIGKILL')\n\t\t\t}\n\n\t\t\t// Wait briefly for process to die\n\t\t\tawait setTimeout(1000)\n\n\t\t\treturn true\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to terminate process ${pid}: ${error instanceof Error ? error.message : 'Unknown error'}`\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Verify that a port is free\n\t * Ports verification logic from merge-and-clean.sh lines 1135-1139\n\t */\n\tasync verifyPortFree(port: number): Promise<boolean> {\n\t\tconst processInfo = await this.detectDevServer(port)\n\t\treturn processInfo === null\n\t}\n\n\t/**\n\t * Calculate dev server port from issue/PR number\n\t * Ports logic from merge-and-clean.sh lines 1093-1098\n\t * Delegates to calculatePortFromIdentifier for the actual calculation.\n\t */\n\tcalculatePort(identifier: string | number): number {\n\t\treturn calculatePortFromIdentifier(identifier, this.basePort)\n\t}\n}\n"],"mappings":"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","names":["port","devServerCommands"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
extractIssueNumber,
|
|
4
4
|
extractPRNumber
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ZA575VLF.js";
|
|
6
6
|
|
|
7
7
|
// src/utils/IdentifierParser.ts
|
|
8
8
|
var IdentifierParser = class {
|
|
@@ -85,4 +85,4 @@ var IdentifierParser = class {
|
|
|
85
85
|
export {
|
|
86
86
|
IdentifierParser
|
|
87
87
|
};
|
|
88
|
-
//# sourceMappingURL=chunk-
|
|
88
|
+
//# sourceMappingURL=chunk-5V74K5ZA.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ClaudeService
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KVS4XGBQ.js";
|
|
5
5
|
import {
|
|
6
6
|
logger
|
|
7
7
|
} from "./chunk-VT4PDUYT.js";
|
|
@@ -63,4 +63,4 @@ var ClaudeContextManager = class {
|
|
|
63
63
|
export {
|
|
64
64
|
ClaudeContextManager
|
|
65
65
|
};
|
|
66
|
-
//# sourceMappingURL=chunk-
|
|
66
|
+
//# sourceMappingURL=chunk-6TL3BYH6.js.map
|
|
@@ -15,7 +15,7 @@ var ClaudeBranchNameStrategy = class {
|
|
|
15
15
|
this.claudeModel = claudeModel;
|
|
16
16
|
}
|
|
17
17
|
async generate(issueNumber, title) {
|
|
18
|
-
const { generateBranchName } = await import("./claude-
|
|
18
|
+
const { generateBranchName } = await import("./claude-6H36IBHO.js");
|
|
19
19
|
return generateBranchName(title, issueNumber, this.claudeModel);
|
|
20
20
|
}
|
|
21
21
|
};
|
|
@@ -52,4 +52,4 @@ export {
|
|
|
52
52
|
ClaudeBranchNameStrategy,
|
|
53
53
|
DefaultBranchNamingService
|
|
54
54
|
};
|
|
55
|
-
//# sourceMappingURL=chunk-
|
|
55
|
+
//# sourceMappingURL=chunk-C3AKFAIR.js.map
|
|
@@ -3,34 +3,24 @@ import {
|
|
|
3
3
|
formatRecapMarkdown
|
|
4
4
|
} from "./chunk-NXMDEL3F.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
fetchLinearIssueComments,
|
|
9
|
-
getLinearComment,
|
|
10
|
-
updateLinearComment
|
|
11
|
-
} from "./chunk-7Q66W4OH.js";
|
|
6
|
+
IssueManagementProviderFactory
|
|
7
|
+
} from "./chunk-GJMEKEI5.js";
|
|
12
8
|
import {
|
|
13
9
|
hasMultipleRemotes
|
|
14
10
|
} from "./chunk-FXDYIV3K.js";
|
|
15
11
|
import {
|
|
16
12
|
PromptTemplateManager
|
|
17
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-TIYJEEVO.js";
|
|
18
14
|
import {
|
|
19
15
|
SettingsManager
|
|
20
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-WFQ5CLTR.js";
|
|
21
17
|
import {
|
|
22
18
|
MetadataManager
|
|
23
|
-
} from "./chunk-
|
|
24
|
-
import {
|
|
25
|
-
createIssueComment,
|
|
26
|
-
createPRComment,
|
|
27
|
-
executeGhCommand,
|
|
28
|
-
updateIssueComment
|
|
29
|
-
} from "./chunk-LT3SGBR7.js";
|
|
19
|
+
} from "./chunk-VWGKGNJP.js";
|
|
30
20
|
import {
|
|
31
21
|
generateDeterministicSessionId,
|
|
32
22
|
launchClaude
|
|
33
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-FP7G7DG3.js";
|
|
34
24
|
import {
|
|
35
25
|
logger
|
|
36
26
|
} from "./chunk-VT4PDUYT.js";
|
|
@@ -113,408 +103,6 @@ async function readSessionContext(worktreePath, sessionId, maxSummaries = 3) {
|
|
|
113
103
|
return formattedSummaries;
|
|
114
104
|
}
|
|
115
105
|
|
|
116
|
-
// src/mcp/GitHubIssueManagementProvider.ts
|
|
117
|
-
function normalizeAuthor(author) {
|
|
118
|
-
if (!author) return null;
|
|
119
|
-
return {
|
|
120
|
-
id: author.id ? String(author.id) : author.login,
|
|
121
|
-
displayName: author.login,
|
|
122
|
-
// GitHub uses login as primary identifier
|
|
123
|
-
login: author.login,
|
|
124
|
-
// Preserve original GitHub field
|
|
125
|
-
...author.avatarUrl && { avatarUrl: author.avatarUrl },
|
|
126
|
-
...author.url && { url: author.url }
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function extractNumericIdFromUrl(url) {
|
|
130
|
-
const match = url.match(/#issuecomment-(\d+)$/);
|
|
131
|
-
if (!(match == null ? void 0 : match[1])) {
|
|
132
|
-
throw new Error(`Cannot extract comment ID from URL: ${url}`);
|
|
133
|
-
}
|
|
134
|
-
return match[1];
|
|
135
|
-
}
|
|
136
|
-
var GitHubIssueManagementProvider = class {
|
|
137
|
-
constructor() {
|
|
138
|
-
this.providerName = "github";
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Fetch issue details using gh CLI
|
|
142
|
-
* Normalizes GitHub-specific fields to provider-agnostic format
|
|
143
|
-
*/
|
|
144
|
-
async getIssue(input) {
|
|
145
|
-
const { number, includeComments = true } = input;
|
|
146
|
-
const issueNumber = parseInt(number, 10);
|
|
147
|
-
if (isNaN(issueNumber)) {
|
|
148
|
-
throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
|
|
149
|
-
}
|
|
150
|
-
const fields = includeComments ? "body,title,comments,labels,assignees,milestone,author,state,number,url" : "body,title,labels,assignees,milestone,author,state,number,url";
|
|
151
|
-
const raw = await executeGhCommand([
|
|
152
|
-
"issue",
|
|
153
|
-
"view",
|
|
154
|
-
String(issueNumber),
|
|
155
|
-
"--json",
|
|
156
|
-
fields
|
|
157
|
-
]);
|
|
158
|
-
const result = {
|
|
159
|
-
// Core fields
|
|
160
|
-
id: String(raw.number),
|
|
161
|
-
title: raw.title,
|
|
162
|
-
body: raw.body,
|
|
163
|
-
state: raw.state,
|
|
164
|
-
url: raw.url,
|
|
165
|
-
provider: "github",
|
|
166
|
-
// Normalized author
|
|
167
|
-
author: normalizeAuthor(raw.author),
|
|
168
|
-
// Optional flexible fields
|
|
169
|
-
...raw.assignees && {
|
|
170
|
-
assignees: raw.assignees.map((a) => normalizeAuthor(a)).filter((a) => a !== null)
|
|
171
|
-
},
|
|
172
|
-
...raw.labels && {
|
|
173
|
-
labels: raw.labels
|
|
174
|
-
},
|
|
175
|
-
// GitHub-specific passthrough fields
|
|
176
|
-
...raw.milestone && {
|
|
177
|
-
milestone: raw.milestone
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
if (raw.comments !== void 0) {
|
|
181
|
-
result.comments = raw.comments.map((comment) => ({
|
|
182
|
-
id: extractNumericIdFromUrl(comment.url),
|
|
183
|
-
body: comment.body,
|
|
184
|
-
createdAt: comment.createdAt,
|
|
185
|
-
author: normalizeAuthor(comment.author),
|
|
186
|
-
...comment.updatedAt && { updatedAt: comment.updatedAt }
|
|
187
|
-
}));
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Fetch a specific comment by ID using gh API
|
|
193
|
-
* Normalizes author to FlexibleAuthor format
|
|
194
|
-
*/
|
|
195
|
-
async getComment(input) {
|
|
196
|
-
const { commentId } = input;
|
|
197
|
-
const numericCommentId = parseInt(commentId, 10);
|
|
198
|
-
if (isNaN(numericCommentId)) {
|
|
199
|
-
throw new Error(`Invalid GitHub comment ID: ${commentId}. GitHub comment IDs must be numeric.`);
|
|
200
|
-
}
|
|
201
|
-
const raw = await executeGhCommand([
|
|
202
|
-
"api",
|
|
203
|
-
`repos/:owner/:repo/issues/comments/${numericCommentId}`,
|
|
204
|
-
"--jq",
|
|
205
|
-
"{id: .id, body: .body, user: .user, created_at: .created_at, updated_at: .updated_at, html_url: .html_url, reactions: .reactions}"
|
|
206
|
-
]);
|
|
207
|
-
return {
|
|
208
|
-
id: String(raw.id),
|
|
209
|
-
body: raw.body,
|
|
210
|
-
author: normalizeAuthor(raw.user),
|
|
211
|
-
created_at: raw.created_at,
|
|
212
|
-
...raw.updated_at && { updated_at: raw.updated_at },
|
|
213
|
-
// Passthrough GitHub-specific fields
|
|
214
|
-
...raw.html_url && { html_url: raw.html_url },
|
|
215
|
-
...raw.reactions && { reactions: raw.reactions }
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Create a new comment on an issue or PR
|
|
220
|
-
*/
|
|
221
|
-
async createComment(input) {
|
|
222
|
-
const { number, body, type } = input;
|
|
223
|
-
const numericId = parseInt(number, 10);
|
|
224
|
-
if (isNaN(numericId)) {
|
|
225
|
-
throw new Error(`Invalid GitHub ${type} number: ${number}. GitHub IDs must be numeric.`);
|
|
226
|
-
}
|
|
227
|
-
const result = type === "issue" ? await createIssueComment(numericId, body) : await createPRComment(numericId, body);
|
|
228
|
-
return {
|
|
229
|
-
...result,
|
|
230
|
-
id: String(result.id)
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Update an existing comment
|
|
235
|
-
*/
|
|
236
|
-
async updateComment(input) {
|
|
237
|
-
const { commentId, body } = input;
|
|
238
|
-
const numericCommentId = parseInt(commentId, 10);
|
|
239
|
-
if (isNaN(numericCommentId)) {
|
|
240
|
-
throw new Error(`Invalid GitHub comment ID: ${commentId}. GitHub comment IDs must be numeric.`);
|
|
241
|
-
}
|
|
242
|
-
const result = await updateIssueComment(numericCommentId, body);
|
|
243
|
-
return {
|
|
244
|
-
...result,
|
|
245
|
-
id: String(result.id)
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// src/utils/linear-markup-converter.ts
|
|
251
|
-
import { appendFileSync } from "fs";
|
|
252
|
-
import { join as join2, dirname, basename, extname } from "path";
|
|
253
|
-
var LinearMarkupConverter = class {
|
|
254
|
-
/**
|
|
255
|
-
* Convert HTML details/summary blocks to Linear's collapsible format
|
|
256
|
-
* Handles nested details blocks recursively
|
|
257
|
-
*
|
|
258
|
-
* @param text - Text containing HTML details/summary blocks
|
|
259
|
-
* @returns Text with details/summary converted to Linear format
|
|
260
|
-
*/
|
|
261
|
-
static convertDetailsToLinear(text) {
|
|
262
|
-
if (!text) {
|
|
263
|
-
return text;
|
|
264
|
-
}
|
|
265
|
-
let previousText = "";
|
|
266
|
-
let currentText = text;
|
|
267
|
-
while (previousText !== currentText) {
|
|
268
|
-
previousText = currentText;
|
|
269
|
-
currentText = this.convertSinglePass(currentText);
|
|
270
|
-
}
|
|
271
|
-
return currentText;
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Perform a single pass of details block conversion
|
|
275
|
-
* Converts the innermost details blocks first
|
|
276
|
-
*/
|
|
277
|
-
static convertSinglePass(text) {
|
|
278
|
-
const detailsRegex = /<details[^>]*>\s*<summary[^>]*>(.*?)<\/summary>\s*(.*?)\s*<\/details>/gis;
|
|
279
|
-
return text.replace(detailsRegex, (_match, summary, content) => {
|
|
280
|
-
const cleanSummary = this.cleanText(summary);
|
|
281
|
-
const cleanContent = this.cleanContent(content);
|
|
282
|
-
if (cleanContent) {
|
|
283
|
-
return `+++ ${cleanSummary}
|
|
284
|
-
|
|
285
|
-
${cleanContent}
|
|
286
|
-
|
|
287
|
-
+++`;
|
|
288
|
-
} else {
|
|
289
|
-
return `+++ ${cleanSummary}
|
|
290
|
-
|
|
291
|
-
+++`;
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Clean text by trimming whitespace and decoding common HTML entities
|
|
297
|
-
*/
|
|
298
|
-
static cleanText(text) {
|
|
299
|
-
return text.trim().replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Clean content while preserving internal structure
|
|
303
|
-
* - Removes leading/trailing whitespace
|
|
304
|
-
* - Normalizes internal blank lines (max 2 consecutive newlines)
|
|
305
|
-
* - Preserves code blocks and other formatting
|
|
306
|
-
*/
|
|
307
|
-
static cleanContent(content) {
|
|
308
|
-
if (!content) {
|
|
309
|
-
return "";
|
|
310
|
-
}
|
|
311
|
-
let cleaned = content.trim();
|
|
312
|
-
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
313
|
-
return cleaned;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Check if text contains HTML details/summary blocks
|
|
317
|
-
* Useful for conditional conversion
|
|
318
|
-
*/
|
|
319
|
-
static hasDetailsBlocks(text) {
|
|
320
|
-
if (!text) {
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
const detailsRegex = /<details[^>]*>.*?<summary[^>]*>.*?<\/summary>.*?<\/details>/is;
|
|
324
|
-
return detailsRegex.test(text);
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Remove wrapper tags from code sample details blocks
|
|
328
|
-
* Identifies details blocks where summary contains "X lines" pattern
|
|
329
|
-
* and removes the details/summary tags while preserving the content
|
|
330
|
-
*
|
|
331
|
-
* @param text - Text containing potential code sample details blocks
|
|
332
|
-
* @returns Text with code sample wrappers removed
|
|
333
|
-
*/
|
|
334
|
-
static removeCodeSampleWrappers(text) {
|
|
335
|
-
if (!text) {
|
|
336
|
-
return text;
|
|
337
|
-
}
|
|
338
|
-
const codeSampleRegex = /<details[^>]*>\s*<summary[^>]*>([^<]*\d+\s+lines[^<]*)<\/summary>\s*([\s\S]*?)<\/details>/gi;
|
|
339
|
-
return text.replace(codeSampleRegex, (_match, _summary, content) => {
|
|
340
|
-
return content.trim();
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Convert text for Linear - applies all necessary conversions
|
|
345
|
-
* Currently only converts details/summary blocks, but can be extended
|
|
346
|
-
* for other HTML to Linear markdown conversions
|
|
347
|
-
*/
|
|
348
|
-
static convertToLinear(text) {
|
|
349
|
-
if (!text) {
|
|
350
|
-
return text;
|
|
351
|
-
}
|
|
352
|
-
this.logConversion("INPUT", text);
|
|
353
|
-
let converted = text;
|
|
354
|
-
converted = this.removeCodeSampleWrappers(converted);
|
|
355
|
-
converted = this.convertDetailsToLinear(converted);
|
|
356
|
-
this.logConversion("OUTPUT", converted);
|
|
357
|
-
return converted;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Log conversion input/output if LINEAR_MARKDOWN_LOG_FILE is set
|
|
361
|
-
*/
|
|
362
|
-
static logConversion(label, content) {
|
|
363
|
-
const logFilePath = process.env.LINEAR_MARKDOWN_LOG_FILE;
|
|
364
|
-
if (!logFilePath) {
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
try {
|
|
368
|
-
const timestampedPath = this.getTimestampedLogPath(logFilePath);
|
|
369
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
370
|
-
const separator = "================================";
|
|
371
|
-
const logEntry = `${separator}
|
|
372
|
-
[${timestamp}] CONVERSION ${label}
|
|
373
|
-
${separator}
|
|
374
|
-
${label}:
|
|
375
|
-
${content}
|
|
376
|
-
|
|
377
|
-
`;
|
|
378
|
-
appendFileSync(timestampedPath, logEntry, "utf-8");
|
|
379
|
-
} catch {
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Generate timestamped log file path
|
|
384
|
-
* Example: debug.log -> debug-20231202-161234.log
|
|
385
|
-
*/
|
|
386
|
-
static getTimestampedLogPath(logFilePath) {
|
|
387
|
-
const dir = dirname(logFilePath);
|
|
388
|
-
const ext = extname(logFilePath);
|
|
389
|
-
const base = basename(logFilePath, ext);
|
|
390
|
-
const now = /* @__PURE__ */ new Date();
|
|
391
|
-
const timestamp = [
|
|
392
|
-
now.getFullYear(),
|
|
393
|
-
String(now.getMonth() + 1).padStart(2, "0"),
|
|
394
|
-
String(now.getDate()).padStart(2, "0")
|
|
395
|
-
].join("") + "-" + [
|
|
396
|
-
String(now.getHours()).padStart(2, "0"),
|
|
397
|
-
String(now.getMinutes()).padStart(2, "0"),
|
|
398
|
-
String(now.getSeconds()).padStart(2, "0")
|
|
399
|
-
].join("");
|
|
400
|
-
return join2(dir, `${base}-${timestamp}${ext}`);
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
// src/mcp/LinearIssueManagementProvider.ts
|
|
405
|
-
var LinearIssueManagementProvider = class {
|
|
406
|
-
constructor() {
|
|
407
|
-
this.providerName = "linear";
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Fetch issue details using Linear SDK
|
|
411
|
-
*/
|
|
412
|
-
async getIssue(input) {
|
|
413
|
-
const { number, includeComments = true } = input;
|
|
414
|
-
const raw = await fetchLinearIssue(number);
|
|
415
|
-
const state = raw.state && (raw.state.toLowerCase().includes("done") || raw.state.toLowerCase().includes("completed") || raw.state.toLowerCase().includes("canceled")) ? "closed" : "open";
|
|
416
|
-
const result = {
|
|
417
|
-
id: raw.identifier,
|
|
418
|
-
title: raw.title,
|
|
419
|
-
body: raw.description ?? "",
|
|
420
|
-
state,
|
|
421
|
-
url: raw.url,
|
|
422
|
-
provider: "linear",
|
|
423
|
-
author: null,
|
|
424
|
-
// Linear SDK doesn't return author in basic fetch
|
|
425
|
-
// Linear-specific fields
|
|
426
|
-
linearState: raw.state,
|
|
427
|
-
createdAt: raw.createdAt,
|
|
428
|
-
updatedAt: raw.updatedAt
|
|
429
|
-
};
|
|
430
|
-
if (includeComments) {
|
|
431
|
-
try {
|
|
432
|
-
const comments = await this.fetchIssueComments(number);
|
|
433
|
-
if (comments) {
|
|
434
|
-
result.comments = comments;
|
|
435
|
-
}
|
|
436
|
-
} catch {
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return result;
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Fetch comments for an issue
|
|
443
|
-
*/
|
|
444
|
-
async fetchIssueComments(identifier) {
|
|
445
|
-
try {
|
|
446
|
-
const comments = await fetchLinearIssueComments(identifier);
|
|
447
|
-
return comments.map((comment) => ({
|
|
448
|
-
id: comment.id,
|
|
449
|
-
body: comment.body,
|
|
450
|
-
createdAt: comment.createdAt,
|
|
451
|
-
author: null,
|
|
452
|
-
// Linear SDK doesn't return comment author info in basic fetch
|
|
453
|
-
...comment.updatedAt && { updatedAt: comment.updatedAt }
|
|
454
|
-
}));
|
|
455
|
-
} catch {
|
|
456
|
-
return [];
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Fetch a specific comment by ID
|
|
461
|
-
*/
|
|
462
|
-
async getComment(input) {
|
|
463
|
-
const { commentId } = input;
|
|
464
|
-
const raw = await getLinearComment(commentId);
|
|
465
|
-
return {
|
|
466
|
-
id: raw.id,
|
|
467
|
-
body: raw.body,
|
|
468
|
-
author: null,
|
|
469
|
-
// Linear SDK doesn't return comment author info in basic fetch
|
|
470
|
-
created_at: raw.createdAt
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Create a new comment on an issue
|
|
475
|
-
*/
|
|
476
|
-
async createComment(input) {
|
|
477
|
-
const { number, body } = input;
|
|
478
|
-
const convertedBody = LinearMarkupConverter.convertToLinear(body);
|
|
479
|
-
const result = await createLinearComment(number, convertedBody);
|
|
480
|
-
return {
|
|
481
|
-
id: result.id,
|
|
482
|
-
url: result.url,
|
|
483
|
-
created_at: result.createdAt
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Update an existing comment
|
|
488
|
-
*/
|
|
489
|
-
async updateComment(input) {
|
|
490
|
-
const { commentId, body } = input;
|
|
491
|
-
const convertedBody = LinearMarkupConverter.convertToLinear(body);
|
|
492
|
-
const result = await updateLinearComment(commentId, convertedBody);
|
|
493
|
-
return {
|
|
494
|
-
id: result.id,
|
|
495
|
-
url: result.url,
|
|
496
|
-
updated_at: result.updatedAt
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
// src/mcp/IssueManagementProviderFactory.ts
|
|
502
|
-
var IssueManagementProviderFactory = class {
|
|
503
|
-
/**
|
|
504
|
-
* Create an issue management provider based on the provider type
|
|
505
|
-
*/
|
|
506
|
-
static create(provider) {
|
|
507
|
-
switch (provider) {
|
|
508
|
-
case "github":
|
|
509
|
-
return new GitHubIssueManagementProvider();
|
|
510
|
-
case "linear":
|
|
511
|
-
return new LinearIssueManagementProvider();
|
|
512
|
-
default:
|
|
513
|
-
throw new Error(`Unsupported issue management provider: ${provider}`);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
|
|
518
106
|
// src/lib/SessionSummaryService.ts
|
|
519
107
|
var RECAPS_DIR = path.join(os.homedir(), ".config", "iloom-ai", "recaps");
|
|
520
108
|
function slugifyPath(loomPath) {
|
|
@@ -776,4 +364,4 @@ var SessionSummaryService = class {
|
|
|
776
364
|
export {
|
|
777
365
|
SessionSummaryService
|
|
778
366
|
};
|
|
779
|
-
//# sourceMappingURL=chunk-
|
|
367
|
+
//# sourceMappingURL=chunk-CNSTXBJ3.js.map
|