metheus-governance-mcp-cli 0.2.9 → 0.2.11
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 +827 -43
- 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,62 @@ function firstNonEmptyString(values) {
|
|
|
140
140
|
return "";
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function isEditorInstallDirectory(candidatePath) {
|
|
144
|
+
const normalized = String(candidatePath || "").replace(/\//g, "\\").toLowerCase();
|
|
145
|
+
if (!normalized) return false;
|
|
146
|
+
// Guard against runtime cwd/env resolving to editor installation directory
|
|
147
|
+
// instead of the actual opened workspace.
|
|
148
|
+
if (normalized.includes("\\appdata\\local\\programs\\microsoft vs code")) return true;
|
|
149
|
+
if (normalized.includes("\\program files\\microsoft vs code")) return true;
|
|
150
|
+
if (normalized.includes("\\program files (x86)\\microsoft vs code")) return true;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sanitizeWorkspaceCandidate(rawCandidate) {
|
|
155
|
+
const fileCandidate = fileURIToLocalPath(rawCandidate);
|
|
156
|
+
const candidate = firstNonEmptyString([fileCandidate, rawCandidate]);
|
|
157
|
+
if (!candidate) return "";
|
|
158
|
+
const resolved = resolveWorkspaceDir(candidate);
|
|
159
|
+
if (!resolved) return "";
|
|
160
|
+
if (isEditorInstallDirectory(resolved)) return "";
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function extractWorkspaceCandidateFromFolders(rawFolders) {
|
|
165
|
+
if (!Array.isArray(rawFolders)) return "";
|
|
166
|
+
for (const folder of rawFolders) {
|
|
167
|
+
if (typeof folder === "string") {
|
|
168
|
+
const direct = sanitizeWorkspaceCandidate(folder);
|
|
169
|
+
if (direct) return direct;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!folder || typeof folder !== "object" || Array.isArray(folder)) continue;
|
|
173
|
+
const uriValue = firstNonEmptyString([
|
|
174
|
+
folder.uri,
|
|
175
|
+
folder.root_uri,
|
|
176
|
+
folder.rootUri,
|
|
177
|
+
folder.path,
|
|
178
|
+
folder.root_path,
|
|
179
|
+
folder.rootPath,
|
|
180
|
+
]);
|
|
181
|
+
const candidate = sanitizeWorkspaceCandidate(uriValue);
|
|
182
|
+
if (candidate) return candidate;
|
|
183
|
+
}
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
|
|
143
187
|
function extractWorkspaceCandidateFromRequest(requestObj, toolArgs) {
|
|
144
188
|
const params = safeObject(requestObj?.params);
|
|
145
189
|
const meta = safeObject(params._meta);
|
|
190
|
+
const workspaceFromFolders = firstNonEmptyString([
|
|
191
|
+
extractWorkspaceCandidateFromFolders(params.workspace_folders),
|
|
192
|
+
extractWorkspaceCandidateFromFolders(params.workspaceFolders),
|
|
193
|
+
extractWorkspaceCandidateFromFolders(meta.workspace_folders),
|
|
194
|
+
extractWorkspaceCandidateFromFolders(meta.workspaceFolders),
|
|
195
|
+
]);
|
|
146
196
|
const args = safeObject(toolArgs);
|
|
147
197
|
const rawCandidate = firstNonEmptyString([
|
|
198
|
+
workspaceFromFolders,
|
|
148
199
|
args.workspace_dir,
|
|
149
200
|
args.workspaceDir,
|
|
150
201
|
params.workspace_dir,
|
|
@@ -162,40 +213,54 @@ function extractWorkspaceCandidateFromRequest(requestObj, toolArgs) {
|
|
|
162
213
|
meta.root_uri,
|
|
163
214
|
meta.rootUri,
|
|
164
215
|
]);
|
|
165
|
-
|
|
166
|
-
const candidate = firstNonEmptyString([fileCandidate, rawCandidate]);
|
|
167
|
-
if (!candidate) return "";
|
|
168
|
-
return resolveWorkspaceDir(candidate);
|
|
216
|
+
return sanitizeWorkspaceCandidate(rawCandidate);
|
|
169
217
|
}
|
|
170
218
|
|
|
171
219
|
function extractWorkspaceCandidateFromEnv() {
|
|
172
220
|
const rawCandidate = firstNonEmptyString([
|
|
173
221
|
process.env.METHEUS_WORKSPACE_DIR,
|
|
174
222
|
process.env.CLAUDE_WORKSPACE_DIR,
|
|
223
|
+
process.env.CLAUDE_WORKSPACE_URI,
|
|
175
224
|
process.env.CLAUDE_PROJECT_DIR,
|
|
176
225
|
process.env.CODEX_WORKSPACE_DIR,
|
|
177
226
|
process.env.WORKSPACE_DIR,
|
|
178
227
|
process.env.WORKSPACE_FOLDER,
|
|
179
|
-
process.env.
|
|
228
|
+
process.env.VSCODE_WORKSPACE_FOLDER,
|
|
180
229
|
process.env.PWD,
|
|
181
230
|
process.env.INIT_CWD,
|
|
231
|
+
process.env.VSCODE_CWD,
|
|
182
232
|
]);
|
|
183
|
-
|
|
184
|
-
const candidate = firstNonEmptyString([fileCandidate, rawCandidate]);
|
|
185
|
-
if (!candidate) return "";
|
|
186
|
-
return resolveWorkspaceDir(candidate);
|
|
233
|
+
return sanitizeWorkspaceCandidate(rawCandidate);
|
|
187
234
|
}
|
|
188
235
|
|
|
189
236
|
function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs) {
|
|
190
237
|
const requestCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
|
|
191
238
|
const envCandidate = extractWorkspaceCandidateFromEnv();
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
239
|
+
const homeCandidate = firstNonEmptyString([process.env.USERPROFILE, process.env.HOME]);
|
|
240
|
+
for (const rawCandidate of [requestCandidate, envCandidate, defaultWorkspaceDir, process.cwd(), homeCandidate]) {
|
|
241
|
+
const resolved = sanitizeWorkspaceCandidate(rawCandidate);
|
|
242
|
+
if (resolved) return resolved;
|
|
243
|
+
}
|
|
244
|
+
return resolveWorkspaceDir(process.cwd());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function resolveProjectIDForRequest({
|
|
248
|
+
toolArgs,
|
|
249
|
+
args,
|
|
250
|
+
workspaceDir,
|
|
251
|
+
responseProjectID,
|
|
252
|
+
envelopeProjectID,
|
|
253
|
+
}) {
|
|
254
|
+
const resolvedWorkspaceDir = resolveWorkspaceDir(workspaceDir || process.cwd());
|
|
255
|
+
const workspaceMeta = loadWorkspaceMeta(resolvedWorkspaceDir);
|
|
256
|
+
return firstNonEmptyString([
|
|
257
|
+
toolArgs?.project_id,
|
|
258
|
+
toolArgs?.projectID,
|
|
259
|
+
responseProjectID,
|
|
260
|
+
envelopeProjectID,
|
|
261
|
+
workspaceMeta.project_id,
|
|
262
|
+
args?.projectID,
|
|
197
263
|
]);
|
|
198
|
-
return resolveWorkspaceDir(candidate);
|
|
199
264
|
}
|
|
200
265
|
|
|
201
266
|
function homeCtxpackCacheRootDir() {
|
|
@@ -1846,7 +1911,10 @@ function postJSON(urlText, timeoutSeconds, token, payload) {
|
|
|
1846
1911
|
resolve(text.trim());
|
|
1847
1912
|
return;
|
|
1848
1913
|
}
|
|
1849
|
-
|
|
1914
|
+
const err = new Error(text.trim() || `http ${statusCode}`);
|
|
1915
|
+
err.statusCode = statusCode;
|
|
1916
|
+
err.responseBody = text;
|
|
1917
|
+
reject(err);
|
|
1850
1918
|
});
|
|
1851
1919
|
},
|
|
1852
1920
|
);
|
|
@@ -1932,6 +2000,7 @@ function jsonRpcResult(requestObj, result) {
|
|
|
1932
2000
|
}
|
|
1933
2001
|
|
|
1934
2002
|
const LOCAL_PROJECT_TOOL_NAMES = ["project.summary", "project.describe", "project.get"];
|
|
2003
|
+
const LOCAL_CTXPACK_MERGE_TOOL_NAMES = ["ctxpack.merge.brief", "ctxpack.merge.execute"];
|
|
1935
2004
|
|
|
1936
2005
|
function buildProjectSummaryInputSchema() {
|
|
1937
2006
|
return {
|
|
@@ -1955,6 +2024,62 @@ function buildProjectSummaryInputSchema() {
|
|
|
1955
2024
|
};
|
|
1956
2025
|
}
|
|
1957
2026
|
|
|
2027
|
+
function buildCtxpackMergeBriefInputSchema() {
|
|
2028
|
+
return {
|
|
2029
|
+
type: "object",
|
|
2030
|
+
properties: {
|
|
2031
|
+
project_id: {
|
|
2032
|
+
type: "string",
|
|
2033
|
+
description: "Project UUID. If omitted, current configured project_id is used.",
|
|
2034
|
+
},
|
|
2035
|
+
status: {
|
|
2036
|
+
type: "string",
|
|
2037
|
+
description: "Merge-request status filter. Default: open.",
|
|
2038
|
+
enum: ["open", "review", "approved", "rejected", "merged", "closed", "all"],
|
|
2039
|
+
},
|
|
2040
|
+
limit: {
|
|
2041
|
+
type: "integer",
|
|
2042
|
+
minimum: 1,
|
|
2043
|
+
maximum: 100,
|
|
2044
|
+
description: "Maximum merge requests to inspect. Default: 20.",
|
|
2045
|
+
},
|
|
2046
|
+
},
|
|
2047
|
+
additionalProperties: false,
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function buildCtxpackMergeExecuteInputSchema() {
|
|
2052
|
+
return {
|
|
2053
|
+
type: "object",
|
|
2054
|
+
properties: {
|
|
2055
|
+
project_id: {
|
|
2056
|
+
type: "string",
|
|
2057
|
+
description: "Project UUID. If omitted, current configured project_id is used.",
|
|
2058
|
+
},
|
|
2059
|
+
merge_request_id: {
|
|
2060
|
+
type: "string",
|
|
2061
|
+
description: "Target merge request UUID.",
|
|
2062
|
+
},
|
|
2063
|
+
action: {
|
|
2064
|
+
type: "string",
|
|
2065
|
+
description: "Action to execute.",
|
|
2066
|
+
enum: ["review", "approve", "reject", "close", "merge"],
|
|
2067
|
+
},
|
|
2068
|
+
owner_confirmation: {
|
|
2069
|
+
type: "string",
|
|
2070
|
+
description:
|
|
2071
|
+
"Explicit owner confirmation text from chat (for safety). Example: confirm / approved / yes.",
|
|
2072
|
+
},
|
|
2073
|
+
note: {
|
|
2074
|
+
type: "string",
|
|
2075
|
+
description: "Optional reviewer note recorded in metadata for non-merge actions.",
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
required: ["merge_request_id", "action", "owner_confirmation"],
|
|
2079
|
+
additionalProperties: false,
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1958
2083
|
function buildLocalToolSpecs() {
|
|
1959
2084
|
return [
|
|
1960
2085
|
{
|
|
@@ -1975,6 +2100,18 @@ function buildLocalToolSpecs() {
|
|
|
1975
2100
|
"Alias of project.summary. Returns project metadata and access status for the given project_id.",
|
|
1976
2101
|
inputSchema: buildProjectSummaryInputSchema(),
|
|
1977
2102
|
},
|
|
2103
|
+
{
|
|
2104
|
+
name: "ctxpack.merge.brief",
|
|
2105
|
+
description:
|
|
2106
|
+
"Before merge, summarize pending ctxpack merge requests (who changed what), show risk/recommendation, and provide owner confirmation checklist.",
|
|
2107
|
+
inputSchema: buildCtxpackMergeBriefInputSchema(),
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
name: "ctxpack.merge.execute",
|
|
2111
|
+
description:
|
|
2112
|
+
"Execute ctxpack merge-request action (review/approve/reject/close/merge) after explicit owner confirmation.",
|
|
2113
|
+
inputSchema: buildCtxpackMergeExecuteInputSchema(),
|
|
2114
|
+
},
|
|
1978
2115
|
];
|
|
1979
2116
|
}
|
|
1980
2117
|
|
|
@@ -2278,6 +2415,10 @@ async function loadProjectSummaryForTool({
|
|
|
2278
2415
|
visibility: String(project.visibility || "").trim() || "private",
|
|
2279
2416
|
template: String(project.template || "").trim() || "blank",
|
|
2280
2417
|
template_label: normalizeTemplateLabel(project.template),
|
|
2418
|
+
owner_user_id: String(project.owner_user_id || "").trim(),
|
|
2419
|
+
program_owner_user_id: String(project.program_owner_user_id || "").trim(),
|
|
2420
|
+
tech_owner_user_id: String(project.tech_owner_user_id || "").trim(),
|
|
2421
|
+
governance_owner_user_id: String(project.governance_owner_user_id || "").trim(),
|
|
2281
2422
|
owner_name: String(project.owner_name || "").trim(),
|
|
2282
2423
|
program_owner_name: String(project.program_owner_name || "").trim(),
|
|
2283
2424
|
tech_owner_name: String(project.tech_owner_name || "").trim(),
|
|
@@ -2300,6 +2441,522 @@ async function loadProjectSummaryForTool({
|
|
|
2300
2441
|
};
|
|
2301
2442
|
}
|
|
2302
2443
|
|
|
2444
|
+
function normalizeMergeStatusFilter(rawValue) {
|
|
2445
|
+
const value = String(rawValue || "").trim().toLowerCase();
|
|
2446
|
+
if (!value) return "open";
|
|
2447
|
+
if (["open", "review", "approved", "rejected", "merged", "closed", "all"].includes(value)) {
|
|
2448
|
+
return value;
|
|
2449
|
+
}
|
|
2450
|
+
return "";
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function normalizeMergeAction(rawValue) {
|
|
2454
|
+
const value = String(rawValue || "").trim().toLowerCase();
|
|
2455
|
+
if (["review", "approve", "reject", "close", "merge"].includes(value)) {
|
|
2456
|
+
return value;
|
|
2457
|
+
}
|
|
2458
|
+
return "";
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function parseMergeMetadata(rawMetadata) {
|
|
2462
|
+
if (!rawMetadata) return {};
|
|
2463
|
+
if (typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) {
|
|
2464
|
+
return rawMetadata;
|
|
2465
|
+
}
|
|
2466
|
+
if (typeof rawMetadata !== "string") return {};
|
|
2467
|
+
try {
|
|
2468
|
+
const parsed = JSON.parse(rawMetadata);
|
|
2469
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
2470
|
+
return parsed;
|
|
2471
|
+
} catch {
|
|
2472
|
+
return {};
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function collectMergeChangedPaths(metadataObj) {
|
|
2477
|
+
const meta = safeObject(metadataObj);
|
|
2478
|
+
const out = [];
|
|
2479
|
+
const pushPath = (raw) => {
|
|
2480
|
+
const value = String(raw || "").trim();
|
|
2481
|
+
if (!value) return;
|
|
2482
|
+
if (!out.includes(value)) out.push(value);
|
|
2483
|
+
};
|
|
2484
|
+
for (const pathValue of ensureArray(meta.changed_paths)) {
|
|
2485
|
+
pushPath(pathValue);
|
|
2486
|
+
}
|
|
2487
|
+
for (const pathValue of ensureArray(meta.conflict_paths)) {
|
|
2488
|
+
pushPath(pathValue);
|
|
2489
|
+
}
|
|
2490
|
+
const files = ensureArray(meta.files);
|
|
2491
|
+
for (const file of files) {
|
|
2492
|
+
pushPath(safeObject(file).path);
|
|
2493
|
+
}
|
|
2494
|
+
const changeSet = safeObject(meta.change_set);
|
|
2495
|
+
const changeSetFiles = ensureArray(changeSet.files);
|
|
2496
|
+
for (const file of changeSetFiles) {
|
|
2497
|
+
pushPath(safeObject(file).path);
|
|
2498
|
+
}
|
|
2499
|
+
return out;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function summarizeMergeMetadata(metadataObj) {
|
|
2503
|
+
const meta = safeObject(metadataObj);
|
|
2504
|
+
const parts = [];
|
|
2505
|
+
const changeSet = safeObject(meta.change_set);
|
|
2506
|
+
const summary = safeObject(changeSet.summary);
|
|
2507
|
+
const changedCount = Number.parseInt(String(summary.changed || 0), 10);
|
|
2508
|
+
if (Number.isFinite(changedCount) && changedCount > 0) {
|
|
2509
|
+
parts.push(`changed files: ${changedCount}`);
|
|
2510
|
+
}
|
|
2511
|
+
const title = String(meta.title || meta.summary || meta.reason || "").trim();
|
|
2512
|
+
if (title) {
|
|
2513
|
+
parts.push(title);
|
|
2514
|
+
}
|
|
2515
|
+
if (!parts.length) return "";
|
|
2516
|
+
return parts.join(" | ");
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
function parseISOTime(rawValue) {
|
|
2520
|
+
const text = String(rawValue || "").trim();
|
|
2521
|
+
if (!text) return 0;
|
|
2522
|
+
const ms = Date.parse(text);
|
|
2523
|
+
if (!Number.isFinite(ms)) return 0;
|
|
2524
|
+
return ms;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
function extractActorFromToken(token) {
|
|
2528
|
+
const payload = safeObject(decodeJwtPayload(token));
|
|
2529
|
+
const userID = firstNonEmptyString([payload.sub, payload.user_id, payload.uid]);
|
|
2530
|
+
const email = firstNonEmptyString([payload.email, payload.preferred_username]);
|
|
2531
|
+
const name = firstNonEmptyString([payload.name, payload.given_name]);
|
|
2532
|
+
return { user_id: userID, email, name };
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
async function loadProjectMemberMap({ siteBaseURL, projectID, token, timeoutSeconds }) {
|
|
2536
|
+
const encodedProjectID = encodeURIComponent(projectID);
|
|
2537
|
+
try {
|
|
2538
|
+
const raw = await getJSONWithAuth(
|
|
2539
|
+
`${siteBaseURL}/api/v1/projects/${encodedProjectID}/members?limit=500&offset=0`,
|
|
2540
|
+
timeoutSeconds,
|
|
2541
|
+
token,
|
|
2542
|
+
);
|
|
2543
|
+
const members = ensureArray(raw);
|
|
2544
|
+
const map = new Map();
|
|
2545
|
+
for (const rowRaw of members) {
|
|
2546
|
+
const row = safeObject(rowRaw);
|
|
2547
|
+
const userID = String(row.user_id || "").trim();
|
|
2548
|
+
if (!userID) continue;
|
|
2549
|
+
map.set(userID, {
|
|
2550
|
+
user_id: userID,
|
|
2551
|
+
name: String(row.name || "").trim(),
|
|
2552
|
+
email: String(row.email || "").trim(),
|
|
2553
|
+
role: String(row.role || "").trim(),
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
return map;
|
|
2557
|
+
} catch {
|
|
2558
|
+
return new Map();
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
async function loadOrgOwnerUserID({ siteBaseURL, orgID, token, timeoutSeconds }) {
|
|
2563
|
+
const normalizedOrgID = String(orgID || "").trim();
|
|
2564
|
+
if (!normalizedOrgID) return "";
|
|
2565
|
+
try {
|
|
2566
|
+
const raw = await getJSONWithAuth(
|
|
2567
|
+
`${siteBaseURL}/api/v1/orgs/${encodeURIComponent(normalizedOrgID)}`,
|
|
2568
|
+
timeoutSeconds,
|
|
2569
|
+
token,
|
|
2570
|
+
);
|
|
2571
|
+
return String(safeObject(raw).owner_user_id || "").trim();
|
|
2572
|
+
} catch {
|
|
2573
|
+
return "";
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
function resolveActorDisplay(userID, membersByUserID) {
|
|
2578
|
+
const id = String(userID || "").trim();
|
|
2579
|
+
if (!id) return "-";
|
|
2580
|
+
const row = membersByUserID.get(id);
|
|
2581
|
+
if (!row) return id;
|
|
2582
|
+
const name = String(row.name || "").trim();
|
|
2583
|
+
const email = String(row.email || "").trim();
|
|
2584
|
+
if (name && email) return `${name} <${email}>`;
|
|
2585
|
+
if (name) return name;
|
|
2586
|
+
if (email) return email;
|
|
2587
|
+
return id;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
function isOwnerConfirmationAccepted(rawValue) {
|
|
2591
|
+
const text = String(rawValue || "").trim().toLowerCase();
|
|
2592
|
+
if (!text) return false;
|
|
2593
|
+
return ["yes", "y", "ok", "approve", "approved", "confirm", "confirmed"].includes(text);
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
async function loadCtxpackMergeBriefForTool({
|
|
2597
|
+
siteBaseURL,
|
|
2598
|
+
projectID,
|
|
2599
|
+
token,
|
|
2600
|
+
timeoutSeconds,
|
|
2601
|
+
statusFilter,
|
|
2602
|
+
limit,
|
|
2603
|
+
workspaceDir,
|
|
2604
|
+
}) {
|
|
2605
|
+
const summary = await loadProjectSummaryForTool({
|
|
2606
|
+
siteBaseURL,
|
|
2607
|
+
projectID,
|
|
2608
|
+
token,
|
|
2609
|
+
timeoutSeconds,
|
|
2610
|
+
includeCtxpack: false,
|
|
2611
|
+
syncCtxpackLocal: false,
|
|
2612
|
+
workspaceDir,
|
|
2613
|
+
});
|
|
2614
|
+
if (String(summary.access || "") !== "granted") {
|
|
2615
|
+
return {
|
|
2616
|
+
project_id: projectID,
|
|
2617
|
+
access: summary.access || "error",
|
|
2618
|
+
status_code: summary.status_code || 500,
|
|
2619
|
+
message: summary.message || "Unable to access project.",
|
|
2620
|
+
merge_requests: [],
|
|
2621
|
+
recommendation: {},
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
const normalizedStatus = normalizeMergeStatusFilter(statusFilter) || "open";
|
|
2626
|
+
const cappedLimit = Math.max(1, Math.min(100, Number.parseInt(String(limit || 20), 10) || 20));
|
|
2627
|
+
const encodedProjectID = encodeURIComponent(projectID);
|
|
2628
|
+
const statusParam =
|
|
2629
|
+
normalizedStatus === "all" || normalizedStatus === "open"
|
|
2630
|
+
? ""
|
|
2631
|
+
: `&status=${encodeURIComponent(normalizedStatus)}`;
|
|
2632
|
+
const listURL = `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack/merge-requests?limit=${cappedLimit}&offset=0${statusParam}`;
|
|
2633
|
+
let mergeFeatureAvailable = true;
|
|
2634
|
+
let mergeFeatureWarning = "";
|
|
2635
|
+
let allItems = [];
|
|
2636
|
+
try {
|
|
2637
|
+
const rawList = await getJSONWithAuth(listURL, timeoutSeconds, token);
|
|
2638
|
+
allItems = ensureArray(rawList);
|
|
2639
|
+
} catch (err) {
|
|
2640
|
+
const statusCode = Number(err?.statusCode || 0) || 500;
|
|
2641
|
+
if (statusCode === 404) {
|
|
2642
|
+
mergeFeatureAvailable = false;
|
|
2643
|
+
mergeFeatureWarning = "Merge-request API is not enabled on this server yet.";
|
|
2644
|
+
allItems = [];
|
|
2645
|
+
} else {
|
|
2646
|
+
throw err;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const statusOpenSet = new Set(["open", "review", "approved"]);
|
|
2650
|
+
const filteredItems =
|
|
2651
|
+
normalizedStatus === "open"
|
|
2652
|
+
? allItems.filter((rowRaw) => statusOpenSet.has(String(safeObject(rowRaw).status || "").trim().toLowerCase()))
|
|
2653
|
+
: allItems;
|
|
2654
|
+
|
|
2655
|
+
const membersByUserID = await loadProjectMemberMap({
|
|
2656
|
+
siteBaseURL,
|
|
2657
|
+
projectID,
|
|
2658
|
+
token,
|
|
2659
|
+
timeoutSeconds,
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
const actor = extractActorFromToken(token);
|
|
2663
|
+
const orgOwnerUserID = await loadOrgOwnerUserID({
|
|
2664
|
+
siteBaseURL,
|
|
2665
|
+
orgID: summary.org_id,
|
|
2666
|
+
token,
|
|
2667
|
+
timeoutSeconds,
|
|
2668
|
+
});
|
|
2669
|
+
const projectOwnerIDs = new Set(
|
|
2670
|
+
[summary.owner_user_id, summary.program_owner_user_id, summary.tech_owner_user_id, summary.governance_owner_user_id]
|
|
2671
|
+
.map((value) => String(value || "").trim())
|
|
2672
|
+
.filter(Boolean),
|
|
2673
|
+
);
|
|
2674
|
+
const isProjectOwner = actor.user_id ? projectOwnerIDs.has(actor.user_id) : false;
|
|
2675
|
+
const isOrgOwner = Boolean(actor.user_id && orgOwnerUserID && actor.user_id === orgOwnerUserID);
|
|
2676
|
+
const ownerGate = isProjectOwner || isOrgOwner;
|
|
2677
|
+
|
|
2678
|
+
const normalizedItems = filteredItems
|
|
2679
|
+
.map((rowRaw) => {
|
|
2680
|
+
const row = safeObject(rowRaw);
|
|
2681
|
+
const metadata = parseMergeMetadata(row.metadata);
|
|
2682
|
+
const changedPaths = collectMergeChangedPaths(metadata);
|
|
2683
|
+
const status = String(row.status || "").trim().toLowerCase();
|
|
2684
|
+
return {
|
|
2685
|
+
merge_request_id: String(row.merge_request_id || "").trim(),
|
|
2686
|
+
status,
|
|
2687
|
+
created_by_user_id: String(row.created_by || "").trim(),
|
|
2688
|
+
created_by: resolveActorDisplay(String(row.created_by || "").trim(), membersByUserID),
|
|
2689
|
+
reviewed_by_user_id: String(row.reviewed_by || "").trim(),
|
|
2690
|
+
reviewed_by: resolveActorDisplay(String(row.reviewed_by || "").trim(), membersByUserID),
|
|
2691
|
+
approved_at: String(row.approved_at || "").trim(),
|
|
2692
|
+
created_at: String(row.created_at || "").trim(),
|
|
2693
|
+
updated_at: String(row.updated_at || "").trim(),
|
|
2694
|
+
source_version_id: String(row.source_version_id || "").trim(),
|
|
2695
|
+
target_version_id: String(row.target_version_id || "").trim(),
|
|
2696
|
+
metadata_summary: summarizeMergeMetadata(metadata),
|
|
2697
|
+
changed_paths: changedPaths,
|
|
2698
|
+
changed_paths_count: changedPaths.length,
|
|
2699
|
+
};
|
|
2700
|
+
})
|
|
2701
|
+
.sort((a, b) => parseISOTime(b.updated_at) - parseISOTime(a.updated_at));
|
|
2702
|
+
|
|
2703
|
+
const statusCount = {
|
|
2704
|
+
open: normalizedItems.filter((row) => row.status === "open").length,
|
|
2705
|
+
review: normalizedItems.filter((row) => row.status === "review").length,
|
|
2706
|
+
approved: normalizedItems.filter((row) => row.status === "approved").length,
|
|
2707
|
+
rejected: normalizedItems.filter((row) => row.status === "rejected").length,
|
|
2708
|
+
merged: normalizedItems.filter((row) => row.status === "merged").length,
|
|
2709
|
+
closed: normalizedItems.filter((row) => row.status === "closed").length,
|
|
2710
|
+
};
|
|
2711
|
+
|
|
2712
|
+
const approvedCandidates = normalizedItems.filter((row) => row.status === "approved");
|
|
2713
|
+
const reviewCandidates = normalizedItems.filter((row) => row.status === "review" || row.status === "open");
|
|
2714
|
+
let recommendationAction = "no_action";
|
|
2715
|
+
let recommendationReason = "No pending merge requests.";
|
|
2716
|
+
let ownerPrompt = "";
|
|
2717
|
+
if (!mergeFeatureAvailable) {
|
|
2718
|
+
recommendationAction = "merge_api_unavailable";
|
|
2719
|
+
recommendationReason = mergeFeatureWarning;
|
|
2720
|
+
ownerPrompt =
|
|
2721
|
+
"Ask platform owner/admin to enable ctxpack merge-request API before merge approval workflow.";
|
|
2722
|
+
} else if (approvedCandidates.length > 0) {
|
|
2723
|
+
recommendationAction = ownerGate ? "merge_ready" : "owner_approval_required";
|
|
2724
|
+
recommendationReason =
|
|
2725
|
+
approvedCandidates.length === 1
|
|
2726
|
+
? "There is 1 approved merge request ready for merge."
|
|
2727
|
+
: `There are ${approvedCandidates.length} approved merge requests ready for merge.`;
|
|
2728
|
+
const ids = approvedCandidates.slice(0, 5).map((row) => row.merge_request_id).filter(Boolean);
|
|
2729
|
+
ownerPrompt = `Ask owner decision: merge now for ${ids.join(", ")} ?`;
|
|
2730
|
+
} else if (reviewCandidates.length > 0) {
|
|
2731
|
+
recommendationAction = ownerGate ? "review_then_approve" : "owner_review_required";
|
|
2732
|
+
recommendationReason =
|
|
2733
|
+
reviewCandidates.length === 1
|
|
2734
|
+
? "There is 1 merge request waiting for review/approval."
|
|
2735
|
+
: `There are ${reviewCandidates.length} merge requests waiting for review/approval.`;
|
|
2736
|
+
const ids = reviewCandidates.slice(0, 5).map((row) => row.merge_request_id).filter(Boolean);
|
|
2737
|
+
ownerPrompt = `Ask owner decision: approve or reject ${ids.join(", ")} ?`;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
return {
|
|
2741
|
+
project_id: projectID,
|
|
2742
|
+
access: "granted",
|
|
2743
|
+
status_code: 200,
|
|
2744
|
+
message: mergeFeatureAvailable
|
|
2745
|
+
? "Ctxpack merge briefing is ready."
|
|
2746
|
+
: "Ctxpack merge briefing is ready, but merge-request API is unavailable on this server.",
|
|
2747
|
+
merge_feature_available: mergeFeatureAvailable,
|
|
2748
|
+
merge_feature_warning: mergeFeatureWarning,
|
|
2749
|
+
owner_gate: {
|
|
2750
|
+
actor_user_id: actor.user_id,
|
|
2751
|
+
actor_email: actor.email,
|
|
2752
|
+
actor_name: actor.name,
|
|
2753
|
+
project_owner_user_id: String(summary.owner_user_id || "").trim(),
|
|
2754
|
+
org_owner_user_id: orgOwnerUserID,
|
|
2755
|
+
is_project_owner: isProjectOwner,
|
|
2756
|
+
is_org_owner: isOrgOwner,
|
|
2757
|
+
can_owner_finalize: ownerGate,
|
|
2758
|
+
},
|
|
2759
|
+
status_filter: normalizedStatus,
|
|
2760
|
+
merge_request_count: normalizedItems.length,
|
|
2761
|
+
status_count: statusCount,
|
|
2762
|
+
merge_requests: normalizedItems,
|
|
2763
|
+
recommendation: {
|
|
2764
|
+
action: recommendationAction,
|
|
2765
|
+
reason: recommendationReason,
|
|
2766
|
+
owner_prompt: ownerPrompt,
|
|
2767
|
+
owner_confirmation_required: true,
|
|
2768
|
+
},
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
function buildCtxpackMergeBriefText(brief) {
|
|
2773
|
+
if (String(brief?.access || "") !== "granted") {
|
|
2774
|
+
return [
|
|
2775
|
+
`Project ID: ${brief?.project_id || "-"}`,
|
|
2776
|
+
`Access: ${brief?.access || "error"}`,
|
|
2777
|
+
`Status: ${brief?.status_code || "-"}`,
|
|
2778
|
+
`${brief?.message || "Unable to load ctxpack merge briefing."}`,
|
|
2779
|
+
].join("\n");
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
const lines = [
|
|
2783
|
+
`Project ID: ${brief.project_id || "-"}`,
|
|
2784
|
+
`Merge Requests: ${Number(brief.merge_request_count || 0)}`,
|
|
2785
|
+
`Status Filter: ${brief.status_filter || "open"}`,
|
|
2786
|
+
`Owner Gate: ${brief.owner_gate?.can_owner_finalize ? "owner-confirmation possible" : "owner token required"}`,
|
|
2787
|
+
"",
|
|
2788
|
+
"Status Summary:",
|
|
2789
|
+
`- Open: ${Number(brief.status_count?.open || 0)}`,
|
|
2790
|
+
`- Review: ${Number(brief.status_count?.review || 0)}`,
|
|
2791
|
+
`- Approved: ${Number(brief.status_count?.approved || 0)}`,
|
|
2792
|
+
`- Rejected: ${Number(brief.status_count?.rejected || 0)}`,
|
|
2793
|
+
`- Merged: ${Number(brief.status_count?.merged || 0)}`,
|
|
2794
|
+
`- Closed: ${Number(brief.status_count?.closed || 0)}`,
|
|
2795
|
+
];
|
|
2796
|
+
if (brief.merge_feature_available === false) {
|
|
2797
|
+
lines.push("");
|
|
2798
|
+
lines.push("Merge API:");
|
|
2799
|
+
lines.push(`- ${brief.merge_feature_warning || "Merge-request API is unavailable on this server."}`);
|
|
2800
|
+
}
|
|
2801
|
+
lines.push("");
|
|
2802
|
+
lines.push("Recommendation:");
|
|
2803
|
+
lines.push(`- Action: ${brief.recommendation?.action || "no_action"}`);
|
|
2804
|
+
lines.push(`- Reason: ${brief.recommendation?.reason || "-"}`);
|
|
2805
|
+
const ownerPrompt = String(brief.recommendation?.owner_prompt || "").trim();
|
|
2806
|
+
if (ownerPrompt) {
|
|
2807
|
+
lines.push(`- Owner Prompt: ${ownerPrompt}`);
|
|
2808
|
+
}
|
|
2809
|
+
const requests = ensureArray(brief.merge_requests);
|
|
2810
|
+
if (requests.length > 0) {
|
|
2811
|
+
lines.push("");
|
|
2812
|
+
lines.push("Pending Details:");
|
|
2813
|
+
for (const row of requests.slice(0, 20)) {
|
|
2814
|
+
const changedPreview =
|
|
2815
|
+
row.changed_paths_count > 0 ? `${row.changed_paths_count} path(s)` : "no changed_paths metadata";
|
|
2816
|
+
const changedList =
|
|
2817
|
+
row.changed_paths_count > 0 ? ` [${ensureArray(row.changed_paths).slice(0, 5).join(", ")}]` : "";
|
|
2818
|
+
lines.push(
|
|
2819
|
+
`- ${row.merge_request_id} | ${row.status} | by ${row.created_by || row.created_by_user_id || "-"} | ${changedPreview}${changedList}`,
|
|
2820
|
+
);
|
|
2821
|
+
if (row.metadata_summary) {
|
|
2822
|
+
lines.push(` summary: ${row.metadata_summary}`);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
lines.push("");
|
|
2827
|
+
lines.push(
|
|
2828
|
+
"Before execute, ask owner for explicit confirmation, then run ctxpack.merge.execute with owner_confirmation.",
|
|
2829
|
+
);
|
|
2830
|
+
return lines.join("\n");
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
async function executeCtxpackMergeActionForTool({
|
|
2834
|
+
siteBaseURL,
|
|
2835
|
+
projectID,
|
|
2836
|
+
mergeRequestID,
|
|
2837
|
+
action,
|
|
2838
|
+
ownerConfirmation,
|
|
2839
|
+
token,
|
|
2840
|
+
timeoutSeconds,
|
|
2841
|
+
workspaceDir,
|
|
2842
|
+
}) {
|
|
2843
|
+
if (!isOwnerConfirmationAccepted(ownerConfirmation)) {
|
|
2844
|
+
return {
|
|
2845
|
+
project_id: projectID,
|
|
2846
|
+
merge_request_id: mergeRequestID,
|
|
2847
|
+
action,
|
|
2848
|
+
ok: false,
|
|
2849
|
+
status_code: 400,
|
|
2850
|
+
message: "Owner confirmation missing. Ask owner and pass owner_confirmation (confirm/approved/yes).",
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
const summary = await loadProjectSummaryForTool({
|
|
2855
|
+
siteBaseURL,
|
|
2856
|
+
projectID,
|
|
2857
|
+
token,
|
|
2858
|
+
timeoutSeconds,
|
|
2859
|
+
includeCtxpack: false,
|
|
2860
|
+
syncCtxpackLocal: false,
|
|
2861
|
+
workspaceDir,
|
|
2862
|
+
});
|
|
2863
|
+
if (String(summary.access || "") !== "granted") {
|
|
2864
|
+
return {
|
|
2865
|
+
project_id: projectID,
|
|
2866
|
+
merge_request_id: mergeRequestID,
|
|
2867
|
+
action,
|
|
2868
|
+
ok: false,
|
|
2869
|
+
status_code: summary.status_code || 500,
|
|
2870
|
+
message: summary.message || "Project access denied.",
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
const actor = extractActorFromToken(token);
|
|
2875
|
+
const orgOwnerUserID = await loadOrgOwnerUserID({
|
|
2876
|
+
siteBaseURL,
|
|
2877
|
+
orgID: summary.org_id,
|
|
2878
|
+
token,
|
|
2879
|
+
timeoutSeconds,
|
|
2880
|
+
});
|
|
2881
|
+
const projectOwnerIDs = new Set(
|
|
2882
|
+
[summary.owner_user_id, summary.program_owner_user_id, summary.tech_owner_user_id, summary.governance_owner_user_id]
|
|
2883
|
+
.map((value) => String(value || "").trim())
|
|
2884
|
+
.filter(Boolean),
|
|
2885
|
+
);
|
|
2886
|
+
const isProjectOwner = actor.user_id ? projectOwnerIDs.has(actor.user_id) : false;
|
|
2887
|
+
const isOrgOwner = Boolean(actor.user_id && orgOwnerUserID && actor.user_id === orgOwnerUserID);
|
|
2888
|
+
if (!(isProjectOwner || isOrgOwner)) {
|
|
2889
|
+
return {
|
|
2890
|
+
project_id: projectID,
|
|
2891
|
+
merge_request_id: mergeRequestID,
|
|
2892
|
+
action,
|
|
2893
|
+
ok: false,
|
|
2894
|
+
status_code: 403,
|
|
2895
|
+
message: "Owner gate blocked: current token is not project/org owner. Ask owner account to execute.",
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
const encodedProjectID = encodeURIComponent(projectID);
|
|
2900
|
+
const encodedMRID = encodeURIComponent(mergeRequestID);
|
|
2901
|
+
const endpoint = `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack/merge-requests/${encodedMRID}/${action}`;
|
|
2902
|
+
|
|
2903
|
+
try {
|
|
2904
|
+
const responseText = await postJSON(endpoint, timeoutSeconds, token, {});
|
|
2905
|
+
const parsed = tryJsonParse(responseText) || {};
|
|
2906
|
+
return {
|
|
2907
|
+
project_id: projectID,
|
|
2908
|
+
merge_request_id: mergeRequestID,
|
|
2909
|
+
action,
|
|
2910
|
+
ok: true,
|
|
2911
|
+
status_code: 200,
|
|
2912
|
+
message: `Merge request action '${action}' completed.`,
|
|
2913
|
+
actor_user_id: actor.user_id,
|
|
2914
|
+
actor_email: actor.email,
|
|
2915
|
+
response: safeObject(parsed),
|
|
2916
|
+
};
|
|
2917
|
+
} catch (err) {
|
|
2918
|
+
const statusCode = Number(err?.statusCode || 0) || 500;
|
|
2919
|
+
const bodyText = String(err?.responseBody || err?.message || "").trim();
|
|
2920
|
+
let message = bodyText || `Failed to execute action '${action}'.`;
|
|
2921
|
+
if (statusCode === 403) {
|
|
2922
|
+
message = "Forbidden by server policy/role. Current account cannot execute this action.";
|
|
2923
|
+
} else if (statusCode === 409) {
|
|
2924
|
+
message =
|
|
2925
|
+
"Merge conflict or approval state mismatch. Refresh briefing (ctxpack.merge.brief), then re-review before merge.";
|
|
2926
|
+
} else if (statusCode === 404) {
|
|
2927
|
+
message = "Merge request not found for this project.";
|
|
2928
|
+
}
|
|
2929
|
+
return {
|
|
2930
|
+
project_id: projectID,
|
|
2931
|
+
merge_request_id: mergeRequestID,
|
|
2932
|
+
action,
|
|
2933
|
+
ok: false,
|
|
2934
|
+
status_code: statusCode,
|
|
2935
|
+
message,
|
|
2936
|
+
error: bodyText,
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
function buildCtxpackMergeExecuteText(result) {
|
|
2942
|
+
const lines = [
|
|
2943
|
+
`Project ID: ${result.project_id || "-"}`,
|
|
2944
|
+
`Merge Request ID: ${result.merge_request_id || "-"}`,
|
|
2945
|
+
`Action: ${result.action || "-"}`,
|
|
2946
|
+
`Result: ${result.ok ? "ok" : "failed"} (status ${result.status_code || "-"})`,
|
|
2947
|
+
`Message: ${result.message || "-"}`,
|
|
2948
|
+
];
|
|
2949
|
+
if (result.ok) {
|
|
2950
|
+
const mergedVersionID = String(result.response?.merged_version_id || "").trim();
|
|
2951
|
+
if (mergedVersionID) {
|
|
2952
|
+
lines.push(`Merged Version ID: ${mergedVersionID}`);
|
|
2953
|
+
}
|
|
2954
|
+
} else if (result.error) {
|
|
2955
|
+
lines.push(`Server: ${result.error}`);
|
|
2956
|
+
}
|
|
2957
|
+
return lines.join("\n");
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2303
2960
|
function buildProjectSummaryText(summary) {
|
|
2304
2961
|
if (String(summary?.access || "") !== "granted") {
|
|
2305
2962
|
return [
|
|
@@ -2372,6 +3029,7 @@ function appendProjectHintToInitialize(responseObj, args) {
|
|
|
2372
3029
|
"- MUST call `project.summary` first when the user provides only a Project ID or asks project overview/agenda.",
|
|
2373
3030
|
"- `project.describe` and `project.get` are aliases of `project.summary`.",
|
|
2374
3031
|
"- After project summary, use workitem/evidence/decision tools as follow-up.",
|
|
3032
|
+
"- Before any ctxpack merge, call `ctxpack.merge.brief` first, share recommendation to owner, get explicit owner confirmation, then call `ctxpack.merge.execute`.",
|
|
2375
3033
|
];
|
|
2376
3034
|
if (args.projectID) {
|
|
2377
3035
|
hintLines.splice(1, 0, `- Default project_id is ${args.projectID}.`);
|
|
@@ -2393,14 +3051,14 @@ async function maybeAutoSyncCtxpackForCall({
|
|
|
2393
3051
|
}) {
|
|
2394
3052
|
if (!isJsonRpcMethod(requestObj, "tools/call")) return null;
|
|
2395
3053
|
if (!token) return null;
|
|
2396
|
-
if (LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) return null;
|
|
3054
|
+
if (LOCAL_PROJECT_TOOL_NAMES.includes(toolName) || LOCAL_CTXPACK_MERGE_TOOL_NAMES.includes(toolName)) return null;
|
|
2397
3055
|
if (String(toolName || "").trim().toLowerCase() === "ctxpack.ensure") return null;
|
|
2398
3056
|
|
|
2399
|
-
const projectID =
|
|
2400
|
-
toolArgs
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
3057
|
+
const projectID = resolveProjectIDForRequest({
|
|
3058
|
+
toolArgs,
|
|
3059
|
+
args,
|
|
3060
|
+
workspaceDir,
|
|
3061
|
+
});
|
|
2404
3062
|
if (!isUUID(projectID)) return null;
|
|
2405
3063
|
|
|
2406
3064
|
const workspacePath = resolveWorkspaceDir(workspaceDir || process.cwd());
|
|
@@ -2493,7 +3151,12 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
|
|
|
2493
3151
|
}
|
|
2494
3152
|
|
|
2495
3153
|
const projectID = String(
|
|
2496
|
-
|
|
3154
|
+
resolveProjectIDForRequest({
|
|
3155
|
+
toolArgs,
|
|
3156
|
+
args,
|
|
3157
|
+
workspaceDir: args.workspaceDir,
|
|
3158
|
+
responseProjectID,
|
|
3159
|
+
}),
|
|
2497
3160
|
).trim();
|
|
2498
3161
|
|
|
2499
3162
|
if (projectID && isUUID(projectID)) {
|
|
@@ -2562,7 +3225,12 @@ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs, requestObj) {
|
|
|
2562
3225
|
if (!Object.keys(body).length) return responseObj;
|
|
2563
3226
|
|
|
2564
3227
|
const projectID = String(
|
|
2565
|
-
|
|
3228
|
+
resolveProjectIDForRequest({
|
|
3229
|
+
toolArgs,
|
|
3230
|
+
args,
|
|
3231
|
+
workspaceDir: args.workspaceDir,
|
|
3232
|
+
envelopeProjectID: envelope.project_id,
|
|
3233
|
+
}),
|
|
2566
3234
|
).trim();
|
|
2567
3235
|
if (!projectID || !isUUID(projectID)) return responseObj;
|
|
2568
3236
|
const workspaceDir = resolveWorkspaceDirForRequest(args.workspaceDir || process.cwd(), requestObj, toolArgs);
|
|
@@ -2677,13 +3345,15 @@ async function injectCtxpackPreflightToken(requestObj, toolName, toolArgs, args,
|
|
|
2677
3345
|
return requestObj;
|
|
2678
3346
|
}
|
|
2679
3347
|
|
|
2680
|
-
const
|
|
2681
|
-
|
|
2682
|
-
rawArgs.projectID,
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
3348
|
+
const mergedToolArgs = {
|
|
3349
|
+
...safeObject(toolArgs),
|
|
3350
|
+
project_id: firstNonEmptyString([rawArgs.project_id, rawArgs.projectID, toolArgs?.project_id, toolArgs?.projectID]),
|
|
3351
|
+
};
|
|
3352
|
+
const projectID = resolveProjectIDForRequest({
|
|
3353
|
+
toolArgs: mergedToolArgs,
|
|
3354
|
+
args,
|
|
3355
|
+
workspaceDir,
|
|
3356
|
+
});
|
|
2687
3357
|
if (!isUUID(projectID)) {
|
|
2688
3358
|
return requestObj;
|
|
2689
3359
|
}
|
|
@@ -2773,12 +3443,13 @@ async function appendCtxpackConflictHintToErrorResponse(responseObj, args, toolN
|
|
|
2773
3443
|
}
|
|
2774
3444
|
|
|
2775
3445
|
async function runProxy(flags) {
|
|
2776
|
-
const
|
|
3446
|
+
const explicitWorkspaceDirRaw = String(flags["workspace-dir"] || "").trim();
|
|
3447
|
+
const explicitWorkspaceDir = explicitWorkspaceDirRaw ? resolveWorkspaceDir(explicitWorkspaceDirRaw) : "";
|
|
2777
3448
|
const args = {
|
|
2778
3449
|
baseURL: flags["base-url"] || DEFAULT_BASE_URL,
|
|
2779
|
-
projectID: String(flags["project-id"] ||
|
|
2780
|
-
ctxpackKey: String(flags["ctxpack-key"] ||
|
|
2781
|
-
workspaceDir:
|
|
3450
|
+
projectID: String(flags["project-id"] || "").trim(),
|
|
3451
|
+
ctxpackKey: String(flags["ctxpack-key"] || "").trim(),
|
|
3452
|
+
workspaceDir: explicitWorkspaceDir,
|
|
2782
3453
|
includeDrafts: boolFromRaw(flags["include-drafts"], true),
|
|
2783
3454
|
autoPullOnConflict: boolFromRaw(flags["auto-pull-on-conflict"], true),
|
|
2784
3455
|
timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
|
|
@@ -2845,16 +3516,16 @@ async function runProxy(flags) {
|
|
|
2845
3516
|
|
|
2846
3517
|
const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
|
|
2847
3518
|
const requestWorkspaceCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
|
|
3519
|
+
const envWorkspaceCandidate = extractWorkspaceCandidateFromEnv();
|
|
2848
3520
|
if (requestWorkspaceCandidate) {
|
|
2849
3521
|
sessionWorkspaceDir = requestWorkspaceCandidate;
|
|
3522
|
+
} else if (envWorkspaceCandidate) {
|
|
3523
|
+
sessionWorkspaceDir = envWorkspaceCandidate;
|
|
2850
3524
|
}
|
|
2851
|
-
const requestWorkspaceDir =
|
|
2852
|
-
firstNonEmptyString([
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
args.workspaceDir,
|
|
2856
|
-
process.cwd(),
|
|
2857
|
-
]),
|
|
3525
|
+
const requestWorkspaceDir = resolveWorkspaceDirForRequest(
|
|
3526
|
+
firstNonEmptyString([sessionWorkspaceDir, args.workspaceDir, process.cwd()]),
|
|
3527
|
+
requestObj,
|
|
3528
|
+
toolArgs,
|
|
2858
3529
|
);
|
|
2859
3530
|
let autoSyncSummary = null;
|
|
2860
3531
|
if (isJsonRpcMethod(requestObj, "tools/call")) {
|
|
@@ -2868,7 +3539,13 @@ async function runProxy(flags) {
|
|
|
2868
3539
|
});
|
|
2869
3540
|
}
|
|
2870
3541
|
if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) {
|
|
2871
|
-
const projectID = String(
|
|
3542
|
+
const projectID = String(
|
|
3543
|
+
resolveProjectIDForRequest({
|
|
3544
|
+
toolArgs,
|
|
3545
|
+
args,
|
|
3546
|
+
workspaceDir: requestWorkspaceDir,
|
|
3547
|
+
}),
|
|
3548
|
+
).trim();
|
|
2872
3549
|
if (!projectID) {
|
|
2873
3550
|
process.stdout.write(
|
|
2874
3551
|
`${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
|
|
@@ -2924,6 +3601,113 @@ async function runProxy(flags) {
|
|
|
2924
3601
|
}
|
|
2925
3602
|
return;
|
|
2926
3603
|
}
|
|
3604
|
+
if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_CTXPACK_MERGE_TOOL_NAMES.includes(toolName)) {
|
|
3605
|
+
const projectID = String(
|
|
3606
|
+
resolveProjectIDForRequest({
|
|
3607
|
+
toolArgs,
|
|
3608
|
+
args,
|
|
3609
|
+
workspaceDir: requestWorkspaceDir,
|
|
3610
|
+
}),
|
|
3611
|
+
).trim();
|
|
3612
|
+
if (!projectID) {
|
|
3613
|
+
process.stdout.write(
|
|
3614
|
+
`${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
|
|
3615
|
+
);
|
|
3616
|
+
return;
|
|
3617
|
+
}
|
|
3618
|
+
if (!isUUID(projectID)) {
|
|
3619
|
+
process.stdout.write(
|
|
3620
|
+
`${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"))}\n`,
|
|
3621
|
+
);
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
try {
|
|
3626
|
+
if (toolName === "ctxpack.merge.brief") {
|
|
3627
|
+
const statusRaw = String(toolArgs.status || "").trim().toLowerCase();
|
|
3628
|
+
const statusFilter = normalizeMergeStatusFilter(statusRaw);
|
|
3629
|
+
if (statusRaw && !statusFilter) {
|
|
3630
|
+
process.stdout.write(
|
|
3631
|
+
`${JSON.stringify(
|
|
3632
|
+
jsonRpcError(
|
|
3633
|
+
requestObj,
|
|
3634
|
+
-32001,
|
|
3635
|
+
"status must be one of: open, review, approved, rejected, merged, closed, all",
|
|
3636
|
+
),
|
|
3637
|
+
)}\n`,
|
|
3638
|
+
);
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
const limit = Number.parseInt(String(toolArgs.limit || 20), 10) || 20;
|
|
3642
|
+
const brief = await loadCtxpackMergeBriefForTool({
|
|
3643
|
+
siteBaseURL: normalizeSiteBaseURL(args.baseURL),
|
|
3644
|
+
projectID,
|
|
3645
|
+
token,
|
|
3646
|
+
timeoutSeconds: args.timeoutSeconds,
|
|
3647
|
+
statusFilter: statusFilter || "open",
|
|
3648
|
+
limit,
|
|
3649
|
+
workspaceDir: requestWorkspaceDir,
|
|
3650
|
+
});
|
|
3651
|
+
const text = buildCtxpackMergeBriefText(brief);
|
|
3652
|
+
process.stdout.write(
|
|
3653
|
+
`${JSON.stringify(
|
|
3654
|
+
jsonRpcResult(requestObj, {
|
|
3655
|
+
content: [{ type: "text", text }],
|
|
3656
|
+
structuredContent: brief,
|
|
3657
|
+
}),
|
|
3658
|
+
)}\n`,
|
|
3659
|
+
);
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
if (toolName === "ctxpack.merge.execute") {
|
|
3664
|
+
const mergeRequestID = String(toolArgs.merge_request_id || toolArgs.mergeRequestID || "").trim();
|
|
3665
|
+
const action = normalizeMergeAction(toolArgs.action);
|
|
3666
|
+
const ownerConfirmation = String(
|
|
3667
|
+
toolArgs.owner_confirmation || toolArgs.ownerConfirmation || "",
|
|
3668
|
+
).trim();
|
|
3669
|
+
if (!mergeRequestID || !isUUID(mergeRequestID)) {
|
|
3670
|
+
process.stdout.write(
|
|
3671
|
+
`${JSON.stringify(jsonRpcError(requestObj, -32001, "merge_request_id must be a valid UUID"))}\n`,
|
|
3672
|
+
);
|
|
3673
|
+
return;
|
|
3674
|
+
}
|
|
3675
|
+
if (!action) {
|
|
3676
|
+
process.stdout.write(
|
|
3677
|
+
`${JSON.stringify(
|
|
3678
|
+
jsonRpcError(requestObj, -32001, "action must be one of: review, approve, reject, close, merge"),
|
|
3679
|
+
)}\n`,
|
|
3680
|
+
);
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
const result = await executeCtxpackMergeActionForTool({
|
|
3684
|
+
siteBaseURL: normalizeSiteBaseURL(args.baseURL),
|
|
3685
|
+
projectID,
|
|
3686
|
+
mergeRequestID,
|
|
3687
|
+
action,
|
|
3688
|
+
ownerConfirmation,
|
|
3689
|
+
token,
|
|
3690
|
+
timeoutSeconds: args.timeoutSeconds,
|
|
3691
|
+
workspaceDir: requestWorkspaceDir,
|
|
3692
|
+
});
|
|
3693
|
+
const text = buildCtxpackMergeExecuteText(result);
|
|
3694
|
+
process.stdout.write(
|
|
3695
|
+
`${JSON.stringify(
|
|
3696
|
+
jsonRpcResult(requestObj, {
|
|
3697
|
+
content: [{ type: "text", text }],
|
|
3698
|
+
structuredContent: result,
|
|
3699
|
+
}),
|
|
3700
|
+
)}\n`,
|
|
3701
|
+
);
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
} catch (err) {
|
|
3705
|
+
process.stdout.write(
|
|
3706
|
+
`${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
|
|
3707
|
+
);
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
2927
3711
|
|
|
2928
3712
|
try {
|
|
2929
3713
|
const requestWithDefaults = injectCtxpackPushDefaults(requestObj, toolName, requestWorkspaceDir);
|