@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 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
- For 0.4.0, the supported operation is `definition`. Pass 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.
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-normalized locations rather than raw LSP wire types. A result 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.
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.literal("definition"),
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 definition(workspaceRoot, input) {
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 await service.definition(input);
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.definition(context.workspaceRoot, input),
760
+ value: await codeIntelligence.run(context.workspaceRoot, input),
761
761
  };
762
762
  }
763
763
  catch (error) {
@@ -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.0 supports the
158
- `definition` operation. Language Servers are external dependencies: ForgeRelay may
159
- discover an executable already installed on the machine, but it never downloads or
160
- installs one automatically.
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.0",
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",
@@ -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.equal(
337
- describedCodeIntelligence.structuredContent.capability.inputSchema.properties.operation.const,
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", {