metheus-governance-mcp-cli 0.2.9 → 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 +795 -21
- 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) {
|
|
@@ -1846,7 +1897,10 @@ function postJSON(urlText, timeoutSeconds, token, payload) {
|
|
|
1846
1897
|
resolve(text.trim());
|
|
1847
1898
|
return;
|
|
1848
1899
|
}
|
|
1849
|
-
|
|
1900
|
+
const err = new Error(text.trim() || `http ${statusCode}`);
|
|
1901
|
+
err.statusCode = statusCode;
|
|
1902
|
+
err.responseBody = text;
|
|
1903
|
+
reject(err);
|
|
1850
1904
|
});
|
|
1851
1905
|
},
|
|
1852
1906
|
);
|
|
@@ -1932,6 +1986,7 @@ function jsonRpcResult(requestObj, result) {
|
|
|
1932
1986
|
}
|
|
1933
1987
|
|
|
1934
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"];
|
|
1935
1990
|
|
|
1936
1991
|
function buildProjectSummaryInputSchema() {
|
|
1937
1992
|
return {
|
|
@@ -1955,6 +2010,62 @@ function buildProjectSummaryInputSchema() {
|
|
|
1955
2010
|
};
|
|
1956
2011
|
}
|
|
1957
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
|
+
|
|
1958
2069
|
function buildLocalToolSpecs() {
|
|
1959
2070
|
return [
|
|
1960
2071
|
{
|
|
@@ -1975,6 +2086,18 @@ function buildLocalToolSpecs() {
|
|
|
1975
2086
|
"Alias of project.summary. Returns project metadata and access status for the given project_id.",
|
|
1976
2087
|
inputSchema: buildProjectSummaryInputSchema(),
|
|
1977
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
|
+
},
|
|
1978
2101
|
];
|
|
1979
2102
|
}
|
|
1980
2103
|
|
|
@@ -2278,6 +2401,10 @@ async function loadProjectSummaryForTool({
|
|
|
2278
2401
|
visibility: String(project.visibility || "").trim() || "private",
|
|
2279
2402
|
template: String(project.template || "").trim() || "blank",
|
|
2280
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(),
|
|
2281
2408
|
owner_name: String(project.owner_name || "").trim(),
|
|
2282
2409
|
program_owner_name: String(project.program_owner_name || "").trim(),
|
|
2283
2410
|
tech_owner_name: String(project.tech_owner_name || "").trim(),
|
|
@@ -2300,6 +2427,522 @@ async function loadProjectSummaryForTool({
|
|
|
2300
2427
|
};
|
|
2301
2428
|
}
|
|
2302
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
|
+
|
|
2303
2946
|
function buildProjectSummaryText(summary) {
|
|
2304
2947
|
if (String(summary?.access || "") !== "granted") {
|
|
2305
2948
|
return [
|
|
@@ -2372,6 +3015,7 @@ function appendProjectHintToInitialize(responseObj, args) {
|
|
|
2372
3015
|
"- MUST call `project.summary` first when the user provides only a Project ID or asks project overview/agenda.",
|
|
2373
3016
|
"- `project.describe` and `project.get` are aliases of `project.summary`.",
|
|
2374
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`.",
|
|
2375
3019
|
];
|
|
2376
3020
|
if (args.projectID) {
|
|
2377
3021
|
hintLines.splice(1, 0, `- Default project_id is ${args.projectID}.`);
|
|
@@ -2393,14 +3037,14 @@ async function maybeAutoSyncCtxpackForCall({
|
|
|
2393
3037
|
}) {
|
|
2394
3038
|
if (!isJsonRpcMethod(requestObj, "tools/call")) return null;
|
|
2395
3039
|
if (!token) return null;
|
|
2396
|
-
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;
|
|
2397
3041
|
if (String(toolName || "").trim().toLowerCase() === "ctxpack.ensure") return null;
|
|
2398
3042
|
|
|
2399
|
-
const projectID =
|
|
2400
|
-
toolArgs
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
3043
|
+
const projectID = resolveProjectIDForRequest({
|
|
3044
|
+
toolArgs,
|
|
3045
|
+
args,
|
|
3046
|
+
workspaceDir,
|
|
3047
|
+
});
|
|
2404
3048
|
if (!isUUID(projectID)) return null;
|
|
2405
3049
|
|
|
2406
3050
|
const workspacePath = resolveWorkspaceDir(workspaceDir || process.cwd());
|
|
@@ -2493,7 +3137,12 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
|
|
|
2493
3137
|
}
|
|
2494
3138
|
|
|
2495
3139
|
const projectID = String(
|
|
2496
|
-
|
|
3140
|
+
resolveProjectIDForRequest({
|
|
3141
|
+
toolArgs,
|
|
3142
|
+
args,
|
|
3143
|
+
workspaceDir: args.workspaceDir,
|
|
3144
|
+
responseProjectID,
|
|
3145
|
+
}),
|
|
2497
3146
|
).trim();
|
|
2498
3147
|
|
|
2499
3148
|
if (projectID && isUUID(projectID)) {
|
|
@@ -2562,7 +3211,12 @@ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs, requestObj) {
|
|
|
2562
3211
|
if (!Object.keys(body).length) return responseObj;
|
|
2563
3212
|
|
|
2564
3213
|
const projectID = String(
|
|
2565
|
-
|
|
3214
|
+
resolveProjectIDForRequest({
|
|
3215
|
+
toolArgs,
|
|
3216
|
+
args,
|
|
3217
|
+
workspaceDir: args.workspaceDir,
|
|
3218
|
+
envelopeProjectID: envelope.project_id,
|
|
3219
|
+
}),
|
|
2566
3220
|
).trim();
|
|
2567
3221
|
if (!projectID || !isUUID(projectID)) return responseObj;
|
|
2568
3222
|
const workspaceDir = resolveWorkspaceDirForRequest(args.workspaceDir || process.cwd(), requestObj, toolArgs);
|
|
@@ -2677,13 +3331,15 @@ async function injectCtxpackPreflightToken(requestObj, toolName, toolArgs, args,
|
|
|
2677
3331
|
return requestObj;
|
|
2678
3332
|
}
|
|
2679
3333
|
|
|
2680
|
-
const
|
|
2681
|
-
|
|
2682
|
-
rawArgs.projectID,
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
+
});
|
|
2687
3343
|
if (!isUUID(projectID)) {
|
|
2688
3344
|
return requestObj;
|
|
2689
3345
|
}
|
|
@@ -2773,12 +3429,13 @@ async function appendCtxpackConflictHintToErrorResponse(responseObj, args, toolN
|
|
|
2773
3429
|
}
|
|
2774
3430
|
|
|
2775
3431
|
async function runProxy(flags) {
|
|
2776
|
-
const
|
|
3432
|
+
const explicitWorkspaceDirRaw = String(flags["workspace-dir"] || "").trim();
|
|
3433
|
+
const explicitWorkspaceDir = explicitWorkspaceDirRaw ? resolveWorkspaceDir(explicitWorkspaceDirRaw) : "";
|
|
2777
3434
|
const args = {
|
|
2778
3435
|
baseURL: flags["base-url"] || DEFAULT_BASE_URL,
|
|
2779
|
-
projectID: String(flags["project-id"] ||
|
|
2780
|
-
ctxpackKey: String(flags["ctxpack-key"] ||
|
|
2781
|
-
workspaceDir:
|
|
3436
|
+
projectID: String(flags["project-id"] || "").trim(),
|
|
3437
|
+
ctxpackKey: String(flags["ctxpack-key"] || "").trim(),
|
|
3438
|
+
workspaceDir: explicitWorkspaceDir,
|
|
2782
3439
|
includeDrafts: boolFromRaw(flags["include-drafts"], true),
|
|
2783
3440
|
autoPullOnConflict: boolFromRaw(flags["auto-pull-on-conflict"], true),
|
|
2784
3441
|
timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
|
|
@@ -2845,12 +3502,16 @@ async function runProxy(flags) {
|
|
|
2845
3502
|
|
|
2846
3503
|
const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
|
|
2847
3504
|
const requestWorkspaceCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
|
|
3505
|
+
const envWorkspaceCandidate = extractWorkspaceCandidateFromEnv();
|
|
2848
3506
|
if (requestWorkspaceCandidate) {
|
|
2849
3507
|
sessionWorkspaceDir = requestWorkspaceCandidate;
|
|
3508
|
+
} else if (envWorkspaceCandidate) {
|
|
3509
|
+
sessionWorkspaceDir = envWorkspaceCandidate;
|
|
2850
3510
|
}
|
|
2851
3511
|
const requestWorkspaceDir = resolveWorkspaceDir(
|
|
2852
3512
|
firstNonEmptyString([
|
|
2853
3513
|
requestWorkspaceCandidate,
|
|
3514
|
+
envWorkspaceCandidate,
|
|
2854
3515
|
sessionWorkspaceDir,
|
|
2855
3516
|
args.workspaceDir,
|
|
2856
3517
|
process.cwd(),
|
|
@@ -2868,7 +3529,13 @@ async function runProxy(flags) {
|
|
|
2868
3529
|
});
|
|
2869
3530
|
}
|
|
2870
3531
|
if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) {
|
|
2871
|
-
const projectID = String(
|
|
3532
|
+
const projectID = String(
|
|
3533
|
+
resolveProjectIDForRequest({
|
|
3534
|
+
toolArgs,
|
|
3535
|
+
args,
|
|
3536
|
+
workspaceDir: requestWorkspaceDir,
|
|
3537
|
+
}),
|
|
3538
|
+
).trim();
|
|
2872
3539
|
if (!projectID) {
|
|
2873
3540
|
process.stdout.write(
|
|
2874
3541
|
`${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
|
|
@@ -2924,6 +3591,113 @@ async function runProxy(flags) {
|
|
|
2924
3591
|
}
|
|
2925
3592
|
return;
|
|
2926
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
|
+
}
|
|
2927
3701
|
|
|
2928
3702
|
try {
|
|
2929
3703
|
const requestWithDefaults = injectCtxpackPushDefaults(requestObj, toolName, requestWorkspaceDir);
|