veryfront 0.1.159 → 0.1.161
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/esm/deno.js +1 -1
- package/esm/src/agent/index.d.ts +1 -1
- package/esm/src/agent/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.d.ts +2 -12
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +62 -25
- package/esm/src/agent/types.d.ts +37 -0
- package/esm/src/agent/types.d.ts.map +1 -1
- package/esm/src/mcp/http-transport.d.ts +33 -0
- package/esm/src/mcp/http-transport.d.ts.map +1 -0
- package/esm/src/mcp/http-transport.js +97 -0
- package/esm/src/mcp/server.d.ts.map +1 -1
- package/esm/src/mcp/server.js +14 -107
- package/esm/src/platform/adapters/fs/veryfront/stat-operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/stat-operations.js +13 -45
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/src/agent/index.ts +6 -0
- package/src/src/agent/runtime/index.ts +118 -11
- package/src/src/agent/types.ts +47 -0
- package/src/src/mcp/http-transport.ts +163 -0
- package/src/src/mcp/server.ts +15 -123
- package/src/src/platform/adapters/fs/veryfront/stat-operations.ts +19 -49
- package/src/src/utils/version-constant.ts +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
2
|
+
import type { ToolExecutionContext } from "../tool/index.js";
|
|
3
|
+
import { VeryfrontError } from "../security/input-validation/errors.js";
|
|
4
|
+
import { validateContentType } from "../security/input-validation/limits.js";
|
|
5
|
+
import { SessionManager } from "./session.js";
|
|
6
|
+
|
|
7
|
+
const MAX_REQUEST_BODY_SIZE = 1_048_576; // 1 MB
|
|
8
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
9
|
+
|
|
10
|
+
type JSONRPCParams = Record<string, unknown> | unknown[];
|
|
11
|
+
|
|
12
|
+
interface JSONRPCRequest {
|
|
13
|
+
jsonrpc: "2.0";
|
|
14
|
+
id?: string | number;
|
|
15
|
+
method: string;
|
|
16
|
+
params?: JSONRPCParams;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface JSONRPCResponse {
|
|
20
|
+
jsonrpc: "2.0";
|
|
21
|
+
id?: string | number;
|
|
22
|
+
result?: unknown;
|
|
23
|
+
error?: {
|
|
24
|
+
code: number;
|
|
25
|
+
message: string;
|
|
26
|
+
data?: unknown;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MCPHTTPTransportDependencies {
|
|
31
|
+
authEnabled: boolean;
|
|
32
|
+
getCORSHeaders: (requestOrigin?: string | null) => Record<string, string>;
|
|
33
|
+
validateAuth: (request: dntShim.Request) => Promise<boolean>;
|
|
34
|
+
handleRequest: (
|
|
35
|
+
request: JSONRPCRequest,
|
|
36
|
+
context?: ToolExecutionContext,
|
|
37
|
+
) => Promise<JSONRPCResponse>;
|
|
38
|
+
extractRequestContext: (request: dntShim.Request) => ToolExecutionContext | undefined;
|
|
39
|
+
isOriginAllowed: (requestOrigin?: string | null) => boolean;
|
|
40
|
+
sessionCapabilities: Map<string, Record<string, unknown>>;
|
|
41
|
+
sessionManager: SessionManager;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createJSONResponse(body: unknown, init?: dntShim.ResponseInit): dntShim.Response {
|
|
45
|
+
const headers = new dntShim.Headers(init?.headers);
|
|
46
|
+
headers.set("Content-Type", JSON_CONTENT_TYPE);
|
|
47
|
+
return new dntShim.Response(JSON.stringify(body), { ...init, headers });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createJSONRPCErrorResponse(status: number, code: number, message: string): dntShim.Response {
|
|
51
|
+
return createJSONResponse(
|
|
52
|
+
{
|
|
53
|
+
jsonrpc: "2.0",
|
|
54
|
+
id: null,
|
|
55
|
+
error: { code, message },
|
|
56
|
+
},
|
|
57
|
+
{ status },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createMCPHTTPHandler(
|
|
62
|
+
dependencies: MCPHTTPTransportDependencies,
|
|
63
|
+
): (request: dntShim.Request) => Promise<dntShim.Response> {
|
|
64
|
+
const {
|
|
65
|
+
authEnabled,
|
|
66
|
+
getCORSHeaders,
|
|
67
|
+
validateAuth,
|
|
68
|
+
handleRequest,
|
|
69
|
+
extractRequestContext,
|
|
70
|
+
isOriginAllowed,
|
|
71
|
+
sessionCapabilities,
|
|
72
|
+
sessionManager,
|
|
73
|
+
} = dependencies;
|
|
74
|
+
|
|
75
|
+
return async (request: dntShim.Request) => {
|
|
76
|
+
const requestOrigin = request.headers.get("Origin");
|
|
77
|
+
|
|
78
|
+
if (request.method === "OPTIONS") {
|
|
79
|
+
return new dntShim.Response(null, { status: 204, headers: getCORSHeaders(requestOrigin) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!isOriginAllowed(requestOrigin)) {
|
|
83
|
+
return createJSONRPCErrorResponse(403, -32600, "Forbidden: Origin not allowed");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (authEnabled) {
|
|
87
|
+
const authorized = await validateAuth(request);
|
|
88
|
+
if (!authorized) return new dntShim.Response("Unauthorized", { status: 401 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (request.method === "DELETE") {
|
|
92
|
+
const sessionId = request.headers.get("MCP-Session-Id");
|
|
93
|
+
if (sessionId) {
|
|
94
|
+
sessionManager.terminate(sessionId);
|
|
95
|
+
sessionCapabilities.delete(sessionId);
|
|
96
|
+
}
|
|
97
|
+
return new dntShim.Response(null, { status: 200, headers: getCORSHeaders(requestOrigin) });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (request.method !== "POST") {
|
|
101
|
+
return new dntShim.Response("Method Not Allowed", { status: 405 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const contentLength = request.headers.get("content-length");
|
|
105
|
+
if (contentLength && Number(contentLength) > MAX_REQUEST_BODY_SIZE) {
|
|
106
|
+
return createJSONRPCErrorResponse(413, -32600, "Request body too large");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
validateContentType(request, JSON_CONTENT_TYPE);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const message = error instanceof VeryfrontError ? error.message : "Invalid Content-Type";
|
|
113
|
+
return createJSONRPCErrorResponse(400, -32700, message);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let rpcRequest: JSONRPCRequest;
|
|
117
|
+
try {
|
|
118
|
+
const bodyText = await request.text();
|
|
119
|
+
if (bodyText.length > MAX_REQUEST_BODY_SIZE) {
|
|
120
|
+
return createJSONRPCErrorResponse(413, -32600, "Request body too large");
|
|
121
|
+
}
|
|
122
|
+
rpcRequest = JSON.parse(bodyText) as JSONRPCRequest;
|
|
123
|
+
} catch (_) {
|
|
124
|
+
return createJSONRPCErrorResponse(400, -32700, "Parse error");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const responseHeaders: Record<string, string> = {
|
|
128
|
+
...getCORSHeaders(requestOrigin),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (rpcRequest.method === "initialize") {
|
|
132
|
+
const context = extractRequestContext(request);
|
|
133
|
+
const rpcResponse = await handleRequest(rpcRequest, context);
|
|
134
|
+
const clientCaps =
|
|
135
|
+
((rpcRequest.params as Record<string, unknown> | undefined)?.capabilities ??
|
|
136
|
+
{}) as Record<string, unknown>;
|
|
137
|
+
const sessionId = sessionManager.create();
|
|
138
|
+
sessionCapabilities.set(sessionId, clientCaps);
|
|
139
|
+
responseHeaders["MCP-Session-Id"] = sessionId;
|
|
140
|
+
return createJSONResponse(rpcResponse, { headers: responseHeaders });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sessionManager.size > 0) {
|
|
144
|
+
const sessionId = request.headers.get("MCP-Session-Id");
|
|
145
|
+
if (!sessionId) {
|
|
146
|
+
return createJSONRPCErrorResponse(400, -32600, "Missing MCP-Session-Id header");
|
|
147
|
+
}
|
|
148
|
+
if (!sessionManager.isValid(sessionId)) {
|
|
149
|
+
return createJSONRPCErrorResponse(404, -32600, "Session not found or expired");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (rpcRequest.id === undefined) {
|
|
154
|
+
const context = extractRequestContext(request);
|
|
155
|
+
await handleRequest(rpcRequest, context);
|
|
156
|
+
return new dntShim.Response(null, { status: 202, headers: responseHeaders });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const context = extractRequestContext(request);
|
|
160
|
+
const rpcResponse = await handleRequest(rpcRequest, context);
|
|
161
|
+
return createJSONResponse(rpcResponse, { headers: responseHeaders });
|
|
162
|
+
};
|
|
163
|
+
}
|
package/src/src/mcp/server.ts
CHANGED
|
@@ -9,18 +9,14 @@ import type { MCPServerConfig, ToolListEntry } from "./types.js";
|
|
|
9
9
|
import { createError, toError } from "../errors/veryfront-error.js";
|
|
10
10
|
import { withSpan } from "../observability/tracing/otlp-setup.js";
|
|
11
11
|
import { VERSION } from "../utils/version.js";
|
|
12
|
-
import { validateContentType } from "../security/input-validation/limits.js";
|
|
13
|
-
import { VeryfrontError } from "../security/input-validation/errors.js";
|
|
14
12
|
import type { IntegrationRuntimeConfig } from "../integrations/types.js";
|
|
15
13
|
import { logger as baseLogger } from "../utils/index.js";
|
|
14
|
+
import { createMCPHTTPHandler } from "./http-transport.js";
|
|
16
15
|
import { SessionManager } from "./session.js";
|
|
17
16
|
import { TaskStore } from "./task-store.js";
|
|
18
17
|
|
|
19
18
|
const logger = baseLogger.component("mcp-server");
|
|
20
|
-
|
|
21
|
-
const MAX_REQUEST_BODY_SIZE = 1_048_576; // 1 MB
|
|
22
19
|
const MAX_CONTEXT_HEADER_LENGTH = 255;
|
|
23
|
-
const JSON_CONTENT_TYPE = "application/json";
|
|
24
20
|
const END_USER_ID_PATTERN = /^[a-zA-Z0-9._@-]+$/;
|
|
25
21
|
const PROJECT_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
26
22
|
|
|
@@ -55,23 +51,6 @@ function toParamsRecord(params: JSONRPCParams | undefined): Record<string, unkno
|
|
|
55
51
|
return params;
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
function createJSONResponse(body: unknown, init?: dntShim.ResponseInit): dntShim.Response {
|
|
59
|
-
const headers = new dntShim.Headers(init?.headers);
|
|
60
|
-
headers.set("Content-Type", JSON_CONTENT_TYPE);
|
|
61
|
-
return new dntShim.Response(JSON.stringify(body), { ...init, headers });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function createJSONRPCErrorResponse(status: number, code: number, message: string): dntShim.Response {
|
|
65
|
-
return createJSONResponse(
|
|
66
|
-
{
|
|
67
|
-
jsonrpc: "2.0",
|
|
68
|
-
id: null,
|
|
69
|
-
error: { code, message },
|
|
70
|
-
},
|
|
71
|
-
{ status },
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
54
|
function readAllowedHeader(
|
|
76
55
|
request: dntShim.Request,
|
|
77
56
|
headerName: string,
|
|
@@ -628,107 +607,20 @@ export class MCPServer {
|
|
|
628
607
|
}
|
|
629
608
|
|
|
630
609
|
createHTTPHandler(): (request: dntShim.Request) => Promise<dntShim.Response> {
|
|
631
|
-
return
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
// Auth check (applies to all methods including DELETE)
|
|
647
|
-
if (this.config.auth?.type && this.config.auth.type !== "none") {
|
|
648
|
-
const authorized = await this.validateAuth(request);
|
|
649
|
-
if (!authorized) return new dntShim.Response("Unauthorized", { status: 401 });
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// DELETE = terminate session
|
|
653
|
-
if (request.method === "DELETE") {
|
|
654
|
-
const sessionId = request.headers.get("MCP-Session-Id");
|
|
655
|
-
if (sessionId) {
|
|
656
|
-
this.sessionManager.terminate(sessionId);
|
|
657
|
-
this.sessionCapabilities.delete(sessionId);
|
|
658
|
-
}
|
|
659
|
-
return new dntShim.Response(null, { status: 200, headers: this.getCORSHeaders(requestOrigin) });
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Only POST allowed for JSON-RPC messages
|
|
663
|
-
if (request.method !== "POST") {
|
|
664
|
-
return new dntShim.Response("Method Not Allowed", { status: 405 });
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Enforce request body size limit (fast path via Content-Length header)
|
|
668
|
-
const contentLength = request.headers.get("content-length");
|
|
669
|
-
if (contentLength && Number(contentLength) > MAX_REQUEST_BODY_SIZE) {
|
|
670
|
-
return createJSONRPCErrorResponse(413, -32600, "Request body too large");
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
try {
|
|
674
|
-
validateContentType(request, JSON_CONTENT_TYPE);
|
|
675
|
-
} catch (error) {
|
|
676
|
-
const message = error instanceof VeryfrontError ? error.message : "Invalid Content-Type";
|
|
677
|
-
return createJSONRPCErrorResponse(400, -32700, message);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
let rpcRequest: JSONRPCRequest;
|
|
681
|
-
try {
|
|
682
|
-
const bodyText = await request.text();
|
|
683
|
-
if (bodyText.length > MAX_REQUEST_BODY_SIZE) {
|
|
684
|
-
return createJSONRPCErrorResponse(413, -32600, "Request body too large");
|
|
685
|
-
}
|
|
686
|
-
rpcRequest = JSON.parse(bodyText) as JSONRPCRequest;
|
|
687
|
-
} catch (_) {
|
|
688
|
-
// expected: malformed JSON in request body
|
|
689
|
-
return createJSONRPCErrorResponse(400, -32700, "Parse error");
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Session management: initialize creates session, everything else requires it
|
|
693
|
-
const responseHeaders: Record<string, string> = {
|
|
694
|
-
...this.getCORSHeaders(requestOrigin),
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
if (rpcRequest.method === "initialize") {
|
|
698
|
-
const context = this.extractRequestContext(request);
|
|
699
|
-
const rpcResponse = await this.handleRequest(rpcRequest, context);
|
|
700
|
-
const clientCaps =
|
|
701
|
-
toParamsRecord(rpcRequest.params).capabilities as Record<string, unknown> ??
|
|
702
|
-
{};
|
|
703
|
-
const sessionId = this.sessionManager.create();
|
|
704
|
-
this.sessionCapabilities.set(sessionId, clientCaps);
|
|
705
|
-
responseHeaders["MCP-Session-Id"] = sessionId;
|
|
706
|
-
return createJSONResponse(rpcResponse, { headers: responseHeaders });
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Post-init: require session ID when sessions are active
|
|
710
|
-
if (this.sessionManager.size > 0) {
|
|
711
|
-
const sessionId = request.headers.get("MCP-Session-Id");
|
|
712
|
-
if (!sessionId) {
|
|
713
|
-
return createJSONRPCErrorResponse(400, -32600, "Missing MCP-Session-Id header");
|
|
714
|
-
}
|
|
715
|
-
if (!this.sessionManager.isValid(sessionId)) {
|
|
716
|
-
return createJSONRPCErrorResponse(404, -32600, "Session not found or expired");
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Notifications have no id member — return 202 Accepted
|
|
721
|
-
// Note: id:0 is a valid request ID per JSON-RPC 2.0, so check for undefined
|
|
722
|
-
if (rpcRequest.id === undefined) {
|
|
723
|
-
const context = this.extractRequestContext(request);
|
|
724
|
-
await this.handleRequest(rpcRequest, context);
|
|
725
|
-
return new dntShim.Response(null, { status: 202, headers: responseHeaders });
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const context = this.extractRequestContext(request);
|
|
729
|
-
const rpcResponse = await this.handleRequest(rpcRequest, context);
|
|
730
|
-
return createJSONResponse(rpcResponse, { headers: responseHeaders });
|
|
731
|
-
};
|
|
610
|
+
return createMCPHTTPHandler({
|
|
611
|
+
authEnabled: Boolean(this.config.auth?.type && this.config.auth.type !== "none"),
|
|
612
|
+
getCORSHeaders: (requestOrigin) => this.getCORSHeaders(requestOrigin),
|
|
613
|
+
validateAuth: (request) => this.validateAuth(request),
|
|
614
|
+
handleRequest: (request, context) => this.handleRequest(request, context),
|
|
615
|
+
extractRequestContext: (request) => this.extractRequestContext(request),
|
|
616
|
+
isOriginAllowed: (requestOrigin) =>
|
|
617
|
+
!requestOrigin ||
|
|
618
|
+
!this.config.cors?.enabled ||
|
|
619
|
+
!this.config.cors.origins?.length ||
|
|
620
|
+
this.config.cors.origins.includes(requestOrigin),
|
|
621
|
+
sessionCapabilities: this.sessionCapabilities,
|
|
622
|
+
sessionManager: this.sessionManager,
|
|
623
|
+
});
|
|
732
624
|
}
|
|
733
625
|
|
|
734
626
|
private extractRequestContext(request: dntShim.Request): ToolExecutionContext | undefined {
|
|
@@ -329,6 +329,7 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
329
329
|
private buildResolveSearchPatterns(
|
|
330
330
|
normalizedPath: string,
|
|
331
331
|
options?: ResolveFileOptions,
|
|
332
|
+
knownExtensionFallback: "exact" | "wildcard" = "exact",
|
|
332
333
|
): string[] {
|
|
333
334
|
const patterns = new Set<string>();
|
|
334
335
|
const pathWithoutExt = stripKnownExtension(normalizedPath, EXTENSION_PRIORITY);
|
|
@@ -338,7 +339,9 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
338
339
|
};
|
|
339
340
|
|
|
340
341
|
if (EXTENSION_PRIORITY.some((ext) => normalizedPath.endsWith(ext))) {
|
|
341
|
-
addPattern(
|
|
342
|
+
addPattern(
|
|
343
|
+
knownExtensionFallback === "wildcard" ? `${pathWithoutExt}.*` : normalizedPath,
|
|
344
|
+
);
|
|
342
345
|
return [...patterns];
|
|
343
346
|
}
|
|
344
347
|
|
|
@@ -366,6 +369,7 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
366
369
|
private async tryResolveViaApiSearch(
|
|
367
370
|
normalizedPath: string,
|
|
368
371
|
options?: ResolveFileOptions,
|
|
372
|
+
knownExtensionFallback: "exact" | "wildcard" = "exact",
|
|
369
373
|
): Promise<string | null | undefined> {
|
|
370
374
|
if (isFrameworkSourcePath(normalizedPath)) {
|
|
371
375
|
logger.debug("Skipping API search for framework path", { normalizedPath });
|
|
@@ -377,7 +381,11 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
377
381
|
return undefined;
|
|
378
382
|
}
|
|
379
383
|
|
|
380
|
-
const patterns = this.buildResolveSearchPatterns(
|
|
384
|
+
const patterns = this.buildResolveSearchPatterns(
|
|
385
|
+
normalizedPath,
|
|
386
|
+
options,
|
|
387
|
+
knownExtensionFallback,
|
|
388
|
+
);
|
|
381
389
|
let sawSuccessfulSearch = false;
|
|
382
390
|
|
|
383
391
|
for (const pattern of patterns) {
|
|
@@ -573,55 +581,17 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
573
581
|
return null;
|
|
574
582
|
}
|
|
575
583
|
|
|
576
|
-
// NOTE:
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
return null;
|
|
584
|
+
// NOTE: Keep the post-index API fallback aligned with the pre-index helper for extensionless
|
|
585
|
+
// paths, while preserving the older wildcard sibling-extension lookup for known-extension
|
|
586
|
+
// paths. Incomplete file-list snapshots otherwise hide valid files until the cache refreshes.
|
|
587
|
+
const apiResolved = await this.tryResolveViaApiSearch(normalizedPath, options, "wildcard");
|
|
588
|
+
if (typeof apiResolved === "string") {
|
|
589
|
+
this.cache.set(cacheKey, apiResolved);
|
|
590
|
+
return apiResolved;
|
|
584
591
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
logger.debug("Searching for file via API", {
|
|
588
|
-
pattern: searchPattern,
|
|
589
|
-
normalizedPath,
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
try {
|
|
593
|
-
const matches = await this.client.searchFiles(searchPattern);
|
|
594
|
-
this.apiSearchCircuitBreaker.recordSuccess();
|
|
595
|
-
|
|
596
|
-
logger.debug("API search result", {
|
|
597
|
-
pattern: searchPattern,
|
|
598
|
-
matchCount: matches.length,
|
|
599
|
-
matches: matches.map((m) => m.path).slice(0, 5),
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
const sortedMatches = sortPathsByExtensionPriority(matches, EXTENSION_PRIORITY);
|
|
603
|
-
const first = sortedMatches[0];
|
|
604
|
-
if (first) {
|
|
605
|
-
logger.debug("resolveFile found via API search", { path: first.path });
|
|
606
|
-
this.cache.set(cacheKey, first.path);
|
|
607
|
-
return first.path;
|
|
608
|
-
}
|
|
609
|
-
} catch (error) {
|
|
610
|
-
const result = this.apiSearchCircuitBreaker.recordFailure();
|
|
611
|
-
if (result.tripped) {
|
|
612
|
-
logger.warn("API search circuit breaker tripped", {
|
|
613
|
-
failures: result.failures,
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
logger.error("API pattern search failed", { pattern: searchPattern, error });
|
|
592
|
+
if (apiResolved === null) {
|
|
593
|
+
this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
|
|
617
594
|
}
|
|
618
|
-
|
|
619
|
-
logger.debug("resolveFile not found after API search", {
|
|
620
|
-
normalizedPath,
|
|
621
|
-
pathWithoutExt,
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
|
|
625
595
|
return null;
|
|
626
596
|
}
|
|
627
597
|
}
|