@octocodeai/octocode-engine 16.6.0 → 16.7.0

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.
Files changed (44) hide show
  1. package/README.md +190 -249
  2. package/dist/lsp/client.d.ts +18 -9
  3. package/dist/lsp/client.js +35 -25
  4. package/dist/lsp/config.d.ts +29 -1
  5. package/dist/lsp/config.js +156 -1
  6. package/dist/lsp/ideContext.d.ts +18 -0
  7. package/dist/lsp/ideContext.js +29 -0
  8. package/dist/lsp/index.d.ts +8 -3
  9. package/dist/lsp/index.js +7 -2
  10. package/dist/lsp/lspClientPool.d.ts +2 -0
  11. package/dist/lsp/lspClientPool.js +9 -4
  12. package/dist/lsp/manager.d.ts +11 -0
  13. package/dist/lsp/manager.js +72 -25
  14. package/dist/lsp/native.d.ts +2 -1
  15. package/dist/lsp/platform.d.ts +20 -0
  16. package/dist/lsp/platform.js +64 -0
  17. package/dist/lsp/resolver.js +10 -4
  18. package/dist/lsp/serverDiscovery.d.ts +63 -0
  19. package/dist/lsp/serverDiscovery.js +226 -0
  20. package/dist/lsp/serverManifest.d.ts +71 -0
  21. package/dist/lsp/serverManifest.js +116 -0
  22. package/dist/lsp/serverManifestData.d.ts +2 -0
  23. package/dist/lsp/serverManifestData.js +96 -0
  24. package/dist/lsp/serverProvisioner.d.ts +19 -0
  25. package/dist/lsp/serverProvisioner.js +262 -0
  26. package/dist/lsp/types.d.ts +18 -0
  27. package/dist/security/commandValidator.js +74 -10
  28. package/dist/security/contentSanitizer.js +4 -3
  29. package/dist/security/discoveryFilter.js +10 -0
  30. package/dist/security/filePatterns.js +6 -0
  31. package/dist/security/ignoredPathFilter.js +10 -2
  32. package/dist/security/mask.js +5 -22
  33. package/dist/security/maskUtils.d.ts +6 -0
  34. package/dist/security/maskUtils.js +22 -0
  35. package/dist/security/native.js +5 -22
  36. package/dist/security/pathPatterns.js +5 -0
  37. package/dist/security/pathValidator.js +38 -37
  38. package/dist/security/registry.js +27 -9
  39. package/dist/security/securityConstants.d.ts +1 -1
  40. package/dist/security/securityConstants.js +22 -1
  41. package/dist/security/withSecurityValidation.js +18 -3
  42. package/docs/LSP_SERVER_LIFECYCLE.md +258 -0
  43. package/index.d.ts +8 -2
  44. package/package.json +54 -10
@@ -1,21 +1,31 @@
1
- import type { CallHierarchyItem, CodeSnippet, ExactPosition, IncomingCall, LanguageServerConfig, OutgoingCall } from './types.js';
1
+ import type { CallHierarchyItem, CodeSnippet, ExactPosition, IncomingCall, LanguageServerConfig, LspReadiness, OutgoingCall } from './types.js';
2
2
  export declare class LSPClient {
3
3
  private readonly nativeClient;
4
+ private readonly command;
4
5
  private initialized;
6
+ private lastReadiness;
5
7
  constructor(config: LanguageServerConfig);
6
8
  start(): Promise<void>;
7
9
  stop(): Promise<void>;
8
- waitForReady(timeoutMs?: number): Promise<void>;
10
+ /**
11
+ * Wait for the server to finish post-`initialized` indexing and record the
12
+ * readiness signal. Runtime tolerance: until the native addon is rebuilt with
13
+ * the readiness return, the old binding resolves to `undefined` — treat that
14
+ * as the conservative `settledFallback` (we cannot confirm indexing finished).
15
+ */
16
+ waitForReady(timeoutMs?: number): Promise<LspReadiness>;
17
+ /**
18
+ * The readiness recorded by the most recent `waitForReady`, or `undefined`
19
+ * if it was never called (e.g. servers that answer immediately and skip the
20
+ * readiness wait). A zero-results semantic query on a client whose readiness
21
+ * is not `progressIdle` may be "not indexed yet" rather than a true absence.
22
+ */
23
+ getReadiness(): LspReadiness | undefined;
9
24
  gotoDefinition(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
10
- getDefinition(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
11
25
  findReferences(filePath: string, position: ExactPosition, includeDeclaration?: boolean, content?: string): Promise<CodeSnippet[]>;
12
- getHover(filePath: string, position: ExactPosition, content?: string): Promise<unknown>;
13
26
  hover(filePath: string, position: ExactPosition, content?: string): Promise<unknown>;
14
- getTypeDefinition(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
15
27
  typeDefinition(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
16
- getImplementation(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
17
28
  implementation(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
18
- getDocumentSymbols(filePath: string, content?: string): Promise<unknown>;
19
29
  documentSymbols(filePath: string, content?: string): Promise<unknown>;
20
30
  prepareCallHierarchy(filePath: string, position: ExactPosition, content?: string): Promise<CallHierarchyItem[]>;
21
31
  getIncomingCalls(item: CallHierarchyItem): Promise<IncomingCall[]>;
@@ -25,9 +35,8 @@ export declare class LSPClient {
25
35
  typeHierarchySupertypes(item: unknown): Promise<unknown[]>;
26
36
  typeHierarchySubtypes(item: unknown): Promise<unknown[]>;
27
37
  getDiagnostics(filePath: string, content?: string): Promise<unknown>;
28
- hasCapability(_capability: string): boolean;
38
+ hasCapability(capability: string): boolean;
29
39
  getRecentStderr(): string[];
30
40
  openDocument(filePath: string, content?: string): Promise<void>;
31
- ensureDocumentSynced(filePath: string, content?: string): Promise<void>;
32
41
  closeDocument(filePath: string): Promise<void>;
33
42
  }
@@ -1,9 +1,13 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { nativeBinding } from './native.js';
3
+ import { validateLSPServerPath } from './validation.js';
3
4
  export class LSPClient {
4
5
  nativeClient;
6
+ command;
5
7
  initialized = false;
8
+ lastReadiness;
6
9
  constructor(config) {
10
+ this.command = config.command;
7
11
  this.nativeClient = new nativeBinding.NativeLspClient({
8
12
  command: config.command,
9
13
  args: config.args,
@@ -14,6 +18,13 @@ export class LSPClient {
14
18
  });
15
19
  }
16
20
  async start() {
21
+ // Security gate: never spawn a command that isn't a real, executable,
22
+ // non-shell server binary — even one resolved from the managed download
23
+ // cache. This is the single chokepoint before the native process spawn.
24
+ const validation = validateLSPServerPath(this.command);
25
+ if (!validation.isValid) {
26
+ throw new Error(`Refusing to start language server: ${validation.error ?? `invalid server path '${this.command}'`}`);
27
+ }
17
28
  await this.nativeClient.start();
18
29
  this.initialized = true;
19
30
  }
@@ -21,13 +32,27 @@ export class LSPClient {
21
32
  await this.nativeClient.stop();
22
33
  this.initialized = false;
23
34
  }
35
+ /**
36
+ * Wait for the server to finish post-`initialized` indexing and record the
37
+ * readiness signal. Runtime tolerance: until the native addon is rebuilt with
38
+ * the readiness return, the old binding resolves to `undefined` — treat that
39
+ * as the conservative `settledFallback` (we cannot confirm indexing finished).
40
+ */
24
41
  async waitForReady(timeoutMs = 45_000) {
25
- await this.nativeClient.waitForReady(timeoutMs);
42
+ const readiness = await this.nativeClient.waitForReady(timeoutMs);
43
+ this.lastReadiness = readiness ?? 'settledFallback';
44
+ return this.lastReadiness;
45
+ }
46
+ /**
47
+ * The readiness recorded by the most recent `waitForReady`, or `undefined`
48
+ * if it was never called (e.g. servers that answer immediately and skip the
49
+ * readiness wait). A zero-results semantic query on a client whose readiness
50
+ * is not `progressIdle` may be "not indexed yet" rather than a true absence.
51
+ */
52
+ getReadiness() {
53
+ return this.lastReadiness;
26
54
  }
27
55
  async gotoDefinition(filePath, position, content) {
28
- return this.getDefinition(filePath, position, content);
29
- }
30
- async getDefinition(filePath, position, content) {
31
56
  await this.openDocument(filePath, content);
32
57
  return (await this.nativeClient.getDefinition(filePath, position.line, position.character));
33
58
  }
@@ -35,34 +60,22 @@ export class LSPClient {
35
60
  await this.openDocument(filePath, content);
36
61
  return (await this.nativeClient.getReferences(filePath, position.line, position.character, includeDeclaration));
37
62
  }
38
- async getHover(filePath, position, content) {
63
+ async hover(filePath, position, content) {
39
64
  await this.openDocument(filePath, content);
40
65
  return this.nativeClient.getHover(filePath, position.line, position.character);
41
66
  }
42
- async hover(filePath, position, content) {
43
- return this.getHover(filePath, position, content);
44
- }
45
- async getTypeDefinition(filePath, position, content) {
67
+ async typeDefinition(filePath, position, content) {
46
68
  await this.openDocument(filePath, content);
47
69
  return (await this.nativeClient.getTypeDefinition(filePath, position.line, position.character));
48
70
  }
49
- async typeDefinition(filePath, position, content) {
50
- return this.getTypeDefinition(filePath, position, content);
51
- }
52
- async getImplementation(filePath, position, content) {
71
+ async implementation(filePath, position, content) {
53
72
  await this.openDocument(filePath, content);
54
73
  return (await this.nativeClient.getImplementation(filePath, position.line, position.character));
55
74
  }
56
- async implementation(filePath, position, content) {
57
- return this.getImplementation(filePath, position, content);
58
- }
59
- async getDocumentSymbols(filePath, content) {
75
+ async documentSymbols(filePath, content) {
60
76
  await this.openDocument(filePath, content);
61
77
  return this.nativeClient.getDocumentSymbols(filePath);
62
78
  }
63
- async documentSymbols(filePath, content) {
64
- return this.getDocumentSymbols(filePath, content);
65
- }
66
79
  async prepareCallHierarchy(filePath, position, content) {
67
80
  await this.openDocument(filePath, content);
68
81
  const result = await this.nativeClient.prepareCallHierarchy(filePath, position.line, position.character);
@@ -97,17 +110,14 @@ export class LSPClient {
97
110
  await this.openDocument(filePath, content);
98
111
  return this.nativeClient.getDiagnostics(filePath);
99
112
  }
100
- hasCapability(_capability) {
113
+ hasCapability(capability) {
101
114
  return (this.initialized &&
102
- (this.nativeClient.hasCapability?.(_capability) ?? true));
115
+ (this.nativeClient.hasCapability?.(capability) ?? true));
103
116
  }
104
117
  getRecentStderr() {
105
118
  return this.nativeClient.getRecentStderr?.() ?? [];
106
119
  }
107
120
  async openDocument(filePath, content) {
108
- await this.ensureDocumentSynced(filePath, content ?? (await fs.readFile(filePath, 'utf8')));
109
- }
110
- async ensureDocumentSynced(filePath, content) {
111
121
  await this.nativeClient.openDocument(filePath, content ?? (await fs.readFile(filePath, 'utf8')));
112
122
  }
113
123
  async closeDocument(filePath) {
@@ -1,3 +1,31 @@
1
- import type { LanguageServerConfig } from './types.js';
1
+ import type { LanguageServerConfig, LspServerSource } from './types.js';
2
+ /**
3
+ * Names of the servers octocode bundles (offline-ready, zero install). Single
4
+ * source for any "what's bundled" listing (e.g. the `lsp-server` CLI), derived
5
+ * from the maps above so it never drifts. pyright is the bundled Python server
6
+ * even though the native default command is `pylsp`.
7
+ */
8
+ export declare const BUNDLED_SERVER_NAMES: readonly string[];
2
9
  export declare function detectLanguageId(filePath: string): string;
10
+ /**
11
+ * Resolve the language-server config for a file, walking the resolution ladder
12
+ * behind a stable signature. The public contract is unchanged — callers still
13
+ * get a `LanguageServerConfig | null`; only the resolution behind it is richer.
14
+ */
3
15
  export declare function getLanguageServerForFile(filePath: string, workspaceRoot?: string): Promise<LanguageServerConfig | null>;
16
+ export interface ServerResolution {
17
+ config: LanguageServerConfig;
18
+ source: LspServerSource;
19
+ }
20
+ /**
21
+ * Full resolution including provenance, for status reporting. Order:
22
+ * L0/L1 explicit override + PATH (already applied natively in config.rs)
23
+ * L2 bundled JS server (npm dep, launched via current Node)
24
+ * L3 project-local / ecosystem (cargo/go/python/npm-global/mason/brew…)
25
+ * L4 managed download cache (~/.octocode/lsp, if pre-provisioned)
26
+ * Returns `source: 'unavailable'` (config still populated) when nothing on the
27
+ * machine provides the server, so the caller can report honest guidance.
28
+ */
29
+ /** Check whether a bare command name is available on the current process PATH. */
30
+ export declare function isCommandOnPath(command: string): boolean;
31
+ export declare function resolveServerForFile(filePath: string, workspaceRoot?: string): Promise<ServerResolution | null>;
@@ -1,7 +1,162 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
1
3
  import { nativeBinding } from './native.js';
4
+ import { discoverServer } from './serverDiscovery.js';
5
+ import { manifestServer, resolveCachedServer } from './serverManifest.js';
6
+ const requireFromPackage = createRequire(import.meta.url);
7
+ /**
8
+ * Pure-JS language servers shipped as npm dependencies of this package. When
9
+ * the named command is not otherwise available we launch the bundled CLI with
10
+ * the current Node runtime — so TS/JS, YAML, and the JSON/HTML/CSS data-format
11
+ * servers work in production with zero user install. Keyed by the bare command
12
+ * name the native spec table (`config.rs`) emits, so the native `args` carry
13
+ * over unchanged.
14
+ */
15
+ const BUNDLED_JS_SERVERS = {
16
+ 'typescript-language-server': 'typescript-language-server/lib/cli.mjs',
17
+ 'yaml-language-server': 'yaml-language-server/bin/yaml-language-server',
18
+ 'vscode-json-language-server': 'vscode-langservers-extracted/bin/vscode-json-language-server',
19
+ 'vscode-html-language-server': 'vscode-langservers-extracted/bin/vscode-html-language-server',
20
+ 'vscode-css-language-server': 'vscode-langservers-extracted/bin/vscode-css-language-server',
21
+ // PHP: native spec defaults to `intelephense` — command-keyed so native args carry over.
22
+ 'intelephense': 'intelephense/lib/intelephense.js',
23
+ };
24
+ /**
25
+ * Bundled servers selected by languageId when the native default command
26
+ * differs from the bundled one. Python's spec defaults to `pylsp`, but we ship
27
+ * `pyright` — a different launch contract (`pyright-langserver --stdio`) — so
28
+ * it is matched by language, not command name, and supplies its own args.
29
+ */
30
+ const BUNDLED_BY_LANGUAGE = {
31
+ python: { cli: 'pyright/langserver.index.js', args: ['--stdio'] },
32
+ // Shell scripts: native spec table has no shellscript server spec, so this
33
+ // is the pre-native fallback path (detectLanguageId → 'shellscript' → here).
34
+ shellscript: { cli: 'bash-language-server/out/cli.js', args: ['start'] },
35
+ };
36
+ /**
37
+ * Names of the servers octocode bundles (offline-ready, zero install). Single
38
+ * source for any "what's bundled" listing (e.g. the `lsp-server` CLI), derived
39
+ * from the maps above so it never drifts. pyright is the bundled Python server
40
+ * even though the native default command is `pylsp`.
41
+ */
42
+ export const BUNDLED_SERVER_NAMES = [
43
+ ...Object.keys(BUNDLED_JS_SERVERS),
44
+ 'pyright',
45
+ 'bash-language-server',
46
+ ];
2
47
  export function detectLanguageId(filePath) {
3
48
  return nativeBinding.detectLanguageId(filePath) ?? 'plaintext';
4
49
  }
50
+ /**
51
+ * Resolve the language-server config for a file, walking the resolution ladder
52
+ * behind a stable signature. The public contract is unchanged — callers still
53
+ * get a `LanguageServerConfig | null`; only the resolution behind it is richer.
54
+ */
5
55
  export async function getLanguageServerForFile(filePath, workspaceRoot = process.cwd()) {
6
- return (nativeBinding.getLanguageServerForFile(filePath, workspaceRoot) ?? null);
56
+ return (await resolveServerForFile(filePath, workspaceRoot))?.config ?? null;
57
+ }
58
+ /**
59
+ * Full resolution including provenance, for status reporting. Order:
60
+ * L0/L1 explicit override + PATH (already applied natively in config.rs)
61
+ * L2 bundled JS server (npm dep, launched via current Node)
62
+ * L3 project-local / ecosystem (cargo/go/python/npm-global/mason/brew…)
63
+ * L4 managed download cache (~/.octocode/lsp, if pre-provisioned)
64
+ * Returns `source: 'unavailable'` (config still populated) when nothing on the
65
+ * machine provides the server, so the caller can report honest guidance.
66
+ */
67
+ /** Check whether a bare command name is available on the current process PATH. */
68
+ export function isCommandOnPath(command) {
69
+ return nativeBinding.isCommandAvailable(command);
70
+ }
71
+ export async function resolveServerForFile(filePath, workspaceRoot = process.cwd()) {
72
+ const base = nativeBinding.getLanguageServerForFile(filePath, workspaceRoot) ?? null;
73
+ // Pre-native: languages absent from the native spec table (e.g. shellscript)
74
+ // can still get a bundled server via BUNDLED_BY_LANGUAGE.
75
+ if (!base) {
76
+ const langId = nativeBinding.detectLanguageId(filePath) ?? '';
77
+ const preNative = langId ? BUNDLED_BY_LANGUAGE[langId] : undefined;
78
+ if (preNative) {
79
+ const cliPath = resolveBundledCli(preNative.cli);
80
+ if (cliPath) {
81
+ return {
82
+ config: {
83
+ command: process.execPath,
84
+ args: [cliPath, ...preNative.args],
85
+ languageId: langId,
86
+ },
87
+ source: 'bundled',
88
+ };
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ // L0/L1: the native command already resolved (explicit override or on PATH).
94
+ if (nativeBinding.isCommandAvailable(base.command)) {
95
+ return { config: base, source: 'path' };
96
+ }
97
+ // L2: bundled JS server launched with the current Node runtime.
98
+ const bundled = withBundledJsServer(base);
99
+ if (bundled)
100
+ return { config: bundled, source: 'bundled' };
101
+ // L3: discover a server installed outside PATH (ecosystem/project-local dirs).
102
+ const discovered = discoverServer(base.command, workspaceRoot);
103
+ if (discovered) {
104
+ return {
105
+ config: { ...base, command: discovered.command },
106
+ source: discovered.source === 'project-local' ? 'project-local' : 'ecosystem',
107
+ };
108
+ }
109
+ // L4: a binary already sitting in the managed download cache.
110
+ const cached = resolveCachedServer(base.command);
111
+ if (cached) {
112
+ const manifest = manifestServer(base.command);
113
+ return {
114
+ config: {
115
+ ...base,
116
+ command: cached,
117
+ args: manifest?.launchArgs ?? base.args,
118
+ },
119
+ source: 'managed-cache',
120
+ };
121
+ }
122
+ return { config: base, source: 'unavailable' };
123
+ }
124
+ function withBundledJsServer(config) {
125
+ // 1) Command-keyed: native command name matches the bundled package; keep
126
+ // the native args.
127
+ const byCommand = BUNDLED_JS_SERVERS[path.basename(config.command)];
128
+ if (byCommand) {
129
+ const cliPath = resolveBundledCli(byCommand);
130
+ if (cliPath) {
131
+ return {
132
+ ...config,
133
+ command: process.execPath,
134
+ args: [cliPath, ...(config.args ?? [])],
135
+ };
136
+ }
137
+ }
138
+ // 2) Language-keyed: a different bundled server than the native default
139
+ // (e.g. pyright for Python); use the bundled server's own launch args.
140
+ const byLanguage = path.basename(config.command) === config.command && config.languageId
141
+ ? BUNDLED_BY_LANGUAGE[config.languageId]
142
+ : undefined;
143
+ if (byLanguage) {
144
+ const cliPath = resolveBundledCli(byLanguage.cli);
145
+ if (cliPath) {
146
+ return {
147
+ ...config,
148
+ command: process.execPath,
149
+ args: [cliPath, ...byLanguage.args],
150
+ };
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ function resolveBundledCli(modulePath) {
156
+ try {
157
+ return requireFromPackage.resolve(modulePath);
158
+ }
159
+ catch {
160
+ return null;
161
+ }
7
162
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Detects which host the octocode engine is running under, so LSP server
3
+ * resolution can prefer the editor's own servers when hosted in an IDE and
4
+ * fall back to standalone discovery in a plain terminal.
5
+ *
6
+ * Signals are environment variables the editor sets deliberately (see
7
+ * jonschlinkert/detect-terminal and VS Code's `terminalEnvironment.ts`).
8
+ * `TERM_PROGRAM` is the primary signal; the vendor `*_PID` / IPC-handle vars
9
+ * corroborate it. `TERM` is intentionally ignored — it is terminfo, not an
10
+ * editor identity.
11
+ */
12
+ export type IdeHost = 'vscode' | 'cursor' | 'windsurf' | 'zed' | 'jetbrains' | 'terminal' | 'unknown';
13
+ export interface IdeContext {
14
+ host: IdeHost;
15
+ /** True when running inside an editor that ships language servers we could reuse. */
16
+ isIde: boolean;
17
+ }
18
+ export declare function detectIdeContext(env?: NodeJS.ProcessEnv): IdeContext;
@@ -0,0 +1,29 @@
1
+ function vscodeFork(env) {
2
+ // Cursor / Windsurf are VS Code forks and also report TERM_PROGRAM=vscode;
3
+ // disambiguate on their vendor markers before defaulting to upstream VS Code.
4
+ if (Object.keys(env).some(key => key.startsWith('CURSOR')))
5
+ return 'cursor';
6
+ if ('WINDSURF_PID' in env ||
7
+ Object.keys(env).some(key => key.startsWith('WINDSURF') || key.startsWith('CODEIUM'))) {
8
+ return 'windsurf';
9
+ }
10
+ return 'vscode';
11
+ }
12
+ export function detectIdeContext(env = process.env) {
13
+ const termProgram = (env.TERM_PROGRAM ?? '').toLowerCase();
14
+ if (termProgram === 'vscode' || 'VSCODE_PID' in env || 'VSCODE_GIT_IPC_HANDLE' in env) {
15
+ const host = vscodeFork(env);
16
+ return { host, isIde: true };
17
+ }
18
+ if (termProgram === 'zed' || 'ZED_TERM' in env) {
19
+ return { host: 'zed', isIde: true };
20
+ }
21
+ // JetBrains does not set TERM_PROGRAM; its tell is the JediTerm emulator.
22
+ if (env.TERMINAL_EMULATOR === 'JetBrains-JediTerm' || 'IDEA_INITIAL_DIRECTORY' in env) {
23
+ return { host: 'jetbrains', isIde: true };
24
+ }
25
+ if (termProgram === 'apple_terminal' || termProgram === 'iterm.app' || 'WT_SESSION' in env) {
26
+ return { host: 'terminal', isIde: false };
27
+ }
28
+ return { host: 'unknown', isIde: false };
29
+ }
@@ -1,7 +1,12 @@
1
1
  export { LSPClient } from './client.js';
2
- export { detectLanguageId, getLanguageServerForFile } from './config.js';
3
- export { acquirePooledClient, getLspStatus, isLanguageServerAvailable, LSP_UNAVAILABLE_HINT, pooledClientCount, releaseAllPooledClients, releasePooledClientForFile, type LspStatusInput, type LspStatusResult, } from './manager.js';
2
+ export { detectLanguageId, getLanguageServerForFile, isCommandOnPath, resolveServerForFile, BUNDLED_SERVER_NAMES, type ServerResolution, } from './config.js';
3
+ export { detectPlatformId, isMuslLinux, type PlatformId } from './platform.js';
4
+ export { detectIdeContext, type IdeContext, type IdeHost, } from './ideContext.js';
5
+ export { clearDiscoveryCache, discoverServer, discoverServerBatch, type DiscoveredServer, type DiscoverySource, } from './serverDiscovery.js';
6
+ export { cachedServerBinPath, isAutoDownloadable, listManifestServers, managedCacheRoot, manifestInstallHint, manifestServer, provisionMode, resolveCachedServer, type ArchiveKind, type ManifestAsset, type ManifestServer, type ProvisionMode, } from './serverManifest.js';
7
+ export { provisionServer, uninstallServer, type ProvisionResult, } from './serverProvisioner.js';
8
+ export { acquirePooledClient, getLspStatus, isLanguageServerAvailable, LSP_UNAVAILABLE_HINT, pooledClientCount, releaseAllPooledClients, releasePooledClientForFile, unavailableHintFor, TOOLCHAIN_SERVERS, type ToolchainServer, type LspStatusInput, type LspStatusResult, } from './manager.js';
4
9
  export { resolveImportAliasDefinitions, SymbolResolver, type ImportAliasDefinitionInput, } from './resolver.js';
5
10
  export { safeReadFile, validateLSPServerPath } from './validation.js';
6
11
  export { resolveWorkspaceRootForFile } from './workspaceRoot.js';
7
- export type { CallHierarchyItem, CodeSnippet, ExactPosition, FuzzyPosition, IncomingCall, InitializationOptions, LanguageServerCommand, LanguageServerConfig, LSPPaginationInfo, LSPRange, OutgoingCall, ReferenceLocation, ReferencesByFile, SymbolKind, UserLanguageServerConfig, } from './types.js';
12
+ export type { CallHierarchyItem, CodeSnippet, ExactPosition, FuzzyPosition, IncomingCall, InitializationOptions, LanguageServerCommand, LanguageServerConfig, LSPPaginationInfo, LSPRange, LspServerSource, OutgoingCall, ReferenceLocation, ReferencesByFile, SymbolKind, UserLanguageServerConfig, } from './types.js';
package/dist/lsp/index.js CHANGED
@@ -1,6 +1,11 @@
1
1
  export { LSPClient } from './client.js';
2
- export { detectLanguageId, getLanguageServerForFile } from './config.js';
3
- export { acquirePooledClient, getLspStatus, isLanguageServerAvailable, LSP_UNAVAILABLE_HINT, pooledClientCount, releaseAllPooledClients, releasePooledClientForFile, } from './manager.js';
2
+ export { detectLanguageId, getLanguageServerForFile, isCommandOnPath, resolveServerForFile, BUNDLED_SERVER_NAMES, } from './config.js';
3
+ export { detectPlatformId, isMuslLinux } from './platform.js';
4
+ export { detectIdeContext, } from './ideContext.js';
5
+ export { clearDiscoveryCache, discoverServer, discoverServerBatch, } from './serverDiscovery.js';
6
+ export { cachedServerBinPath, isAutoDownloadable, listManifestServers, managedCacheRoot, manifestInstallHint, manifestServer, provisionMode, resolveCachedServer, } from './serverManifest.js';
7
+ export { provisionServer, uninstallServer, } from './serverProvisioner.js';
8
+ export { acquirePooledClient, getLspStatus, isLanguageServerAvailable, LSP_UNAVAILABLE_HINT, pooledClientCount, releaseAllPooledClients, releasePooledClientForFile, unavailableHintFor, TOOLCHAIN_SERVERS, } from './manager.js';
4
9
  export { resolveImportAliasDefinitions, SymbolResolver, } from './resolver.js';
5
10
  export { safeReadFile, validateLSPServerPath } from './validation.js';
6
11
  export { resolveWorkspaceRootForFile } from './workspaceRoot.js';
@@ -21,7 +21,9 @@ export declare class LspClientPool<T extends PooledClient> {
21
21
  clearAll(): Promise<void>;
22
22
  size(): number;
23
23
  keys(): PoolKey[];
24
+ has(key: PoolKey): boolean;
24
25
  private resetIdleTimer;
25
26
  private startIdleTimer;
26
27
  }
28
+ export declare function serializeKey(key: PoolKey): string;
27
29
  export {};
@@ -53,6 +53,10 @@ export class LspClientPool {
53
53
  keys() {
54
54
  return [...this.entries.values()].map(entry => entry.key);
55
55
  }
56
+ has(key) {
57
+ const k = serializeKey(key);
58
+ return this.entries.has(k) || this.inflight.has(k);
59
+ }
56
60
  resetIdleTimer(k) {
57
61
  const entry = this.entries.get(k);
58
62
  if (!entry)
@@ -68,13 +72,14 @@ export class LspClientPool {
68
72
  this.entries.delete(k);
69
73
  void safeStop(entry.client);
70
74
  }, this.options.idleTimeoutMs);
71
- if (typeof timer === 'object' && 'unref' in timer) {
72
- timer.unref();
73
- }
75
+ // This package is Node-only (napi-rs). The timer is always a NodeJS.Timeout
76
+ // with an unref() method; calling it keeps the idle timer from preventing
77
+ // a clean process exit when no real work is in flight.
78
+ timer.unref?.();
74
79
  return timer;
75
80
  }
76
81
  }
77
- function serializeKey(key) {
82
+ export function serializeKey(key) {
78
83
  return `${key.serverId ?? key.languageId}\u0000${key.workspaceRoot}`;
79
84
  }
80
85
  async function safeStop(client) {
@@ -1,7 +1,16 @@
1
1
  import { LSPClient } from './client.js';
2
2
  import { type PoolKey } from './lspClientPool.js';
3
+ import type { LspServerSource } from './types.js';
3
4
  export declare function isLanguageServerAvailable(filePath: string, workspaceRoot?: string): Promise<boolean>;
4
5
  export declare const LSP_UNAVAILABLE_HINT = "No language server is available for this file, so no semantic results were returned. Install a matching language server or set the relevant OCTOCODE_*_SERVER_PATH environment variable. For a text-based search meanwhile, use localSearchCode.";
6
+ export interface ToolchainServer {
7
+ server: string;
8
+ languageId: string;
9
+ hint: string;
10
+ }
11
+ export declare const TOOLCHAIN_SERVERS: readonly ToolchainServer[];
12
+ /** Honest, actionable guidance for a file whose server did not resolve. */
13
+ export declare function unavailableHintFor(languageId?: string, command?: string): string;
5
14
  export declare function acquirePooledClient(workspaceRoot: string, filePath: string): Promise<LSPClient | null>;
6
15
  export declare function releaseAllPooledClients(): Promise<void>;
7
16
  export declare function releasePooledClientForFile(workspaceRoot: string, filePath: string): Promise<boolean>;
@@ -17,6 +26,8 @@ export type LspStatusResult = {
17
26
  workspaceRoot?: string;
18
27
  languageId?: string;
19
28
  serverAvailable?: boolean;
29
+ /** Which layer of the resolution ladder provided the server (or `unavailable`). */
30
+ serverSource?: LspServerSource;
20
31
  hints: string[];
21
32
  };
22
33
  export declare function getLspStatus(input?: LspStatusInput): Promise<LspStatusResult>;