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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/cli.mjs +827 -43
  3. 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
- const fileCandidate = fileURIToLocalPath(rawCandidate);
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.VSCODE_CWD,
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
- const fileCandidate = fileURIToLocalPath(rawCandidate);
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 candidate = firstNonEmptyString([
193
- requestCandidate,
194
- envCandidate,
195
- defaultWorkspaceDir,
196
- process.cwd(),
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
- reject(new Error(text.trim() || `http ${statusCode}`));
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 = firstNonEmptyString([
2400
- toolArgs?.project_id,
2401
- toolArgs?.projectID,
2402
- args.projectID,
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
- toolArgs?.project_id || toolArgs?.projectID || responseProjectID || args.projectID || "",
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
- toolArgs?.project_id || toolArgs?.projectID || envelope.project_id || args.projectID || "",
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 projectID = firstNonEmptyString([
2681
- rawArgs.project_id,
2682
- rawArgs.projectID,
2683
- toolArgs?.project_id,
2684
- toolArgs?.projectID,
2685
- args.projectID,
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 workspaceMeta = loadWorkspaceMeta(process.cwd());
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"] || workspaceMeta.project_id || "").trim(),
2780
- ctxpackKey: String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim(),
2781
- workspaceDir: resolveWorkspaceDir(flags["workspace-dir"] || process.cwd()),
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 = resolveWorkspaceDir(
2852
- firstNonEmptyString([
2853
- requestWorkspaceCandidate,
2854
- sessionWorkspaceDir,
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(toolArgs.project_id || toolArgs.projectID || args.projectID || "").trim();
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [