metheus-governance-mcp-cli 0.2.5 → 0.2.7
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 +21 -14
- package/cli.mjs +533 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Metheus Governance MCP helper CLI.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Compatibility note: legacy command alias `metheus-governance-mcp` is still supported.
|
|
6
|
+
|
|
7
|
+
- `metheus-governance-mcp-cli` (no args): bootstrap mode
|
|
6
8
|
- checks auth token
|
|
7
9
|
- if token is missing/expired, starts `auth login`
|
|
8
10
|
- checks Codex/Claude MCP registration
|
|
@@ -23,7 +25,7 @@ npm install -g metheus-governance-mcp-cli
|
|
|
23
25
|
Run in your project folder:
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
|
-
metheus-governance-mcp --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
|
|
28
|
+
metheus-governance-mcp-cli --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
If auth is not ready, login starts automatically, then MCP registration is completed.
|
|
@@ -31,7 +33,7 @@ If auth is not ready, login starts automatically, then MCP registration is compl
|
|
|
31
33
|
## Setup
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
|
-
metheus-governance-mcp setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
|
|
36
|
+
metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
`project-id` can be omitted if your current folder (or parent) has `.metheus_ctxpack_sync.json`.
|
|
@@ -39,7 +41,7 @@ metheus-governance-mcp setup --project-id <project_uuid> --ctxpack-key "<ctxpack
|
|
|
39
41
|
## Doctor
|
|
40
42
|
|
|
41
43
|
```bash
|
|
42
|
-
metheus-governance-mcp doctor --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
|
|
44
|
+
metheus-governance-mcp-cli doctor --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
Checks:
|
|
@@ -70,30 +72,35 @@ These tools accept `project_id` and return:
|
|
|
70
72
|
- missing local cache -> download
|
|
71
73
|
- same version -> keep current
|
|
72
74
|
- newer server version -> update local cache
|
|
75
|
+
- workspace path -> follows active client workspace/root when provided by Codex/Claude
|
|
73
76
|
|
|
74
77
|
Manual ctxpack pull/update:
|
|
75
78
|
|
|
76
79
|
```bash
|
|
77
|
-
metheus-governance-mcp ctxpack pull --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
|
|
80
|
+
metheus-governance-mcp-cli ctxpack pull --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
When `workitem.list` returns empty, proxy appends a hint to call `project.summary` first.
|
|
81
84
|
|
|
85
|
+
For `ctxpack` push conflicts (`ctxpack_conflict` / stale baseline), proxy behavior:
|
|
86
|
+
- standard conflict message with pull/merge/retry guidance
|
|
87
|
+
- auto ctxpack pull-to-local on conflict by default (`--auto-pull-on-conflict=true`)
|
|
88
|
+
|
|
82
89
|
## Auth flow (recommended)
|
|
83
90
|
|
|
84
91
|
1. Auto login and save token once (default flow: `device -> callback -> manual hint`):
|
|
85
92
|
|
|
86
93
|
```bash
|
|
87
|
-
metheus-governance-mcp auth login --base-url https://metheus.gesiaplatform.com
|
|
94
|
+
metheus-governance-mcp-cli auth login --base-url https://metheus.gesiaplatform.com
|
|
88
95
|
```
|
|
89
96
|
|
|
90
97
|
Optional flags:
|
|
91
98
|
|
|
92
99
|
```bash
|
|
93
|
-
metheus-governance-mcp auth login --callback-port 43819 --open-browser true
|
|
94
|
-
metheus-governance-mcp auth login --flow device
|
|
95
|
-
metheus-governance-mcp auth login --flow callback --callback-port 43819
|
|
96
|
-
metheus-governance-mcp auth login --client-id metheus-cli --realm master --keycloak-url https://oauth3.gesia.io
|
|
100
|
+
metheus-governance-mcp-cli auth login --callback-port 43819 --open-browser true
|
|
101
|
+
metheus-governance-mcp-cli auth login --flow device
|
|
102
|
+
metheus-governance-mcp-cli auth login --flow callback --callback-port 43819
|
|
103
|
+
metheus-governance-mcp-cli auth login --client-id metheus-cli --realm master --keycloak-url https://oauth3.gesia.io
|
|
97
104
|
```
|
|
98
105
|
|
|
99
106
|
Keycloak client requirement for auto login:
|
|
@@ -106,25 +113,25 @@ Keycloak client requirement for auto login:
|
|
|
106
113
|
Manual fallback:
|
|
107
114
|
|
|
108
115
|
```bash
|
|
109
|
-
metheus-governance-mcp auth login --manual true --base-url https://metheus.gesiaplatform.com
|
|
116
|
+
metheus-governance-mcp-cli auth login --manual true --base-url https://metheus.gesiaplatform.com
|
|
110
117
|
```
|
|
111
118
|
|
|
112
119
|
2. Check token status:
|
|
113
120
|
|
|
114
121
|
```bash
|
|
115
|
-
metheus-governance-mcp auth status
|
|
122
|
+
metheus-governance-mcp-cli auth status
|
|
116
123
|
```
|
|
117
124
|
|
|
118
125
|
3. If token is rotated, set directly:
|
|
119
126
|
|
|
120
127
|
```bash
|
|
121
|
-
metheus-governance-mcp auth set --token "<access_token>" --refresh-token "<refresh_token>"
|
|
128
|
+
metheus-governance-mcp-cli auth set --token "<access_token>" --refresh-token "<refresh_token>"
|
|
122
129
|
```
|
|
123
130
|
|
|
124
131
|
4. Remove local token:
|
|
125
132
|
|
|
126
133
|
```bash
|
|
127
|
-
metheus-governance-mcp auth clear
|
|
134
|
+
metheus-governance-mcp-cli auth clear
|
|
128
135
|
```
|
|
129
136
|
|
|
130
137
|
`proxy` priority:
|
package/cli.mjs
CHANGED
|
@@ -16,27 +16,31 @@ const DEFAULT_SERVER_NAME = "metheus-governance-mcp";
|
|
|
16
16
|
const AUTH_STORE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-auth.json");
|
|
17
17
|
const CTXPACK_CACHE_RELATIVE_DIR = path.join(".metheus", "ctxpack-cache");
|
|
18
18
|
const CTXPACK_META_FILENAME = ".metheus_ctxpack_sync.json";
|
|
19
|
+
const CTXPACK_PUSH_TOOL_NAMES = ["ctxpack.push", "ctxpack.update", "ctxpack.save"];
|
|
19
20
|
const CLI_META = loadCLIMeta();
|
|
20
21
|
const CLI_NAME = CLI_META.name || "metheus-governance-mcp-cli";
|
|
21
22
|
const CLI_VERSION = CLI_META.version || "0.0.0";
|
|
23
|
+
const AUTO_CTXPACK_SYNC_INTERVAL_MS = 60 * 1000;
|
|
24
|
+
const autoCtxpackSyncTracker = new Map();
|
|
22
25
|
|
|
23
26
|
function printUsage() {
|
|
27
|
+
const cmd = CLI_NAME;
|
|
24
28
|
process.stderr.write(
|
|
25
29
|
[
|
|
26
30
|
"Metheus Governance MCP CLI",
|
|
27
31
|
"",
|
|
28
32
|
"Usage:",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
` ${cmd} [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--flow <auto|device|callback|manual>]`,
|
|
34
|
+
` ${cmd} init [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]`,
|
|
35
|
+
` ${cmd} setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--name <server_name>]`,
|
|
36
|
+
` ${cmd} doctor [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--timeout-seconds <n>]`,
|
|
37
|
+
` ${cmd} proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--include-drafts <true|false>] [--auto-pull-on-conflict <true|false>] [--timeout-seconds <n>]`,
|
|
38
|
+
` ${cmd} ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path>] [--paths <csv>] [--timeout-seconds <n>]`,
|
|
39
|
+
` ${cmd} auth status`,
|
|
40
|
+
` ${cmd} auth login [--base-url <url>] [--flow <auto|device|callback|manual>] [--keycloak-url <url>] [--realm <name>] [--client-id <id>] [--open-browser <true|false>] [--callback-port <n>] [--timeout-seconds <n>] [--manual <true|false>]`,
|
|
41
|
+
` ${cmd} auth set --token <jwt> [--refresh-token <token>] [--base-url <url>]`,
|
|
42
|
+
` ${cmd} auth clear`,
|
|
43
|
+
` ${cmd} -v | --version`,
|
|
40
44
|
"",
|
|
41
45
|
"Environment:",
|
|
42
46
|
" METHEUS_TOKEN or MCP_AUTH_TOKEN is used first for proxy requests.",
|
|
@@ -109,6 +113,67 @@ function resolveWorkspaceDir(rawPath) {
|
|
|
109
113
|
return path.resolve(process.cwd());
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
function fileURIToLocalPath(rawValue) {
|
|
117
|
+
const value = String(rawValue || "").trim();
|
|
118
|
+
if (!value || !/^file:\/\//i.test(value)) return "";
|
|
119
|
+
try {
|
|
120
|
+
const parsed = new URL(value);
|
|
121
|
+
if (parsed.protocol !== "file:") return "";
|
|
122
|
+
const pathname = decodeURIComponent(parsed.pathname || "");
|
|
123
|
+
if (!pathname) return "";
|
|
124
|
+
if (process.platform === "win32") {
|
|
125
|
+
// file:///C:/repo -> C:\repo
|
|
126
|
+
const winPath = pathname.replace(/^\/+([a-zA-Z]:\/)/, "$1").replace(/\//g, "\\");
|
|
127
|
+
return winPath;
|
|
128
|
+
}
|
|
129
|
+
return pathname;
|
|
130
|
+
} catch {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function firstNonEmptyString(values) {
|
|
136
|
+
for (const value of values) {
|
|
137
|
+
const text = String(value || "").trim();
|
|
138
|
+
if (text) return text;
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractWorkspaceCandidateFromRequest(requestObj, toolArgs) {
|
|
144
|
+
const params = safeObject(requestObj?.params);
|
|
145
|
+
const meta = safeObject(params._meta);
|
|
146
|
+
const args = safeObject(toolArgs);
|
|
147
|
+
const rawCandidate = firstNonEmptyString([
|
|
148
|
+
args.workspace_dir,
|
|
149
|
+
args.workspaceDir,
|
|
150
|
+
params.workspace_dir,
|
|
151
|
+
params.workspaceDir,
|
|
152
|
+
params.cwd,
|
|
153
|
+
params.root_path,
|
|
154
|
+
params.rootPath,
|
|
155
|
+
params.root_uri,
|
|
156
|
+
params.rootUri,
|
|
157
|
+
meta.workspace_dir,
|
|
158
|
+
meta.workspaceDir,
|
|
159
|
+
meta.cwd,
|
|
160
|
+
meta.root_path,
|
|
161
|
+
meta.rootPath,
|
|
162
|
+
meta.root_uri,
|
|
163
|
+
meta.rootUri,
|
|
164
|
+
]);
|
|
165
|
+
const fileCandidate = fileURIToLocalPath(rawCandidate);
|
|
166
|
+
const candidate = firstNonEmptyString([fileCandidate, rawCandidate]);
|
|
167
|
+
if (!candidate) return "";
|
|
168
|
+
return resolveWorkspaceDir(candidate);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs) {
|
|
172
|
+
const requestCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
|
|
173
|
+
const candidate = firstNonEmptyString([requestCandidate, defaultWorkspaceDir, process.cwd()]);
|
|
174
|
+
return resolveWorkspaceDir(candidate);
|
|
175
|
+
}
|
|
176
|
+
|
|
112
177
|
function homeCtxpackCacheRootDir() {
|
|
113
178
|
const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
|
|
114
179
|
if (!home) {
|
|
@@ -1258,6 +1323,7 @@ async function runCtxpackPull(flags) {
|
|
|
1258
1323
|
const workspaceMeta = loadWorkspaceMeta(process.cwd());
|
|
1259
1324
|
const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
|
|
1260
1325
|
const workspaceDir = resolveWorkspaceDir(flags["workspace-dir"] || process.cwd());
|
|
1326
|
+
const pathsCSV = String(flags.paths || "").trim();
|
|
1261
1327
|
if (!projectID) {
|
|
1262
1328
|
process.stderr.write("Missing project_id. Use --project-id <uuid> or run inside a synced ctxpack workspace.\n");
|
|
1263
1329
|
process.exitCode = 1;
|
|
@@ -1289,6 +1355,7 @@ async function runCtxpackPull(flags) {
|
|
|
1289
1355
|
includeCtxpack: true,
|
|
1290
1356
|
syncCtxpackLocal: true,
|
|
1291
1357
|
workspaceDir,
|
|
1358
|
+
ctxpackPaths: pathsCSV,
|
|
1292
1359
|
});
|
|
1293
1360
|
process.stdout.write(`${buildProjectSummaryText(summary)}\n`);
|
|
1294
1361
|
|
|
@@ -1509,6 +1576,110 @@ async function runDoctor(flags) {
|
|
|
1509
1576
|
addDoctorCheck(rows, "fail", "ctxpack sync", summary.ctxpack_sync_message || "ctxpack sync failed");
|
|
1510
1577
|
}
|
|
1511
1578
|
|
|
1579
|
+
const localMeta = loadWorkspaceMeta(context.workspaceDir);
|
|
1580
|
+
const localBaseVersionID = firstNonEmptyString([localMeta.base_version_id, localMeta.version_id]);
|
|
1581
|
+
const serverCurrentVersionID = firstNonEmptyString([summary.current_version_id, summary.ctxpack_version_id]);
|
|
1582
|
+
if (summary.ctxpack_id) {
|
|
1583
|
+
if (!serverCurrentVersionID) {
|
|
1584
|
+
addDoctorCheck(rows, "warn", "stale baseline", "server current_version_id is missing");
|
|
1585
|
+
} else if (!localBaseVersionID) {
|
|
1586
|
+
addDoctorCheck(
|
|
1587
|
+
rows,
|
|
1588
|
+
"warn",
|
|
1589
|
+
"stale baseline",
|
|
1590
|
+
"local base_version_id is missing. Run: metheus-governance-mcp ctxpack pull --project-id <project_id>",
|
|
1591
|
+
);
|
|
1592
|
+
} else if (localBaseVersionID !== serverCurrentVersionID) {
|
|
1593
|
+
addDoctorCheck(
|
|
1594
|
+
rows,
|
|
1595
|
+
"fail",
|
|
1596
|
+
"stale baseline",
|
|
1597
|
+
`local ${localBaseVersionID} != server ${serverCurrentVersionID}. Run ctxpack pull before safe push.`,
|
|
1598
|
+
);
|
|
1599
|
+
} else {
|
|
1600
|
+
addDoctorCheck(rows, "ok", "stale baseline", `baseline is current (${serverCurrentVersionID})`);
|
|
1601
|
+
}
|
|
1602
|
+
} else {
|
|
1603
|
+
addDoctorCheck(rows, "warn", "stale baseline", "ctxpack is not available for this project");
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
if (summary.ctxpack_id) {
|
|
1607
|
+
try {
|
|
1608
|
+
const encodedProjectID = encodeURIComponent(context.projectID);
|
|
1609
|
+
const statsRaw = await getJSONWithAuth(
|
|
1610
|
+
`${context.baseURL}/api/v1/projects/${encodedProjectID}/ctxpack/stats`,
|
|
1611
|
+
timeoutSeconds,
|
|
1612
|
+
token,
|
|
1613
|
+
);
|
|
1614
|
+
const stats = safeObject(statsRaw);
|
|
1615
|
+
const strictConflict = Boolean(stats.strict_conflict);
|
|
1616
|
+
const preflightMode = String(stats.preflight_mode || "").trim().toLowerCase() || "off";
|
|
1617
|
+
const directPushMode = String(stats.direct_push_mode || "").trim().toLowerCase() || "unknown";
|
|
1618
|
+
const allowLegacyPush = Boolean(stats.allow_legacy_push);
|
|
1619
|
+
const forceAdminOnly = Boolean(stats.force_admin_only);
|
|
1620
|
+
const readOnlyMode = Boolean(stats.read_only_mode);
|
|
1621
|
+
const mergeMinApprovalsRaw = Number.parseInt(String(stats.merge_min_approvals ?? "1"), 10);
|
|
1622
|
+
const mergeMinApprovals = Number.isFinite(mergeMinApprovalsRaw) ? mergeMinApprovalsRaw : 1;
|
|
1623
|
+
|
|
1624
|
+
if (strictConflict) {
|
|
1625
|
+
addDoctorCheck(rows, "ok", "server policy strict_conflict", "enabled");
|
|
1626
|
+
} else {
|
|
1627
|
+
addDoctorCheck(rows, "warn", "server policy strict_conflict", "disabled (stale overwrite risk)");
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (preflightMode === "enforce") {
|
|
1631
|
+
addDoctorCheck(rows, "ok", "server policy preflight_mode", "enforce");
|
|
1632
|
+
} else if (preflightMode === "warn") {
|
|
1633
|
+
addDoctorCheck(rows, "warn", "server policy preflight_mode", "warn (blocking not enabled yet)");
|
|
1634
|
+
} else {
|
|
1635
|
+
addDoctorCheck(rows, "warn", "server policy preflight_mode", `${preflightMode || "off"} (recommended: enforce)`);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (directPushMode === "disabled") {
|
|
1639
|
+
addDoctorCheck(rows, "ok", "server policy direct_push_mode", "disabled (merge-request workflow)");
|
|
1640
|
+
} else if (directPushMode === "admin_only") {
|
|
1641
|
+
addDoctorCheck(rows, "ok", "server policy direct_push_mode", "admin_only (merge-first default)");
|
|
1642
|
+
} else if (directPushMode === "enabled") {
|
|
1643
|
+
addDoctorCheck(rows, "warn", "server policy direct_push_mode", "enabled (writer direct push is open)");
|
|
1644
|
+
} else {
|
|
1645
|
+
addDoctorCheck(rows, "warn", "server policy direct_push_mode", directPushMode || "unknown");
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (mergeMinApprovals >= 2) {
|
|
1649
|
+
addDoctorCheck(rows, "ok", "server policy merge_min_approvals", String(mergeMinApprovals));
|
|
1650
|
+
} else if (mergeMinApprovals === 1) {
|
|
1651
|
+
addDoctorCheck(rows, "warn", "server policy merge_min_approvals", "1 (recommended: >=2 for team review)");
|
|
1652
|
+
} else {
|
|
1653
|
+
addDoctorCheck(rows, "fail", "server policy merge_min_approvals", String(mergeMinApprovals));
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (allowLegacyPush) {
|
|
1657
|
+
addDoctorCheck(rows, "warn", "server policy allow_legacy_push", "enabled (recommended: false)");
|
|
1658
|
+
} else {
|
|
1659
|
+
addDoctorCheck(rows, "ok", "server policy allow_legacy_push", "disabled");
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (forceAdminOnly) {
|
|
1663
|
+
addDoctorCheck(rows, "ok", "server policy force_admin_only", "enabled");
|
|
1664
|
+
} else {
|
|
1665
|
+
addDoctorCheck(rows, "warn", "server policy force_admin_only", "disabled (writers may force/rollback)");
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (readOnlyMode) {
|
|
1669
|
+
addDoctorCheck(rows, "warn", "server mode", "ctxpack read-only mode is enabled");
|
|
1670
|
+
} else {
|
|
1671
|
+
addDoctorCheck(rows, "ok", "server mode", "ctxpack writable");
|
|
1672
|
+
}
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
addDoctorCheck(
|
|
1675
|
+
rows,
|
|
1676
|
+
"warn",
|
|
1677
|
+
"server ctxpack policy",
|
|
1678
|
+
`unable to read /ctxpack/stats (${String(err?.message || err)})`,
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1512
1683
|
const smokeCalls = [
|
|
1513
1684
|
{
|
|
1514
1685
|
tool: "workitem.list",
|
|
@@ -1854,6 +2025,7 @@ function hasAllCtxpackFiles(baseDir, files) {
|
|
|
1854
2025
|
function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack, workspaceDir }) {
|
|
1855
2026
|
const ctxpackID = String(ctxpack?.ctxpack_id || "").trim();
|
|
1856
2027
|
const version = String(ctxpack?.version || "").trim();
|
|
2028
|
+
const versionID = String(ctxpack?.version_id || ctxpack?.current_version_id || "").trim();
|
|
1857
2029
|
const status = String(ctxpack?.status || "").trim() || "draft";
|
|
1858
2030
|
const files = normalizeCtxpackFiles(ctxpack?.files);
|
|
1859
2031
|
const resolvedWorkspaceDir = resolveWorkspaceDir(workspaceDir);
|
|
@@ -1871,13 +2043,29 @@ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack, workspaceDir
|
|
|
1871
2043
|
};
|
|
1872
2044
|
}
|
|
1873
2045
|
|
|
1874
|
-
const remoteSig = `${ctxpackID}|${version}|${status}|${files.length}`;
|
|
2046
|
+
const remoteSig = `${ctxpackID}|${versionID}|${version}|${status}|${files.length}`;
|
|
1875
2047
|
const previousMeta = loadCtxpackMeta(metaPath);
|
|
1876
2048
|
const previousSig = previousMeta
|
|
1877
|
-
? `${String(previousMeta.ctxpack_id || "").trim()}|${String(previousMeta.version || "").trim()}|${String(previousMeta.status || "").trim()}|${Number.parseInt(String(previousMeta.files_count || 0), 10) || 0}`
|
|
2049
|
+
? `${String(previousMeta.ctxpack_id || "").trim()}|${String(previousMeta.version_id || "").trim()}|${String(previousMeta.version || "").trim()}|${String(previousMeta.status || "").trim()}|${Number.parseInt(String(previousMeta.files_count || 0), 10) || 0}`
|
|
1878
2050
|
: "";
|
|
2051
|
+
const payload = {
|
|
2052
|
+
project_id: projectID,
|
|
2053
|
+
ctxpack_id: ctxpackID,
|
|
2054
|
+
version_id: versionID,
|
|
2055
|
+
version,
|
|
2056
|
+
status,
|
|
2057
|
+
files_count: files.length,
|
|
2058
|
+
files: files.map((f) => ({ path: f.path, doc_type: f.docType || "" })),
|
|
2059
|
+
source: `${String(siteBaseURL || "").replace(/\/+$/, "")}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`,
|
|
2060
|
+
synced_at: new Date().toISOString(),
|
|
2061
|
+
};
|
|
1879
2062
|
|
|
1880
2063
|
if (previousSig === remoteSig && hasAllCtxpackFiles(cacheDir, files)) {
|
|
2064
|
+
try {
|
|
2065
|
+
fs.writeFileSync(workspaceMetaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
2066
|
+
} catch {
|
|
2067
|
+
// Best-effort metadata refresh for workspace-root auto-detection.
|
|
2068
|
+
}
|
|
1881
2069
|
return {
|
|
1882
2070
|
sync_status: "current",
|
|
1883
2071
|
sync_message: "Local ctxpack cache is up to date.",
|
|
@@ -1897,16 +2085,6 @@ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack, workspaceDir
|
|
|
1897
2085
|
fs.writeFileSync(target, file.content, "utf8");
|
|
1898
2086
|
}
|
|
1899
2087
|
|
|
1900
|
-
const payload = {
|
|
1901
|
-
project_id: projectID,
|
|
1902
|
-
ctxpack_id: ctxpackID,
|
|
1903
|
-
version,
|
|
1904
|
-
status,
|
|
1905
|
-
files_count: files.length,
|
|
1906
|
-
files: files.map((f) => ({ path: f.path, doc_type: f.docType || "" })),
|
|
1907
|
-
source: `${String(siteBaseURL || "").replace(/\/+$/, "")}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`,
|
|
1908
|
-
synced_at: new Date().toISOString(),
|
|
1909
|
-
};
|
|
1910
2088
|
fs.writeFileSync(metaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
1911
2089
|
fs.writeFileSync(workspaceMetaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
1912
2090
|
|
|
@@ -1938,6 +2116,7 @@ async function loadProjectSummaryForTool({
|
|
|
1938
2116
|
includeCtxpack,
|
|
1939
2117
|
syncCtxpackLocal,
|
|
1940
2118
|
workspaceDir,
|
|
2119
|
+
ctxpackPaths,
|
|
1941
2120
|
}) {
|
|
1942
2121
|
const encodedProjectID = encodeURIComponent(projectID);
|
|
1943
2122
|
let projectRaw = null;
|
|
@@ -1975,6 +2154,7 @@ async function loadProjectSummaryForTool({
|
|
|
1975
2154
|
let agendaSource = "";
|
|
1976
2155
|
let ctxpackID = "";
|
|
1977
2156
|
let ctxpackVersion = "";
|
|
2157
|
+
let ctxpackVersionID = "";
|
|
1978
2158
|
let ctxpackFileCount = 0;
|
|
1979
2159
|
let ctxpackSyncStatus = includeCtxpack && syncCtxpackLocal ? "not_attempted" : "disabled";
|
|
1980
2160
|
let ctxpackSyncMessage = "";
|
|
@@ -1982,7 +2162,12 @@ async function loadProjectSummaryForTool({
|
|
|
1982
2162
|
let ctxpackLocalFileCount = 0;
|
|
1983
2163
|
if (includeCtxpack) {
|
|
1984
2164
|
try {
|
|
1985
|
-
const
|
|
2165
|
+
const pathQuery = String(ctxpackPaths || "").trim();
|
|
2166
|
+
const ctxpackURL =
|
|
2167
|
+
pathQuery.length > 0
|
|
2168
|
+
? `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack?paths=${encodeURIComponent(pathQuery)}`
|
|
2169
|
+
: `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack`;
|
|
2170
|
+
const ctxpackRaw = await getJSONWithAuth(ctxpackURL, timeoutSeconds, token);
|
|
1986
2171
|
const ctxpack = safeObject(ctxpackRaw);
|
|
1987
2172
|
const extracted = extractAgendaFromFiles(ctxpack.files);
|
|
1988
2173
|
agenda = extracted.agenda;
|
|
@@ -1990,6 +2175,7 @@ async function loadProjectSummaryForTool({
|
|
|
1990
2175
|
ctxpackFileCount = extracted.ctxpackFileCount;
|
|
1991
2176
|
ctxpackID = String(ctxpack.ctxpack_id || "").trim();
|
|
1992
2177
|
ctxpackVersion = String(ctxpack.version || "").trim();
|
|
2178
|
+
ctxpackVersionID = String(ctxpack.version_id || "").trim();
|
|
1993
2179
|
if (syncCtxpackLocal) {
|
|
1994
2180
|
const syncResult = syncCtxpackToLocalCache({
|
|
1995
2181
|
siteBaseURL,
|
|
@@ -2030,7 +2216,10 @@ async function loadProjectSummaryForTool({
|
|
|
2030
2216
|
member_count: Number(project.member_count || 0),
|
|
2031
2217
|
created_at: String(project.created_at || "").trim(),
|
|
2032
2218
|
ctxpack_id: ctxpackID,
|
|
2219
|
+
ctxpack_version_id: ctxpackVersionID,
|
|
2033
2220
|
ctxpack_version: ctxpackVersion,
|
|
2221
|
+
base_version_id: ctxpackVersionID,
|
|
2222
|
+
current_version_id: ctxpackVersionID,
|
|
2034
2223
|
ctxpack_file_count: ctxpackFileCount,
|
|
2035
2224
|
ctxpack_sync_status: ctxpackSyncStatus,
|
|
2036
2225
|
ctxpack_sync_message: ctxpackSyncMessage,
|
|
@@ -2125,6 +2314,75 @@ function appendProjectHintToInitialize(responseObj, args) {
|
|
|
2125
2314
|
return responseObj;
|
|
2126
2315
|
}
|
|
2127
2316
|
|
|
2317
|
+
async function maybeAutoSyncCtxpackForCall({
|
|
2318
|
+
requestObj,
|
|
2319
|
+
toolName,
|
|
2320
|
+
toolArgs,
|
|
2321
|
+
args,
|
|
2322
|
+
token,
|
|
2323
|
+
workspaceDir,
|
|
2324
|
+
}) {
|
|
2325
|
+
if (!isJsonRpcMethod(requestObj, "tools/call")) return null;
|
|
2326
|
+
if (!token) return null;
|
|
2327
|
+
if (LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) return null;
|
|
2328
|
+
if (String(toolName || "").trim().toLowerCase() === "ctxpack.ensure") return null;
|
|
2329
|
+
|
|
2330
|
+
const projectID = firstNonEmptyString([
|
|
2331
|
+
toolArgs?.project_id,
|
|
2332
|
+
toolArgs?.projectID,
|
|
2333
|
+
args.projectID,
|
|
2334
|
+
]);
|
|
2335
|
+
if (!isUUID(projectID)) return null;
|
|
2336
|
+
|
|
2337
|
+
const workspacePath = resolveWorkspaceDir(workspaceDir || process.cwd());
|
|
2338
|
+
const trackerKey = `${workspacePath}::${projectID}`;
|
|
2339
|
+
const now = Date.now();
|
|
2340
|
+
const lastAt = Number(autoCtxpackSyncTracker.get(trackerKey) || 0);
|
|
2341
|
+
if (lastAt > 0 && now-lastAt < AUTO_CTXPACK_SYNC_INTERVAL_MS) {
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
autoCtxpackSyncTracker.set(trackerKey, now);
|
|
2345
|
+
|
|
2346
|
+
try {
|
|
2347
|
+
return await loadProjectSummaryForTool({
|
|
2348
|
+
siteBaseURL: normalizeSiteBaseURL(args.baseURL),
|
|
2349
|
+
projectID,
|
|
2350
|
+
token,
|
|
2351
|
+
timeoutSeconds: args.timeoutSeconds,
|
|
2352
|
+
includeCtxpack: true,
|
|
2353
|
+
syncCtxpackLocal: true,
|
|
2354
|
+
workspaceDir: workspacePath,
|
|
2355
|
+
});
|
|
2356
|
+
} catch {
|
|
2357
|
+
return null;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
function appendAutoCtxpackSyncHint(responseObj, summary) {
|
|
2362
|
+
const status = String(summary?.ctxpack_sync_status || "").trim();
|
|
2363
|
+
if (!status || status === "disabled" || status === "not_attempted" || status === "current") {
|
|
2364
|
+
return responseObj;
|
|
2365
|
+
}
|
|
2366
|
+
const result = safeObject(responseObj.result);
|
|
2367
|
+
const content = ensureArray(result.content);
|
|
2368
|
+
if (!content.length) return responseObj;
|
|
2369
|
+
const first = safeObject(content[0]);
|
|
2370
|
+
if (String(first.type || "").trim() !== "text") return responseObj;
|
|
2371
|
+
const text = String(first.text || "");
|
|
2372
|
+
if (!text.trim() || text.includes("ctxpack_auto_sync:")) return responseObj;
|
|
2373
|
+
|
|
2374
|
+
let suffix = `ctxpack_auto_sync: ${status}`;
|
|
2375
|
+
const localPath = String(summary?.ctxpack_local_path || "").trim();
|
|
2376
|
+
if (localPath) suffix += ` (${localPath})`;
|
|
2377
|
+
const message = String(summary?.ctxpack_sync_message || "").trim();
|
|
2378
|
+
if (message) suffix += ` - ${message}`;
|
|
2379
|
+
|
|
2380
|
+
content[0] = { ...first, text: `${text}\n\n${suffix}` };
|
|
2381
|
+
result.content = content;
|
|
2382
|
+
responseObj.result = result;
|
|
2383
|
+
return responseObj;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2128
2386
|
async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
|
|
2129
2387
|
const result = safeObject(responseObj.result);
|
|
2130
2388
|
const content = ensureArray(result.content);
|
|
@@ -2216,7 +2474,7 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
|
|
|
2216
2474
|
return responseObj;
|
|
2217
2475
|
}
|
|
2218
2476
|
|
|
2219
|
-
function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs) {
|
|
2477
|
+
function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs, requestObj) {
|
|
2220
2478
|
const result = safeObject(responseObj.result);
|
|
2221
2479
|
const content = ensureArray(result.content);
|
|
2222
2480
|
if (!content.length) return responseObj;
|
|
@@ -2238,13 +2496,16 @@ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs) {
|
|
|
2238
2496
|
toolArgs?.project_id || toolArgs?.projectID || envelope.project_id || args.projectID || "",
|
|
2239
2497
|
).trim();
|
|
2240
2498
|
if (!projectID || !isUUID(projectID)) return responseObj;
|
|
2499
|
+
const workspaceDir = resolveWorkspaceDirForRequest(args.workspaceDir || process.cwd(), requestObj, toolArgs);
|
|
2241
2500
|
|
|
2242
2501
|
const syncResult = syncCtxpackToLocalCache({
|
|
2243
2502
|
siteBaseURL: normalizeSiteBaseURL(args.baseURL),
|
|
2244
2503
|
projectID,
|
|
2245
2504
|
ctxpack: body,
|
|
2246
|
-
workspaceDir
|
|
2505
|
+
workspaceDir,
|
|
2247
2506
|
});
|
|
2507
|
+
const baseVersionID = firstNonEmptyString([body.base_version_id, body.version_id, body.current_version_id]);
|
|
2508
|
+
const currentVersionID = firstNonEmptyString([body.current_version_id, body.version_id, body.base_version_id]);
|
|
2248
2509
|
|
|
2249
2510
|
const lines = [
|
|
2250
2511
|
"",
|
|
@@ -2259,16 +2520,189 @@ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs) {
|
|
|
2259
2520
|
if (syncResult.workspace_path) {
|
|
2260
2521
|
lines.push(`ctxpack_workspace_path: ${String(syncResult.workspace_path)}`);
|
|
2261
2522
|
}
|
|
2523
|
+
if (baseVersionID) {
|
|
2524
|
+
lines.push(`ctxpack_base_version_id: ${baseVersionID}`);
|
|
2525
|
+
}
|
|
2526
|
+
if (currentVersionID) {
|
|
2527
|
+
lines.push(`ctxpack_current_version_id: ${currentVersionID}`);
|
|
2528
|
+
}
|
|
2262
2529
|
|
|
2263
2530
|
content[0] = {
|
|
2264
2531
|
...first,
|
|
2265
2532
|
text: `${text}\n${lines.join("\n")}`,
|
|
2266
2533
|
};
|
|
2267
2534
|
result.content = content;
|
|
2535
|
+
const structured = safeObject(result.structuredContent);
|
|
2536
|
+
result.structuredContent = {
|
|
2537
|
+
...structured,
|
|
2538
|
+
sync_status: String(syncResult.sync_status || "error"),
|
|
2539
|
+
base_version_id: baseVersionID,
|
|
2540
|
+
current_version_id: currentVersionID,
|
|
2541
|
+
local_workspace_path: String(syncResult.workspace_path || workspaceDir),
|
|
2542
|
+
local_ctxpack_path: String(syncResult.local_path || ""),
|
|
2543
|
+
};
|
|
2268
2544
|
responseObj.result = result;
|
|
2269
2545
|
return responseObj;
|
|
2270
2546
|
}
|
|
2271
2547
|
|
|
2548
|
+
function injectCtxpackPushDefaults(requestObj, toolName, workspaceDir) {
|
|
2549
|
+
if (!isJsonRpcMethod(requestObj, "tools/call")) return requestObj;
|
|
2550
|
+
const normalizedTool = String(toolName || "").trim().toLowerCase();
|
|
2551
|
+
if (!CTXPACK_PUSH_TOOL_NAMES.includes(normalizedTool)) {
|
|
2552
|
+
return requestObj;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const params = safeObject(requestObj.params);
|
|
2556
|
+
const rawArgs = safeObject(params.arguments ?? params.args);
|
|
2557
|
+
if (String(rawArgs.base_version_id || "").trim() && String(rawArgs.push_mode || "").trim()) {
|
|
2558
|
+
return requestObj;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
const workspaceMeta = loadWorkspaceMeta(workspaceDir);
|
|
2562
|
+
const baseVersionID = firstNonEmptyString([
|
|
2563
|
+
rawArgs.base_version_id,
|
|
2564
|
+
workspaceMeta.base_version_id,
|
|
2565
|
+
workspaceMeta.version_id,
|
|
2566
|
+
]);
|
|
2567
|
+
const nextArgs = { ...rawArgs };
|
|
2568
|
+
if (!String(nextArgs.base_version_id || "").trim() && isUUID(baseVersionID)) {
|
|
2569
|
+
nextArgs.base_version_id = baseVersionID;
|
|
2570
|
+
}
|
|
2571
|
+
if (!String(nextArgs.push_mode || "").trim()) {
|
|
2572
|
+
nextArgs.push_mode = "safe";
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
const nextParams = { ...params };
|
|
2576
|
+
nextParams.arguments = nextArgs;
|
|
2577
|
+
const patched = { ...requestObj, params: nextParams };
|
|
2578
|
+
return patched;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function collectChangedPathsForPreflight(rawArgs) {
|
|
2582
|
+
const args = safeObject(rawArgs);
|
|
2583
|
+
const explicit = ensureArray(args.changed_paths ?? args.changedPaths);
|
|
2584
|
+
const raw = explicit.length > 0 ? explicit : ensureArray(args.files).map((file) => safeObject(file).path);
|
|
2585
|
+
const dedup = new Set();
|
|
2586
|
+
const out = [];
|
|
2587
|
+
for (const value of raw) {
|
|
2588
|
+
const text = String(value || "").trim();
|
|
2589
|
+
if (!text || dedup.has(text)) continue;
|
|
2590
|
+
dedup.add(text);
|
|
2591
|
+
out.push(text);
|
|
2592
|
+
}
|
|
2593
|
+
return out;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
async function injectCtxpackPreflightToken(requestObj, toolName, toolArgs, args, token, workspaceDir) {
|
|
2597
|
+
if (!isJsonRpcMethod(requestObj, "tools/call")) return requestObj;
|
|
2598
|
+
if (!token) return requestObj;
|
|
2599
|
+
|
|
2600
|
+
const normalizedTool = String(toolName || "").trim().toLowerCase();
|
|
2601
|
+
if (!CTXPACK_PUSH_TOOL_NAMES.includes(normalizedTool)) {
|
|
2602
|
+
return requestObj;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
const params = safeObject(requestObj.params);
|
|
2606
|
+
const rawArgs = safeObject(params.arguments ?? params.args);
|
|
2607
|
+
if (String(rawArgs.preflight_token || rawArgs.preflightToken || "").trim()) {
|
|
2608
|
+
return requestObj;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
const projectID = firstNonEmptyString([
|
|
2612
|
+
rawArgs.project_id,
|
|
2613
|
+
rawArgs.projectID,
|
|
2614
|
+
toolArgs?.project_id,
|
|
2615
|
+
toolArgs?.projectID,
|
|
2616
|
+
args.projectID,
|
|
2617
|
+
]);
|
|
2618
|
+
if (!isUUID(projectID)) {
|
|
2619
|
+
return requestObj;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
const workspaceMeta = loadWorkspaceMeta(workspaceDir);
|
|
2623
|
+
const baseVersionID = firstNonEmptyString([
|
|
2624
|
+
rawArgs.base_version_id,
|
|
2625
|
+
rawArgs.baseVersionID,
|
|
2626
|
+
workspaceMeta.base_version_id,
|
|
2627
|
+
workspaceMeta.version_id,
|
|
2628
|
+
]);
|
|
2629
|
+
if (!isUUID(baseVersionID)) {
|
|
2630
|
+
return requestObj;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
const changedPaths = collectChangedPathsForPreflight(rawArgs);
|
|
2634
|
+
const payload = {
|
|
2635
|
+
base_version_id: baseVersionID,
|
|
2636
|
+
};
|
|
2637
|
+
if (changedPaths.length > 0) {
|
|
2638
|
+
payload.changed_paths = changedPaths;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
try {
|
|
2642
|
+
const baseURL = normalizeSiteBaseURL(args.baseURL);
|
|
2643
|
+
const preflightURL = `${baseURL}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack/preflight`;
|
|
2644
|
+
const responseText = await postJSON(preflightURL, args.timeoutSeconds, token, payload);
|
|
2645
|
+
const responseBody = safeObject(tryJsonParse(responseText));
|
|
2646
|
+
const preflightID = firstNonEmptyString([responseBody.preflight_id, responseBody.preflightID, responseBody.id]);
|
|
2647
|
+
if (!preflightID) return requestObj;
|
|
2648
|
+
|
|
2649
|
+
const nextArgs = { ...rawArgs, preflight_token: preflightID };
|
|
2650
|
+
if (!ensureArray(rawArgs.changed_paths ?? rawArgs.changedPaths).length && changedPaths.length > 0) {
|
|
2651
|
+
nextArgs.changed_paths = changedPaths;
|
|
2652
|
+
}
|
|
2653
|
+
const nextParams = { ...params, arguments: nextArgs };
|
|
2654
|
+
return { ...requestObj, params: nextParams };
|
|
2655
|
+
} catch {
|
|
2656
|
+
// Best-effort: if preflight auto-issue fails, server-side validation still enforces policy.
|
|
2657
|
+
return requestObj;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
async function appendCtxpackConflictHintToErrorResponse(responseObj, args, toolName, toolArgs, token, workspaceDir) {
|
|
2662
|
+
const out = safeObject(responseObj);
|
|
2663
|
+
const errObj = safeObject(out.error);
|
|
2664
|
+
const message = String(errObj.message || "").trim();
|
|
2665
|
+
if (!message) return responseObj;
|
|
2666
|
+
const normalizedTool = String(toolName || "").trim().toLowerCase();
|
|
2667
|
+
const normalizedMessage = message.toLowerCase();
|
|
2668
|
+
const isCtxpackConflict = normalizedMessage.includes("ctxpack_conflict") || normalizedMessage.includes("ctxpack conflict");
|
|
2669
|
+
if (!isCtxpackConflict) return responseObj;
|
|
2670
|
+
if (!normalizedTool.startsWith("ctxpack.")) return responseObj;
|
|
2671
|
+
|
|
2672
|
+
const projectIDHint = firstNonEmptyString([
|
|
2673
|
+
toolArgs?.project_id,
|
|
2674
|
+
toolArgs?.projectID,
|
|
2675
|
+
args.projectID,
|
|
2676
|
+
"<project_id>",
|
|
2677
|
+
]);
|
|
2678
|
+
const baseURL = normalizeSiteBaseURL(args.baseURL);
|
|
2679
|
+
let autoPullHint = "";
|
|
2680
|
+
if (args.autoPullOnConflict && token && isUUID(projectIDHint)) {
|
|
2681
|
+
try {
|
|
2682
|
+
const encodedProjectID = encodeURIComponent(projectIDHint);
|
|
2683
|
+
const ctxpackRaw = await getJSONWithAuth(
|
|
2684
|
+
`${baseURL}/api/v1/projects/${encodedProjectID}/ctxpack`,
|
|
2685
|
+
args.timeoutSeconds,
|
|
2686
|
+
token,
|
|
2687
|
+
);
|
|
2688
|
+
const syncResult = syncCtxpackToLocalCache({
|
|
2689
|
+
siteBaseURL: baseURL,
|
|
2690
|
+
projectID: projectIDHint,
|
|
2691
|
+
ctxpack: safeObject(ctxpackRaw),
|
|
2692
|
+
workspaceDir,
|
|
2693
|
+
});
|
|
2694
|
+
autoPullHint = ` auto-pull: ${String(syncResult.sync_status || "error")}${
|
|
2695
|
+
syncResult.local_path ? ` (${String(syncResult.local_path)})` : ""
|
|
2696
|
+
}`;
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
autoPullHint = ` auto-pull failed: ${String(err?.message || err)}`;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
const hint = `stale baseline detected. run 'metheus-governance-mcp ctxpack pull --project-id ${projectIDHint} --base-url ${baseURL}', merge local edits, then retry safe push.${autoPullHint}`;
|
|
2702
|
+
const nextError = { ...errObj, message: `${message}; ${hint}` };
|
|
2703
|
+
return { ...out, error: nextError };
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2272
2706
|
async function runProxy(flags) {
|
|
2273
2707
|
const workspaceMeta = loadWorkspaceMeta(process.cwd());
|
|
2274
2708
|
const args = {
|
|
@@ -2277,11 +2711,13 @@ async function runProxy(flags) {
|
|
|
2277
2711
|
ctxpackKey: String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim(),
|
|
2278
2712
|
workspaceDir: resolveWorkspaceDir(flags["workspace-dir"] || process.cwd()),
|
|
2279
2713
|
includeDrafts: boolFromRaw(flags["include-drafts"], true),
|
|
2714
|
+
autoPullOnConflict: boolFromRaw(flags["auto-pull-on-conflict"], true),
|
|
2280
2715
|
timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
|
|
2281
2716
|
};
|
|
2282
2717
|
const gatewayURL = buildGatewayURL(args);
|
|
2283
2718
|
let lastRefreshAttemptAtMs = 0;
|
|
2284
2719
|
let lastRefreshError = "";
|
|
2720
|
+
let sessionWorkspaceDir = "";
|
|
2285
2721
|
|
|
2286
2722
|
const rl = readline.createInterface({
|
|
2287
2723
|
input: process.stdin,
|
|
@@ -2339,6 +2775,29 @@ async function runProxy(flags) {
|
|
|
2339
2775
|
}
|
|
2340
2776
|
|
|
2341
2777
|
const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
|
|
2778
|
+
const requestWorkspaceCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
|
|
2779
|
+
if (requestWorkspaceCandidate) {
|
|
2780
|
+
sessionWorkspaceDir = requestWorkspaceCandidate;
|
|
2781
|
+
}
|
|
2782
|
+
const requestWorkspaceDir = resolveWorkspaceDir(
|
|
2783
|
+
firstNonEmptyString([
|
|
2784
|
+
requestWorkspaceCandidate,
|
|
2785
|
+
sessionWorkspaceDir,
|
|
2786
|
+
args.workspaceDir,
|
|
2787
|
+
process.cwd(),
|
|
2788
|
+
]),
|
|
2789
|
+
);
|
|
2790
|
+
let autoSyncSummary = null;
|
|
2791
|
+
if (isJsonRpcMethod(requestObj, "tools/call")) {
|
|
2792
|
+
autoSyncSummary = await maybeAutoSyncCtxpackForCall({
|
|
2793
|
+
requestObj,
|
|
2794
|
+
toolName,
|
|
2795
|
+
toolArgs,
|
|
2796
|
+
args,
|
|
2797
|
+
token,
|
|
2798
|
+
workspaceDir: requestWorkspaceDir,
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2342
2801
|
if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) {
|
|
2343
2802
|
const projectID = String(toolArgs.project_id || toolArgs.projectID || args.projectID || "").trim();
|
|
2344
2803
|
if (!projectID) {
|
|
@@ -2373,7 +2832,7 @@ async function runProxy(flags) {
|
|
|
2373
2832
|
timeoutSeconds: args.timeoutSeconds,
|
|
2374
2833
|
includeCtxpack,
|
|
2375
2834
|
syncCtxpackLocal,
|
|
2376
|
-
workspaceDir:
|
|
2835
|
+
workspaceDir: requestWorkspaceDir,
|
|
2377
2836
|
});
|
|
2378
2837
|
const text = buildProjectSummaryText(summary);
|
|
2379
2838
|
process.stdout.write(
|
|
@@ -2398,11 +2857,32 @@ async function runProxy(flags) {
|
|
|
2398
2857
|
}
|
|
2399
2858
|
|
|
2400
2859
|
try {
|
|
2401
|
-
const
|
|
2860
|
+
const requestWithDefaults = injectCtxpackPushDefaults(requestObj, toolName, requestWorkspaceDir);
|
|
2861
|
+
const outboundRequestObj = await injectCtxpackPreflightToken(
|
|
2862
|
+
requestWithDefaults,
|
|
2863
|
+
toolName,
|
|
2864
|
+
toolArgs,
|
|
2865
|
+
args,
|
|
2866
|
+
token,
|
|
2867
|
+
requestWorkspaceDir,
|
|
2868
|
+
);
|
|
2869
|
+
const responseText = await postJSON(gatewayURL, args.timeoutSeconds, token, outboundRequestObj);
|
|
2402
2870
|
if (responseText) {
|
|
2403
2871
|
const parsed = parseGatewayResponseText(responseText);
|
|
2404
|
-
if (parsed
|
|
2872
|
+
if (parsed) {
|
|
2405
2873
|
let patched = parsed;
|
|
2874
|
+
if (parsed.error) {
|
|
2875
|
+
patched = await appendCtxpackConflictHintToErrorResponse(
|
|
2876
|
+
patched,
|
|
2877
|
+
args,
|
|
2878
|
+
toolName,
|
|
2879
|
+
toolArgs,
|
|
2880
|
+
token,
|
|
2881
|
+
requestWorkspaceDir,
|
|
2882
|
+
);
|
|
2883
|
+
process.stdout.write(`${JSON.stringify(patched)}\n`);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2406
2886
|
if (isJsonRpcMethod(requestObj, "tools/list")) {
|
|
2407
2887
|
patched = appendLocalToolToToolsList(patched);
|
|
2408
2888
|
} else if (isJsonRpcMethod(requestObj, "initialize")) {
|
|
@@ -2410,7 +2890,10 @@ async function runProxy(flags) {
|
|
|
2410
2890
|
} else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "workitem.list") {
|
|
2411
2891
|
patched = await appendWorkitemListHints(patched, args, toolArgs, token);
|
|
2412
2892
|
} else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "ctxpack.ensure") {
|
|
2413
|
-
patched = appendCtxpackEnsureSyncHints(patched, args, toolArgs);
|
|
2893
|
+
patched = appendCtxpackEnsureSyncHints(patched, args, toolArgs, requestObj);
|
|
2894
|
+
}
|
|
2895
|
+
if (autoSyncSummary) {
|
|
2896
|
+
patched = appendAutoCtxpackSyncHint(patched, autoSyncSummary);
|
|
2414
2897
|
}
|
|
2415
2898
|
process.stdout.write(`${JSON.stringify(patched)}\n`);
|
|
2416
2899
|
return;
|
|
@@ -2495,13 +2978,27 @@ function resolveSetupContext(flags) {
|
|
|
2495
2978
|
const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
|
|
2496
2979
|
const ctxpackKey = String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim();
|
|
2497
2980
|
const baseURL = String(flags["base-url"] || DEFAULT_SITE_URL).trim().replace(/\/+$/, "");
|
|
2498
|
-
const
|
|
2981
|
+
const workspaceDirRaw = String(flags["workspace-dir"] || "").trim();
|
|
2982
|
+
const hasExplicitWorkspaceDir = workspaceDirRaw.length > 0;
|
|
2983
|
+
const workspaceDir = resolveWorkspaceDir(hasExplicitWorkspaceDir ? workspaceDirRaw : process.cwd());
|
|
2499
2984
|
const serverName = String(flags.name || DEFAULT_SERVER_NAME).trim() || DEFAULT_SERVER_NAME;
|
|
2500
2985
|
const proxyArgs = ["--base-url", `${baseURL}/governance/mcp`];
|
|
2501
|
-
|
|
2986
|
+
// Default mode: do not pin workspace path in MCP registration.
|
|
2987
|
+
// Let client runtime cwd resolve per open folder/workspace.
|
|
2988
|
+
if (hasExplicitWorkspaceDir) {
|
|
2989
|
+
proxyArgs.push("--workspace-dir", workspaceDir);
|
|
2990
|
+
}
|
|
2502
2991
|
if (projectID) proxyArgs.push("--project-id", projectID);
|
|
2503
2992
|
if (ctxpackKey) proxyArgs.push("--ctxpack-key", ctxpackKey);
|
|
2504
|
-
return {
|
|
2993
|
+
return {
|
|
2994
|
+
projectID,
|
|
2995
|
+
ctxpackKey,
|
|
2996
|
+
baseURL,
|
|
2997
|
+
workspaceDir,
|
|
2998
|
+
hasExplicitWorkspaceDir,
|
|
2999
|
+
serverName,
|
|
3000
|
+
proxyArgs,
|
|
3001
|
+
};
|
|
2505
3002
|
}
|
|
2506
3003
|
|
|
2507
3004
|
function runSetupInternal(flags, options = {}) {
|
|
@@ -2530,7 +3027,9 @@ function runSetupInternal(flags, options = {}) {
|
|
|
2530
3027
|
process.stdout.write(ensureOnly ? "\nEnsure complete.\n" : "\nInstall complete.\n");
|
|
2531
3028
|
process.stdout.write(`Server: ${context.serverName}\n`);
|
|
2532
3029
|
process.stdout.write(`Gateway: ${context.baseURL}/governance/mcp\n`);
|
|
2533
|
-
process.stdout.write(
|
|
3030
|
+
process.stdout.write(
|
|
3031
|
+
`Workspace: ${context.hasExplicitWorkspaceDir ? context.workspaceDir : "auto (client current folder)"}\n`,
|
|
3032
|
+
);
|
|
2534
3033
|
process.stdout.write(`Project: ${context.projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
|
|
2535
3034
|
if (context.ctxpackKey) {
|
|
2536
3035
|
process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
|