docdex 0.2.30 → 0.2.32

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
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
  - Remove legacy stdio MCP (`docdexd mcp` / `docdex-mcp-server`); MCP is served only over HTTP/SSE.
5
+ - Ensure npm tarballs include CLI wrapper entrypoints and restore missing wrappers during postinstall.
6
+ - Retry Windows file operations during install to reduce EPERM/EACCES failures.
7
+ - Nightly HTTP soak waits for index readiness before load testing.
8
+ - Windows CLI shims now target stable `lib/` entrypoints to avoid missing `bin/docdex.js`.
5
9
 
6
10
  ## 0.2.23
7
11
  - Add Smithery session config schema metadata (titles/descriptions, defaults, example config) for local MCP sessions.
package/assets/agents.md CHANGED
@@ -1,4 +1,4 @@
1
- ---- START OF DOCDEX INFO V0.2.30 ----
1
+ ---- START OF DOCDEX INFO V0.2.32 ----
2
2
  Docdex URL: http://127.0.0.1:28491
3
3
  Use this base URL for Docdex HTTP endpoints.
4
4
  Health check endpoint: `GET /healthz` (not `/v1/health`).
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const { spawn } = require("node:child_process");
7
+
8
+ const pkg = require("../package.json");
9
+ const { resolveDistBaseDir, resolveDistBaseCandidates } = require("./paths");
10
+ const {
11
+ artifactName,
12
+ detectLibcFromRuntime,
13
+ detectPlatformKey,
14
+ targetTripleForPlatformKey,
15
+ assetPatternForPlatformKey,
16
+ UnsupportedPlatformError
17
+ } = require("./platform");
18
+ const { checkForUpdateOnce } = require("./update_check");
19
+
20
+ function isDoctorCommand(argv) {
21
+ const sub = argv[0];
22
+ return sub === "doctor" || sub === "diagnostics";
23
+ }
24
+
25
+ function printLines(lines, { stderr } = {}) {
26
+ for (const line of lines) {
27
+ if (!line) continue;
28
+ if (stderr) console.error(line);
29
+ else console.log(line);
30
+ }
31
+ }
32
+
33
+ function envBool(value) {
34
+ if (!value) return false;
35
+ const normalized = String(value).trim().toLowerCase();
36
+ return ["1", "true", "t", "yes", "y", "on"].includes(normalized);
37
+ }
38
+
39
+ function readInstallMetadata({ fsModule, pathModule, basePath }) {
40
+ if (!fsModule || typeof fsModule.readFileSync !== "function") return null;
41
+ const metadataPath = pathModule.join(basePath, "docdexd-install.json");
42
+ try {
43
+ const raw = fsModule.readFileSync(metadataPath, "utf8");
44
+ return JSON.parse(raw);
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function formatInstallSource(meta) {
51
+ const source = meta?.archive?.source;
52
+ if (!source || typeof source !== "string") return "unknown";
53
+ if (source === "local") return "local binary";
54
+ return `release (${source})`;
55
+ }
56
+
57
+ function resolveInstallPaths(platformKey) {
58
+ const binaryName = process.platform === "win32" ? "docdexd.exe" : "docdexd";
59
+ const candidates = [];
60
+ for (const distBase of resolveDistBaseCandidates({ env: process.env })) {
61
+ candidates.push(path.join(distBase, platformKey));
62
+ }
63
+ candidates.push(path.join(__dirname, "..", "dist", platformKey));
64
+ const seen = new Set();
65
+ const unique = candidates.filter((candidate) => {
66
+ if (!candidate || seen.has(candidate)) return false;
67
+ seen.add(candidate);
68
+ return true;
69
+ });
70
+ for (const basePath of unique) {
71
+ const binaryPath = path.join(basePath, binaryName);
72
+ if (fs.existsSync(binaryPath)) {
73
+ return { basePath, binaryPath };
74
+ }
75
+ }
76
+ const fallbackBase =
77
+ unique[0] || path.join(resolveDistBaseDir({ env: process.env, fsModule: fs }), platformKey);
78
+ return { basePath: fallbackBase, binaryPath: path.join(fallbackBase, binaryName) };
79
+ }
80
+
81
+ function runDoctor() {
82
+ const platform = process.platform;
83
+ const arch = process.arch;
84
+
85
+ let libc = null;
86
+ if (platform === "linux") {
87
+ try {
88
+ libc = detectLibcFromRuntime();
89
+ } catch (err) {
90
+ printLines(
91
+ [
92
+ "[docdex] doctor failed: could not detect libc",
93
+ `[docdex] Detected platform: ${platform}/${arch}`,
94
+ `[docdex] Error: ${err?.message || String(err)}`
95
+ ],
96
+ { stderr: true }
97
+ );
98
+ process.exit(1);
99
+ return;
100
+ }
101
+ }
102
+
103
+ let report;
104
+ try {
105
+ const platformKey = detectPlatformKey();
106
+ const targetTriple = targetTripleForPlatformKey(platformKey);
107
+ const expectedAssetName = artifactName(platformKey);
108
+ const expectedAssetPattern = assetPatternForPlatformKey(platformKey, { exampleAssetName: expectedAssetName });
109
+ const distCandidates = resolveDistBaseCandidates({ env: process.env });
110
+ const distBase =
111
+ distCandidates[0] || resolveDistBaseDir({ env: process.env, fsModule: null });
112
+ const basePath = path.join(distBase, platformKey);
113
+ const installMeta = readInstallMetadata({ fsModule: fs, pathModule: path, basePath });
114
+ const installSource = formatInstallSource(installMeta);
115
+
116
+ report = {
117
+ exitCode: 0,
118
+ stderr: false,
119
+ lines: [
120
+ "[docdex] doctor",
121
+ `[docdex] Detected platform: ${platform}/${arch}${libc ? `/${libc}` : ""}`,
122
+ "[docdex] Supported: yes",
123
+ `[docdex] Platform key: ${platformKey}`,
124
+ `[docdex] Expected target triple: ${targetTriple}`,
125
+ `[docdex] Expected release asset: ${expectedAssetName}`,
126
+ `[docdex] Asset naming pattern: ${expectedAssetPattern}`,
127
+ `[docdex] Install source: ${installSource}`
128
+ ]
129
+ };
130
+ } catch (err) {
131
+ if (err instanceof UnsupportedPlatformError) {
132
+ const detected = `${err.details?.platform ?? platform}/${err.details?.arch ?? arch}`;
133
+ const libcSuffix = err.details?.libc ? `/${err.details.libc}` : "";
134
+ const candidatePlatformKey =
135
+ typeof err.details?.candidatePlatformKey === "string" ? err.details.candidatePlatformKey : null;
136
+ const candidateTargetTriple =
137
+ typeof err.details?.candidateTargetTriple === "string" ? err.details.candidateTargetTriple : null;
138
+ const supportedKeys = Array.isArray(err.details?.supportedPlatformKeys) ? err.details.supportedPlatformKeys : [];
139
+
140
+ const candidateAssetName = candidatePlatformKey ? artifactName(candidatePlatformKey) : null;
141
+ const candidateAssetPattern = candidatePlatformKey
142
+ ? assetPatternForPlatformKey(candidatePlatformKey, { exampleAssetName: candidateAssetName })
143
+ : null;
144
+
145
+ report = {
146
+ exitCode: err.exitCode || 3,
147
+ stderr: true,
148
+ lines: [
149
+ "[docdex] doctor",
150
+ `[docdex] Detected platform: ${detected}${libcSuffix}`,
151
+ "[docdex] Supported: no",
152
+ `[docdex] error code: ${err.code}`,
153
+ "[docdex] No download/install is attempted for this platform.",
154
+ candidatePlatformKey ? `[docdex] Platform key: ${candidatePlatformKey}` : null,
155
+ candidateTargetTriple ? `[docdex] Target triple: ${candidateTargetTriple}` : null,
156
+ candidateAssetPattern ? `[docdex] Asset naming pattern: ${candidateAssetPattern}` : null,
157
+ supportedKeys.length ? `[docdex] Supported platforms: ${supportedKeys.join(", ")}` : null,
158
+ "[docdex] Next steps: use a supported platform or build from source (Rust)."
159
+ ]
160
+ };
161
+ } else {
162
+ report = {
163
+ exitCode: 1,
164
+ stderr: true,
165
+ lines: [
166
+ "[docdex] doctor failed: unexpected error",
167
+ `[docdex] Detected platform: ${platform}/${arch}${libc ? `/${libc}` : ""}`,
168
+ `[docdex] Error: ${err?.message || String(err)}`
169
+ ]
170
+ };
171
+ }
172
+ }
173
+
174
+ printLines(report.lines, { stderr: report.stderr });
175
+ process.exit(report.exitCode);
176
+ }
177
+
178
+ async function run() {
179
+ const argv = process.argv.slice(2);
180
+ if (isDoctorCommand(argv)) {
181
+ runDoctor();
182
+ return;
183
+ }
184
+
185
+ let platformKey;
186
+ try {
187
+ platformKey = detectPlatformKey();
188
+ } catch (err) {
189
+ if (err instanceof UnsupportedPlatformError) {
190
+ const detected = `${err.details?.platform ?? process.platform}/${err.details?.arch ?? process.arch}`;
191
+ const libc = err.details?.libc ? `/${err.details.libc}` : "";
192
+ console.error(`[docdex] unsupported platform (${detected}${libc})`);
193
+ console.error(`[docdex] error code: ${err.code}`);
194
+ console.error("[docdex] No download/run was attempted for this platform.");
195
+ if (Array.isArray(err.details?.supportedPlatformKeys) && err.details.supportedPlatformKeys.length) {
196
+ console.error(`[docdex] Supported platforms: ${err.details.supportedPlatformKeys.join(", ")}`);
197
+ }
198
+ if (typeof err.details?.candidatePlatformKey === "string") {
199
+ console.error(`[docdex] Asset naming pattern: ${assetPatternForPlatformKey(err.details.candidatePlatformKey)}`);
200
+ }
201
+ console.error("[docdex] Next steps: use a supported platform or build from source (Rust).");
202
+ process.exit(err.exitCode || 3);
203
+ return;
204
+ }
205
+ console.error(`[docdex] failed to detect platform: ${err?.message || String(err)}`);
206
+ process.exit(1);
207
+ return;
208
+ }
209
+
210
+ const { binaryPath } = resolveInstallPaths(platformKey);
211
+
212
+ if (!fs.existsSync(binaryPath)) {
213
+ console.error(`[docdex] Missing binary for ${platformKey}. Try reinstalling or set DOCDEX_DOWNLOAD_REPO to a repo with release assets.`);
214
+ try {
215
+ console.error(`[docdex] Expected target triple: ${targetTripleForPlatformKey(platformKey)}`);
216
+ console.error(`[docdex] Asset naming pattern: ${assetPatternForPlatformKey(platformKey)}`);
217
+ } catch {}
218
+ process.exit(1);
219
+ return;
220
+ }
221
+
222
+ await checkForUpdateOnce({
223
+ currentVersion: pkg.version,
224
+ env: process.env,
225
+ stdout: process.stdout,
226
+ stderr: process.stderr,
227
+ logger: console
228
+ });
229
+
230
+ const env = { ...process.env };
231
+ const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit", env });
232
+ child.on("exit", (code) => process.exit(code ?? 1));
233
+ child.on("error", (err) => {
234
+ console.error(`[docdex] failed to launch binary: ${err.message}`);
235
+ process.exit(1);
236
+ });
237
+ }
238
+
239
+ run().catch((err) => {
240
+ console.error(`[docdex] unexpected error: ${err?.message || String(err)}`);
241
+ process.exit(1);
242
+ });
package/lib/install.js CHANGED
@@ -42,6 +42,31 @@ const DEFAULT_INTEGRITY_CONFIG = Object.freeze({
42
42
  const LOCAL_FALLBACK_ENV = "DOCDEX_LOCAL_FALLBACK";
43
43
  const LOCAL_BINARY_ENV = "DOCDEX_LOCAL_BINARY";
44
44
  const AGENTS_DOC_FILENAME = "agents.md";
45
+ const CLI_WRAPPER_SCRIPT = [
46
+ "#!/usr/bin/env node",
47
+ "\"use strict\";",
48
+ "",
49
+ "require(\"../lib/cli_entry\");",
50
+ ""
51
+ ].join("\n");
52
+ const MCP_STDIO_WRAPPER_SCRIPT = [
53
+ "#!/usr/bin/env node",
54
+ "\"use strict\";",
55
+ "",
56
+ "const { runBridge } = require(\"../lib/mcp_stdio_bridge\");",
57
+ "",
58
+ "async function main() {",
59
+ " try {",
60
+ " await runBridge({ stdin: process.stdin, stdout: process.stdout, stderr: process.stderr });",
61
+ " } catch (err) {",
62
+ " process.stderr.write(`[docdex-mcp-stdio] fatal: ${err}\\n`);",
63
+ " process.exit(1);",
64
+ " }",
65
+ "}",
66
+ "",
67
+ "main();",
68
+ ""
69
+ ].join("\n");
45
70
 
46
71
  const EXIT_CODE_BY_ERROR_CODE = Object.freeze({
47
72
  DOCDEX_INSTALLER_CONFIG: 2,
@@ -231,6 +256,53 @@ function writeAgentInstructions() {
231
256
  }
232
257
  }
233
258
 
259
+ function shouldWriteWrapper(fsModule, filePath) {
260
+ if (!fsModule?.existsSync) return true;
261
+ if (!fsModule.existsSync(filePath)) return true;
262
+ try {
263
+ const stat = fsModule.statSync(filePath);
264
+ if (!stat.isFile()) return true;
265
+ return stat.size < 8;
266
+ } catch {
267
+ return true;
268
+ }
269
+ }
270
+
271
+ function ensureCliWrappers({ fsModule = fs, pathModule = path, logger } = {}) {
272
+ const log = logger || console;
273
+ const binDir = pathModule.join(__dirname, "..", "bin");
274
+ const wrappers = [
275
+ { path: pathModule.join(binDir, "docdex.js"), contents: CLI_WRAPPER_SCRIPT },
276
+ { path: pathModule.join(binDir, "docdex-mcp-stdio.js"), contents: MCP_STDIO_WRAPPER_SCRIPT }
277
+ ];
278
+
279
+ try {
280
+ fsModule.mkdirSync(binDir, { recursive: true });
281
+ } catch (err) {
282
+ log?.warn?.(`[docdex] unable to ensure CLI wrappers (mkdir failed): ${err?.message || err}`);
283
+ return false;
284
+ }
285
+
286
+ let repaired = 0;
287
+ for (const wrapper of wrappers) {
288
+ if (!shouldWriteWrapper(fsModule, wrapper.path)) continue;
289
+ try {
290
+ fsModule.writeFileSync(wrapper.path, wrapper.contents, "utf8");
291
+ if (process.platform !== "win32" && fsModule.chmodSync) {
292
+ fsModule.chmodSync(wrapper.path, 0o755);
293
+ }
294
+ repaired += 1;
295
+ } catch (err) {
296
+ log?.warn?.(`[docdex] unable to write CLI wrapper at ${wrapper.path}: ${err?.message || err}`);
297
+ }
298
+ }
299
+
300
+ if (repaired > 0) {
301
+ log?.warn?.(`[docdex] restored ${repaired} CLI wrapper(s) under ${binDir}`);
302
+ }
303
+ return repaired > 0;
304
+ }
305
+
234
306
  function selectHttpClient(url) {
235
307
  try {
236
308
  const protocol = new URL(url).protocol;
@@ -421,6 +493,38 @@ function nowIso() {
421
493
  return new Date().toISOString();
422
494
  }
423
495
 
496
+ function sleep(ms) {
497
+ return new Promise((resolve) => setTimeout(resolve, ms));
498
+ }
499
+
500
+ function isRetryableFsError(err) {
501
+ if (!err || typeof err !== "object") return false;
502
+ const code = err.code;
503
+ return code === "EPERM" || code === "EACCES" || code === "EBUSY" || code === "ENOTEMPTY";
504
+ }
505
+
506
+ async function withWindowsRetry(fn, { attempts = 6, delayMs = 50 } = {}) {
507
+ if (process.platform !== "win32") {
508
+ return fn();
509
+ }
510
+ let lastErr = null;
511
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
512
+ try {
513
+ return await fn();
514
+ } catch (err) {
515
+ lastErr = err;
516
+ if (!isRetryableFsError(err)) throw err;
517
+ const waitMs = delayMs * Math.pow(2, attempt);
518
+ await sleep(waitMs);
519
+ }
520
+ }
521
+ throw lastErr;
522
+ }
523
+
524
+ async function renameWithRetry(fsModule, fromPath, toPath) {
525
+ return withWindowsRetry(() => fsModule.promises.rename(fromPath, toPath));
526
+ }
527
+
424
528
  async function readJsonFileIfPossible({ fsModule, filePath }) {
425
529
  if (!fsModule?.promises?.readFile) {
426
530
  return { value: null, error: "readFile_unavailable", errorCode: "READFILE_UNAVAILABLE" };
@@ -447,7 +551,7 @@ async function writeJsonFileAtomic({ fsModule, pathModule, filePath, value }) {
447
551
  const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
448
552
  const payload = `${JSON.stringify(value, null, 2)}\n`;
449
553
  await fsModule.promises.writeFile(tmp, payload, "utf8");
450
- await fsModule.promises.rename(tmp, filePath);
554
+ await renameWithRetry(fsModule, tmp, filePath);
451
555
  }
452
556
 
453
557
  function isValidInstallMetadata(meta) {
@@ -605,7 +709,7 @@ async function installFromLocalBinary({
605
709
  writeJsonFileAtomicFn,
606
710
  logger
607
711
  }) {
608
- await fsModule.promises.rm(distDir, { recursive: true, force: true });
712
+ await withWindowsRetry(() => fsModule.promises.rm(distDir, { recursive: true, force: true }));
609
713
  await fsModule.promises.mkdir(distDir, { recursive: true });
610
714
  const filename = isWin32 ? "docdexd.exe" : "docdexd";
611
715
  const destPath = pathModule.join(distDir, filename);
@@ -723,7 +827,7 @@ function failedDirName({ distDir, nonce }) {
723
827
  async function removeDirSafe(fsModule, dirPath) {
724
828
  if (!dirPath) return false;
725
829
  try {
726
- await fsModule.promises.rm(dirPath, { recursive: true, force: true });
830
+ await withWindowsRetry(() => fsModule.promises.rm(dirPath, { recursive: true, force: true }));
727
831
  return true;
728
832
  } catch {
729
833
  return false;
@@ -865,7 +969,7 @@ async function recoverInterruptedInstall({ fsModule, pathModule, distDir, isWin3
865
969
  const candidate = await selectLatestCandidate(fsModule, backups);
866
970
  if (candidate) {
867
971
  try {
868
- await fsModule.promises.rename(candidate.path, distDir);
972
+ await renameWithRetry(fsModule, candidate.path, distDir);
869
973
  recoveredFrom = candidate.path;
870
974
  action = "recovered";
871
975
  } catch (err) {
@@ -2002,11 +2106,11 @@ async function runInstaller(options) {
2002
2106
 
2003
2107
  if (existsSync && existsSync(distDir)) {
2004
2108
  await fsModule.promises.rm(backupDir, { recursive: true, force: true }).catch(() => {});
2005
- await fsModule.promises.rename(distDir, backupDir);
2109
+ await renameWithRetry(fsModule, distDir, backupDir);
2006
2110
  backupMoved = true;
2007
2111
  }
2008
2112
 
2009
- await fsModule.promises.rename(stagingDir, distDir);
2113
+ await renameWithRetry(fsModule, stagingDir, distDir);
2010
2114
  promoted = true;
2011
2115
 
2012
2116
  if (typeof restartFn === "function") {
@@ -2076,7 +2180,7 @@ async function runInstaller(options) {
2076
2180
  if (existsSync(distDir)) {
2077
2181
  await fsModule.promises.rm(distDir, { recursive: true, force: true });
2078
2182
  }
2079
- await fsModule.promises.rename(backupDir, distDir);
2183
+ await renameWithRetry(fsModule, backupDir, distDir);
2080
2184
  rollbackStatus = "restored previous installation";
2081
2185
  rollbackSucceeded = true;
2082
2186
  } else if (promoted && !backupMoved) {
@@ -2137,6 +2241,11 @@ async function main() {
2137
2241
  const env = process.env;
2138
2242
  const distBaseCandidates = resolveDistBaseCandidates({ env });
2139
2243
  const distBaseDir = resolveDistBaseDir({ env, fsModule: fs });
2244
+ try {
2245
+ ensureCliWrappers({ logger: console });
2246
+ } catch (err) {
2247
+ console.warn(`[docdex] CLI wrapper check failed: ${err?.message || err}`);
2248
+ }
2140
2249
  if (
2141
2250
  process.platform === "win32" &&
2142
2251
  !env?.DOCDEX_DIST_DIR &&
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { runBridge } = require("./mcp_stdio_bridge");
5
+
6
+ async function main() {
7
+ try {
8
+ await runBridge({ stdin: process.stdin, stdout: process.stdout, stderr: process.stderr });
9
+ } catch (err) {
10
+ process.stderr.write(`[docdex-mcp-stdio] fatal: ${err}\n`);
11
+ process.exit(1);
12
+ }
13
+ }
14
+
15
+ main();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "docdex",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "mcpName": "io.github.bekirdag/docdex",
5
5
  "description": "Local-first documentation and code indexer with HTTP/MCP search, AST, and agent memory.",
6
6
  "bin": {
7
- "docdex": "bin/docdex.js",
8
- "docdexd": "bin/docdex.js",
9
- "docdex-mcp-stdio": "bin/docdex-mcp-stdio.js"
7
+ "docdex": "lib/cli_entry.js",
8
+ "docdexd": "lib/cli_entry.js",
9
+ "docdex-mcp-stdio": "lib/mcp_stdio_cli.js"
10
10
  },
11
11
  "files": [
12
12
  "bin",