@akira-tl/forgerelay 0.4.0 → 0.4.2
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/CHANGELOG.md +23 -0
- package/capabilities/code-intelligence/GUIDE.md +4 -2
- package/dist/capability-registry.js +12 -3
- package/dist/lsp/code-intelligence-types.js +2 -0
- package/dist/lsp/code-intelligence.js +92 -190
- package/dist/lsp/normalization/hover.js +29 -0
- package/dist/lsp/normalization/locations.js +53 -0
- package/dist/lsp/runtime/manager.js +152 -0
- package/dist/lsp/test-support/server-fixture.js +68 -0
- package/dist/server.js +3 -2
- package/docs/configuration.md +10 -4
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +10 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.2] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added bounded `code.intelligence` `references` support through the shared Language-service path, using the same normalized location contract as definition results.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- References default to 100 returned locations, accept an explicit limit up to 1000, and report `returned`, `truncated`, and the real `total` when the complete Language-server response is known.
|
|
16
|
+
- Shared semantic-location normalization now preserves External code location metadata for both definition and references without expanding ForgeRelay file authority.
|
|
17
|
+
- Split Language-service management from the protocol runtime so later code-intelligence operations can grow without concentrating lifecycle and request logic in one module.
|
|
18
|
+
|
|
19
|
+
## [0.4.1] - 2026-08-11
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Added `code.intelligence` `hover` support through the same shared Language-service and filesystem synchronization path as definition lookup.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Hover responses now normalize Markdown/plaintext `MarkupContent` and supported legacy `MarkedString` payloads into a stable Agent-facing `contents` value with optional language and normalized range metadata.
|
|
28
|
+
- Language Servers that do not advertise hover return `code.operation_unsupported` without invalidating the shared Language service.
|
|
29
|
+
|
|
7
30
|
## [0.4.0] - 2026-08-11
|
|
8
31
|
|
|
9
32
|
### Added
|
|
@@ -4,8 +4,10 @@ Use the `code.intelligence` Capability for read-only semantic code navigation ba
|
|
|
4
4
|
|
|
5
5
|
ForgeRelay does not install Language servers. It discovers supported executables when available and accepts explicit definitions from the global ForgeRelay config or `<workspace>/.forgerelay/language-servers.json`. Project definitions override global definitions, and global definitions override built-in discovery. An explicit definition may disable discovery with `enabled: false`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
ForgeRelay 0.4.2 supports `definition`, `hover`, and `references`. Position-based operations accept a workspace-relative source `path` plus 1-based `line` and `column` values. Columns are Unicode code-point positions; ForgeRelay converts them to the position encoding negotiated with the Language server.
|
|
8
8
|
|
|
9
|
-
Code-intelligence results use ForgeRelay-
|
|
9
|
+
Code-intelligence results use ForgeRelay-owned shapes rather than raw LSP wire types. Definition returns normalized locations. Hover returns one `contents` string, an optional legacy `language`, and an optional ForgeRelay-normalized `range`; plaintext, Markdown `MarkupContent`, and supported legacy `MarkedString` payloads are normalized before reaching the Agent. References uses the same normalized location shape, defaults to `limit: 100`, and accepts limits up to 1000. Its result reports `returned`, `truncated`, and `total` when the complete Language-server response makes the total known.
|
|
10
|
+
|
|
11
|
+
Semantic locations may identify External code locations outside the Workspace, but that does not expand ForgeRelay's allowed roots or grant the file tools permission to read those paths.
|
|
10
12
|
|
|
11
13
|
Language-server definitions use structured process configuration rather than shell command strings. A project configuration entry may contain `command`, `args`, `env`, `languages`, `extensions`, `languageIdByExtension`, `projectMarkers`, and `enabled` fields. Use `languageIdByExtension` when one server definition covers multiple language IDs whose extensions do not map one-to-one by array position. The server command is launched directly without a shell.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { MAX_CODE_INTELLIGENCE_RESULT_LIMIT, } from "./lsp/code-intelligence-types.js";
|
|
2
3
|
export class CapabilityError extends Error {
|
|
3
4
|
code;
|
|
4
5
|
constructor(code, message) {
|
|
@@ -115,12 +116,20 @@ export class CapabilityRegistry {
|
|
|
115
116
|
}
|
|
116
117
|
export function createCapabilityRegistry(dependencies) {
|
|
117
118
|
const hooksCheckInput = z.object({}).strict();
|
|
118
|
-
const
|
|
119
|
-
operation: z.literal("definition"),
|
|
119
|
+
const positionInput = {
|
|
120
120
|
path: z.string().min(1),
|
|
121
121
|
line: z.number().int(),
|
|
122
122
|
column: z.number().int(),
|
|
123
|
-
}
|
|
123
|
+
};
|
|
124
|
+
const codeIntelligenceInput = z.discriminatedUnion("operation", [
|
|
125
|
+
z.object({ operation: z.literal("definition"), ...positionInput }).strict(),
|
|
126
|
+
z.object({ operation: z.literal("hover"), ...positionInput }).strict(),
|
|
127
|
+
z.object({
|
|
128
|
+
operation: z.literal("references"),
|
|
129
|
+
...positionInput,
|
|
130
|
+
limit: z.number().int().min(1).max(MAX_CODE_INTELLIGENCE_RESULT_LIMIT).optional(),
|
|
131
|
+
}).strict(),
|
|
132
|
+
]);
|
|
124
133
|
return new CapabilityRegistry([
|
|
125
134
|
{
|
|
126
135
|
name: "hooks.check",
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
-
import { basename,
|
|
4
|
-
import {
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { createMessageConnection } from "vscode-jsonrpc/node";
|
|
6
|
-
import { DefinitionRequest, DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, ExitNotification, InitializeRequest, InitializedNotification, PositionEncodingKind, ShutdownRequest, TextDocumentSyncKind, } from "vscode-languageserver-protocol";
|
|
7
|
-
import { LanguageServerConfigurationError,
|
|
6
|
+
import { DefinitionRequest, DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, ExitNotification, HoverRequest, InitializeRequest, ReferencesRequest, InitializedNotification, MarkupKind, PositionEncodingKind, ShutdownRequest, TextDocumentSyncKind, } from "vscode-languageserver-protocol";
|
|
7
|
+
import { LanguageServerConfigurationError, } from "./language-server-config.js";
|
|
8
8
|
import { terminateProcessTree } from "../process-platform.js";
|
|
9
9
|
import { CodeIntelligenceError } from "./code-intelligence-error.js";
|
|
10
|
+
import { DEFAULT_CODE_INTELLIGENCE_RESULT_LIMIT } from "./code-intelligence-types.js";
|
|
11
|
+
import { normalizeHoverContents } from "./normalization/hover.js";
|
|
12
|
+
import { isWithin, locationEntries, normalizeLocations, workspaceDisplayPath, } from "./normalization/locations.js";
|
|
10
13
|
import { lspPositionFromUser, rangeFromLsp, wholeDocumentRange } from "./position-encoding.js";
|
|
11
14
|
export { CodeIntelligenceError } from "./code-intelligence-error.js";
|
|
12
|
-
const LANGUAGE_SERVICE_IDLE_MS = 10 * 60 * 1_000;
|
|
13
|
-
const LANGUAGE_SERVICE_CLEANUP_INTERVAL_MS = 60 * 1_000;
|
|
14
|
-
const MAX_LANGUAGE_SERVICES = 16;
|
|
15
|
-
const LANGUAGE_SERVICE_START_TIMEOUT_MS = 15_000;
|
|
16
|
-
const LANGUAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
17
|
-
const LANGUAGE_SERVICE_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
18
15
|
const STDERR_TAIL_BYTES = 64 * 1024;
|
|
19
|
-
class LanguageService {
|
|
16
|
+
export class LanguageService {
|
|
20
17
|
workspaceRoot;
|
|
21
18
|
project;
|
|
22
19
|
policy;
|
|
@@ -58,7 +55,7 @@ class LanguageService {
|
|
|
58
55
|
textDocument: { uri: document.uri },
|
|
59
56
|
position,
|
|
60
57
|
}), this.policy.requestTimeoutMs, () => new CodeIntelligenceError("code.request_timeout", `Definition request timed out for ${input.path}.`));
|
|
61
|
-
const locations = await
|
|
58
|
+
const locations = await normalizeLocations(locationEntries(response), this.workspaceRoot, this.positionEncoding);
|
|
62
59
|
return {
|
|
63
60
|
operation: "definition",
|
|
64
61
|
selectedServer: this.project.definition.id,
|
|
@@ -75,6 +72,81 @@ class LanguageService {
|
|
|
75
72
|
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
76
73
|
}
|
|
77
74
|
}
|
|
75
|
+
async hover(input) {
|
|
76
|
+
try {
|
|
77
|
+
await this.ensureStarted();
|
|
78
|
+
if (!this.capabilities?.hoverProvider) {
|
|
79
|
+
throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not advertise hover support.`);
|
|
80
|
+
}
|
|
81
|
+
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
82
|
+
const document = await this.syncDocument(sourcePath);
|
|
83
|
+
const position = lspPositionFromUser(document.text, input.line, input.column, this.positionEncoding);
|
|
84
|
+
const response = await withTimeout(this.connection.sendRequest(HoverRequest.type, {
|
|
85
|
+
textDocument: { uri: document.uri },
|
|
86
|
+
position,
|
|
87
|
+
}), this.policy.requestTimeoutMs, () => new CodeIntelligenceError("code.request_timeout", `Hover request timed out for ${input.path}.`));
|
|
88
|
+
const common = {
|
|
89
|
+
operation: "hover",
|
|
90
|
+
selectedServer: this.project.definition.id,
|
|
91
|
+
projectRoot: workspaceDisplayPath(this.workspaceRoot, this.project.projectRoot),
|
|
92
|
+
};
|
|
93
|
+
if (!response)
|
|
94
|
+
return { ...common, contents: null };
|
|
95
|
+
const normalized = normalizeHoverContents(response.contents);
|
|
96
|
+
return {
|
|
97
|
+
...common,
|
|
98
|
+
...normalized,
|
|
99
|
+
...(response.range
|
|
100
|
+
? { range: rangeFromLsp(document.text, response.range, this.positionEncoding) }
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof CodeIntelligenceError)
|
|
106
|
+
throw error;
|
|
107
|
+
if (error instanceof LanguageServerConfigurationError) {
|
|
108
|
+
throw new CodeIntelligenceError(error.code, error.message);
|
|
109
|
+
}
|
|
110
|
+
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async references(input) {
|
|
114
|
+
try {
|
|
115
|
+
await this.ensureStarted();
|
|
116
|
+
if (!this.capabilities?.referencesProvider) {
|
|
117
|
+
throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not advertise references support.`);
|
|
118
|
+
}
|
|
119
|
+
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
120
|
+
const document = await this.syncDocument(sourcePath);
|
|
121
|
+
const position = lspPositionFromUser(document.text, input.line, input.column, this.positionEncoding);
|
|
122
|
+
const response = await withTimeout(this.connection.sendRequest(ReferencesRequest.type, {
|
|
123
|
+
textDocument: { uri: document.uri },
|
|
124
|
+
position,
|
|
125
|
+
context: { includeDeclaration: true },
|
|
126
|
+
}), this.policy.requestTimeoutMs, () => new CodeIntelligenceError("code.request_timeout", `References request timed out for ${input.path}.`));
|
|
127
|
+
const entries = locationEntries(response ?? []);
|
|
128
|
+
const limit = input.limit ?? DEFAULT_CODE_INTELLIGENCE_RESULT_LIMIT;
|
|
129
|
+
const selected = entries.slice(0, limit);
|
|
130
|
+
const locations = await normalizeLocations(selected, this.workspaceRoot, this.positionEncoding);
|
|
131
|
+
return {
|
|
132
|
+
operation: "references",
|
|
133
|
+
selectedServer: this.project.definition.id,
|
|
134
|
+
projectRoot: workspaceDisplayPath(this.workspaceRoot, this.project.projectRoot),
|
|
135
|
+
locations,
|
|
136
|
+
returned: locations.length,
|
|
137
|
+
truncated: entries.length > locations.length,
|
|
138
|
+
total: entries.length,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof CodeIntelligenceError)
|
|
143
|
+
throw error;
|
|
144
|
+
if (error instanceof LanguageServerConfigurationError) {
|
|
145
|
+
throw new CodeIntelligenceError(error.code, error.message);
|
|
146
|
+
}
|
|
147
|
+
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
78
150
|
async shutdown() {
|
|
79
151
|
if (this.closed)
|
|
80
152
|
return;
|
|
@@ -189,6 +261,13 @@ class LanguageService {
|
|
|
189
261
|
dynamicRegistration: false,
|
|
190
262
|
linkSupport: true,
|
|
191
263
|
},
|
|
264
|
+
hover: {
|
|
265
|
+
dynamicRegistration: false,
|
|
266
|
+
contentFormat: [MarkupKind.Markdown, MarkupKind.PlainText],
|
|
267
|
+
},
|
|
268
|
+
references: {
|
|
269
|
+
dynamicRegistration: false,
|
|
270
|
+
},
|
|
192
271
|
},
|
|
193
272
|
},
|
|
194
273
|
};
|
|
@@ -278,135 +357,7 @@ class LanguageService {
|
|
|
278
357
|
return text ? ` Server stderr: ${text}` : "";
|
|
279
358
|
}
|
|
280
359
|
}
|
|
281
|
-
export
|
|
282
|
-
config;
|
|
283
|
-
services = new Map();
|
|
284
|
-
serviceCreations = new Map();
|
|
285
|
-
serviceCreationQueue = Promise.resolve();
|
|
286
|
-
cleanupTimer;
|
|
287
|
-
policy;
|
|
288
|
-
constructor(config, options = {}) {
|
|
289
|
-
this.config = config;
|
|
290
|
-
this.policy = {
|
|
291
|
-
idleMs: positiveInteger(options.idleMs, LANGUAGE_SERVICE_IDLE_MS, "idleMs"),
|
|
292
|
-
cleanupIntervalMs: positiveInteger(options.cleanupIntervalMs, LANGUAGE_SERVICE_CLEANUP_INTERVAL_MS, "cleanupIntervalMs"),
|
|
293
|
-
maxServices: positiveInteger(options.maxServices, MAX_LANGUAGE_SERVICES, "maxServices"),
|
|
294
|
-
startTimeoutMs: positiveInteger(options.startTimeoutMs, LANGUAGE_SERVICE_START_TIMEOUT_MS, "startTimeoutMs"),
|
|
295
|
-
requestTimeoutMs: positiveInteger(options.requestTimeoutMs, LANGUAGE_REQUEST_TIMEOUT_MS, "requestTimeoutMs"),
|
|
296
|
-
shutdownTimeoutMs: positiveInteger(options.shutdownTimeoutMs, LANGUAGE_SERVICE_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs"),
|
|
297
|
-
};
|
|
298
|
-
this.cleanupTimer = setInterval(() => {
|
|
299
|
-
void this.closeIdle();
|
|
300
|
-
}, this.policy.cleanupIntervalMs);
|
|
301
|
-
this.cleanupTimer.unref();
|
|
302
|
-
}
|
|
303
|
-
async definition(workspaceRoot, input) {
|
|
304
|
-
let project;
|
|
305
|
-
let canonicalWorkspaceRoot;
|
|
306
|
-
try {
|
|
307
|
-
canonicalWorkspaceRoot = await realpath(resolve(workspaceRoot));
|
|
308
|
-
project = await resolveLanguageProject({
|
|
309
|
-
workspaceRoot: canonicalWorkspaceRoot,
|
|
310
|
-
sourcePath: input.path,
|
|
311
|
-
globalConfig: this.config.languageServers,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
if (error instanceof LanguageServerConfigurationError) {
|
|
316
|
-
throw new CodeIntelligenceError(error.code, error.message);
|
|
317
|
-
}
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
const service = await this.acquireService(canonicalWorkspaceRoot, project);
|
|
321
|
-
try {
|
|
322
|
-
return await service.definition(input);
|
|
323
|
-
}
|
|
324
|
-
finally {
|
|
325
|
-
service.release();
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
async shutdown() {
|
|
329
|
-
clearInterval(this.cleanupTimer);
|
|
330
|
-
await Promise.allSettled(this.serviceCreations.values());
|
|
331
|
-
this.serviceCreations.clear();
|
|
332
|
-
const services = [...this.services.values()];
|
|
333
|
-
this.services.clear();
|
|
334
|
-
await Promise.allSettled(services.map((service) => service.shutdown()));
|
|
335
|
-
}
|
|
336
|
-
get size() {
|
|
337
|
-
return this.services.size;
|
|
338
|
-
}
|
|
339
|
-
async acquireService(workspaceRoot, project) {
|
|
340
|
-
const key = languageServiceKey(project);
|
|
341
|
-
const existing = this.services.get(key);
|
|
342
|
-
if (existing) {
|
|
343
|
-
existing.acquire();
|
|
344
|
-
return existing;
|
|
345
|
-
}
|
|
346
|
-
const pending = this.serviceCreations.get(key);
|
|
347
|
-
if (pending) {
|
|
348
|
-
const service = await pending;
|
|
349
|
-
service.acquire();
|
|
350
|
-
return service;
|
|
351
|
-
}
|
|
352
|
-
const creation = this.withServiceCreationLock(async () => {
|
|
353
|
-
const current = this.services.get(key);
|
|
354
|
-
if (current) {
|
|
355
|
-
current.acquire();
|
|
356
|
-
return current;
|
|
357
|
-
}
|
|
358
|
-
await this.ensureCapacity();
|
|
359
|
-
const service = new LanguageService(workspaceRoot, project, this.policy);
|
|
360
|
-
service.acquire();
|
|
361
|
-
this.services.set(key, service);
|
|
362
|
-
return service;
|
|
363
|
-
});
|
|
364
|
-
this.serviceCreations.set(key, creation);
|
|
365
|
-
try {
|
|
366
|
-
return await creation;
|
|
367
|
-
}
|
|
368
|
-
finally {
|
|
369
|
-
if (this.serviceCreations.get(key) === creation) {
|
|
370
|
-
this.serviceCreations.delete(key);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
async withServiceCreationLock(operation) {
|
|
375
|
-
const previous = this.serviceCreationQueue;
|
|
376
|
-
let release = () => undefined;
|
|
377
|
-
this.serviceCreationQueue = new Promise((resolvePromise) => {
|
|
378
|
-
release = resolvePromise;
|
|
379
|
-
});
|
|
380
|
-
await previous;
|
|
381
|
-
try {
|
|
382
|
-
return await operation();
|
|
383
|
-
}
|
|
384
|
-
finally {
|
|
385
|
-
release();
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
async closeIdle(now = Date.now()) {
|
|
389
|
-
const stale = [...this.services.entries()].filter(([, service]) => service.inFlight === 0 && now - service.lastUsedAt >= this.policy.idleMs);
|
|
390
|
-
for (const [key, service] of stale) {
|
|
391
|
-
this.services.delete(key);
|
|
392
|
-
await service.shutdown();
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
async ensureCapacity() {
|
|
396
|
-
if (this.services.size < this.policy.maxServices)
|
|
397
|
-
return;
|
|
398
|
-
const idle = [...this.services.entries()]
|
|
399
|
-
.filter(([, service]) => service.inFlight === 0)
|
|
400
|
-
.sort((left, right) => left[1].lastUsedAt - right[1].lastUsedAt);
|
|
401
|
-
const candidate = idle[0];
|
|
402
|
-
if (!candidate) {
|
|
403
|
-
throw new CodeIntelligenceError("code.language_service_capacity", `Language service capacity reached (${this.policy.maxServices}) with no idle service available for eviction.`);
|
|
404
|
-
}
|
|
405
|
-
this.services.delete(candidate[0]);
|
|
406
|
-
await candidate[1].shutdown();
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
function languageServiceKey(project) {
|
|
360
|
+
export function languageServiceKey(project) {
|
|
410
361
|
return JSON.stringify([
|
|
411
362
|
resolve(project.projectRoot),
|
|
412
363
|
project.definition.id,
|
|
@@ -449,48 +400,6 @@ async function workspaceSourcePath(workspaceRoot, inputPath) {
|
|
|
449
400
|
throw new CodeIntelligenceError("code.language_service_unavailable", `Unable to read code-intelligence source ${inputPath}: ${errorMessage(error)}`);
|
|
450
401
|
}
|
|
451
402
|
}
|
|
452
|
-
async function normalizeDefinitionResponse(response, workspaceRoot, encoding) {
|
|
453
|
-
if (!response)
|
|
454
|
-
return [];
|
|
455
|
-
const entries = Array.isArray(response) ? response : [response];
|
|
456
|
-
return Promise.all(entries.map(async (entry) => {
|
|
457
|
-
const uri = isLocationLink(entry) ? entry.targetUri : entry.uri;
|
|
458
|
-
const range = isLocationLink(entry) ? entry.targetRange : entry.range;
|
|
459
|
-
if (!uri.startsWith("file:")) {
|
|
460
|
-
throw new CodeIntelligenceError("code.result_outside_policy", `Language server returned a non-file definition URI: ${uri}`);
|
|
461
|
-
}
|
|
462
|
-
const targetPath = fileURLToPath(uri);
|
|
463
|
-
const root = resolve(workspaceRoot);
|
|
464
|
-
let resolvedTarget;
|
|
465
|
-
let text;
|
|
466
|
-
try {
|
|
467
|
-
resolvedTarget = await realpath(targetPath);
|
|
468
|
-
text = await readFile(resolvedTarget, "utf8");
|
|
469
|
-
}
|
|
470
|
-
catch (error) {
|
|
471
|
-
throw new CodeIntelligenceError("code.result_outside_policy", `Unable to normalize definition location ${targetPath}: ${errorMessage(error)}`);
|
|
472
|
-
}
|
|
473
|
-
const external = !isWithin(root, resolvedTarget);
|
|
474
|
-
return {
|
|
475
|
-
path: external ? resolvedTarget : workspaceDisplayPath(root, resolvedTarget),
|
|
476
|
-
external,
|
|
477
|
-
range: rangeFromLsp(text, range, encoding),
|
|
478
|
-
};
|
|
479
|
-
}));
|
|
480
|
-
}
|
|
481
|
-
function isLocationLink(value) {
|
|
482
|
-
return "targetUri" in value;
|
|
483
|
-
}
|
|
484
|
-
function workspaceDisplayPath(workspaceRoot, path) {
|
|
485
|
-
const rel = relative(resolve(workspaceRoot), resolve(path));
|
|
486
|
-
if (!rel)
|
|
487
|
-
return ".";
|
|
488
|
-
return rel.split(sep).join("/");
|
|
489
|
-
}
|
|
490
|
-
function isWithin(root, candidate) {
|
|
491
|
-
const rel = relative(root, candidate);
|
|
492
|
-
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
493
|
-
}
|
|
494
403
|
async function withTimeout(promise, timeoutMs, timeoutError) {
|
|
495
404
|
let timer;
|
|
496
405
|
try {
|
|
@@ -538,13 +447,6 @@ async function waitForChildExit(child, timeoutMs) {
|
|
|
538
447
|
timer.unref();
|
|
539
448
|
});
|
|
540
449
|
}
|
|
541
|
-
function positiveInteger(value, fallback, label) {
|
|
542
|
-
const resolvedValue = value ?? fallback;
|
|
543
|
-
if (!Number.isInteger(resolvedValue) || resolvedValue < 1) {
|
|
544
|
-
throw new Error(`Code-intelligence ${label} must be a positive integer.`);
|
|
545
|
-
}
|
|
546
|
-
return resolvedValue;
|
|
547
|
-
}
|
|
548
450
|
function errorMessage(error) {
|
|
549
451
|
return error instanceof Error ? error.message : String(error);
|
|
550
452
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function normalizeHoverContents(contents) {
|
|
2
|
+
if (Array.isArray(contents)) {
|
|
3
|
+
if (contents.length === 1)
|
|
4
|
+
return normalizeMarkedString(contents[0]);
|
|
5
|
+
return {
|
|
6
|
+
contents: contents.map(renderMarkedString).join("\n\n"),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
if (isMarkupContent(contents)) {
|
|
10
|
+
return { contents: contents.value };
|
|
11
|
+
}
|
|
12
|
+
return normalizeMarkedString(contents);
|
|
13
|
+
}
|
|
14
|
+
function isMarkupContent(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && "kind" in value;
|
|
16
|
+
}
|
|
17
|
+
function normalizeMarkedString(value) {
|
|
18
|
+
if (typeof value === "string")
|
|
19
|
+
return { contents: value };
|
|
20
|
+
return {
|
|
21
|
+
contents: value.value,
|
|
22
|
+
language: value.language,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function renderMarkedString(value) {
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
return value;
|
|
28
|
+
return `\`\`\`${value.language}\n${value.value}\n\`\`\``;
|
|
29
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CodeIntelligenceError } from "../code-intelligence-error.js";
|
|
5
|
+
import { rangeFromLsp } from "../position-encoding.js";
|
|
6
|
+
export function locationEntries(response) {
|
|
7
|
+
if (!response)
|
|
8
|
+
return [];
|
|
9
|
+
const entries = Array.isArray(response) ? response : [response];
|
|
10
|
+
return entries.map((entry) => isLocationLink(entry)
|
|
11
|
+
? { uri: entry.targetUri, range: entry.targetRange }
|
|
12
|
+
: { uri: entry.uri, range: entry.range });
|
|
13
|
+
}
|
|
14
|
+
export async function normalizeLocations(entries, workspaceRoot, encoding) {
|
|
15
|
+
return Promise.all(entries.map(async ({ uri, range }) => {
|
|
16
|
+
if (!uri.startsWith("file:")) {
|
|
17
|
+
throw new CodeIntelligenceError("code.result_outside_policy", `Language server returned a non-file code location: ${uri}`);
|
|
18
|
+
}
|
|
19
|
+
const targetPath = fileURLToPath(uri);
|
|
20
|
+
const root = resolve(workspaceRoot);
|
|
21
|
+
let resolvedTarget;
|
|
22
|
+
let text;
|
|
23
|
+
try {
|
|
24
|
+
resolvedTarget = await realpath(targetPath);
|
|
25
|
+
text = await readFile(resolvedTarget, "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new CodeIntelligenceError("code.result_outside_policy", `Unable to normalize code location ${targetPath}: ${errorMessage(error)}`);
|
|
29
|
+
}
|
|
30
|
+
const external = !isWithin(root, resolvedTarget);
|
|
31
|
+
return {
|
|
32
|
+
path: external ? resolvedTarget : workspaceDisplayPath(root, resolvedTarget),
|
|
33
|
+
external,
|
|
34
|
+
range: rangeFromLsp(text, range, encoding),
|
|
35
|
+
};
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
export function workspaceDisplayPath(workspaceRoot, path) {
|
|
39
|
+
const rel = relative(resolve(workspaceRoot), resolve(path));
|
|
40
|
+
if (!rel)
|
|
41
|
+
return ".";
|
|
42
|
+
return rel.split(sep).join("/");
|
|
43
|
+
}
|
|
44
|
+
export function isWithin(root, candidate) {
|
|
45
|
+
const rel = relative(root, candidate);
|
|
46
|
+
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
47
|
+
}
|
|
48
|
+
function isLocationLink(value) {
|
|
49
|
+
return "targetUri" in value;
|
|
50
|
+
}
|
|
51
|
+
function errorMessage(error) {
|
|
52
|
+
return error instanceof Error ? error.message : String(error);
|
|
53
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { CodeIntelligenceError, LanguageService, languageServiceKey, } from "../code-intelligence.js";
|
|
4
|
+
import { LanguageServerConfigurationError, resolveLanguageProject, } from "../language-server-config.js";
|
|
5
|
+
const LANGUAGE_SERVICE_IDLE_MS = 10 * 60 * 1_000;
|
|
6
|
+
const LANGUAGE_SERVICE_CLEANUP_INTERVAL_MS = 60 * 1_000;
|
|
7
|
+
const MAX_LANGUAGE_SERVICES = 16;
|
|
8
|
+
const LANGUAGE_SERVICE_START_TIMEOUT_MS = 15_000;
|
|
9
|
+
const LANGUAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
10
|
+
const LANGUAGE_SERVICE_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
11
|
+
export class CodeIntelligenceManager {
|
|
12
|
+
config;
|
|
13
|
+
services = new Map();
|
|
14
|
+
serviceCreations = new Map();
|
|
15
|
+
serviceCreationQueue = Promise.resolve();
|
|
16
|
+
cleanupTimer;
|
|
17
|
+
policy;
|
|
18
|
+
constructor(config, options = {}) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.policy = {
|
|
21
|
+
idleMs: positiveInteger(options.idleMs, LANGUAGE_SERVICE_IDLE_MS, "idleMs"),
|
|
22
|
+
cleanupIntervalMs: positiveInteger(options.cleanupIntervalMs, LANGUAGE_SERVICE_CLEANUP_INTERVAL_MS, "cleanupIntervalMs"),
|
|
23
|
+
maxServices: positiveInteger(options.maxServices, MAX_LANGUAGE_SERVICES, "maxServices"),
|
|
24
|
+
startTimeoutMs: positiveInteger(options.startTimeoutMs, LANGUAGE_SERVICE_START_TIMEOUT_MS, "startTimeoutMs"),
|
|
25
|
+
requestTimeoutMs: positiveInteger(options.requestTimeoutMs, LANGUAGE_REQUEST_TIMEOUT_MS, "requestTimeoutMs"),
|
|
26
|
+
shutdownTimeoutMs: positiveInteger(options.shutdownTimeoutMs, LANGUAGE_SERVICE_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs"),
|
|
27
|
+
};
|
|
28
|
+
this.cleanupTimer = setInterval(() => {
|
|
29
|
+
void this.closeIdle();
|
|
30
|
+
}, this.policy.cleanupIntervalMs);
|
|
31
|
+
this.cleanupTimer.unref();
|
|
32
|
+
}
|
|
33
|
+
async run(workspaceRoot, input) {
|
|
34
|
+
let project;
|
|
35
|
+
let canonicalWorkspaceRoot;
|
|
36
|
+
try {
|
|
37
|
+
canonicalWorkspaceRoot = await realpath(resolve(workspaceRoot));
|
|
38
|
+
project = await resolveLanguageProject({
|
|
39
|
+
workspaceRoot: canonicalWorkspaceRoot,
|
|
40
|
+
sourcePath: input.path,
|
|
41
|
+
globalConfig: this.config.languageServers,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof LanguageServerConfigurationError) {
|
|
46
|
+
throw new CodeIntelligenceError(error.code, error.message);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const service = await this.acquireService(canonicalWorkspaceRoot, project);
|
|
51
|
+
try {
|
|
52
|
+
switch (input.operation) {
|
|
53
|
+
case "definition":
|
|
54
|
+
return await service.definition(input);
|
|
55
|
+
case "hover":
|
|
56
|
+
return await service.hover(input);
|
|
57
|
+
case "references":
|
|
58
|
+
return await service.references(input);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
service.release();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async shutdown() {
|
|
66
|
+
clearInterval(this.cleanupTimer);
|
|
67
|
+
await Promise.allSettled(this.serviceCreations.values());
|
|
68
|
+
this.serviceCreations.clear();
|
|
69
|
+
const services = [...this.services.values()];
|
|
70
|
+
this.services.clear();
|
|
71
|
+
await Promise.allSettled(services.map((service) => service.shutdown()));
|
|
72
|
+
}
|
|
73
|
+
get size() {
|
|
74
|
+
return this.services.size;
|
|
75
|
+
}
|
|
76
|
+
async acquireService(workspaceRoot, project) {
|
|
77
|
+
const key = languageServiceKey(project);
|
|
78
|
+
const existing = this.services.get(key);
|
|
79
|
+
if (existing) {
|
|
80
|
+
existing.acquire();
|
|
81
|
+
return existing;
|
|
82
|
+
}
|
|
83
|
+
const pending = this.serviceCreations.get(key);
|
|
84
|
+
if (pending) {
|
|
85
|
+
const service = await pending;
|
|
86
|
+
service.acquire();
|
|
87
|
+
return service;
|
|
88
|
+
}
|
|
89
|
+
const creation = this.withServiceCreationLock(async () => {
|
|
90
|
+
const current = this.services.get(key);
|
|
91
|
+
if (current) {
|
|
92
|
+
current.acquire();
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
await this.ensureCapacity();
|
|
96
|
+
const service = new LanguageService(workspaceRoot, project, this.policy);
|
|
97
|
+
service.acquire();
|
|
98
|
+
this.services.set(key, service);
|
|
99
|
+
return service;
|
|
100
|
+
});
|
|
101
|
+
this.serviceCreations.set(key, creation);
|
|
102
|
+
try {
|
|
103
|
+
return await creation;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (this.serviceCreations.get(key) === creation) {
|
|
107
|
+
this.serviceCreations.delete(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async withServiceCreationLock(operation) {
|
|
112
|
+
const previous = this.serviceCreationQueue;
|
|
113
|
+
let release = () => undefined;
|
|
114
|
+
this.serviceCreationQueue = new Promise((resolvePromise) => {
|
|
115
|
+
release = resolvePromise;
|
|
116
|
+
});
|
|
117
|
+
await previous;
|
|
118
|
+
try {
|
|
119
|
+
return await operation();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
release();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async closeIdle(now = Date.now()) {
|
|
126
|
+
const stale = [...this.services.entries()].filter(([, service]) => service.inFlight === 0 && now - service.lastUsedAt >= this.policy.idleMs);
|
|
127
|
+
for (const [key, service] of stale) {
|
|
128
|
+
this.services.delete(key);
|
|
129
|
+
await service.shutdown();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async ensureCapacity() {
|
|
133
|
+
if (this.services.size < this.policy.maxServices)
|
|
134
|
+
return;
|
|
135
|
+
const idle = [...this.services.entries()]
|
|
136
|
+
.filter(([, service]) => service.inFlight === 0)
|
|
137
|
+
.sort((left, right) => left[1].lastUsedAt - right[1].lastUsedAt);
|
|
138
|
+
const candidate = idle[0];
|
|
139
|
+
if (!candidate) {
|
|
140
|
+
throw new CodeIntelligenceError("code.language_service_capacity", `Language service capacity reached (${this.policy.maxServices}) with no idle service available for eviction.`);
|
|
141
|
+
}
|
|
142
|
+
this.services.delete(candidate[0]);
|
|
143
|
+
await candidate[1].shutdown();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function positiveInteger(value, fallback, label) {
|
|
147
|
+
const resolvedValue = value ?? fallback;
|
|
148
|
+
if (!Number.isInteger(resolvedValue) || resolvedValue < 1) {
|
|
149
|
+
throw new Error(`Code-intelligence ${label} must be a positive integer.`);
|
|
150
|
+
}
|
|
151
|
+
return resolvedValue;
|
|
152
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
7
|
+
import { loadConfig } from "../../config.js";
|
|
8
|
+
import { createReviewCheckpointManager } from "../../review-checkpoints.js";
|
|
9
|
+
import { ProcessManager } from "../../process-sessions.js";
|
|
10
|
+
import { createMcpServer } from "../../server.js";
|
|
11
|
+
import { SqliteWorkspaceStore } from "../../workspace-store.js";
|
|
12
|
+
import { WorkspaceRegistry } from "../../workspaces.js";
|
|
13
|
+
import { CodeIntelligenceManager } from "../runtime/manager.js";
|
|
14
|
+
export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
15
|
+
const root = await mkdtemp(join(tmpdir(), "forgerelay-code-intelligence-server-test-"));
|
|
16
|
+
const project = join(root, "project");
|
|
17
|
+
const agentDir = join(root, "agent");
|
|
18
|
+
const stateDir = join(root, ".state");
|
|
19
|
+
await mkdir(project, { recursive: true });
|
|
20
|
+
await mkdir(agentDir, { recursive: true });
|
|
21
|
+
await writeFile(join(agentDir, "AGENTS.md"), "global instructions\n");
|
|
22
|
+
await writeFile(join(project, "AGENTS.md"), "project instructions\n");
|
|
23
|
+
const config = loadConfig({
|
|
24
|
+
DEVSPACE_CONFIG_DIR: join(root, ".config"),
|
|
25
|
+
DEVSPACE_ALLOWED_ROOTS: root,
|
|
26
|
+
DEVSPACE_WORKTREE_ROOT: join(root, ".worktrees"),
|
|
27
|
+
DEVSPACE_AGENT_DIR: agentDir,
|
|
28
|
+
DEVSPACE_WIDGETS: "full",
|
|
29
|
+
DEVSPACE_TOOL_MODE: "full",
|
|
30
|
+
DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
|
|
31
|
+
PORT: "1",
|
|
32
|
+
});
|
|
33
|
+
const store = new SqliteWorkspaceStore(stateDir);
|
|
34
|
+
const workspaces = new WorkspaceRegistry(config, store);
|
|
35
|
+
const processSessions = new ProcessManager();
|
|
36
|
+
const codeIntelligence = new CodeIntelligenceManager(config, options.codeIntelligenceOptions);
|
|
37
|
+
const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence);
|
|
38
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
39
|
+
const client = new Client({ name: "forgerelay-code-intelligence-test-client", version: "1.0.0" });
|
|
40
|
+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
|
|
41
|
+
let closed = false;
|
|
42
|
+
const close = async () => {
|
|
43
|
+
if (closed)
|
|
44
|
+
return;
|
|
45
|
+
closed = true;
|
|
46
|
+
await client.close();
|
|
47
|
+
await server.close();
|
|
48
|
+
await codeIntelligence.shutdown();
|
|
49
|
+
processSessions.shutdown();
|
|
50
|
+
store.close();
|
|
51
|
+
};
|
|
52
|
+
t.after(async () => {
|
|
53
|
+
await close();
|
|
54
|
+
await rm(root, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
return { client, project, close };
|
|
57
|
+
}
|
|
58
|
+
export async function callOpen(client, path, conversationScopeId) {
|
|
59
|
+
return client.callTool({
|
|
60
|
+
name: "open_workspace",
|
|
61
|
+
arguments: { path },
|
|
62
|
+
_meta: { "openai/session": conversationScopeId },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function structuredContent(result) {
|
|
66
|
+
assert.ok(result.structuredContent);
|
|
67
|
+
return result.structuredContent;
|
|
68
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -21,7 +21,8 @@ import { deletePath, renamePath } from "./file-mutations.js";
|
|
|
21
21
|
import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, } from "./artifact-tools.js";
|
|
22
22
|
import { ArtifactError } from "./artifact-error.js";
|
|
23
23
|
import { loadConfig } from "./config.js";
|
|
24
|
-
import { CodeIntelligenceError
|
|
24
|
+
import { CodeIntelligenceError } from "./lsp/code-intelligence.js";
|
|
25
|
+
import { CodeIntelligenceManager } from "./lsp/runtime/manager.js";
|
|
25
26
|
import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
|
|
26
27
|
import { checkHookConfiguration } from "./hook-cli.js";
|
|
27
28
|
import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
|
|
@@ -757,7 +758,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
757
758
|
run: async (input, context) => {
|
|
758
759
|
try {
|
|
759
760
|
return {
|
|
760
|
-
value: await codeIntelligence.
|
|
761
|
+
value: await codeIntelligence.run(context.workspaceRoot, input),
|
|
761
762
|
};
|
|
762
763
|
}
|
|
763
764
|
catch (error) {
|
package/docs/configuration.md
CHANGED
|
@@ -154,10 +154,16 @@ force a Host to invalidate its cached schema.
|
|
|
154
154
|
### LSP code intelligence
|
|
155
155
|
|
|
156
156
|
ForgeRelay advertises `code.intelligence` through the Capability Gateway; it does
|
|
157
|
-
not add language-specific top-level MCP tools. ForgeRelay 0.4.
|
|
158
|
-
`definition`
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
not add language-specific top-level MCP tools. ForgeRelay 0.4.2 supports
|
|
158
|
+
`definition`, `hover`, and `references`. Position-based operations accept the same
|
|
159
|
+
workspace-relative source position. Hover results normalize plaintext, Markdown,
|
|
160
|
+
and supported legacy LSP payloads into one `contents` string with optional
|
|
161
|
+
`language` and normalized `range` metadata. References use the same normalized
|
|
162
|
+
location shape as definition, default to 100 returned locations, and accept a
|
|
163
|
+
`limit` from 1 through 1000; results report `returned`, `truncated`, and the real
|
|
164
|
+
`total` when the complete Language-server response makes it known. Language Servers
|
|
165
|
+
are external dependencies: ForgeRelay may discover an executable already installed
|
|
166
|
+
on the machine, but it never downloads or installs one automatically.
|
|
161
167
|
|
|
162
168
|
Effective Language-server definitions resolve in this order:
|
|
163
169
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"debug:accept": "node scripts/debug/accept.mjs",
|
|
43
43
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
44
44
|
"start": "node dist/cli.js serve",
|
|
45
|
-
"test": "tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
45
|
+
"test": "tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
47
|
"release:check": "node scripts/release-version.mjs check",
|
|
48
48
|
"release:tag-check": "node scripts/release-version.mjs tag",
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -332,11 +332,17 @@ try {
|
|
|
332
332
|
});
|
|
333
333
|
assert.equal(describedCodeIntelligence.isError, undefined);
|
|
334
334
|
assert.equal(describedCodeIntelligence.structuredContent.capability.guide.name, "code-intelligence");
|
|
335
|
-
|
|
336
|
-
assert.
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
const codeIntelligenceSchema = describedCodeIntelligence.structuredContent.capability.inputSchema;
|
|
336
|
+
assert.ok(Array.isArray(codeIntelligenceSchema.oneOf));
|
|
337
|
+
assert.deepEqual(
|
|
338
|
+
codeIntelligenceSchema.oneOf.map((variant) => variant.properties.operation.const),
|
|
339
|
+
["definition", "hover", "references"],
|
|
340
|
+
);
|
|
341
|
+
const referencesSchema = codeIntelligenceSchema.oneOf.find(
|
|
342
|
+
(variant) => variant.properties.operation.const === "references",
|
|
339
343
|
);
|
|
344
|
+
assert.equal(referencesSchema.properties.limit.minimum, 1);
|
|
345
|
+
assert.equal(referencesSchema.properties.limit.maximum, 1000);
|
|
340
346
|
if (process.platform === "linux") {
|
|
341
347
|
const describedArtifact = callTool(oauth.accessToken, sessionId, 82, "capability", {
|
|
342
348
|
workspaceId,
|