@akira-tl/forgerelay 0.4.0 → 0.4.1
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 +11 -0
- package/capabilities/code-intelligence/GUIDE.md +2 -2
- package/dist/capability-registry.js +1 -1
- package/dist/lsp/code-intelligence-types.js +1 -0
- package/dist/lsp/code-intelligence.js +48 -3
- package/dist/lsp/normalization/hover.js +29 -0
- package/dist/server.js +1 -1
- package/docs/configuration.md +7 -4
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.1] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `code.intelligence` `hover` support through the same shared Language-service and filesystem synchronization path as definition lookup.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- Language Servers that do not advertise hover return `code.operation_unsupported` without invalidating the shared Language service.
|
|
17
|
+
|
|
7
18
|
## [0.4.0] - 2026-08-11
|
|
8
19
|
|
|
9
20
|
### Added
|
|
@@ -4,8 +4,8 @@ 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.1 supports `definition` and `hover`. Both 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. A definition may identify an External code location outside the Workspace, but that does not expand ForgeRelay's allowed roots or grant the file tools permission to read that path.
|
|
10
10
|
|
|
11
11
|
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.
|
|
@@ -116,7 +116,7 @@ export class CapabilityRegistry {
|
|
|
116
116
|
export function createCapabilityRegistry(dependencies) {
|
|
117
117
|
const hooksCheckInput = z.object({}).strict();
|
|
118
118
|
const codeIntelligenceInput = z.object({
|
|
119
|
-
operation: z.
|
|
119
|
+
operation: z.enum(["definition", "hover"]),
|
|
120
120
|
path: z.string().min(1),
|
|
121
121
|
line: z.number().int(),
|
|
122
122
|
column: z.number().int(),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,10 +3,11 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
3
3
|
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath, 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";
|
|
6
|
+
import { DefinitionRequest, DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, ExitNotification, HoverRequest, InitializeRequest, InitializedNotification, MarkupKind, PositionEncodingKind, ShutdownRequest, TextDocumentSyncKind, } from "vscode-languageserver-protocol";
|
|
7
7
|
import { LanguageServerConfigurationError, resolveLanguageProject, } from "./language-server-config.js";
|
|
8
8
|
import { terminateProcessTree } from "../process-platform.js";
|
|
9
9
|
import { CodeIntelligenceError } from "./code-intelligence-error.js";
|
|
10
|
+
import { normalizeHoverContents } from "./normalization/hover.js";
|
|
10
11
|
import { lspPositionFromUser, rangeFromLsp, wholeDocumentRange } from "./position-encoding.js";
|
|
11
12
|
export { CodeIntelligenceError } from "./code-intelligence-error.js";
|
|
12
13
|
const LANGUAGE_SERVICE_IDLE_MS = 10 * 60 * 1_000;
|
|
@@ -75,6 +76,44 @@ class LanguageService {
|
|
|
75
76
|
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
async hover(input) {
|
|
80
|
+
try {
|
|
81
|
+
await this.ensureStarted();
|
|
82
|
+
if (!this.capabilities?.hoverProvider) {
|
|
83
|
+
throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not advertise hover support.`);
|
|
84
|
+
}
|
|
85
|
+
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
86
|
+
const document = await this.syncDocument(sourcePath);
|
|
87
|
+
const position = lspPositionFromUser(document.text, input.line, input.column, this.positionEncoding);
|
|
88
|
+
const response = await withTimeout(this.connection.sendRequest(HoverRequest.type, {
|
|
89
|
+
textDocument: { uri: document.uri },
|
|
90
|
+
position,
|
|
91
|
+
}), this.policy.requestTimeoutMs, () => new CodeIntelligenceError("code.request_timeout", `Hover request timed out for ${input.path}.`));
|
|
92
|
+
const common = {
|
|
93
|
+
operation: "hover",
|
|
94
|
+
selectedServer: this.project.definition.id,
|
|
95
|
+
projectRoot: workspaceDisplayPath(this.workspaceRoot, this.project.projectRoot),
|
|
96
|
+
};
|
|
97
|
+
if (!response)
|
|
98
|
+
return { ...common, contents: null };
|
|
99
|
+
const normalized = normalizeHoverContents(response.contents);
|
|
100
|
+
return {
|
|
101
|
+
...common,
|
|
102
|
+
...normalized,
|
|
103
|
+
...(response.range
|
|
104
|
+
? { range: rangeFromLsp(document.text, response.range, this.positionEncoding) }
|
|
105
|
+
: {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof CodeIntelligenceError)
|
|
110
|
+
throw error;
|
|
111
|
+
if (error instanceof LanguageServerConfigurationError) {
|
|
112
|
+
throw new CodeIntelligenceError(error.code, error.message);
|
|
113
|
+
}
|
|
114
|
+
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
78
117
|
async shutdown() {
|
|
79
118
|
if (this.closed)
|
|
80
119
|
return;
|
|
@@ -189,6 +228,10 @@ class LanguageService {
|
|
|
189
228
|
dynamicRegistration: false,
|
|
190
229
|
linkSupport: true,
|
|
191
230
|
},
|
|
231
|
+
hover: {
|
|
232
|
+
dynamicRegistration: false,
|
|
233
|
+
contentFormat: [MarkupKind.Markdown, MarkupKind.PlainText],
|
|
234
|
+
},
|
|
192
235
|
},
|
|
193
236
|
},
|
|
194
237
|
};
|
|
@@ -300,7 +343,7 @@ export class CodeIntelligenceManager {
|
|
|
300
343
|
}, this.policy.cleanupIntervalMs);
|
|
301
344
|
this.cleanupTimer.unref();
|
|
302
345
|
}
|
|
303
|
-
async
|
|
346
|
+
async run(workspaceRoot, input) {
|
|
304
347
|
let project;
|
|
305
348
|
let canonicalWorkspaceRoot;
|
|
306
349
|
try {
|
|
@@ -319,7 +362,9 @@ export class CodeIntelligenceManager {
|
|
|
319
362
|
}
|
|
320
363
|
const service = await this.acquireService(canonicalWorkspaceRoot, project);
|
|
321
364
|
try {
|
|
322
|
-
return
|
|
365
|
+
return input.operation === "definition"
|
|
366
|
+
? await service.definition(input)
|
|
367
|
+
: await service.hover(input);
|
|
323
368
|
}
|
|
324
369
|
finally {
|
|
325
370
|
service.release();
|
|
@@ -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
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -757,7 +757,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
757
757
|
run: async (input, context) => {
|
|
758
758
|
try {
|
|
759
759
|
return {
|
|
760
|
-
value: await codeIntelligence.
|
|
760
|
+
value: await codeIntelligence.run(context.workspaceRoot, input),
|
|
761
761
|
};
|
|
762
762
|
}
|
|
763
763
|
catch (error) {
|
package/docs/configuration.md
CHANGED
|
@@ -154,10 +154,13 @@ 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.1 supports
|
|
158
|
+
`definition` and `hover`. Both operations accept the same workspace-relative source
|
|
159
|
+
position. Hover results normalize plaintext, Markdown, and supported legacy LSP
|
|
160
|
+
payloads into one `contents` string with optional `language` and normalized `range`
|
|
161
|
+
metadata. Language Servers are external dependencies: ForgeRelay may discover an
|
|
162
|
+
executable already installed on the machine, but it never downloads or installs one
|
|
163
|
+
automatically.
|
|
161
164
|
|
|
162
165
|
Effective Language-server definitions resolve in this order:
|
|
163
166
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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/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
|
@@ -333,9 +333,9 @@ try {
|
|
|
333
333
|
assert.equal(describedCodeIntelligence.isError, undefined);
|
|
334
334
|
assert.equal(describedCodeIntelligence.structuredContent.capability.guide.name, "code-intelligence");
|
|
335
335
|
assert.equal(describedCodeIntelligence.structuredContent.capability.inputSchema.type, "object");
|
|
336
|
-
assert.
|
|
337
|
-
describedCodeIntelligence.structuredContent.capability.inputSchema.properties.operation.
|
|
338
|
-
"definition",
|
|
336
|
+
assert.deepEqual(
|
|
337
|
+
describedCodeIntelligence.structuredContent.capability.inputSchema.properties.operation.enum,
|
|
338
|
+
["definition", "hover"],
|
|
339
339
|
);
|
|
340
340
|
if (process.platform === "linux") {
|
|
341
341
|
const describedArtifact = callTool(oauth.accessToken, sessionId, 82, "capability", {
|