@octocodeai/octocode-engine 16.6.0 → 16.6.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.
@@ -0,0 +1,253 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { chmodSync, existsSync, mkdirSync, openSync, closeSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { gunzipSync, inflateRawSync } from 'node:zlib';
5
+ import { detectPlatformId } from './platform.js';
6
+ import { cachedServerBinPath, managedCacheRoot, manifestServer, provisionMode, resolveCachedServer, } from './serverManifest.js';
7
+ /**
8
+ * The write half of LSP provisioning — downloads a pinned, verified portable
9
+ * server binary into the managed cache. This is invoked ONLY by the explicit
10
+ * `octocode lsp-server install` path, never lazily during a semantic query
11
+ * (the resolution ladder's L4 stays read-only). Provisioning a server is
12
+ * orthogonal to the runtime contract: when no server is available at query
13
+ * time the tool layer throws and the agent pivots to text search.
14
+ *
15
+ * Security invariants (all enforced here):
16
+ * - opt-in: refuses unless OCTOCODE_LSP_AUTO_INSTALL is prompt|auto
17
+ * - pinned SHA-256 gate: refuses if the manifest sha256 is null
18
+ * - host allowlist on the request URL AND every redirect hop; https only
19
+ * - atomic: download to a temp file in the dest dir, verify, chmod, rename
20
+ * - `.ok` completion marker written last, so a partial install never resolves
21
+ * - per-target lock so editor + CLI don't race the same download
22
+ * v1 handles `gz` and `none` (raw) assets with zero new deps; archive formats
23
+ * needing zip/tar/xz extraction return a clear "install manually" error.
24
+ */
25
+ const ALLOWED_HOSTS = new Set([
26
+ 'github.com',
27
+ // GitHub release assets redirect to a CDN host; both the legacy
28
+ // (objects.) and current (release-assets.) hosts are GitHub-owned.
29
+ 'objects.githubusercontent.com',
30
+ 'release-assets.githubusercontent.com',
31
+ 'releases.hashicorp.com',
32
+ ]);
33
+ const MAX_REDIRECTS = 5;
34
+ const LOCK_STALE_MS = 10 * 60 * 1000;
35
+ function fail(error) {
36
+ return { ok: false, error };
37
+ }
38
+ function hostAllowed(url) {
39
+ try {
40
+ const parsed = new URL(url);
41
+ return parsed.protocol === 'https:' && ALLOWED_HOSTS.has(parsed.hostname);
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ /** Fetch following redirects manually, re-checking the host allowlist each hop. */
48
+ async function fetchAllowlisted(url, signal) {
49
+ let current = url;
50
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
51
+ if (!hostAllowed(current)) {
52
+ // Report only the host — release-asset URLs carry signed query tokens we
53
+ // must not echo into logs/CLI output.
54
+ let host;
55
+ try {
56
+ host = new URL(current).host;
57
+ }
58
+ catch {
59
+ host = '(unparseable url)';
60
+ }
61
+ return { ok: false, error: `Blocked non-allowlisted/insecure host: ${host}` };
62
+ }
63
+ let response;
64
+ try {
65
+ response = await fetch(current, { redirect: 'manual', signal });
66
+ }
67
+ catch (err) {
68
+ return { ok: false, error: `Download failed: ${err instanceof Error ? err.message : String(err)}` };
69
+ }
70
+ if (response.status >= 300 && response.status < 400) {
71
+ const location = response.headers.get('location');
72
+ if (!location)
73
+ return { ok: false, error: 'Redirect without Location header' };
74
+ current = new URL(location, current).toString();
75
+ continue;
76
+ }
77
+ if (!response.ok) {
78
+ return { ok: false, error: `Download failed: HTTP ${response.status}` };
79
+ }
80
+ return { ok: true, body: await response.arrayBuffer() };
81
+ }
82
+ return { ok: false, error: 'Too many redirects' };
83
+ }
84
+ function sha256(buffer) {
85
+ return createHash('sha256').update(buffer).digest('hex');
86
+ }
87
+ /** Decode the downloaded asset into the final executable bytes. */
88
+ function extractBinary(asset, raw) {
89
+ switch (asset.archive) {
90
+ case 'none':
91
+ return { ok: true, bytes: raw };
92
+ case 'gz':
93
+ return { ok: true, bytes: gunzipSync(raw) };
94
+ case 'zip': {
95
+ if (!asset.binPath) {
96
+ return { ok: false, error: 'zip asset is missing binPath in manifest' };
97
+ }
98
+ const extracted = extractFromZip(raw, asset.binPath);
99
+ if (!extracted) {
100
+ return {
101
+ ok: false,
102
+ error: `Could not find '${asset.binPath}' in zip archive — try installing via your package manager.`,
103
+ };
104
+ }
105
+ return { ok: true, bytes: extracted };
106
+ }
107
+ case 'tar.gz':
108
+ case 'tar.xz':
109
+ return {
110
+ ok: false,
111
+ error: `Archive format '${asset.archive}' is not supported; install the server via your package manager.`,
112
+ };
113
+ default:
114
+ return { ok: false, error: `Unknown archive format '${asset.archive}'` };
115
+ }
116
+ }
117
+ /**
118
+ * Minimal ZIP local-file-header parser. Handles stored (method 0) and deflate
119
+ * (method 8) entries — the two methods used by all known LSP server releases.
120
+ * Searches for `targetPath` by exact match or trailing-segment match so both
121
+ * `clangd_22.1.0/bin/clangd` and `./clangd_22.1.0/bin/clangd` resolve.
122
+ */
123
+ function extractFromZip(zipBuffer, targetPath) {
124
+ const LOCAL_HEADER_SIG = 0x04034b50;
125
+ const normalizedTarget = targetPath.replace(/^\.?\//u, '');
126
+ let offset = 0;
127
+ while (offset + 30 < zipBuffer.length) {
128
+ if (zipBuffer.readUInt32LE(offset) !== LOCAL_HEADER_SIG)
129
+ break;
130
+ const compressionMethod = zipBuffer.readUInt16LE(offset + 8);
131
+ const compressedSize = zipBuffer.readUInt32LE(offset + 18);
132
+ const filenameLen = zipBuffer.readUInt16LE(offset + 26);
133
+ const extraLen = zipBuffer.readUInt16LE(offset + 28);
134
+ const filename = zipBuffer
135
+ .subarray(offset + 30, offset + 30 + filenameLen)
136
+ .toString('utf8')
137
+ .replace(/^\.?\//u, '');
138
+ const dataOffset = offset + 30 + filenameLen + extraLen;
139
+ if (filename === normalizedTarget) {
140
+ const compressed = zipBuffer.subarray(dataOffset, dataOffset + compressedSize);
141
+ if (compressionMethod === 0)
142
+ return compressed; // stored
143
+ if (compressionMethod === 8)
144
+ return inflateRawSync(compressed); // deflate
145
+ return null; // unsupported method within zip
146
+ }
147
+ offset = dataOffset + compressedSize;
148
+ }
149
+ return null;
150
+ }
151
+ function acquireLock(lockPath) {
152
+ try {
153
+ closeSync(openSync(lockPath, 'wx'));
154
+ return true;
155
+ }
156
+ catch {
157
+ // Reclaim a stale lock from a crashed/killed installer.
158
+ try {
159
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
160
+ rmSync(lockPath, { force: true });
161
+ closeSync(openSync(lockPath, 'wx'));
162
+ return true;
163
+ }
164
+ }
165
+ catch {
166
+ /* fall through */
167
+ }
168
+ return false;
169
+ }
170
+ }
171
+ /**
172
+ * Provision `serverName` into the managed cache. Idempotent: returns the
173
+ * already-present path when a verified copy exists. Pure-data inputs come from
174
+ * `serverManifest.json`; nothing is executed during install.
175
+ */
176
+ export async function provisionServer(serverName, options = {}) {
177
+ const platformId = detectPlatformId();
178
+ const mode = options.mode ?? provisionMode();
179
+ const server = manifestServer(serverName);
180
+ if (!server)
181
+ return fail(`${serverName} is not an auto-downloadable server.`);
182
+ const unsupported = server.unsupportedPlatforms?.[platformId];
183
+ if (unsupported)
184
+ return fail(unsupported);
185
+ const asset = server.platforms[platformId];
186
+ if (!asset) {
187
+ return fail(`No ${serverName} asset for platform ${platformId}.`);
188
+ }
189
+ // Idempotent: a verified copy is already present.
190
+ const existing = resolveCachedServer(serverName, platformId);
191
+ if (existing)
192
+ return { ok: true, path: existing, source: 'already-present' };
193
+ if (mode === 'off') {
194
+ return fail('Auto-install is off. Set OCTOCODE_LSP_AUTO_INSTALL=prompt|auto (or pass --yes) to allow downloading.');
195
+ }
196
+ if (!asset.sha256) {
197
+ return fail(`${serverName} has no pinned sha256 in the manifest yet; refusing to download unverified bytes.`);
198
+ }
199
+ const binPath = cachedServerBinPath(serverName, platformId);
200
+ if (!binPath)
201
+ return fail(`Cannot compute cache path for ${serverName}.`);
202
+ const dir = path.dirname(binPath);
203
+ mkdirSync(dir, { recursive: true });
204
+ const lockPath = path.join(dir, '.lock');
205
+ if (!acquireLock(lockPath)) {
206
+ return fail(`Another install of ${serverName} is in progress (${lockPath}).`);
207
+ }
208
+ try {
209
+ // Re-check after acquiring the lock (another process may have finished).
210
+ const racedWinner = resolveCachedServer(serverName, platformId);
211
+ if (racedWinner)
212
+ return { ok: true, path: racedWinner, source: 'already-present' };
213
+ const fetched = await fetchAllowlisted(asset.url, options.signal);
214
+ if (!fetched.ok)
215
+ return fail(fetched.error);
216
+ const downloaded = Buffer.from(fetched.body);
217
+ const actualSha = sha256(downloaded);
218
+ if (actualSha !== asset.sha256) {
219
+ return fail(`Checksum mismatch for ${serverName}: expected ${asset.sha256}, got ${actualSha}.`);
220
+ }
221
+ const extracted = extractBinary(asset, downloaded);
222
+ if (!extracted.ok)
223
+ return fail(extracted.error);
224
+ // Atomic: write a temp file in the same dir, chmod, then rename into place.
225
+ const tmpPath = `${binPath}.tmp-${process.pid}`;
226
+ writeFileSync(tmpPath, extracted.bytes);
227
+ if (process.platform !== 'win32')
228
+ chmodSync(tmpPath, 0o755);
229
+ renameSync(tmpPath, binPath);
230
+ // Completion marker LAST — resolveCachedServer trusts the binary only now.
231
+ writeFileSync(`${binPath}.ok`, JSON.stringify({ sha256: asset.sha256, size: extracted.bytes.length }));
232
+ return { ok: true, path: binPath, source: 'downloaded' };
233
+ }
234
+ finally {
235
+ rmSync(lockPath, { force: true });
236
+ }
237
+ }
238
+ /** Remove a server from the managed cache only (never touches external installs). */
239
+ export function uninstallServer(serverName, platformId = detectPlatformId()) {
240
+ const binPath = cachedServerBinPath(serverName, platformId);
241
+ if (!binPath)
242
+ return false;
243
+ const serverDir = path.dirname(path.dirname(binPath)); // <root>/<server>
244
+ if (!existsSync(serverDir))
245
+ return false;
246
+ // Guard: only ever delete inside the managed cache root.
247
+ const root = managedCacheRoot();
248
+ if (!path.resolve(serverDir).startsWith(path.resolve(root) + path.sep)) {
249
+ return false;
250
+ }
251
+ rmSync(serverDir, { recursive: true, force: true });
252
+ return true;
253
+ }
@@ -1,4 +1,11 @@
1
1
  export type InitializationOptions = Record<string, unknown>;
2
+ /**
3
+ * Where a language-server executable was resolved from, for honest status
4
+ * reporting. Ordered by the resolution ladder (see docs/context/LSP_GUIDE.md):
5
+ * explicit override / PATH → bundled JS dep → project-local → ecosystem dir →
6
+ * managed download cache → IDE bridge; `unavailable` when nothing provides it.
7
+ */
8
+ export type LspServerSource = 'path' | 'bundled' | 'project-local' | 'ecosystem' | 'managed-cache' | 'ide-bridge' | 'unavailable';
2
9
  export interface LanguageServerConfig {
3
10
  command: string;
4
11
  args?: string[];
@@ -201,6 +201,9 @@ function validateCommandArgs(command, args) {
201
201
  };
202
202
  }
203
203
  }
204
+ // grep: no per-flag allowlist — only the shared dangerous-pattern scan below
205
+ // applies. grep flags are wide (e.g. -r, -Z, --include) and intentionally
206
+ // left to the pattern scan rather than a strict allowlist.
204
207
  const patternPositions = getPatternArgPositions(command, args);
205
208
  for (let i = 0; i < args.length; i++) {
206
209
  const arg = args[i];
@@ -295,16 +298,13 @@ function getFindPatternPositions(args) {
295
298
  return positions;
296
299
  }
297
300
  function getPatternArgPositions(command, args) {
298
- switch (true) {
299
- case isRgCommand(command):
300
- return getRgPatternPositions(args);
301
- case command === 'grep':
302
- return getGrepPatternPositions(args);
303
- case command === 'find':
304
- return getFindPatternPositions(args);
305
- default:
306
- return new Set();
307
- }
301
+ if (isRgCommand(command))
302
+ return getRgPatternPositions(args);
303
+ if (command === 'grep')
304
+ return getGrepPatternPositions(args);
305
+ if (command === 'find')
306
+ return getFindPatternPositions(args);
307
+ return new Set();
308
308
  }
309
309
  function findDisallowedRgFlag(args) {
310
310
  for (let i = 0; i < args.length; i++) {
@@ -1,3 +1,10 @@
1
+ // Discovery pruning lists: skip these dirs/files/extensions during tree walks.
2
+ // SYNC NOTE: pathPatterns.ts:IGNORED_PATH_PATTERNS and
3
+ // filePatterns.ts:IGNORED_FILE_PATTERNS overlap these lists (e.g. .git, .aws,
4
+ // .ssh, .docker, .kube, id_rsa, .pem, .key). The two systems have different
5
+ // roles — pathPatterns/filePatterns block *access* (read-time check);
6
+ // discoveryFilter blocks *visibility* (tree-walk pruning). Both must be kept
7
+ // in sync so that adding a sensitive path/file to one is reflected in the other.
1
8
  export const DISCOVERY_IGNORED_FOLDER_NAMES = [
2
9
  '.github',
3
10
  '.git',
@@ -1,3 +1,9 @@
1
+ // File-access block list: prevents reading sensitive files by name/extension.
2
+ // SYNC NOTE: discoveryFilter.ts:DISCOVERY_IGNORED_FILE_NAMES and
3
+ // DISCOVERY_IGNORED_FILE_EXTENSIONS partially overlap this list (e.g. id_rsa,
4
+ // id_dsa, .pem, .key, .crt). Both sets must be kept in sync — a new sensitive
5
+ // file added here should also be added to discoveryFilter.ts to prevent it
6
+ // from appearing in discovery results, and vice versa.
1
7
  export const IGNORED_FILE_PATTERNS = [
2
8
  /\.env$/,
3
9
  /\.env\..+$/,
@@ -51,6 +51,11 @@ export function shouldIgnorePath(pathToCheck) {
51
51
  return regex.test(normalizedPath);
52
52
  }
53
53
  function normalizePathForIgnoreMatching(normalizedPath) {
54
+ if (normalizedPath === '/private/tmp')
55
+ return '/tmp';
56
+ if (normalizedPath.startsWith('/private/tmp/')) {
57
+ return normalizedPath.slice('/private'.length);
58
+ }
54
59
  if (normalizedPath === '/private/var')
55
60
  return '/var';
56
61
  if (normalizedPath.startsWith('/private/var/')) {
@@ -1,41 +1,24 @@
1
1
  import { nativeMaskSensitiveData } from './native.js';
2
2
  import { securityRegistry } from './registry.js';
3
- import { maskEveryOtherChar } from './maskUtils.js';
3
+ import { deduplicateSpans, applyMaskToSpans } from './maskUtils.js';
4
4
  function applyJsMask(text, patterns) {
5
5
  const applicable = patterns.filter(p => !p.fileContext);
6
6
  if (applicable.length === 0)
7
7
  return text;
8
- const matches = [];
8
+ const spans = [];
9
9
  for (const p of applicable) {
10
10
  p.regex.lastIndex = 0;
11
11
  let m;
12
12
  const re = new RegExp(p.regex.source, p.regex.flags.replace('g', '') + 'g');
13
13
  while ((m = re.exec(text)) !== null) {
14
- matches.push({ start: m.index, end: m.index + m[0].length });
14
+ spans.push({ start: m.index, end: m.index + m[0].length });
15
15
  if (m[0].length === 0)
16
16
  re.lastIndex++;
17
17
  }
18
18
  }
19
- if (matches.length === 0)
19
+ if (spans.length === 0)
20
20
  return text;
21
- matches.sort((a, b) => a.start - b.start);
22
- const deduped = [];
23
- let lastEnd = 0;
24
- for (const m of matches) {
25
- if (m.start >= lastEnd) {
26
- deduped.push(m);
27
- lastEnd = m.end;
28
- }
29
- }
30
- let result = text;
31
- for (let i = deduped.length - 1; i >= 0; i--) {
32
- const { start, end } = deduped[i];
33
- result =
34
- result.slice(0, start) +
35
- maskEveryOtherChar(text.slice(start, end)) +
36
- result.slice(end);
37
- }
38
- return result;
21
+ return applyMaskToSpans(text, deduplicateSpans(spans));
39
22
  }
40
23
  export function maskSensitiveData(text) {
41
24
  if (!text)
@@ -1 +1,7 @@
1
+ export type Span = {
2
+ start: number;
3
+ end: number;
4
+ };
1
5
  export declare function maskEveryOtherChar(text: string): string;
6
+ export declare function deduplicateSpans(spans: Span[]): Span[];
7
+ export declare function applyMaskToSpans(text: string, spans: Span[]): string;
@@ -5,3 +5,25 @@ export function maskEveryOtherChar(text) {
5
5
  }
6
6
  return result;
7
7
  }
8
+ export function deduplicateSpans(spans) {
9
+ spans.sort((a, b) => a.start - b.start);
10
+ const result = [];
11
+ let lastEnd = 0;
12
+ for (const span of spans) {
13
+ if (span.start >= lastEnd) {
14
+ result.push(span);
15
+ lastEnd = span.end;
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+ export function applyMaskToSpans(text, spans) {
21
+ let result = '';
22
+ let position = 0;
23
+ for (const span of spans) {
24
+ result += text.slice(position, span.start);
25
+ result += maskEveryOtherChar(text.slice(span.start, span.end));
26
+ position = span.end;
27
+ }
28
+ return result + text.slice(position);
29
+ }
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from 'module';
2
2
  import { dirname, join } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { maskEveryOtherChar } from './maskUtils.js';
4
+ import { deduplicateSpans, applyMaskToSpans } from './maskUtils.js';
5
5
  import { allRegexPatterns } from './regexes/index.js';
6
6
  const _require = createRequire(import.meta.url);
7
7
  const _dir = dirname(fileURLToPath(import.meta.url));
@@ -122,38 +122,21 @@ function sanitizeWithJsFallback(content, filePath) {
122
122
  function maskWithJsFallback(text) {
123
123
  if (!text)
124
124
  return text;
125
- const matches = [];
125
+ const spans = [];
126
126
  for (const pattern of allRegexPatterns) {
127
127
  if (pattern.fileContext)
128
128
  continue;
129
129
  const regex = cloneGlobalRegex(pattern.regex);
130
130
  let match;
131
131
  while ((match = regex.exec(text)) !== null) {
132
- matches.push({ start: match.index, end: match.index + match[0].length });
132
+ spans.push({ start: match.index, end: match.index + match[0].length });
133
133
  if (match[0].length === 0)
134
134
  regex.lastIndex++;
135
135
  }
136
136
  }
137
- if (matches.length === 0)
137
+ if (spans.length === 0)
138
138
  return text;
139
- matches.sort((left, right) => left.start - right.start);
140
- const nonOverlapping = [];
141
- let lastEnd = 0;
142
- for (const match of matches) {
143
- if (match.start >= lastEnd) {
144
- nonOverlapping.push(match);
145
- lastEnd = match.end;
146
- }
147
- }
148
- let result = '';
149
- let position = 0;
150
- for (const match of nonOverlapping) {
151
- result += text.slice(position, match.start);
152
- result += maskEveryOtherChar(text.slice(match.start, match.end));
153
- position = match.end;
154
- }
155
- result += text.slice(position);
156
- return result;
139
+ return applyMaskToSpans(text, deduplicateSpans(spans));
157
140
  }
158
141
  export const nativeSanitizeContent = (content, filePath) => getNativeModule()?.sanitizeContent(content, filePath) ??
159
142
  sanitizeWithJsFallback(content, filePath);
@@ -1,3 +1,8 @@
1
+ // Path-access block list: prevents reading from sensitive directories.
2
+ // SYNC NOTE: discoveryFilter.ts:DISCOVERY_IGNORED_FOLDER_NAMES overlaps this
3
+ // list (e.g. .git, .aws, .ssh, .docker, .kube). Both lists must be kept in
4
+ // sync — changes here that protect against directory traversal attacks should
5
+ // be reflected there, and vice versa.
1
6
  export const IGNORED_PATH_PATTERNS = [
2
7
  /(?:^|\/)\.git(?:\/|$)/,
3
8
  /(?:^|\/)\.ssh(?:\/|$)/,
@@ -49,6 +49,15 @@ export class PathValidator {
49
49
  if (!this.allowedRoots.includes(resolvedRoot)) {
50
50
  this.allowedRoots.push(resolvedRoot);
51
51
  }
52
+ try {
53
+ const realRoot = fs.realpathSync(resolvedRoot);
54
+ if (!this.allowedRoots.includes(realRoot)) {
55
+ this.allowedRoots.push(realRoot);
56
+ }
57
+ }
58
+ catch {
59
+ // Non-existent roots are still useful for validating future output paths.
60
+ }
52
61
  }
53
62
  isResolvedPathAllowed(absolutePath, resolvedPath) {
54
63
  return this.allowedRoots.some(root => {
@@ -76,24 +85,6 @@ export class PathValidator {
76
85
  }
77
86
  const expandedPath = this.expandTilde(inputPath);
78
87
  const absolutePath = path.resolve(expandedPath);
79
- const isAllowed = this.allowedRoots.some(root => {
80
- if (absolutePath === root) {
81
- return true;
82
- }
83
- return absolutePath.startsWith(root + path.sep);
84
- });
85
- if (!isAllowed) {
86
- return {
87
- isValid: false,
88
- error: `Path '${redactPath(inputPath)}' is outside allowed directories`,
89
- };
90
- }
91
- if (shouldIgnore(absolutePath)) {
92
- return {
93
- isValid: false,
94
- error: `Path '${redactPath(inputPath)}' is in an ignored directory or matches an ignored pattern`,
95
- };
96
- }
97
88
  try {
98
89
  const realPath = fs.realpathSync(absolutePath);
99
90
  const isRealPathAllowed = this.isResolvedPathAllowed(absolutePath, realPath);
@@ -103,6 +94,12 @@ export class PathValidator {
103
94
  error: `Symlink target '${redactPath(realPath)}' is outside allowed directories`,
104
95
  };
105
96
  }
97
+ if (shouldIgnore(absolutePath)) {
98
+ return {
99
+ isValid: false,
100
+ error: `Path '${redactPath(inputPath)}' is in an ignored directory or matches an ignored pattern`,
101
+ };
102
+ }
106
103
  if (shouldIgnore(realPath)) {
107
104
  return {
108
105
  isValid: false,
@@ -177,6 +174,12 @@ export class PathValidator {
177
174
  error: `Path '${redactPath(inputPath)}' is in an ignored directory or matches an ignored pattern`,
178
175
  };
179
176
  }
177
+ if (shouldIgnore(resolvedPath)) {
178
+ return {
179
+ isValid: false,
180
+ error: `Path '${redactPath(inputPath)}' resolves into an ignored directory or matches an ignored pattern`,
181
+ };
182
+ }
180
183
  return {
181
184
  isValid: true,
182
185
  sanitizedPath: resolvedPath,
@@ -27,6 +27,9 @@ function withToolTimeout(toolName, promise, signal, timeoutMs) {
27
27
  resolve(createErrorResult(`Tool '${toolName}' was cancelled by the client.`));
28
28
  };
29
29
  signal?.addEventListener('abort', onAbort, { once: true });
30
+ // Re-check after registering the listener: the signal may have been aborted
31
+ // in the window between addEventListener and this line, which the listener
32
+ // alone cannot catch.
30
33
  if (signal?.aborted) {
31
34
  clearTimeout(timer);
32
35
  signal.removeEventListener('abort', onAbort);
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@octocodeai/octocode-engine",
3
- "version": "16.6.0",
3
+ "version": "16.6.2",
4
4
  "description": "Rust native Octocode engine for context compression, structural search, and LSP semantic navigation",
5
5
  "type": "module",
6
6
  "main": "index.cjs",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
9
9
  "author": "Guy Bary <bgauryy@octocodeai.com>",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
10
13
  "repository": {
11
14
  "type": "git",
12
15
  "url": "https://github.com/bgauryy/octocode-mcp.git",
@@ -109,6 +112,26 @@
109
112
  "import": "./dist/lsp/manager.js",
110
113
  "default": "./dist/lsp/manager.js"
111
114
  },
115
+ "./lsp/platform": {
116
+ "types": "./dist/lsp/platform.d.ts",
117
+ "import": "./dist/lsp/platform.js",
118
+ "default": "./dist/lsp/platform.js"
119
+ },
120
+ "./lsp/serverDiscovery": {
121
+ "types": "./dist/lsp/serverDiscovery.d.ts",
122
+ "import": "./dist/lsp/serverDiscovery.js",
123
+ "default": "./dist/lsp/serverDiscovery.js"
124
+ },
125
+ "./lsp/serverManifest": {
126
+ "types": "./dist/lsp/serverManifest.d.ts",
127
+ "import": "./dist/lsp/serverManifest.js",
128
+ "default": "./dist/lsp/serverManifest.js"
129
+ },
130
+ "./lsp/serverProvisioner": {
131
+ "types": "./dist/lsp/serverProvisioner.d.ts",
132
+ "import": "./dist/lsp/serverProvisioner.js",
133
+ "default": "./dist/lsp/serverProvisioner.js"
134
+ },
112
135
  "./lsp/resolver": {
113
136
  "types": "./dist/lsp/resolver.d.ts",
114
137
  "import": "./dist/lsp/resolver.js",
@@ -196,6 +219,18 @@
196
219
  "lsp/manager": [
197
220
  "./dist/lsp/manager.d.ts"
198
221
  ],
222
+ "lsp/platform": [
223
+ "./dist/lsp/platform.d.ts"
224
+ ],
225
+ "lsp/serverDiscovery": [
226
+ "./dist/lsp/serverDiscovery.d.ts"
227
+ ],
228
+ "lsp/serverManifest": [
229
+ "./dist/lsp/serverManifest.d.ts"
230
+ ],
231
+ "lsp/serverProvisioner": [
232
+ "./dist/lsp/serverProvisioner.d.ts"
233
+ ],
199
234
  "lsp/resolver": [
200
235
  "./dist/lsp/resolver.d.ts"
201
236
  ],
@@ -227,12 +262,12 @@
227
262
  "README.md"
228
263
  ],
229
264
  "optionalDependencies": {
230
- "@octocodeai/octocode-engine-darwin-arm64": "16.6.0",
231
- "@octocodeai/octocode-engine-darwin-x64": "16.6.0",
232
- "@octocodeai/octocode-engine-linux-arm64-gnu": "16.6.0",
233
- "@octocodeai/octocode-engine-linux-x64-gnu": "16.6.0",
234
- "@octocodeai/octocode-engine-linux-x64-musl": "16.6.0",
235
- "@octocodeai/octocode-engine-win32-x64-msvc": "16.6.0"
265
+ "@octocodeai/octocode-engine-darwin-arm64": "16.6.2",
266
+ "@octocodeai/octocode-engine-darwin-x64": "16.6.2",
267
+ "@octocodeai/octocode-engine-linux-arm64-gnu": "16.6.2",
268
+ "@octocodeai/octocode-engine-linux-x64-gnu": "16.6.2",
269
+ "@octocodeai/octocode-engine-linux-x64-musl": "16.6.2",
270
+ "@octocodeai/octocode-engine-win32-x64-msvc": "16.6.2"
236
271
  },
237
272
  "engines": {
238
273
  "node": ">=20.0.0"
@@ -256,10 +291,11 @@
256
291
  "build:linux-x64-musl": "node scripts/prebuild.cjs && napi build --platform --release --cross-compile --target x86_64-unknown-linux-musl && node scripts/postbuild.cjs",
257
292
  "build:linux-arm64-gnu": "node scripts/prebuild.cjs && napi build --platform --release --cross-compile --target aarch64-unknown-linux-gnu && node scripts/postbuild.cjs",
258
293
  "build:windows-x64": "node scripts/prebuild.cjs && napi build --platform --release --cross-compile --target x86_64-pc-windows-msvc && node scripts/postbuild.cjs",
259
- "build:all": "yarn build:darwin-arm64 && yarn build:darwin-x64 && yarn build:linux-x64-gnu && yarn build:linux-x64-musl && yarn build:linux-arm64-gnu && yarn build:windows-x64 && yarn build:ts",
294
+ "build:all": "yarn clean:binaries && yarn build:darwin-arm64 && yarn build:darwin-x64 && yarn build:linux-x64-gnu && yarn build:linux-x64-musl && yarn build:linux-arm64-gnu && yarn build:windows-x64 && yarn build:ts",
260
295
  "build:dev": "yarn readme:sync && node scripts/prebuild.cjs && napi build --platform && node scripts/postbuild.cjs && tsc -p tsconfig.build.json",
261
296
  "build:ts": "yarn readme:sync && rm -rf dist && tsc -p tsconfig.build.json",
262
297
  "clean": "rm -rf dist/ && rm -f *.node",
298
+ "clean:binaries": "rm -f *.node && find npm -name '*.node' -delete",
263
299
  "gen": "node scripts/gen-patterns.mjs",
264
300
  "verify:patterns": "node scripts/gen-patterns.mjs && git diff --exit-code src/security/patterns.rs || (echo 'ERROR: src/security/patterns.rs is out of sync with the TS source — run yarn gen and commit the result' && exit 1)",
265
301
  "pack:check": "node scripts/check-pack-size.cjs",
@@ -302,8 +338,13 @@
302
338
  "prepublishOnly": "yarn version:sync"
303
339
  },
304
340
  "dependencies": {
341
+ "bash-language-server": "^5.6.0",
342
+ "intelephense": "^1.14.1",
343
+ "pyright": "^1.1.411",
305
344
  "typescript": "^5.9.3",
306
345
  "typescript-language-server": "^4.4.0",
346
+ "vscode-langservers-extracted": "^4.10.0",
347
+ "yaml-language-server": "^1.23.0",
307
348
  "zod": "^4.4.3"
308
349
  },
309
350
  "devDependencies": {