metheus-governance-mcp-cli 0.2.8 → 0.2.10
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/README.md +9 -1
- package/cli.mjs +853 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Compatibility note: legacy command alias `metheus-governance-mcp` is still suppo
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install -g metheus-governance-mcp-cli
|
|
20
|
+
npm install -g metheus-governance-mcp-cli@latest
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## One command bootstrap (recommended)
|
|
@@ -61,6 +61,8 @@ Local bootstrap tools exposed by proxy:
|
|
|
61
61
|
- `project.summary`
|
|
62
62
|
- `project.describe` (alias)
|
|
63
63
|
- `project.get` (alias)
|
|
64
|
+
- `ctxpack.merge.brief`
|
|
65
|
+
- `ctxpack.merge.execute`
|
|
64
66
|
|
|
65
67
|
These tools accept `project_id` and return:
|
|
66
68
|
|
|
@@ -74,6 +76,12 @@ These tools accept `project_id` and return:
|
|
|
74
76
|
- newer server version -> update local cache
|
|
75
77
|
- workspace path -> follows active client workspace/root when provided by Codex/Claude
|
|
76
78
|
|
|
79
|
+
Ctxpack merge safety flow:
|
|
80
|
+
- call `ctxpack.merge.brief` first
|
|
81
|
+
- review who changed what (`created_by`, `changed_paths`, metadata summary)
|
|
82
|
+
- get explicit owner decision in chat
|
|
83
|
+
- then call `ctxpack.merge.execute` with `owner_confirmation`
|
|
84
|
+
|
|
77
85
|
Manual ctxpack pull/update:
|
|
78
86
|
|
|
79
87
|
```bash
|
package/cli.mjs
CHANGED
|
@@ -140,11 +140,41 @@ function firstNonEmptyString(values) {
|
|
|
140
140
|
return "";
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function extractWorkspaceCandidateFromFolders(rawFolders) {
|
|
144
|
+
if (!Array.isArray(rawFolders)) return "";
|
|
145
|
+
for (const folder of rawFolders) {
|
|
146
|
+
if (typeof folder === "string") {
|
|
147
|
+
const direct = firstNonEmptyString([fileURIToLocalPath(folder), folder]);
|
|
148
|
+
if (direct) return resolveWorkspaceDir(direct);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!folder || typeof folder !== "object" || Array.isArray(folder)) continue;
|
|
152
|
+
const uriValue = firstNonEmptyString([
|
|
153
|
+
folder.uri,
|
|
154
|
+
folder.root_uri,
|
|
155
|
+
folder.rootUri,
|
|
156
|
+
folder.path,
|
|
157
|
+
folder.root_path,
|
|
158
|
+
folder.rootPath,
|
|
159
|
+
]);
|
|
160
|
+
const candidate = firstNonEmptyString([fileURIToLocalPath(uriValue), uriValue]);
|
|
161
|
+
if (candidate) return resolveWorkspaceDir(candidate);
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
|
|
143
166
|
function extractWorkspaceCandidateFromRequest(requestObj, toolArgs) {
|
|
144
167
|
const params = safeObject(requestObj?.params);
|
|
145
168
|
const meta = safeObject(params._meta);
|
|
169
|
+
const workspaceFromFolders = firstNonEmptyString([
|
|
170
|
+
extractWorkspaceCandidateFromFolders(params.workspace_folders),
|
|
171
|
+
extractWorkspaceCandidateFromFolders(params.workspaceFolders),
|
|
172
|
+
extractWorkspaceCandidateFromFolders(meta.workspace_folders),
|
|
173
|
+
extractWorkspaceCandidateFromFolders(meta.workspaceFolders),
|
|
174
|
+
]);
|
|
146
175
|
const args = safeObject(toolArgs);
|
|
147
176
|
const rawCandidate = firstNonEmptyString([
|
|
177
|
+
workspaceFromFolders,
|
|
148
178
|
args.workspace_dir,
|
|
149
179
|
args.workspaceDir,
|
|
150
180
|
params.workspace_dir,
|
|
@@ -172,10 +202,12 @@ function extractWorkspaceCandidateFromEnv() {
|
|
|
172
202
|
const rawCandidate = firstNonEmptyString([
|
|
173
203
|
process.env.METHEUS_WORKSPACE_DIR,
|
|
174
204
|
process.env.CLAUDE_WORKSPACE_DIR,
|
|
205
|
+
process.env.CLAUDE_WORKSPACE_URI,
|
|
175
206
|
process.env.CLAUDE_PROJECT_DIR,
|
|
176
207
|
process.env.CODEX_WORKSPACE_DIR,
|
|
177
208
|
process.env.WORKSPACE_DIR,
|
|
178
209
|
process.env.WORKSPACE_FOLDER,
|
|
210
|
+
process.env.VSCODE_WORKSPACE_FOLDER,
|
|
179
211
|
process.env.VSCODE_CWD,
|
|
180
212
|
process.env.PWD,
|
|
181
213
|
process.env.INIT_CWD,
|
|
@@ -198,6 +230,25 @@ function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs
|
|
|
198
230
|
return resolveWorkspaceDir(candidate);
|
|
199
231
|
}
|
|
200
232
|
|
|
233
|
+
function resolveProjectIDForRequest({
|
|
234
|
+
toolArgs,
|
|
235
|
+
args,
|
|
236
|
+
workspaceDir,
|
|
237
|
+
responseProjectID,
|
|
238
|
+
envelopeProjectID,
|
|
239
|
+
}) {
|
|
240
|
+
const resolvedWorkspaceDir = resolveWorkspaceDir(workspaceDir || process.cwd());
|
|
241
|
+
const workspaceMeta = loadWorkspaceMeta(resolvedWorkspaceDir);
|
|
242
|
+
return firstNonEmptyString([
|
|
243
|
+
toolArgs?.project_id,
|
|
244
|
+
toolArgs?.projectID,
|
|
245
|
+
responseProjectID,
|
|
246
|
+
envelopeProjectID,
|
|
247
|
+
workspaceMeta.project_id,
|
|
248
|
+
args?.projectID,
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
|
|
201
252
|
function homeCtxpackCacheRootDir() {
|
|
202
253
|
const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
|
|
203
254
|
if (!home) {
|
|
@@ -1411,6 +1462,19 @@ function addDoctorCheck(rows, status, label, detail) {
|
|
|
1411
1462
|
});
|
|
1412
1463
|
}
|
|
1413
1464
|
|
|
1465
|
+
function isUnauthorizedLike(rawText) {
|
|
1466
|
+
const text = String(rawText || "").trim().toLowerCase();
|
|
1467
|
+
if (!text) return false;
|
|
1468
|
+
return (
|
|
1469
|
+
text.includes("unauthorized") ||
|
|
1470
|
+
text.includes("access denied") ||
|
|
1471
|
+
text.includes("status 401") ||
|
|
1472
|
+
text.includes("status 403") ||
|
|
1473
|
+
text.includes("http 401") ||
|
|
1474
|
+
text.includes("http 403")
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1414
1478
|
function statusIcon(status) {
|
|
1415
1479
|
if (status === "ok") return "OK";
|
|
1416
1480
|
if (status === "fail") return "FAIL";
|
|
@@ -1695,12 +1759,25 @@ async function runDoctor(flags) {
|
|
|
1695
1759
|
addDoctorCheck(rows, "ok", "server mode", "ctxpack writable");
|
|
1696
1760
|
}
|
|
1697
1761
|
} catch (err) {
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1762
|
+
const statusCode = Number(err?.statusCode || 0);
|
|
1763
|
+
const message = String(err?.message || err);
|
|
1764
|
+
if (statusCode === 404) {
|
|
1765
|
+
addDoctorCheck(
|
|
1766
|
+
rows,
|
|
1767
|
+
"ok",
|
|
1768
|
+
"server ctxpack policy",
|
|
1769
|
+
"ctxpack/stats endpoint is not exposed in this deployment (optional check skipped)",
|
|
1770
|
+
);
|
|
1771
|
+
} else if (statusCode === 401 || statusCode === 403 || isUnauthorizedLike(message)) {
|
|
1772
|
+
addDoctorCheck(
|
|
1773
|
+
rows,
|
|
1774
|
+
"warn",
|
|
1775
|
+
"server ctxpack policy",
|
|
1776
|
+
"no permission to read ctxpack/stats in this environment",
|
|
1777
|
+
);
|
|
1778
|
+
} else {
|
|
1779
|
+
addDoctorCheck(rows, "warn", "server ctxpack policy", `unable to read /ctxpack/stats (${message})`);
|
|
1780
|
+
}
|
|
1704
1781
|
}
|
|
1705
1782
|
}
|
|
1706
1783
|
|
|
@@ -1736,7 +1813,17 @@ async function runDoctor(flags) {
|
|
|
1736
1813
|
},
|
|
1737
1814
|
});
|
|
1738
1815
|
if (!rpc.ok) {
|
|
1739
|
-
|
|
1816
|
+
const errorText = String(rpc.error || "rpc error");
|
|
1817
|
+
if (isUnauthorizedLike(errorText)) {
|
|
1818
|
+
addDoctorCheck(
|
|
1819
|
+
rows,
|
|
1820
|
+
"warn",
|
|
1821
|
+
`smoke ${row.tool}`,
|
|
1822
|
+
"unauthorized for this tool with current token/project role",
|
|
1823
|
+
);
|
|
1824
|
+
} else {
|
|
1825
|
+
addDoctorCheck(rows, "fail", `smoke ${row.tool}`, errorText);
|
|
1826
|
+
}
|
|
1740
1827
|
continue;
|
|
1741
1828
|
}
|
|
1742
1829
|
const envelope = parseToolEnvelopeFromRPCResult(safeObject(rpc.response).result);
|
|
@@ -1749,12 +1836,21 @@ async function runDoctor(flags) {
|
|
|
1749
1836
|
if (ok && status >= 200 && status < 300) {
|
|
1750
1837
|
addDoctorCheck(rows, "ok", `smoke ${row.tool}`, `status ${status}`);
|
|
1751
1838
|
} else {
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1839
|
+
if (status === 401 || status === 403) {
|
|
1840
|
+
addDoctorCheck(
|
|
1841
|
+
rows,
|
|
1842
|
+
"warn",
|
|
1843
|
+
`smoke ${row.tool}`,
|
|
1844
|
+
`status ${status}: unauthorized for this tool with current token/project role`,
|
|
1845
|
+
);
|
|
1846
|
+
} else {
|
|
1847
|
+
addDoctorCheck(
|
|
1848
|
+
rows,
|
|
1849
|
+
"fail",
|
|
1850
|
+
`smoke ${row.tool}`,
|
|
1851
|
+
`status ${status || "-"}, ok=${ok ? "true" : "false"}`,
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1758
1854
|
}
|
|
1759
1855
|
}
|
|
1760
1856
|
|
|
@@ -1801,7 +1897,10 @@ function postJSON(urlText, timeoutSeconds, token, payload) {
|
|
|
1801
1897
|
resolve(text.trim());
|
|
1802
1898
|
return;
|
|
1803
1899
|
}
|
|
1804
|
-
|
|
1900
|
+
const err = new Error(text.trim() || `http ${statusCode}`);
|
|
1901
|
+
err.statusCode = statusCode;
|
|
1902
|
+
err.responseBody = text;
|
|
1903
|
+
reject(err);
|
|
1805
1904
|
});
|
|
1806
1905
|
},
|
|
1807
1906
|
);
|
|
@@ -1887,6 +1986,7 @@ function jsonRpcResult(requestObj, result) {
|
|
|
1887
1986
|
}
|
|
1888
1987
|
|
|
1889
1988
|
const LOCAL_PROJECT_TOOL_NAMES = ["project.summary", "project.describe", "project.get"];
|
|
1989
|
+
const LOCAL_CTXPACK_MERGE_TOOL_NAMES = ["ctxpack.merge.brief", "ctxpack.merge.execute"];
|
|
1890
1990
|
|
|
1891
1991
|
function buildProjectSummaryInputSchema() {
|
|
1892
1992
|
return {
|
|
@@ -1910,6 +2010,62 @@ function buildProjectSummaryInputSchema() {
|
|
|
1910
2010
|
};
|
|
1911
2011
|
}
|
|
1912
2012
|
|
|
2013
|
+
function buildCtxpackMergeBriefInputSchema() {
|
|
2014
|
+
return {
|
|
2015
|
+
type: "object",
|
|
2016
|
+
properties: {
|
|
2017
|
+
project_id: {
|
|
2018
|
+
type: "string",
|
|
2019
|
+
description: "Project UUID. If omitted, current configured project_id is used.",
|
|
2020
|
+
},
|
|
2021
|
+
status: {
|
|
2022
|
+
type: "string",
|
|
2023
|
+
description: "Merge-request status filter. Default: open.",
|
|
2024
|
+
enum: ["open", "review", "approved", "rejected", "merged", "closed", "all"],
|
|
2025
|
+
},
|
|
2026
|
+
limit: {
|
|
2027
|
+
type: "integer",
|
|
2028
|
+
minimum: 1,
|
|
2029
|
+
maximum: 100,
|
|
2030
|
+
description: "Maximum merge requests to inspect. Default: 20.",
|
|
2031
|
+
},
|
|
2032
|
+
},
|
|
2033
|
+
additionalProperties: false,
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function buildCtxpackMergeExecuteInputSchema() {
|
|
2038
|
+
return {
|
|
2039
|
+
type: "object",
|
|
2040
|
+
properties: {
|
|
2041
|
+
project_id: {
|
|
2042
|
+
type: "string",
|
|
2043
|
+
description: "Project UUID. If omitted, current configured project_id is used.",
|
|
2044
|
+
},
|
|
2045
|
+
merge_request_id: {
|
|
2046
|
+
type: "string",
|
|
2047
|
+
description: "Target merge request UUID.",
|
|
2048
|
+
},
|
|
2049
|
+
action: {
|
|
2050
|
+
type: "string",
|
|
2051
|
+
description: "Action to execute.",
|
|
2052
|
+
enum: ["review", "approve", "reject", "close", "merge"],
|
|
2053
|
+
},
|
|
2054
|
+
owner_confirmation: {
|
|
2055
|
+
type: "string",
|
|
2056
|
+
description:
|
|
2057
|
+
"Explicit owner confirmation text from chat (for safety). Example: confirm / approved / yes.",
|
|
2058
|
+
},
|
|
2059
|
+
note: {
|
|
2060
|
+
type: "string",
|
|
2061
|
+
description: "Optional reviewer note recorded in metadata for non-merge actions.",
|
|
2062
|
+
},
|
|
2063
|
+
},
|
|
2064
|
+
required: ["merge_request_id", "action", "owner_confirmation"],
|
|
2065
|
+
additionalProperties: false,
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1913
2069
|
function buildLocalToolSpecs() {
|
|
1914
2070
|
return [
|
|
1915
2071
|
{
|
|
@@ -1930,6 +2086,18 @@ function buildLocalToolSpecs() {
|
|
|
1930
2086
|
"Alias of project.summary. Returns project metadata and access status for the given project_id.",
|
|
1931
2087
|
inputSchema: buildProjectSummaryInputSchema(),
|
|
1932
2088
|
},
|
|
2089
|
+
{
|
|
2090
|
+
name: "ctxpack.merge.brief",
|
|
2091
|
+
description:
|
|
2092
|
+
"Before merge, summarize pending ctxpack merge requests (who changed what), show risk/recommendation, and provide owner confirmation checklist.",
|
|
2093
|
+
inputSchema: buildCtxpackMergeBriefInputSchema(),
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
name: "ctxpack.merge.execute",
|
|
2097
|
+
description:
|
|
2098
|
+
"Execute ctxpack merge-request action (review/approve/reject/close/merge) after explicit owner confirmation.",
|
|
2099
|
+
inputSchema: buildCtxpackMergeExecuteInputSchema(),
|
|
2100
|
+
},
|
|
1933
2101
|
];
|
|
1934
2102
|
}
|
|
1935
2103
|
|
|
@@ -2233,6 +2401,10 @@ async function loadProjectSummaryForTool({
|
|
|
2233
2401
|
visibility: String(project.visibility || "").trim() || "private",
|
|
2234
2402
|
template: String(project.template || "").trim() || "blank",
|
|
2235
2403
|
template_label: normalizeTemplateLabel(project.template),
|
|
2404
|
+
owner_user_id: String(project.owner_user_id || "").trim(),
|
|
2405
|
+
program_owner_user_id: String(project.program_owner_user_id || "").trim(),
|
|
2406
|
+
tech_owner_user_id: String(project.tech_owner_user_id || "").trim(),
|
|
2407
|
+
governance_owner_user_id: String(project.governance_owner_user_id || "").trim(),
|
|
2236
2408
|
owner_name: String(project.owner_name || "").trim(),
|
|
2237
2409
|
program_owner_name: String(project.program_owner_name || "").trim(),
|
|
2238
2410
|
tech_owner_name: String(project.tech_owner_name || "").trim(),
|
|
@@ -2255,6 +2427,522 @@ async function loadProjectSummaryForTool({
|
|
|
2255
2427
|
};
|
|
2256
2428
|
}
|
|
2257
2429
|
|
|
2430
|
+
function normalizeMergeStatusFilter(rawValue) {
|
|
2431
|
+
const value = String(rawValue || "").trim().toLowerCase();
|
|
2432
|
+
if (!value) return "open";
|
|
2433
|
+
if (["open", "review", "approved", "rejected", "merged", "closed", "all"].includes(value)) {
|
|
2434
|
+
return value;
|
|
2435
|
+
}
|
|
2436
|
+
return "";
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function normalizeMergeAction(rawValue) {
|
|
2440
|
+
const value = String(rawValue || "").trim().toLowerCase();
|
|
2441
|
+
if (["review", "approve", "reject", "close", "merge"].includes(value)) {
|
|
2442
|
+
return value;
|
|
2443
|
+
}
|
|
2444
|
+
return "";
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function parseMergeMetadata(rawMetadata) {
|
|
2448
|
+
if (!rawMetadata) return {};
|
|
2449
|
+
if (typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) {
|
|
2450
|
+
return rawMetadata;
|
|
2451
|
+
}
|
|
2452
|
+
if (typeof rawMetadata !== "string") return {};
|
|
2453
|
+
try {
|
|
2454
|
+
const parsed = JSON.parse(rawMetadata);
|
|
2455
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
2456
|
+
return parsed;
|
|
2457
|
+
} catch {
|
|
2458
|
+
return {};
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function collectMergeChangedPaths(metadataObj) {
|
|
2463
|
+
const meta = safeObject(metadataObj);
|
|
2464
|
+
const out = [];
|
|
2465
|
+
const pushPath = (raw) => {
|
|
2466
|
+
const value = String(raw || "").trim();
|
|
2467
|
+
if (!value) return;
|
|
2468
|
+
if (!out.includes(value)) out.push(value);
|
|
2469
|
+
};
|
|
2470
|
+
for (const pathValue of ensureArray(meta.changed_paths)) {
|
|
2471
|
+
pushPath(pathValue);
|
|
2472
|
+
}
|
|
2473
|
+
for (const pathValue of ensureArray(meta.conflict_paths)) {
|
|
2474
|
+
pushPath(pathValue);
|
|
2475
|
+
}
|
|
2476
|
+
const files = ensureArray(meta.files);
|
|
2477
|
+
for (const file of files) {
|
|
2478
|
+
pushPath(safeObject(file).path);
|
|
2479
|
+
}
|
|
2480
|
+
const changeSet = safeObject(meta.change_set);
|
|
2481
|
+
const changeSetFiles = ensureArray(changeSet.files);
|
|
2482
|
+
for (const file of changeSetFiles) {
|
|
2483
|
+
pushPath(safeObject(file).path);
|
|
2484
|
+
}
|
|
2485
|
+
return out;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function summarizeMergeMetadata(metadataObj) {
|
|
2489
|
+
const meta = safeObject(metadataObj);
|
|
2490
|
+
const parts = [];
|
|
2491
|
+
const changeSet = safeObject(meta.change_set);
|
|
2492
|
+
const summary = safeObject(changeSet.summary);
|
|
2493
|
+
const changedCount = Number.parseInt(String(summary.changed || 0), 10);
|
|
2494
|
+
if (Number.isFinite(changedCount) && changedCount > 0) {
|
|
2495
|
+
parts.push(`changed files: ${changedCount}`);
|
|
2496
|
+
}
|
|
2497
|
+
const title = String(meta.title || meta.summary || meta.reason || "").trim();
|
|
2498
|
+
if (title) {
|
|
2499
|
+
parts.push(title);
|
|
2500
|
+
}
|
|
2501
|
+
if (!parts.length) return "";
|
|
2502
|
+
return parts.join(" | ");
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function parseISOTime(rawValue) {
|
|
2506
|
+
const text = String(rawValue || "").trim();
|
|
2507
|
+
if (!text) return 0;
|
|
2508
|
+
const ms = Date.parse(text);
|
|
2509
|
+
if (!Number.isFinite(ms)) return 0;
|
|
2510
|
+
return ms;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function extractActorFromToken(token) {
|
|
2514
|
+
const payload = safeObject(decodeJwtPayload(token));
|
|
2515
|
+
const userID = firstNonEmptyString([payload.sub, payload.user_id, payload.uid]);
|
|
2516
|
+
const email = firstNonEmptyString([payload.email, payload.preferred_username]);
|
|
2517
|
+
const name = firstNonEmptyString([payload.name, payload.given_name]);
|
|
2518
|
+
return { user_id: userID, email, name };
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
async function loadProjectMemberMap({ siteBaseURL, projectID, token, timeoutSeconds }) {
|
|
2522
|
+
const encodedProjectID = encodeURIComponent(projectID);
|
|
2523
|
+
try {
|
|
2524
|
+
const raw = await getJSONWithAuth(
|
|
2525
|
+
`${siteBaseURL}/api/v1/projects/${encodedProjectID}/members?limit=500&offset=0`,
|
|
2526
|
+
timeoutSeconds,
|
|
2527
|
+
token,
|
|
2528
|
+
);
|
|
2529
|
+
const members = ensureArray(raw);
|
|
2530
|
+
const map = new Map();
|
|
2531
|
+
for (const rowRaw of members) {
|
|
2532
|
+
const row = safeObject(rowRaw);
|
|
2533
|
+
const userID = String(row.user_id || "").trim();
|
|
2534
|
+
if (!userID) continue;
|
|
2535
|
+
map.set(userID, {
|
|
2536
|
+
user_id: userID,
|
|
2537
|
+
name: String(row.name || "").trim(),
|
|
2538
|
+
email: String(row.email || "").trim(),
|
|
2539
|
+
role: String(row.role || "").trim(),
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
return map;
|
|
2543
|
+
} catch {
|
|
2544
|
+
return new Map();
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
async function loadOrgOwnerUserID({ siteBaseURL, orgID, token, timeoutSeconds }) {
|
|
2549
|
+
const normalizedOrgID = String(orgID || "").trim();
|
|
2550
|
+
if (!normalizedOrgID) return "";
|
|
2551
|
+
try {
|
|
2552
|
+
const raw = await getJSONWithAuth(
|
|
2553
|
+
`${siteBaseURL}/api/v1/orgs/${encodeURIComponent(normalizedOrgID)}`,
|
|
2554
|
+
timeoutSeconds,
|
|
2555
|
+
token,
|
|
2556
|
+
);
|
|
2557
|
+
return String(safeObject(raw).owner_user_id || "").trim();
|
|
2558
|
+
} catch {
|
|
2559
|
+
return "";
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
function resolveActorDisplay(userID, membersByUserID) {
|
|
2564
|
+
const id = String(userID || "").trim();
|
|
2565
|
+
if (!id) return "-";
|
|
2566
|
+
const row = membersByUserID.get(id);
|
|
2567
|
+
if (!row) return id;
|
|
2568
|
+
const name = String(row.name || "").trim();
|
|
2569
|
+
const email = String(row.email || "").trim();
|
|
2570
|
+
if (name && email) return `${name} <${email}>`;
|
|
2571
|
+
if (name) return name;
|
|
2572
|
+
if (email) return email;
|
|
2573
|
+
return id;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
function isOwnerConfirmationAccepted(rawValue) {
|
|
2577
|
+
const text = String(rawValue || "").trim().toLowerCase();
|
|
2578
|
+
if (!text) return false;
|
|
2579
|
+
return ["yes", "y", "ok", "approve", "approved", "confirm", "confirmed"].includes(text);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
async function loadCtxpackMergeBriefForTool({
|
|
2583
|
+
siteBaseURL,
|
|
2584
|
+
projectID,
|
|
2585
|
+
token,
|
|
2586
|
+
timeoutSeconds,
|
|
2587
|
+
statusFilter,
|
|
2588
|
+
limit,
|
|
2589
|
+
workspaceDir,
|
|
2590
|
+
}) {
|
|
2591
|
+
const summary = await loadProjectSummaryForTool({
|
|
2592
|
+
siteBaseURL,
|
|
2593
|
+
projectID,
|
|
2594
|
+
token,
|
|
2595
|
+
timeoutSeconds,
|
|
2596
|
+
includeCtxpack: false,
|
|
2597
|
+
syncCtxpackLocal: false,
|
|
2598
|
+
workspaceDir,
|
|
2599
|
+
});
|
|
2600
|
+
if (String(summary.access || "") !== "granted") {
|
|
2601
|
+
return {
|
|
2602
|
+
project_id: projectID,
|
|
2603
|
+
access: summary.access || "error",
|
|
2604
|
+
status_code: summary.status_code || 500,
|
|
2605
|
+
message: summary.message || "Unable to access project.",
|
|
2606
|
+
merge_requests: [],
|
|
2607
|
+
recommendation: {},
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
const normalizedStatus = normalizeMergeStatusFilter(statusFilter) || "open";
|
|
2612
|
+
const cappedLimit = Math.max(1, Math.min(100, Number.parseInt(String(limit || 20), 10) || 20));
|
|
2613
|
+
const encodedProjectID = encodeURIComponent(projectID);
|
|
2614
|
+
const statusParam =
|
|
2615
|
+
normalizedStatus === "all" || normalizedStatus === "open"
|
|
2616
|
+
? ""
|
|
2617
|
+
: `&status=${encodeURIComponent(normalizedStatus)}`;
|
|
2618
|
+
const listURL = `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack/merge-requests?limit=${cappedLimit}&offset=0${statusParam}`;
|
|
2619
|
+
let mergeFeatureAvailable = true;
|
|
2620
|
+
let mergeFeatureWarning = "";
|
|
2621
|
+
let allItems = [];
|
|
2622
|
+
try {
|
|
2623
|
+
const rawList = await getJSONWithAuth(listURL, timeoutSeconds, token);
|
|
2624
|
+
allItems = ensureArray(rawList);
|
|
2625
|
+
} catch (err) {
|
|
2626
|
+
const statusCode = Number(err?.statusCode || 0) || 500;
|
|
2627
|
+
if (statusCode === 404) {
|
|
2628
|
+
mergeFeatureAvailable = false;
|
|
2629
|
+
mergeFeatureWarning = "Merge-request API is not enabled on this server yet.";
|
|
2630
|
+
allItems = [];
|
|
2631
|
+
} else {
|
|
2632
|
+
throw err;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
const statusOpenSet = new Set(["open", "review", "approved"]);
|
|
2636
|
+
const filteredItems =
|
|
2637
|
+
normalizedStatus === "open"
|
|
2638
|
+
? allItems.filter((rowRaw) => statusOpenSet.has(String(safeObject(rowRaw).status || "").trim().toLowerCase()))
|
|
2639
|
+
: allItems;
|
|
2640
|
+
|
|
2641
|
+
const membersByUserID = await loadProjectMemberMap({
|
|
2642
|
+
siteBaseURL,
|
|
2643
|
+
projectID,
|
|
2644
|
+
token,
|
|
2645
|
+
timeoutSeconds,
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
const actor = extractActorFromToken(token);
|
|
2649
|
+
const orgOwnerUserID = await loadOrgOwnerUserID({
|
|
2650
|
+
siteBaseURL,
|
|
2651
|
+
orgID: summary.org_id,
|
|
2652
|
+
token,
|
|
2653
|
+
timeoutSeconds,
|
|
2654
|
+
});
|
|
2655
|
+
const projectOwnerIDs = new Set(
|
|
2656
|
+
[summary.owner_user_id, summary.program_owner_user_id, summary.tech_owner_user_id, summary.governance_owner_user_id]
|
|
2657
|
+
.map((value) => String(value || "").trim())
|
|
2658
|
+
.filter(Boolean),
|
|
2659
|
+
);
|
|
2660
|
+
const isProjectOwner = actor.user_id ? projectOwnerIDs.has(actor.user_id) : false;
|
|
2661
|
+
const isOrgOwner = Boolean(actor.user_id && orgOwnerUserID && actor.user_id === orgOwnerUserID);
|
|
2662
|
+
const ownerGate = isProjectOwner || isOrgOwner;
|
|
2663
|
+
|
|
2664
|
+
const normalizedItems = filteredItems
|
|
2665
|
+
.map((rowRaw) => {
|
|
2666
|
+
const row = safeObject(rowRaw);
|
|
2667
|
+
const metadata = parseMergeMetadata(row.metadata);
|
|
2668
|
+
const changedPaths = collectMergeChangedPaths(metadata);
|
|
2669
|
+
const status = String(row.status || "").trim().toLowerCase();
|
|
2670
|
+
return {
|
|
2671
|
+
merge_request_id: String(row.merge_request_id || "").trim(),
|
|
2672
|
+
status,
|
|
2673
|
+
created_by_user_id: String(row.created_by || "").trim(),
|
|
2674
|
+
created_by: resolveActorDisplay(String(row.created_by || "").trim(), membersByUserID),
|
|
2675
|
+
reviewed_by_user_id: String(row.reviewed_by || "").trim(),
|
|
2676
|
+
reviewed_by: resolveActorDisplay(String(row.reviewed_by || "").trim(), membersByUserID),
|
|
2677
|
+
approved_at: String(row.approved_at || "").trim(),
|
|
2678
|
+
created_at: String(row.created_at || "").trim(),
|
|
2679
|
+
updated_at: String(row.updated_at || "").trim(),
|
|
2680
|
+
source_version_id: String(row.source_version_id || "").trim(),
|
|
2681
|
+
target_version_id: String(row.target_version_id || "").trim(),
|
|
2682
|
+
metadata_summary: summarizeMergeMetadata(metadata),
|
|
2683
|
+
changed_paths: changedPaths,
|
|
2684
|
+
changed_paths_count: changedPaths.length,
|
|
2685
|
+
};
|
|
2686
|
+
})
|
|
2687
|
+
.sort((a, b) => parseISOTime(b.updated_at) - parseISOTime(a.updated_at));
|
|
2688
|
+
|
|
2689
|
+
const statusCount = {
|
|
2690
|
+
open: normalizedItems.filter((row) => row.status === "open").length,
|
|
2691
|
+
review: normalizedItems.filter((row) => row.status === "review").length,
|
|
2692
|
+
approved: normalizedItems.filter((row) => row.status === "approved").length,
|
|
2693
|
+
rejected: normalizedItems.filter((row) => row.status === "rejected").length,
|
|
2694
|
+
merged: normalizedItems.filter((row) => row.status === "merged").length,
|
|
2695
|
+
closed: normalizedItems.filter((row) => row.status === "closed").length,
|
|
2696
|
+
};
|
|
2697
|
+
|
|
2698
|
+
const approvedCandidates = normalizedItems.filter((row) => row.status === "approved");
|
|
2699
|
+
const reviewCandidates = normalizedItems.filter((row) => row.status === "review" || row.status === "open");
|
|
2700
|
+
let recommendationAction = "no_action";
|
|
2701
|
+
let recommendationReason = "No pending merge requests.";
|
|
2702
|
+
let ownerPrompt = "";
|
|
2703
|
+
if (!mergeFeatureAvailable) {
|
|
2704
|
+
recommendationAction = "merge_api_unavailable";
|
|
2705
|
+
recommendationReason = mergeFeatureWarning;
|
|
2706
|
+
ownerPrompt =
|
|
2707
|
+
"Ask platform owner/admin to enable ctxpack merge-request API before merge approval workflow.";
|
|
2708
|
+
} else if (approvedCandidates.length > 0) {
|
|
2709
|
+
recommendationAction = ownerGate ? "merge_ready" : "owner_approval_required";
|
|
2710
|
+
recommendationReason =
|
|
2711
|
+
approvedCandidates.length === 1
|
|
2712
|
+
? "There is 1 approved merge request ready for merge."
|
|
2713
|
+
: `There are ${approvedCandidates.length} approved merge requests ready for merge.`;
|
|
2714
|
+
const ids = approvedCandidates.slice(0, 5).map((row) => row.merge_request_id).filter(Boolean);
|
|
2715
|
+
ownerPrompt = `Ask owner decision: merge now for ${ids.join(", ")} ?`;
|
|
2716
|
+
} else if (reviewCandidates.length > 0) {
|
|
2717
|
+
recommendationAction = ownerGate ? "review_then_approve" : "owner_review_required";
|
|
2718
|
+
recommendationReason =
|
|
2719
|
+
reviewCandidates.length === 1
|
|
2720
|
+
? "There is 1 merge request waiting for review/approval."
|
|
2721
|
+
: `There are ${reviewCandidates.length} merge requests waiting for review/approval.`;
|
|
2722
|
+
const ids = reviewCandidates.slice(0, 5).map((row) => row.merge_request_id).filter(Boolean);
|
|
2723
|
+
ownerPrompt = `Ask owner decision: approve or reject ${ids.join(", ")} ?`;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
return {
|
|
2727
|
+
project_id: projectID,
|
|
2728
|
+
access: "granted",
|
|
2729
|
+
status_code: 200,
|
|
2730
|
+
message: mergeFeatureAvailable
|
|
2731
|
+
? "Ctxpack merge briefing is ready."
|
|
2732
|
+
: "Ctxpack merge briefing is ready, but merge-request API is unavailable on this server.",
|
|
2733
|
+
merge_feature_available: mergeFeatureAvailable,
|
|
2734
|
+
merge_feature_warning: mergeFeatureWarning,
|
|
2735
|
+
owner_gate: {
|
|
2736
|
+
actor_user_id: actor.user_id,
|
|
2737
|
+
actor_email: actor.email,
|
|
2738
|
+
actor_name: actor.name,
|
|
2739
|
+
project_owner_user_id: String(summary.owner_user_id || "").trim(),
|
|
2740
|
+
org_owner_user_id: orgOwnerUserID,
|
|
2741
|
+
is_project_owner: isProjectOwner,
|
|
2742
|
+
is_org_owner: isOrgOwner,
|
|
2743
|
+
can_owner_finalize: ownerGate,
|
|
2744
|
+
},
|
|
2745
|
+
status_filter: normalizedStatus,
|
|
2746
|
+
merge_request_count: normalizedItems.length,
|
|
2747
|
+
status_count: statusCount,
|
|
2748
|
+
merge_requests: normalizedItems,
|
|
2749
|
+
recommendation: {
|
|
2750
|
+
action: recommendationAction,
|
|
2751
|
+
reason: recommendationReason,
|
|
2752
|
+
owner_prompt: ownerPrompt,
|
|
2753
|
+
owner_confirmation_required: true,
|
|
2754
|
+
},
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
function buildCtxpackMergeBriefText(brief) {
|
|
2759
|
+
if (String(brief?.access || "") !== "granted") {
|
|
2760
|
+
return [
|
|
2761
|
+
`Project ID: ${brief?.project_id || "-"}`,
|
|
2762
|
+
`Access: ${brief?.access || "error"}`,
|
|
2763
|
+
`Status: ${brief?.status_code || "-"}`,
|
|
2764
|
+
`${brief?.message || "Unable to load ctxpack merge briefing."}`,
|
|
2765
|
+
].join("\n");
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
const lines = [
|
|
2769
|
+
`Project ID: ${brief.project_id || "-"}`,
|
|
2770
|
+
`Merge Requests: ${Number(brief.merge_request_count || 0)}`,
|
|
2771
|
+
`Status Filter: ${brief.status_filter || "open"}`,
|
|
2772
|
+
`Owner Gate: ${brief.owner_gate?.can_owner_finalize ? "owner-confirmation possible" : "owner token required"}`,
|
|
2773
|
+
"",
|
|
2774
|
+
"Status Summary:",
|
|
2775
|
+
`- Open: ${Number(brief.status_count?.open || 0)}`,
|
|
2776
|
+
`- Review: ${Number(brief.status_count?.review || 0)}`,
|
|
2777
|
+
`- Approved: ${Number(brief.status_count?.approved || 0)}`,
|
|
2778
|
+
`- Rejected: ${Number(brief.status_count?.rejected || 0)}`,
|
|
2779
|
+
`- Merged: ${Number(brief.status_count?.merged || 0)}`,
|
|
2780
|
+
`- Closed: ${Number(brief.status_count?.closed || 0)}`,
|
|
2781
|
+
];
|
|
2782
|
+
if (brief.merge_feature_available === false) {
|
|
2783
|
+
lines.push("");
|
|
2784
|
+
lines.push("Merge API:");
|
|
2785
|
+
lines.push(`- ${brief.merge_feature_warning || "Merge-request API is unavailable on this server."}`);
|
|
2786
|
+
}
|
|
2787
|
+
lines.push("");
|
|
2788
|
+
lines.push("Recommendation:");
|
|
2789
|
+
lines.push(`- Action: ${brief.recommendation?.action || "no_action"}`);
|
|
2790
|
+
lines.push(`- Reason: ${brief.recommendation?.reason || "-"}`);
|
|
2791
|
+
const ownerPrompt = String(brief.recommendation?.owner_prompt || "").trim();
|
|
2792
|
+
if (ownerPrompt) {
|
|
2793
|
+
lines.push(`- Owner Prompt: ${ownerPrompt}`);
|
|
2794
|
+
}
|
|
2795
|
+
const requests = ensureArray(brief.merge_requests);
|
|
2796
|
+
if (requests.length > 0) {
|
|
2797
|
+
lines.push("");
|
|
2798
|
+
lines.push("Pending Details:");
|
|
2799
|
+
for (const row of requests.slice(0, 20)) {
|
|
2800
|
+
const changedPreview =
|
|
2801
|
+
row.changed_paths_count > 0 ? `${row.changed_paths_count} path(s)` : "no changed_paths metadata";
|
|
2802
|
+
const changedList =
|
|
2803
|
+
row.changed_paths_count > 0 ? ` [${ensureArray(row.changed_paths).slice(0, 5).join(", ")}]` : "";
|
|
2804
|
+
lines.push(
|
|
2805
|
+
`- ${row.merge_request_id} | ${row.status} | by ${row.created_by || row.created_by_user_id || "-"} | ${changedPreview}${changedList}`,
|
|
2806
|
+
);
|
|
2807
|
+
if (row.metadata_summary) {
|
|
2808
|
+
lines.push(` summary: ${row.metadata_summary}`);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
lines.push("");
|
|
2813
|
+
lines.push(
|
|
2814
|
+
"Before execute, ask owner for explicit confirmation, then run ctxpack.merge.execute with owner_confirmation.",
|
|
2815
|
+
);
|
|
2816
|
+
return lines.join("\n");
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
async function executeCtxpackMergeActionForTool({
|
|
2820
|
+
siteBaseURL,
|
|
2821
|
+
projectID,
|
|
2822
|
+
mergeRequestID,
|
|
2823
|
+
action,
|
|
2824
|
+
ownerConfirmation,
|
|
2825
|
+
token,
|
|
2826
|
+
timeoutSeconds,
|
|
2827
|
+
workspaceDir,
|
|
2828
|
+
}) {
|
|
2829
|
+
if (!isOwnerConfirmationAccepted(ownerConfirmation)) {
|
|
2830
|
+
return {
|
|
2831
|
+
project_id: projectID,
|
|
2832
|
+
merge_request_id: mergeRequestID,
|
|
2833
|
+
action,
|
|
2834
|
+
ok: false,
|
|
2835
|
+
status_code: 400,
|
|
2836
|
+
message: "Owner confirmation missing. Ask owner and pass owner_confirmation (confirm/approved/yes).",
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
const summary = await loadProjectSummaryForTool({
|
|
2841
|
+
siteBaseURL,
|
|
2842
|
+
projectID,
|
|
2843
|
+
token,
|
|
2844
|
+
timeoutSeconds,
|
|
2845
|
+
includeCtxpack: false,
|
|
2846
|
+
syncCtxpackLocal: false,
|
|
2847
|
+
workspaceDir,
|
|
2848
|
+
});
|
|
2849
|
+
if (String(summary.access || "") !== "granted") {
|
|
2850
|
+
return {
|
|
2851
|
+
project_id: projectID,
|
|
2852
|
+
merge_request_id: mergeRequestID,
|
|
2853
|
+
action,
|
|
2854
|
+
ok: false,
|
|
2855
|
+
status_code: summary.status_code || 500,
|
|
2856
|
+
message: summary.message || "Project access denied.",
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
const actor = extractActorFromToken(token);
|
|
2861
|
+
const orgOwnerUserID = await loadOrgOwnerUserID({
|
|
2862
|
+
siteBaseURL,
|
|
2863
|
+
orgID: summary.org_id,
|
|
2864
|
+
token,
|
|
2865
|
+
timeoutSeconds,
|
|
2866
|
+
});
|
|
2867
|
+
const projectOwnerIDs = new Set(
|
|
2868
|
+
[summary.owner_user_id, summary.program_owner_user_id, summary.tech_owner_user_id, summary.governance_owner_user_id]
|
|
2869
|
+
.map((value) => String(value || "").trim())
|
|
2870
|
+
.filter(Boolean),
|
|
2871
|
+
);
|
|
2872
|
+
const isProjectOwner = actor.user_id ? projectOwnerIDs.has(actor.user_id) : false;
|
|
2873
|
+
const isOrgOwner = Boolean(actor.user_id && orgOwnerUserID && actor.user_id === orgOwnerUserID);
|
|
2874
|
+
if (!(isProjectOwner || isOrgOwner)) {
|
|
2875
|
+
return {
|
|
2876
|
+
project_id: projectID,
|
|
2877
|
+
merge_request_id: mergeRequestID,
|
|
2878
|
+
action,
|
|
2879
|
+
ok: false,
|
|
2880
|
+
status_code: 403,
|
|
2881
|
+
message: "Owner gate blocked: current token is not project/org owner. Ask owner account to execute.",
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
const encodedProjectID = encodeURIComponent(projectID);
|
|
2886
|
+
const encodedMRID = encodeURIComponent(mergeRequestID);
|
|
2887
|
+
const endpoint = `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack/merge-requests/${encodedMRID}/${action}`;
|
|
2888
|
+
|
|
2889
|
+
try {
|
|
2890
|
+
const responseText = await postJSON(endpoint, timeoutSeconds, token, {});
|
|
2891
|
+
const parsed = tryJsonParse(responseText) || {};
|
|
2892
|
+
return {
|
|
2893
|
+
project_id: projectID,
|
|
2894
|
+
merge_request_id: mergeRequestID,
|
|
2895
|
+
action,
|
|
2896
|
+
ok: true,
|
|
2897
|
+
status_code: 200,
|
|
2898
|
+
message: `Merge request action '${action}' completed.`,
|
|
2899
|
+
actor_user_id: actor.user_id,
|
|
2900
|
+
actor_email: actor.email,
|
|
2901
|
+
response: safeObject(parsed),
|
|
2902
|
+
};
|
|
2903
|
+
} catch (err) {
|
|
2904
|
+
const statusCode = Number(err?.statusCode || 0) || 500;
|
|
2905
|
+
const bodyText = String(err?.responseBody || err?.message || "").trim();
|
|
2906
|
+
let message = bodyText || `Failed to execute action '${action}'.`;
|
|
2907
|
+
if (statusCode === 403) {
|
|
2908
|
+
message = "Forbidden by server policy/role. Current account cannot execute this action.";
|
|
2909
|
+
} else if (statusCode === 409) {
|
|
2910
|
+
message =
|
|
2911
|
+
"Merge conflict or approval state mismatch. Refresh briefing (ctxpack.merge.brief), then re-review before merge.";
|
|
2912
|
+
} else if (statusCode === 404) {
|
|
2913
|
+
message = "Merge request not found for this project.";
|
|
2914
|
+
}
|
|
2915
|
+
return {
|
|
2916
|
+
project_id: projectID,
|
|
2917
|
+
merge_request_id: mergeRequestID,
|
|
2918
|
+
action,
|
|
2919
|
+
ok: false,
|
|
2920
|
+
status_code: statusCode,
|
|
2921
|
+
message,
|
|
2922
|
+
error: bodyText,
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
function buildCtxpackMergeExecuteText(result) {
|
|
2928
|
+
const lines = [
|
|
2929
|
+
`Project ID: ${result.project_id || "-"}`,
|
|
2930
|
+
`Merge Request ID: ${result.merge_request_id || "-"}`,
|
|
2931
|
+
`Action: ${result.action || "-"}`,
|
|
2932
|
+
`Result: ${result.ok ? "ok" : "failed"} (status ${result.status_code || "-"})`,
|
|
2933
|
+
`Message: ${result.message || "-"}`,
|
|
2934
|
+
];
|
|
2935
|
+
if (result.ok) {
|
|
2936
|
+
const mergedVersionID = String(result.response?.merged_version_id || "").trim();
|
|
2937
|
+
if (mergedVersionID) {
|
|
2938
|
+
lines.push(`Merged Version ID: ${mergedVersionID}`);
|
|
2939
|
+
}
|
|
2940
|
+
} else if (result.error) {
|
|
2941
|
+
lines.push(`Server: ${result.error}`);
|
|
2942
|
+
}
|
|
2943
|
+
return lines.join("\n");
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2258
2946
|
function buildProjectSummaryText(summary) {
|
|
2259
2947
|
if (String(summary?.access || "") !== "granted") {
|
|
2260
2948
|
return [
|
|
@@ -2327,6 +3015,7 @@ function appendProjectHintToInitialize(responseObj, args) {
|
|
|
2327
3015
|
"- MUST call `project.summary` first when the user provides only a Project ID or asks project overview/agenda.",
|
|
2328
3016
|
"- `project.describe` and `project.get` are aliases of `project.summary`.",
|
|
2329
3017
|
"- After project summary, use workitem/evidence/decision tools as follow-up.",
|
|
3018
|
+
"- Before any ctxpack merge, call `ctxpack.merge.brief` first, share recommendation to owner, get explicit owner confirmation, then call `ctxpack.merge.execute`.",
|
|
2330
3019
|
];
|
|
2331
3020
|
if (args.projectID) {
|
|
2332
3021
|
hintLines.splice(1, 0, `- Default project_id is ${args.projectID}.`);
|
|
@@ -2348,14 +3037,14 @@ async function maybeAutoSyncCtxpackForCall({
|
|
|
2348
3037
|
}) {
|
|
2349
3038
|
if (!isJsonRpcMethod(requestObj, "tools/call")) return null;
|
|
2350
3039
|
if (!token) return null;
|
|
2351
|
-
if (LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) return null;
|
|
3040
|
+
if (LOCAL_PROJECT_TOOL_NAMES.includes(toolName) || LOCAL_CTXPACK_MERGE_TOOL_NAMES.includes(toolName)) return null;
|
|
2352
3041
|
if (String(toolName || "").trim().toLowerCase() === "ctxpack.ensure") return null;
|
|
2353
3042
|
|
|
2354
|
-
const projectID =
|
|
2355
|
-
toolArgs
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
3043
|
+
const projectID = resolveProjectIDForRequest({
|
|
3044
|
+
toolArgs,
|
|
3045
|
+
args,
|
|
3046
|
+
workspaceDir,
|
|
3047
|
+
});
|
|
2359
3048
|
if (!isUUID(projectID)) return null;
|
|
2360
3049
|
|
|
2361
3050
|
const workspacePath = resolveWorkspaceDir(workspaceDir || process.cwd());
|
|
@@ -2448,7 +3137,12 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
|
|
|
2448
3137
|
}
|
|
2449
3138
|
|
|
2450
3139
|
const projectID = String(
|
|
2451
|
-
|
|
3140
|
+
resolveProjectIDForRequest({
|
|
3141
|
+
toolArgs,
|
|
3142
|
+
args,
|
|
3143
|
+
workspaceDir: args.workspaceDir,
|
|
3144
|
+
responseProjectID,
|
|
3145
|
+
}),
|
|
2452
3146
|
).trim();
|
|
2453
3147
|
|
|
2454
3148
|
if (projectID && isUUID(projectID)) {
|
|
@@ -2517,7 +3211,12 @@ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs, requestObj) {
|
|
|
2517
3211
|
if (!Object.keys(body).length) return responseObj;
|
|
2518
3212
|
|
|
2519
3213
|
const projectID = String(
|
|
2520
|
-
|
|
3214
|
+
resolveProjectIDForRequest({
|
|
3215
|
+
toolArgs,
|
|
3216
|
+
args,
|
|
3217
|
+
workspaceDir: args.workspaceDir,
|
|
3218
|
+
envelopeProjectID: envelope.project_id,
|
|
3219
|
+
}),
|
|
2521
3220
|
).trim();
|
|
2522
3221
|
if (!projectID || !isUUID(projectID)) return responseObj;
|
|
2523
3222
|
const workspaceDir = resolveWorkspaceDirForRequest(args.workspaceDir || process.cwd(), requestObj, toolArgs);
|
|
@@ -2632,13 +3331,15 @@ async function injectCtxpackPreflightToken(requestObj, toolName, toolArgs, args,
|
|
|
2632
3331
|
return requestObj;
|
|
2633
3332
|
}
|
|
2634
3333
|
|
|
2635
|
-
const
|
|
2636
|
-
|
|
2637
|
-
rawArgs.projectID,
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
3334
|
+
const mergedToolArgs = {
|
|
3335
|
+
...safeObject(toolArgs),
|
|
3336
|
+
project_id: firstNonEmptyString([rawArgs.project_id, rawArgs.projectID, toolArgs?.project_id, toolArgs?.projectID]),
|
|
3337
|
+
};
|
|
3338
|
+
const projectID = resolveProjectIDForRequest({
|
|
3339
|
+
toolArgs: mergedToolArgs,
|
|
3340
|
+
args,
|
|
3341
|
+
workspaceDir,
|
|
3342
|
+
});
|
|
2642
3343
|
if (!isUUID(projectID)) {
|
|
2643
3344
|
return requestObj;
|
|
2644
3345
|
}
|
|
@@ -2728,12 +3429,13 @@ async function appendCtxpackConflictHintToErrorResponse(responseObj, args, toolN
|
|
|
2728
3429
|
}
|
|
2729
3430
|
|
|
2730
3431
|
async function runProxy(flags) {
|
|
2731
|
-
const
|
|
3432
|
+
const explicitWorkspaceDirRaw = String(flags["workspace-dir"] || "").trim();
|
|
3433
|
+
const explicitWorkspaceDir = explicitWorkspaceDirRaw ? resolveWorkspaceDir(explicitWorkspaceDirRaw) : "";
|
|
2732
3434
|
const args = {
|
|
2733
3435
|
baseURL: flags["base-url"] || DEFAULT_BASE_URL,
|
|
2734
|
-
projectID: String(flags["project-id"] ||
|
|
2735
|
-
ctxpackKey: String(flags["ctxpack-key"] ||
|
|
2736
|
-
workspaceDir:
|
|
3436
|
+
projectID: String(flags["project-id"] || "").trim(),
|
|
3437
|
+
ctxpackKey: String(flags["ctxpack-key"] || "").trim(),
|
|
3438
|
+
workspaceDir: explicitWorkspaceDir,
|
|
2737
3439
|
includeDrafts: boolFromRaw(flags["include-drafts"], true),
|
|
2738
3440
|
autoPullOnConflict: boolFromRaw(flags["auto-pull-on-conflict"], true),
|
|
2739
3441
|
timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
|
|
@@ -2800,12 +3502,16 @@ async function runProxy(flags) {
|
|
|
2800
3502
|
|
|
2801
3503
|
const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
|
|
2802
3504
|
const requestWorkspaceCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
|
|
3505
|
+
const envWorkspaceCandidate = extractWorkspaceCandidateFromEnv();
|
|
2803
3506
|
if (requestWorkspaceCandidate) {
|
|
2804
3507
|
sessionWorkspaceDir = requestWorkspaceCandidate;
|
|
3508
|
+
} else if (envWorkspaceCandidate) {
|
|
3509
|
+
sessionWorkspaceDir = envWorkspaceCandidate;
|
|
2805
3510
|
}
|
|
2806
3511
|
const requestWorkspaceDir = resolveWorkspaceDir(
|
|
2807
3512
|
firstNonEmptyString([
|
|
2808
3513
|
requestWorkspaceCandidate,
|
|
3514
|
+
envWorkspaceCandidate,
|
|
2809
3515
|
sessionWorkspaceDir,
|
|
2810
3516
|
args.workspaceDir,
|
|
2811
3517
|
process.cwd(),
|
|
@@ -2823,7 +3529,13 @@ async function runProxy(flags) {
|
|
|
2823
3529
|
});
|
|
2824
3530
|
}
|
|
2825
3531
|
if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) {
|
|
2826
|
-
const projectID = String(
|
|
3532
|
+
const projectID = String(
|
|
3533
|
+
resolveProjectIDForRequest({
|
|
3534
|
+
toolArgs,
|
|
3535
|
+
args,
|
|
3536
|
+
workspaceDir: requestWorkspaceDir,
|
|
3537
|
+
}),
|
|
3538
|
+
).trim();
|
|
2827
3539
|
if (!projectID) {
|
|
2828
3540
|
process.stdout.write(
|
|
2829
3541
|
`${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
|
|
@@ -2879,6 +3591,113 @@ async function runProxy(flags) {
|
|
|
2879
3591
|
}
|
|
2880
3592
|
return;
|
|
2881
3593
|
}
|
|
3594
|
+
if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_CTXPACK_MERGE_TOOL_NAMES.includes(toolName)) {
|
|
3595
|
+
const projectID = String(
|
|
3596
|
+
resolveProjectIDForRequest({
|
|
3597
|
+
toolArgs,
|
|
3598
|
+
args,
|
|
3599
|
+
workspaceDir: requestWorkspaceDir,
|
|
3600
|
+
}),
|
|
3601
|
+
).trim();
|
|
3602
|
+
if (!projectID) {
|
|
3603
|
+
process.stdout.write(
|
|
3604
|
+
`${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
|
|
3605
|
+
);
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
if (!isUUID(projectID)) {
|
|
3609
|
+
process.stdout.write(
|
|
3610
|
+
`${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"))}\n`,
|
|
3611
|
+
);
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
try {
|
|
3616
|
+
if (toolName === "ctxpack.merge.brief") {
|
|
3617
|
+
const statusRaw = String(toolArgs.status || "").trim().toLowerCase();
|
|
3618
|
+
const statusFilter = normalizeMergeStatusFilter(statusRaw);
|
|
3619
|
+
if (statusRaw && !statusFilter) {
|
|
3620
|
+
process.stdout.write(
|
|
3621
|
+
`${JSON.stringify(
|
|
3622
|
+
jsonRpcError(
|
|
3623
|
+
requestObj,
|
|
3624
|
+
-32001,
|
|
3625
|
+
"status must be one of: open, review, approved, rejected, merged, closed, all",
|
|
3626
|
+
),
|
|
3627
|
+
)}\n`,
|
|
3628
|
+
);
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
const limit = Number.parseInt(String(toolArgs.limit || 20), 10) || 20;
|
|
3632
|
+
const brief = await loadCtxpackMergeBriefForTool({
|
|
3633
|
+
siteBaseURL: normalizeSiteBaseURL(args.baseURL),
|
|
3634
|
+
projectID,
|
|
3635
|
+
token,
|
|
3636
|
+
timeoutSeconds: args.timeoutSeconds,
|
|
3637
|
+
statusFilter: statusFilter || "open",
|
|
3638
|
+
limit,
|
|
3639
|
+
workspaceDir: requestWorkspaceDir,
|
|
3640
|
+
});
|
|
3641
|
+
const text = buildCtxpackMergeBriefText(brief);
|
|
3642
|
+
process.stdout.write(
|
|
3643
|
+
`${JSON.stringify(
|
|
3644
|
+
jsonRpcResult(requestObj, {
|
|
3645
|
+
content: [{ type: "text", text }],
|
|
3646
|
+
structuredContent: brief,
|
|
3647
|
+
}),
|
|
3648
|
+
)}\n`,
|
|
3649
|
+
);
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
if (toolName === "ctxpack.merge.execute") {
|
|
3654
|
+
const mergeRequestID = String(toolArgs.merge_request_id || toolArgs.mergeRequestID || "").trim();
|
|
3655
|
+
const action = normalizeMergeAction(toolArgs.action);
|
|
3656
|
+
const ownerConfirmation = String(
|
|
3657
|
+
toolArgs.owner_confirmation || toolArgs.ownerConfirmation || "",
|
|
3658
|
+
).trim();
|
|
3659
|
+
if (!mergeRequestID || !isUUID(mergeRequestID)) {
|
|
3660
|
+
process.stdout.write(
|
|
3661
|
+
`${JSON.stringify(jsonRpcError(requestObj, -32001, "merge_request_id must be a valid UUID"))}\n`,
|
|
3662
|
+
);
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
if (!action) {
|
|
3666
|
+
process.stdout.write(
|
|
3667
|
+
`${JSON.stringify(
|
|
3668
|
+
jsonRpcError(requestObj, -32001, "action must be one of: review, approve, reject, close, merge"),
|
|
3669
|
+
)}\n`,
|
|
3670
|
+
);
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
const result = await executeCtxpackMergeActionForTool({
|
|
3674
|
+
siteBaseURL: normalizeSiteBaseURL(args.baseURL),
|
|
3675
|
+
projectID,
|
|
3676
|
+
mergeRequestID,
|
|
3677
|
+
action,
|
|
3678
|
+
ownerConfirmation,
|
|
3679
|
+
token,
|
|
3680
|
+
timeoutSeconds: args.timeoutSeconds,
|
|
3681
|
+
workspaceDir: requestWorkspaceDir,
|
|
3682
|
+
});
|
|
3683
|
+
const text = buildCtxpackMergeExecuteText(result);
|
|
3684
|
+
process.stdout.write(
|
|
3685
|
+
`${JSON.stringify(
|
|
3686
|
+
jsonRpcResult(requestObj, {
|
|
3687
|
+
content: [{ type: "text", text }],
|
|
3688
|
+
structuredContent: result,
|
|
3689
|
+
}),
|
|
3690
|
+
)}\n`,
|
|
3691
|
+
);
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
} catch (err) {
|
|
3695
|
+
process.stdout.write(
|
|
3696
|
+
`${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
|
|
3697
|
+
);
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
2882
3701
|
|
|
2883
3702
|
try {
|
|
2884
3703
|
const requestWithDefaults = injectCtxpackPushDefaults(requestObj, toolName, requestWorkspaceDir);
|