@context-engine-bridge/context-engine-mcp-bridge 0.0.6 → 0.0.8
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/package.json +1 -1
- package/src/authCli.js +88 -10
- package/src/mcpServer.js +40 -23
- package/src/resultPathMapping.js +338 -0
package/package.json
CHANGED
package/src/authCli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { loadAuthEntry, saveAuthEntry, deleteAuthEntry } from "./authConfig.js";
|
|
2
|
+
import { loadAuthEntry, saveAuthEntry, deleteAuthEntry, loadAnyAuthEntry } from "./authConfig.js";
|
|
3
3
|
|
|
4
4
|
function parseAuthArgs(args) {
|
|
5
5
|
let backendUrl = process.env.CTXCE_AUTH_BACKEND_URL || "";
|
|
@@ -19,9 +19,16 @@ function parseAuthArgs(args) {
|
|
|
19
19
|
i += 1;
|
|
20
20
|
continue;
|
|
21
21
|
}
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
i
|
|
22
|
+
if (a === "--username" || a === "--user") {
|
|
23
|
+
const hasNext = i + 1 < args.length;
|
|
24
|
+
const next = hasNext ? String(args[i + 1]) : "";
|
|
25
|
+
if (hasNext && !next.startsWith("-")) {
|
|
26
|
+
username = args[i + 1];
|
|
27
|
+
i += 1;
|
|
28
|
+
} else {
|
|
29
|
+
console.error("[ctxce] Missing value for --username/--user; expected a username.");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
25
32
|
continue;
|
|
26
33
|
}
|
|
27
34
|
if ((a === "--password" || a === "--pass") && i + 1 < args.length) {
|
|
@@ -41,6 +48,11 @@ function getBackendUrl(backendUrl) {
|
|
|
41
48
|
return (backendUrl || process.env.CTXCE_AUTH_BACKEND_URL || "").trim();
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
function getDefaultUploadBackend() {
|
|
52
|
+
// Default to upload service when nothing else is configured
|
|
53
|
+
return (process.env.CTXCE_UPLOAD_ENDPOINT || process.env.UPLOAD_ENDPOINT || "http://localhost:8004").trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
function requireBackendUrl(backendUrl) {
|
|
45
57
|
const url = getBackendUrl(backendUrl);
|
|
46
58
|
if (!url) {
|
|
@@ -67,7 +79,26 @@ function outputJsonStatus(url, state, entry, rawExpires) {
|
|
|
67
79
|
|
|
68
80
|
async function doLogin(args) {
|
|
69
81
|
const { backendUrl, token, username, password } = parseAuthArgs(args);
|
|
70
|
-
|
|
82
|
+
let url = getBackendUrl(backendUrl);
|
|
83
|
+
if (!url) {
|
|
84
|
+
// Fallback: use any stored auth entry when no backend is provided
|
|
85
|
+
const any = loadAnyAuthEntry();
|
|
86
|
+
if (any && any.backendUrl) {
|
|
87
|
+
url = any.backendUrl;
|
|
88
|
+
console.error("[ctxce] Using stored backend for login:", url);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!url) {
|
|
92
|
+
// Final fallback: default upload endpoint (extension's upload endpoint or localhost:8004)
|
|
93
|
+
url = getDefaultUploadBackend();
|
|
94
|
+
if (url) {
|
|
95
|
+
console.error("[ctxce] Using default upload backend for login:", url);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!url) {
|
|
99
|
+
console.error("[ctxce] Auth backend URL not configured. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
71
102
|
const trimmedUser = (username || "").trim();
|
|
72
103
|
const usePassword = trimmedUser && (password || "").length > 0;
|
|
73
104
|
|
|
@@ -124,13 +155,30 @@ async function doLogin(args) {
|
|
|
124
155
|
|
|
125
156
|
async function doStatus(args) {
|
|
126
157
|
const { backendUrl, outputJson } = parseAuthArgs(args);
|
|
127
|
-
|
|
158
|
+
let url = getBackendUrl(backendUrl);
|
|
159
|
+
let usedFallback = false;
|
|
160
|
+
if (!url) {
|
|
161
|
+
// Fallback: use any stored auth entry when no backend is provided
|
|
162
|
+
const any = loadAnyAuthEntry();
|
|
163
|
+
if (any && any.backendUrl) {
|
|
164
|
+
url = any.backendUrl;
|
|
165
|
+
usedFallback = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!url) {
|
|
169
|
+
// Final fallback: default upload endpoint
|
|
170
|
+
url = getDefaultUploadBackend();
|
|
171
|
+
if (url) {
|
|
172
|
+
usedFallback = true;
|
|
173
|
+
console.error("[ctxce] Using default upload backend for status:", url);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
128
176
|
if (!url) {
|
|
129
177
|
if (outputJson) {
|
|
130
178
|
outputJsonStatus("", "missing_backend", null, null);
|
|
131
179
|
process.exit(1);
|
|
132
180
|
}
|
|
133
|
-
console.error("[ctxce] Auth backend URL not configured. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
|
|
181
|
+
console.error("[ctxce] Auth backend URL not configured and no stored sessions found. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
|
|
134
182
|
process.exit(1);
|
|
135
183
|
}
|
|
136
184
|
let entry;
|
|
@@ -149,7 +197,11 @@ async function doStatus(args) {
|
|
|
149
197
|
outputJsonStatus(url, "missing", null, rawExpires);
|
|
150
198
|
process.exit(1);
|
|
151
199
|
}
|
|
152
|
-
|
|
200
|
+
if (usedFallback) {
|
|
201
|
+
console.error("[ctxce] Not logged in for stored backend", url);
|
|
202
|
+
} else {
|
|
203
|
+
console.error("[ctxce] Not logged in for", url);
|
|
204
|
+
}
|
|
153
205
|
process.exit(1);
|
|
154
206
|
}
|
|
155
207
|
|
|
@@ -158,7 +210,11 @@ async function doStatus(args) {
|
|
|
158
210
|
outputJsonStatus(url, "expired", entry, rawExpires);
|
|
159
211
|
process.exit(2);
|
|
160
212
|
}
|
|
161
|
-
|
|
213
|
+
if (usedFallback) {
|
|
214
|
+
console.error("[ctxce] Stored auth session appears expired for stored backend", url);
|
|
215
|
+
} else {
|
|
216
|
+
console.error("[ctxce] Stored auth session appears expired for", url);
|
|
217
|
+
}
|
|
162
218
|
if (rawExpires) {
|
|
163
219
|
console.error("[ctxce] Session expired at", rawExpires);
|
|
164
220
|
}
|
|
@@ -169,6 +225,9 @@ async function doStatus(args) {
|
|
|
169
225
|
outputJsonStatus(url, "ok", entry, rawExpires);
|
|
170
226
|
return;
|
|
171
227
|
}
|
|
228
|
+
if (usedFallback) {
|
|
229
|
+
console.error("[ctxce] Using stored backend for status:", url);
|
|
230
|
+
}
|
|
172
231
|
console.error("[ctxce] Logged in to", url, "as", entry.userId || "<unknown>");
|
|
173
232
|
if (rawExpires) {
|
|
174
233
|
console.error("[ctxce] Session expires at", rawExpires);
|
|
@@ -177,7 +236,26 @@ async function doStatus(args) {
|
|
|
177
236
|
|
|
178
237
|
async function doLogout(args) {
|
|
179
238
|
const { backendUrl } = parseAuthArgs(args);
|
|
180
|
-
|
|
239
|
+
let url = getBackendUrl(backendUrl);
|
|
240
|
+
if (!url) {
|
|
241
|
+
// Fallback: use any stored auth entry when no backend is provided
|
|
242
|
+
const any = loadAnyAuthEntry();
|
|
243
|
+
if (any && any.backendUrl) {
|
|
244
|
+
url = any.backendUrl;
|
|
245
|
+
console.error("[ctxce] Using stored backend for logout:", url);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!url) {
|
|
249
|
+
// Final fallback: default upload endpoint
|
|
250
|
+
url = getDefaultUploadBackend();
|
|
251
|
+
if (url) {
|
|
252
|
+
console.error("[ctxce] Using default upload backend for logout:", url);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!url) {
|
|
256
|
+
console.error("[ctxce] Auth backend URL not configured and no stored sessions found. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
181
259
|
const entry = loadAuthEntry(url);
|
|
182
260
|
if (!entry) {
|
|
183
261
|
console.error("[ctxce] No stored auth session for", url);
|
package/src/mcpServer.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
10
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
11
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { loadAnyAuthEntry, loadAuthEntry } from "./authConfig.js";
|
|
13
|
+
import { maybeRemapToolResult } from "./resultPathMapping.js";
|
|
14
|
+
|
|
1
15
|
function debugLog(message) {
|
|
2
16
|
try {
|
|
3
17
|
const text = typeof message === "string" ? message : String(message);
|
|
@@ -236,19 +250,6 @@ function isTransientToolError(error) {
|
|
|
236
250
|
// Acts as a low-level proxy for tools, forwarding tools/list and tools/call
|
|
237
251
|
// to the remote qdrant-indexer MCP server while adding a local `ping` tool.
|
|
238
252
|
|
|
239
|
-
import process from "node:process";
|
|
240
|
-
import fs from "node:fs";
|
|
241
|
-
import path from "node:path";
|
|
242
|
-
import { execSync } from "node:child_process";
|
|
243
|
-
import { createServer } from "node:http";
|
|
244
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
245
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
246
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
247
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
248
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
249
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
250
|
-
import { loadAnyAuthEntry, loadAuthEntry } from "./authConfig.js";
|
|
251
|
-
|
|
252
253
|
async function createBridgeServer(options) {
|
|
253
254
|
const workspace = options.workspace || process.cwd();
|
|
254
255
|
const indexerUrl = options.indexerUrl;
|
|
@@ -286,18 +287,20 @@ async function createBridgeServer(options) {
|
|
|
286
287
|
const authBackendUrl = process.env.CTXCE_AUTH_BACKEND_URL || "";
|
|
287
288
|
let sessionId = explicitSession;
|
|
288
289
|
|
|
289
|
-
|
|
290
|
+
function resolveSessionId() {
|
|
291
|
+
const explicit = process.env.CTXCE_SESSION_ID || "";
|
|
292
|
+
if (explicit) {
|
|
293
|
+
return explicit;
|
|
294
|
+
}
|
|
290
295
|
let backendToUse = authBackendUrl;
|
|
291
296
|
let entry = null;
|
|
292
|
-
|
|
293
297
|
if (backendToUse) {
|
|
294
298
|
try {
|
|
295
299
|
entry = loadAuthEntry(backendToUse);
|
|
296
|
-
} catch
|
|
300
|
+
} catch {
|
|
297
301
|
entry = null;
|
|
298
302
|
}
|
|
299
303
|
}
|
|
300
|
-
|
|
301
304
|
if (!entry) {
|
|
302
305
|
try {
|
|
303
306
|
const any = loadAnyAuthEntry();
|
|
@@ -305,11 +308,10 @@ async function createBridgeServer(options) {
|
|
|
305
308
|
backendToUse = any.backendUrl;
|
|
306
309
|
entry = any.entry;
|
|
307
310
|
}
|
|
308
|
-
} catch
|
|
311
|
+
} catch {
|
|
309
312
|
entry = null;
|
|
310
313
|
}
|
|
311
314
|
}
|
|
312
|
-
|
|
313
315
|
if (entry) {
|
|
314
316
|
let expired = false;
|
|
315
317
|
const rawExpires = entry.expiresAt;
|
|
@@ -320,11 +322,17 @@ async function createBridgeServer(options) {
|
|
|
320
322
|
}
|
|
321
323
|
}
|
|
322
324
|
if (!expired && typeof entry.sessionId === "string" && entry.sessionId) {
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
+
return entry.sessionId;
|
|
326
|
+
}
|
|
327
|
+
if (expired) {
|
|
325
328
|
debugLog("[ctxce] Stored auth session appears expired; please run `ctxce auth login` again.");
|
|
326
329
|
}
|
|
327
330
|
}
|
|
331
|
+
return "";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!sessionId) {
|
|
335
|
+
sessionId = resolveSessionId();
|
|
328
336
|
}
|
|
329
337
|
|
|
330
338
|
if (!sessionId) {
|
|
@@ -477,7 +485,16 @@ async function createBridgeServer(options) {
|
|
|
477
485
|
|
|
478
486
|
debugLog(`[ctxce] tools/call: ${name || "<no-name>"}`);
|
|
479
487
|
|
|
480
|
-
//
|
|
488
|
+
// Refresh session before each call; re-init clients if session changes.
|
|
489
|
+
const freshSession = resolveSessionId() || sessionId;
|
|
490
|
+
if (freshSession && freshSession !== sessionId) {
|
|
491
|
+
sessionId = freshSession;
|
|
492
|
+
try {
|
|
493
|
+
await initializeRemoteClients(true);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
debugLog("[ctxce] Failed to reinitialize clients after session refresh: " + String(err));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
481
498
|
if (sessionId && (args === undefined || args === null || typeof args === "object")) {
|
|
482
499
|
const obj = args && typeof args === "object" ? { ...args } : {};
|
|
483
500
|
if (!Object.prototype.hasOwnProperty.call(obj, "session")) {
|
|
@@ -525,7 +542,7 @@ async function createBridgeServer(options) {
|
|
|
525
542
|
undefined,
|
|
526
543
|
{ timeout: timeoutMs },
|
|
527
544
|
);
|
|
528
|
-
return result;
|
|
545
|
+
return maybeRemapToolResult(name, result, workspace);
|
|
529
546
|
} catch (err) {
|
|
530
547
|
lastError = err;
|
|
531
548
|
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
function envTruthy(value, defaultVal = false) {
|
|
6
|
+
try {
|
|
7
|
+
if (value === undefined || value === null) {
|
|
8
|
+
return defaultVal;
|
|
9
|
+
}
|
|
10
|
+
const s = String(value).trim().toLowerCase();
|
|
11
|
+
if (!s) {
|
|
12
|
+
return defaultVal;
|
|
13
|
+
}
|
|
14
|
+
return s === "1" || s === "true" || s === "yes" || s === "on";
|
|
15
|
+
} catch {
|
|
16
|
+
return defaultVal;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _posixToNative(rel) {
|
|
21
|
+
try {
|
|
22
|
+
if (!rel) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return String(rel).split("/").join(path.sep);
|
|
26
|
+
} catch {
|
|
27
|
+
return rel;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function computeWorkspaceRelativePath(containerPath, hostPath) {
|
|
32
|
+
try {
|
|
33
|
+
const cont = typeof containerPath === "string" ? containerPath.trim() : "";
|
|
34
|
+
if (cont.startsWith("/work/")) {
|
|
35
|
+
const rest = cont.slice("/work/".length);
|
|
36
|
+
const parts = rest.split("/").filter(Boolean);
|
|
37
|
+
if (parts.length >= 2) {
|
|
38
|
+
return parts.slice(1).join("/");
|
|
39
|
+
}
|
|
40
|
+
if (parts.length === 1) {
|
|
41
|
+
return parts[0];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const hp = typeof hostPath === "string" ? hostPath.trim() : "";
|
|
48
|
+
if (!hp) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
// If we don't have a container path, at least try to return a basename.
|
|
52
|
+
return path.posix.basename(hp.replace(/\\/g, "/"));
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function remapRelatedPathToClient(p, workspaceRoot) {
|
|
59
|
+
try {
|
|
60
|
+
const s = typeof p === "string" ? p : "";
|
|
61
|
+
const root = typeof workspaceRoot === "string" ? workspaceRoot : "";
|
|
62
|
+
if (!s || !root) {
|
|
63
|
+
return p;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sNorm = s.replace(/\\/g, path.sep);
|
|
67
|
+
if (sNorm.startsWith(root + path.sep) || sNorm === root) {
|
|
68
|
+
return sNorm;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (s.startsWith("/work/")) {
|
|
72
|
+
const rest = s.slice("/work/".length);
|
|
73
|
+
const parts = rest.split("/").filter(Boolean);
|
|
74
|
+
if (parts.length >= 2) {
|
|
75
|
+
const rel = parts.slice(1).join("/");
|
|
76
|
+
const relNative = _posixToNative(rel);
|
|
77
|
+
return path.join(root, relNative);
|
|
78
|
+
}
|
|
79
|
+
return p;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If it's already a relative path, join it to the workspace root.
|
|
83
|
+
if (!s.startsWith("/") && !s.includes(":") && !s.includes("\\")) {
|
|
84
|
+
const relPosix = s.trim();
|
|
85
|
+
if (relPosix && relPosix !== "." && !relPosix.startsWith("../") && relPosix !== "..") {
|
|
86
|
+
const relNative = _posixToNative(relPosix);
|
|
87
|
+
const joined = path.join(root, relNative);
|
|
88
|
+
const relCheck = path.relative(root, joined);
|
|
89
|
+
if (relCheck && !relCheck.startsWith(`..${path.sep}`) && relCheck !== "..") {
|
|
90
|
+
return joined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return p;
|
|
96
|
+
} catch {
|
|
97
|
+
return p;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function remapHitPaths(hit, workspaceRoot) {
|
|
102
|
+
if (!hit || typeof hit !== "object") {
|
|
103
|
+
return hit;
|
|
104
|
+
}
|
|
105
|
+
const rawPath = typeof hit.path === "string" ? hit.path : "";
|
|
106
|
+
let hostPath = typeof hit.host_path === "string" ? hit.host_path : "";
|
|
107
|
+
let containerPath = typeof hit.container_path === "string" ? hit.container_path : "";
|
|
108
|
+
if (!hostPath && rawPath) {
|
|
109
|
+
hostPath = rawPath;
|
|
110
|
+
}
|
|
111
|
+
if (!containerPath && rawPath) {
|
|
112
|
+
containerPath = rawPath;
|
|
113
|
+
}
|
|
114
|
+
const relPath = computeWorkspaceRelativePath(containerPath, hostPath);
|
|
115
|
+
const out = { ...hit };
|
|
116
|
+
if (relPath) {
|
|
117
|
+
out.rel_path = relPath;
|
|
118
|
+
}
|
|
119
|
+
// Remap related_paths nested under each hit (repo_search/hybrid_search emit this per result).
|
|
120
|
+
try {
|
|
121
|
+
if (Array.isArray(out.related_paths)) {
|
|
122
|
+
out.related_paths = out.related_paths.map((p) => remapRelatedPathToClient(p, workspaceRoot));
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// ignore
|
|
126
|
+
}
|
|
127
|
+
if (workspaceRoot && relPath) {
|
|
128
|
+
try {
|
|
129
|
+
const relNative = _posixToNative(relPath);
|
|
130
|
+
const candidate = path.join(workspaceRoot, relNative);
|
|
131
|
+
const diagnostics = envTruthy(process.env.CTXCE_BRIDGE_PATH_DIAGNOSTICS, false);
|
|
132
|
+
const strictClientPath = envTruthy(process.env.CTXCE_BRIDGE_CLIENT_PATH_STRICT, false);
|
|
133
|
+
if (strictClientPath) {
|
|
134
|
+
out.client_path = candidate;
|
|
135
|
+
if (diagnostics) {
|
|
136
|
+
out.client_path_joined = candidate;
|
|
137
|
+
out.client_path_source = "workspace_join";
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// Prefer a host_path that is within the current bridge workspace.
|
|
141
|
+
// This keeps provenance (host_path) intact while providing a user-local
|
|
142
|
+
// absolute path even when the bridge workspace is a parent directory.
|
|
143
|
+
const hp = typeof hostPath === "string" ? hostPath : "";
|
|
144
|
+
const hpNorm = hp ? hp.replace(/\\/g, path.sep) : "";
|
|
145
|
+
if (
|
|
146
|
+
hpNorm &&
|
|
147
|
+
hpNorm.startsWith(workspaceRoot) &&
|
|
148
|
+
(!fs.existsSync(candidate) || fs.existsSync(hpNorm))
|
|
149
|
+
) {
|
|
150
|
+
out.client_path = hpNorm;
|
|
151
|
+
if (diagnostics) {
|
|
152
|
+
out.client_path_joined = candidate;
|
|
153
|
+
out.client_path_source = "host_path";
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
out.client_path = candidate;
|
|
157
|
+
if (diagnostics) {
|
|
158
|
+
out.client_path_joined = candidate;
|
|
159
|
+
out.client_path_source = "workspace_join";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// ignore
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const overridePath = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
|
|
168
|
+
if (overridePath) {
|
|
169
|
+
if (typeof out.client_path === "string" && out.client_path) {
|
|
170
|
+
out.path = out.client_path;
|
|
171
|
+
} else if (relPath) {
|
|
172
|
+
out.path = relPath;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function remapStringPath(p, workspaceRoot) {
|
|
179
|
+
try {
|
|
180
|
+
const s = typeof p === "string" ? p : "";
|
|
181
|
+
if (!s) {
|
|
182
|
+
return p;
|
|
183
|
+
}
|
|
184
|
+
// If this is already a path within the current client workspace, rewrite to a
|
|
185
|
+
// workspace-relative string when override is enabled.
|
|
186
|
+
try {
|
|
187
|
+
const root = typeof workspaceRoot === "string" ? workspaceRoot : "";
|
|
188
|
+
if (root) {
|
|
189
|
+
const sNorm = s.replace(/\\/g, path.sep);
|
|
190
|
+
if (sNorm.startsWith(root + path.sep) || sNorm === root) {
|
|
191
|
+
const relNative = path.relative(root, sNorm);
|
|
192
|
+
const relPosix = String(relNative).split(path.sep).join("/");
|
|
193
|
+
if (relPosix && !relPosix.startsWith("../") && relPosix !== "..") {
|
|
194
|
+
const override = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
|
|
195
|
+
if (override) {
|
|
196
|
+
return relPosix;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
// ignore
|
|
203
|
+
}
|
|
204
|
+
if (s.startsWith("/work/")) {
|
|
205
|
+
const rest = s.slice("/work/".length);
|
|
206
|
+
const parts = rest.split("/").filter(Boolean);
|
|
207
|
+
if (parts.length >= 2) {
|
|
208
|
+
const rel = parts.slice(1).join("/");
|
|
209
|
+
const override = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
|
|
210
|
+
if (override) {
|
|
211
|
+
return rel;
|
|
212
|
+
}
|
|
213
|
+
return p;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return p;
|
|
217
|
+
} catch {
|
|
218
|
+
return p;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function maybeParseToolJson(result) {
|
|
223
|
+
try {
|
|
224
|
+
if (
|
|
225
|
+
result &&
|
|
226
|
+
typeof result === "object" &&
|
|
227
|
+
result.structuredContent &&
|
|
228
|
+
typeof result.structuredContent === "object"
|
|
229
|
+
) {
|
|
230
|
+
return { mode: "structured", value: result.structuredContent };
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const content = result && result.content;
|
|
236
|
+
if (!Array.isArray(content)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const first = content.find(
|
|
240
|
+
(c) => c && c.type === "text" && typeof c.text === "string",
|
|
241
|
+
);
|
|
242
|
+
if (!first) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const txt = String(first.text || "").trim();
|
|
246
|
+
if (!txt || !(txt.startsWith("{") || txt.startsWith("["))) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return { mode: "text", value: JSON.parse(txt) };
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function applyPathMappingToPayload(payload, workspaceRoot) {
|
|
256
|
+
if (!payload || typeof payload !== "object") {
|
|
257
|
+
return payload;
|
|
258
|
+
}
|
|
259
|
+
const out = Array.isArray(payload) ? payload.slice() : { ...payload };
|
|
260
|
+
|
|
261
|
+
const mapHitsArray = (arr) => {
|
|
262
|
+
if (!Array.isArray(arr)) {
|
|
263
|
+
return arr;
|
|
264
|
+
}
|
|
265
|
+
return arr.map((h) => remapHitPaths(h, workspaceRoot));
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Common result shapes across tools
|
|
269
|
+
if (Array.isArray(out.results)) {
|
|
270
|
+
out.results = mapHitsArray(out.results);
|
|
271
|
+
}
|
|
272
|
+
if (Array.isArray(out.citations)) {
|
|
273
|
+
out.citations = mapHitsArray(out.citations);
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(out.related_paths)) {
|
|
276
|
+
out.related_paths = out.related_paths.map((p) => remapRelatedPathToClient(p, workspaceRoot));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Some tools nest under {result:{...}}
|
|
280
|
+
if (out.result && typeof out.result === "object") {
|
|
281
|
+
out.result = applyPathMappingToPayload(out.result, workspaceRoot);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function maybeRemapToolResult(name, result, workspaceRoot) {
|
|
288
|
+
try {
|
|
289
|
+
if (!name || !result || !workspaceRoot) {
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
const enabled = envTruthy(process.env.CTXCE_BRIDGE_MAP_PATHS, true);
|
|
293
|
+
if (!enabled) {
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
const lower = String(name).toLowerCase();
|
|
297
|
+
const shouldMap = (
|
|
298
|
+
lower === "repo_search" ||
|
|
299
|
+
lower === "context_search" ||
|
|
300
|
+
lower === "context_answer" ||
|
|
301
|
+
lower.endsWith("search_tests_for") ||
|
|
302
|
+
lower.endsWith("search_config_for") ||
|
|
303
|
+
lower.endsWith("search_callers_for") ||
|
|
304
|
+
lower.endsWith("search_importers_for")
|
|
305
|
+
);
|
|
306
|
+
if (!shouldMap) {
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const parsed = maybeParseToolJson(result);
|
|
311
|
+
if (!parsed) {
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const mapped = applyPathMappingToPayload(parsed.value, workspaceRoot);
|
|
316
|
+
let outResult = result;
|
|
317
|
+
if (parsed.mode === "structured") {
|
|
318
|
+
outResult = { ...result, structuredContent: mapped };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Replace text payload for clients that only read `content[].text`
|
|
322
|
+
try {
|
|
323
|
+
const content = Array.isArray(outResult.content) ? outResult.content.slice() : [];
|
|
324
|
+
const idx = content.findIndex(
|
|
325
|
+
(c) => c && c.type === "text" && typeof c.text === "string",
|
|
326
|
+
);
|
|
327
|
+
if (idx >= 0) {
|
|
328
|
+
content[idx] = { ...content[idx], text: JSON.stringify(mapped) };
|
|
329
|
+
outResult = { ...outResult, content };
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// ignore
|
|
333
|
+
}
|
|
334
|
+
return outResult;
|
|
335
|
+
} catch {
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
}
|