veryfront 0.1.159 → 0.1.162
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/base-operations.d.ts +1 -1
- package/esm/src/platform/adapters/fs/veryfront/base-operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/directory-operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/directory-operations.js +9 -52
- package/esm/src/platform/adapters/fs/veryfront/file-list-access.d.ts +33 -0
- package/esm/src/platform/adapters/fs/veryfront/file-list-access.d.ts.map +1 -0
- package/esm/src/platform/adapters/fs/veryfront/file-list-access.js +49 -0
- package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts +1 -20
- package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/stat-operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/stat-operations.js +21 -94
- package/esm/src/tool/factory.d.ts +3 -5
- package/esm/src/tool/factory.d.ts.map +1 -1
- package/esm/src/tool/factory.js +2 -1
- package/esm/src/tool/index.d.ts +2 -0
- package/esm/src/tool/index.d.ts.map +1 -1
- package/esm/src/tool/index.js +1 -0
- package/esm/src/tool/remote-source-tools.d.ts +8 -0
- package/esm/src/tool/remote-source-tools.d.ts.map +1 -0
- package/esm/src/tool/remote-source-tools.js +33 -0
- 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/base-operations.ts +1 -1
- package/src/src/platform/adapters/fs/veryfront/directory-operations.ts +10 -75
- package/src/src/platform/adapters/fs/veryfront/file-list-access.ts +109 -0
- package/src/src/platform/adapters/fs/veryfront/read-operations.ts +1 -22
- package/src/src/platform/adapters/fs/veryfront/stat-operations.ts +27 -120
- package/src/src/tool/factory.ts +4 -6
- package/src/src/tool/index.ts +5 -0
- package/src/src/tool/remote-source-tools.ts +54 -0
- package/src/src/utils/version-constant.ts +1 -1
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 {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { VeryfrontApiClient } from "../../veryfront-api-client/index.js";
|
|
2
2
|
import { FileCache } from "../cache/file-cache.js";
|
|
3
|
+
import type { ContentContextProvider } from "./file-list-access.js";
|
|
3
4
|
import { PathNormalizer } from "./path-normalizer.js";
|
|
4
|
-
import type { ContentContextProvider } from "./read-operations.js";
|
|
5
5
|
|
|
6
6
|
export class VeryfrontOperationsBase {
|
|
7
7
|
constructor(
|
|
@@ -2,13 +2,9 @@ import { logger as baseLogger } from "../../../../utils/index.js";
|
|
|
2
2
|
import type { DirectoryEntry } from "./types.js";
|
|
3
3
|
import type { ProjectFile } from "../../veryfront-api-client/index.js";
|
|
4
4
|
import { VeryfrontOperationsBase } from "./base-operations.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
buildFileCacheKeyPrefix,
|
|
8
|
-
buildFileListCacheKey,
|
|
9
|
-
} from "./cache-keys.js";
|
|
5
|
+
import { buildDirCacheKeyPrefix } from "./cache-keys.js";
|
|
6
|
+
import { loadAllProjectFiles } from "./file-list-access.js";
|
|
10
7
|
import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
|
|
11
|
-
import { withRetryOnTransient } from "./retry.js";
|
|
12
8
|
|
|
13
9
|
const logger = baseLogger.component("directory-operations");
|
|
14
10
|
|
|
@@ -154,74 +150,13 @@ export class DirectoryOperations extends VeryfrontOperationsBase {
|
|
|
154
150
|
}
|
|
155
151
|
|
|
156
152
|
private getAllFilesRaw(): Promise<ProjectFile[]> {
|
|
157
|
-
return withSpan("fs.veryfront.getAllFilesRaw",
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
? await this.contextProvider?.getFileList?.()
|
|
166
|
-
: undefined;
|
|
167
|
-
|
|
168
|
-
if (adapterFiles) {
|
|
169
|
-
const cacheMs = Math.round(performance.now() - cacheStart);
|
|
170
|
-
logger.debug("getAllFilesRaw - from adapter cache", {
|
|
171
|
-
cacheMs,
|
|
172
|
-
fileCount: adapterFiles.length,
|
|
173
|
-
});
|
|
174
|
-
return adapterFiles as ProjectFile[];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const cacheKey = buildFileListCacheKey(ctx);
|
|
178
|
-
|
|
179
|
-
if (skipPersistentCache) {
|
|
180
|
-
logger.debug("getAllFilesRaw - skipping persistent cache", {
|
|
181
|
-
cacheKey,
|
|
182
|
-
cacheKeyPrefix,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const cached = skipPersistentCache
|
|
187
|
-
? undefined
|
|
188
|
-
: await this.cache.getAsync<ProjectFile[]>(cacheKey);
|
|
189
|
-
|
|
190
|
-
const cacheMs = Math.round(performance.now() - cacheStart);
|
|
191
|
-
if (cached) {
|
|
192
|
-
logger.debug("getAllFilesRaw - fallback cache HIT", {
|
|
193
|
-
cacheKey,
|
|
194
|
-
cacheMs,
|
|
195
|
-
fileCount: cached.length,
|
|
196
|
-
});
|
|
197
|
-
return cached;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
logger.warn("getAllFilesRaw - cache MISS, fetching from API", {
|
|
201
|
-
cacheKey,
|
|
202
|
-
cacheMs,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const isPublished = ctx?.sourceType !== "branch";
|
|
206
|
-
logger.debug("Fetching files from API", {
|
|
207
|
-
sourceType: ctx?.sourceType,
|
|
208
|
-
cacheKey,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const files = await withRetryOnTransient(
|
|
212
|
-
() =>
|
|
213
|
-
isPublished
|
|
214
|
-
? this.client.listPublishedFiles(
|
|
215
|
-
undefined,
|
|
216
|
-
ctx?.releaseId ?? undefined,
|
|
217
|
-
ctx?.environmentName ?? undefined,
|
|
218
|
-
)
|
|
219
|
-
: this.client.listAllFiles(),
|
|
220
|
-
"getAllFilesRaw (dir)",
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
this.cache.set(cacheKey, files);
|
|
224
|
-
return files;
|
|
225
|
-
});
|
|
153
|
+
return withSpan("fs.veryfront.getAllFilesRaw", () =>
|
|
154
|
+
loadAllProjectFiles({
|
|
155
|
+
client: this.client,
|
|
156
|
+
cache: this.cache,
|
|
157
|
+
contextProvider: this.contextProvider,
|
|
158
|
+
logger,
|
|
159
|
+
operationLabel: "dir",
|
|
160
|
+
}));
|
|
226
161
|
}
|
|
227
162
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ProjectFile, VeryfrontApiClient } from "../../veryfront-api-client/index.js";
|
|
2
|
+
import { FileCache } from "../cache/file-cache.js";
|
|
3
|
+
import { buildFileCacheKeyPrefix, buildFileListCacheKey } from "./cache-keys.js";
|
|
4
|
+
import { withRetryOnTransient } from "./retry.js";
|
|
5
|
+
import type { ResolvedContentContext } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface ContentContextProvider {
|
|
8
|
+
isProductionMode: () => boolean;
|
|
9
|
+
getReleaseId: () => string | null;
|
|
10
|
+
getContentContext: () => ResolvedContentContext | null;
|
|
11
|
+
getFileList?: () => Promise<
|
|
12
|
+
Array<{
|
|
13
|
+
id?: string;
|
|
14
|
+
path: string;
|
|
15
|
+
content?: string;
|
|
16
|
+
type?: string;
|
|
17
|
+
size?: number;
|
|
18
|
+
updated_at?: string;
|
|
19
|
+
}> | undefined
|
|
20
|
+
>;
|
|
21
|
+
hasCachedFileList?: () => Promise<boolean>;
|
|
22
|
+
isPersistentCacheInvalidated?: (prefix: string) => boolean;
|
|
23
|
+
isReleaseBeingInvalidated?: (releaseId: string) => boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FileListLogger {
|
|
27
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
28
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface LoadAllProjectFilesOptions {
|
|
32
|
+
client: VeryfrontApiClient;
|
|
33
|
+
cache: FileCache;
|
|
34
|
+
contextProvider?: ContentContextProvider;
|
|
35
|
+
logger: FileListLogger;
|
|
36
|
+
operationLabel: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function loadAllProjectFiles({
|
|
40
|
+
client,
|
|
41
|
+
cache,
|
|
42
|
+
contextProvider,
|
|
43
|
+
logger,
|
|
44
|
+
operationLabel,
|
|
45
|
+
}: LoadAllProjectFilesOptions): Promise<ProjectFile[]> {
|
|
46
|
+
const cacheStart = performance.now();
|
|
47
|
+
const ctx = contextProvider?.getContentContext();
|
|
48
|
+
const cacheKeyPrefix = buildFileCacheKeyPrefix(ctx);
|
|
49
|
+
const skipPersistentCache = contextProvider?.isPersistentCacheInvalidated?.(cacheKeyPrefix) ??
|
|
50
|
+
false;
|
|
51
|
+
|
|
52
|
+
const adapterFiles = !skipPersistentCache ? await contextProvider?.getFileList?.() : undefined;
|
|
53
|
+
|
|
54
|
+
if (adapterFiles) {
|
|
55
|
+
const cacheMs = Math.round(performance.now() - cacheStart);
|
|
56
|
+
logger.debug("getAllFilesRaw - from adapter cache", {
|
|
57
|
+
cacheMs,
|
|
58
|
+
fileCount: adapterFiles.length,
|
|
59
|
+
});
|
|
60
|
+
return adapterFiles as ProjectFile[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cacheKey = buildFileListCacheKey(ctx);
|
|
64
|
+
|
|
65
|
+
if (skipPersistentCache) {
|
|
66
|
+
logger.debug("getAllFilesRaw - skipping persistent cache", {
|
|
67
|
+
cacheKey,
|
|
68
|
+
cacheKeyPrefix,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const cached = skipPersistentCache ? undefined : await cache.getAsync<ProjectFile[]>(cacheKey);
|
|
73
|
+
const cacheMs = Math.round(performance.now() - cacheStart);
|
|
74
|
+
|
|
75
|
+
if (cached) {
|
|
76
|
+
logger.debug("getAllFilesRaw - fallback cache HIT", {
|
|
77
|
+
cacheKey,
|
|
78
|
+
cacheMs,
|
|
79
|
+
fileCount: cached.length,
|
|
80
|
+
});
|
|
81
|
+
return cached;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logger.warn("getAllFilesRaw - cache MISS, fetching from API", {
|
|
85
|
+
cacheKey,
|
|
86
|
+
cacheMs,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const isPublished = ctx?.sourceType !== "branch";
|
|
90
|
+
logger.debug("Fetching files from API", {
|
|
91
|
+
sourceType: ctx?.sourceType,
|
|
92
|
+
cacheKey,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const files = await withRetryOnTransient(
|
|
96
|
+
() =>
|
|
97
|
+
isPublished
|
|
98
|
+
? client.listPublishedFiles(
|
|
99
|
+
undefined,
|
|
100
|
+
ctx?.releaseId ?? undefined,
|
|
101
|
+
ctx?.environmentName ?? undefined,
|
|
102
|
+
)
|
|
103
|
+
: client.listAllFiles(),
|
|
104
|
+
`getAllFilesRaw (${operationLabel})`,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
cache.set(cacheKey, files);
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
@@ -7,6 +7,7 @@ import { FileListIndex, type FileListMatchResult } from "./file-list-index.js";
|
|
|
7
7
|
import { InFlightRequestDeduper } from "./in-flight-dedupe.js";
|
|
8
8
|
import { getRequestScopedFile, setRequestScopedFile } from "./multi-project-adapter.js";
|
|
9
9
|
import { PathNormalizer } from "./path-normalizer.js";
|
|
10
|
+
import type { ContentContextProvider } from "./file-list-access.js";
|
|
10
11
|
import {
|
|
11
12
|
assertProjectSourcePath,
|
|
12
13
|
buildExtensionCandidatePaths,
|
|
@@ -28,28 +29,6 @@ export {
|
|
|
28
29
|
|
|
29
30
|
const logger = baseLogger.component("read-operations");
|
|
30
31
|
|
|
31
|
-
export interface ContentContextProvider {
|
|
32
|
-
isProductionMode: () => boolean;
|
|
33
|
-
getReleaseId: () => string | null;
|
|
34
|
-
getContentContext: () => ResolvedContentContext | null;
|
|
35
|
-
/** Cached file list from adapter initialization (single source of truth) */
|
|
36
|
-
getFileList?: () => Promise<
|
|
37
|
-
Array<{
|
|
38
|
-
id?: string;
|
|
39
|
-
path: string;
|
|
40
|
-
content?: string;
|
|
41
|
-
type?: string;
|
|
42
|
-
size?: number;
|
|
43
|
-
updated_at?: string;
|
|
44
|
-
}> | undefined
|
|
45
|
-
>;
|
|
46
|
-
hasCachedFileList?: () => Promise<boolean>;
|
|
47
|
-
/** True if cache prefix is being deleted - skip persistent cache reads */
|
|
48
|
-
isPersistentCacheInvalidated?: (prefix: string) => boolean;
|
|
49
|
-
/** Back-compat: release-scoped invalidation */
|
|
50
|
-
isReleaseBeingInvalidated?: (releaseId: string) => boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
32
|
const IN_FLIGHT_REQUEST_TIMEOUT_MS = 15_000;
|
|
54
33
|
const MAX_IN_FLIGHT_REQUESTS = 100;
|
|
55
34
|
const IN_FLIGHT_CLEANUP_INTERVAL_MS = 1_000;
|
|
@@ -4,12 +4,7 @@ import type { FileInfo, ResolveFileOptions } from "../../base.js";
|
|
|
4
4
|
import type { ProjectFile } from "../../veryfront-api-client/index.js";
|
|
5
5
|
import { VeryfrontOperationsBase } from "./base-operations.js";
|
|
6
6
|
import { createError, toError } from "../../../../errors/index.js";
|
|
7
|
-
import {
|
|
8
|
-
buildFileCacheKeyPrefix,
|
|
9
|
-
buildFileListCacheKey,
|
|
10
|
-
buildStatCacheKeyPrefix,
|
|
11
|
-
} from "./cache-keys.js";
|
|
12
|
-
import { withRetryOnTransient } from "./retry.js";
|
|
7
|
+
import { buildStatCacheKeyPrefix } from "./cache-keys.js";
|
|
13
8
|
import { STAT_OPERATION_EXTENSION_PRIORITY as EXTENSION_PRIORITY } from "./extension-priority.js";
|
|
14
9
|
import {
|
|
15
10
|
collectParentDirectories,
|
|
@@ -21,6 +16,7 @@ import {
|
|
|
21
16
|
} from "./stat-operations-helpers.js";
|
|
22
17
|
import { ApiSearchCircuitBreaker } from "./api-search-circuit-breaker.js";
|
|
23
18
|
import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
|
|
19
|
+
import { loadAllProjectFiles } from "./file-list-access.js";
|
|
24
20
|
|
|
25
21
|
const logger = baseLogger.component("stat-operations");
|
|
26
22
|
|
|
@@ -258,77 +254,19 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
258
254
|
}
|
|
259
255
|
|
|
260
256
|
private async getAllFilesRaw(): Promise<ProjectFile[]> {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!skipPersistentCache) {
|
|
268
|
-
const files = await this.contextProvider?.getFileList?.();
|
|
269
|
-
if (files) {
|
|
270
|
-
const cacheMs = Math.round(performance.now() - cacheStart);
|
|
271
|
-
logger.debug("getAllFilesRaw - from adapter cache", {
|
|
272
|
-
cacheMs,
|
|
273
|
-
fileCount: files.length,
|
|
274
|
-
});
|
|
275
|
-
return files as ProjectFile[];
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const cacheKey = buildFileListCacheKey(ctx);
|
|
280
|
-
|
|
281
|
-
if (skipPersistentCache) {
|
|
282
|
-
logger.debug("getAllFilesRaw - skipping persistent cache (invalidation)", {
|
|
283
|
-
cacheKey,
|
|
284
|
-
cacheKeyPrefix,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const cached = skipPersistentCache
|
|
289
|
-
? undefined
|
|
290
|
-
: await this.cache.getAsync<ProjectFile[]>(cacheKey);
|
|
291
|
-
const cacheMs = Math.round(performance.now() - cacheStart);
|
|
292
|
-
|
|
293
|
-
if (cached) {
|
|
294
|
-
logger.debug("getAllFilesRaw - fallback cache HIT", {
|
|
295
|
-
cacheKey,
|
|
296
|
-
cacheMs,
|
|
297
|
-
fileCount: cached.length,
|
|
298
|
-
});
|
|
299
|
-
return cached;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
logger.warn("getAllFilesRaw - cache MISS, fetching from API", {
|
|
303
|
-
cacheKey,
|
|
304
|
-
cacheMs,
|
|
257
|
+
return await loadAllProjectFiles({
|
|
258
|
+
client: this.client,
|
|
259
|
+
cache: this.cache,
|
|
260
|
+
contextProvider: this.contextProvider,
|
|
261
|
+
logger,
|
|
262
|
+
operationLabel: "stat",
|
|
305
263
|
});
|
|
306
|
-
|
|
307
|
-
const isPublished = ctx?.sourceType !== "branch";
|
|
308
|
-
logger.debug("Fetching files from API", {
|
|
309
|
-
sourceType: ctx?.sourceType,
|
|
310
|
-
cacheKey,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
const files = await withRetryOnTransient(
|
|
314
|
-
() =>
|
|
315
|
-
isPublished
|
|
316
|
-
? this.client.listPublishedFiles(
|
|
317
|
-
undefined,
|
|
318
|
-
ctx?.releaseId ?? undefined,
|
|
319
|
-
ctx?.environmentName ?? undefined,
|
|
320
|
-
)
|
|
321
|
-
: this.client.listAllFiles(),
|
|
322
|
-
"getAllFilesRaw (stat)",
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
this.cache.set(cacheKey, files);
|
|
326
|
-
return files;
|
|
327
264
|
}
|
|
328
265
|
|
|
329
266
|
private buildResolveSearchPatterns(
|
|
330
267
|
normalizedPath: string,
|
|
331
268
|
options?: ResolveFileOptions,
|
|
269
|
+
knownExtensionFallback: "exact" | "wildcard" = "exact",
|
|
332
270
|
): string[] {
|
|
333
271
|
const patterns = new Set<string>();
|
|
334
272
|
const pathWithoutExt = stripKnownExtension(normalizedPath, EXTENSION_PRIORITY);
|
|
@@ -338,7 +276,9 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
338
276
|
};
|
|
339
277
|
|
|
340
278
|
if (EXTENSION_PRIORITY.some((ext) => normalizedPath.endsWith(ext))) {
|
|
341
|
-
addPattern(
|
|
279
|
+
addPattern(
|
|
280
|
+
knownExtensionFallback === "wildcard" ? `${pathWithoutExt}.*` : normalizedPath,
|
|
281
|
+
);
|
|
342
282
|
return [...patterns];
|
|
343
283
|
}
|
|
344
284
|
|
|
@@ -366,6 +306,7 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
366
306
|
private async tryResolveViaApiSearch(
|
|
367
307
|
normalizedPath: string,
|
|
368
308
|
options?: ResolveFileOptions,
|
|
309
|
+
knownExtensionFallback: "exact" | "wildcard" = "exact",
|
|
369
310
|
): Promise<string | null | undefined> {
|
|
370
311
|
if (isFrameworkSourcePath(normalizedPath)) {
|
|
371
312
|
logger.debug("Skipping API search for framework path", { normalizedPath });
|
|
@@ -377,7 +318,11 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
377
318
|
return undefined;
|
|
378
319
|
}
|
|
379
320
|
|
|
380
|
-
const patterns = this.buildResolveSearchPatterns(
|
|
321
|
+
const patterns = this.buildResolveSearchPatterns(
|
|
322
|
+
normalizedPath,
|
|
323
|
+
options,
|
|
324
|
+
knownExtensionFallback,
|
|
325
|
+
);
|
|
381
326
|
let sawSuccessfulSearch = false;
|
|
382
327
|
|
|
383
328
|
for (const pattern of patterns) {
|
|
@@ -573,55 +518,17 @@ export class StatOperations extends VeryfrontOperationsBase {
|
|
|
573
518
|
return null;
|
|
574
519
|
}
|
|
575
520
|
|
|
576
|
-
// NOTE:
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
return null;
|
|
521
|
+
// NOTE: Keep the post-index API fallback aligned with the pre-index helper for extensionless
|
|
522
|
+
// paths, while preserving the older wildcard sibling-extension lookup for known-extension
|
|
523
|
+
// paths. Incomplete file-list snapshots otherwise hide valid files until the cache refreshes.
|
|
524
|
+
const apiResolved = await this.tryResolveViaApiSearch(normalizedPath, options, "wildcard");
|
|
525
|
+
if (typeof apiResolved === "string") {
|
|
526
|
+
this.cache.set(cacheKey, apiResolved);
|
|
527
|
+
return apiResolved;
|
|
584
528
|
}
|
|
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 });
|
|
529
|
+
if (apiResolved === null) {
|
|
530
|
+
this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
|
|
617
531
|
}
|
|
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
532
|
return null;
|
|
626
533
|
}
|
|
627
534
|
}
|
package/src/src/tool/factory.ts
CHANGED
|
@@ -179,20 +179,18 @@ export interface DynamicToolConfig {
|
|
|
179
179
|
id?: string;
|
|
180
180
|
description: string;
|
|
181
181
|
inputSchema: unknown;
|
|
182
|
+
inputSchemaJson?: JsonSchema;
|
|
182
183
|
execute: (input: unknown, context?: ToolExecutionContext) => Promise<unknown> | unknown;
|
|
183
184
|
toModelOutput?: (output: unknown) => unknown;
|
|
184
|
-
mcp?:
|
|
185
|
-
enabled?: boolean;
|
|
186
|
-
requiresAuth?: boolean;
|
|
187
|
-
cachePolicy?: "no-cache" | "cache" | "cache-first";
|
|
188
|
-
};
|
|
185
|
+
mcp?: ToolConfig["mcp"];
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
export function dynamicTool(config: DynamicToolConfig): Tool<unknown, unknown> {
|
|
192
189
|
const explicitId = typeof config.id === "string" && config.id.length > 0 ? config.id : undefined;
|
|
193
190
|
const id = explicitId ?? generateToolId();
|
|
194
191
|
|
|
195
|
-
const inputSchemaJson =
|
|
192
|
+
const inputSchemaJson = config.inputSchemaJson ??
|
|
193
|
+
convertSchemaToJson(config.inputSchema, id, "DYNAMIC_TOOL", true);
|
|
196
194
|
|
|
197
195
|
const createdTool: Tool<unknown, unknown> = {
|
|
198
196
|
id,
|
package/src/src/tool/index.ts
CHANGED
|
@@ -57,6 +57,11 @@ export { dynamicTool, tool } from "./factory.js";
|
|
|
57
57
|
export type { DynamicToolConfig } from "./factory.js";
|
|
58
58
|
export { createRemoteMCPToolSource } from "./remote-mcp.js";
|
|
59
59
|
export type { RemoteMCPToolSourceConfig } from "./remote-mcp.js";
|
|
60
|
+
export {
|
|
61
|
+
createToolsFromRemoteDefinitions,
|
|
62
|
+
loadRemoteToolsFromSource,
|
|
63
|
+
} from "./remote-source-tools.js";
|
|
64
|
+
export type { RemoteToolMaterializationOptions } from "./remote-source-tools.js";
|
|
60
65
|
|
|
61
66
|
export { toolRegistry } from "./registry.js";
|
|
62
67
|
|