docdex 0.2.28 → 0.2.29

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/assets/agents.md CHANGED
@@ -1,4 +1,4 @@
1
- ---- START OF DOCDEX INFO V0.2.27 ----
1
+ ---- START OF DOCDEX INFO V0.2.29 ----
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`).
@@ -18,6 +18,8 @@ const DAEMON_HEALTH_TIMEOUT_MS = 8000;
18
18
  const DAEMON_HEALTH_REQUEST_TIMEOUT_MS = 1000;
19
19
  const DAEMON_HEALTH_POLL_INTERVAL_MS = 200;
20
20
  const DAEMON_HEALTH_PATH = "/healthz";
21
+ const DAEMON_INFO_PATH = "/ai-help";
22
+ const DAEMON_PORT_RELEASE_TIMEOUT_MS = 5000;
21
23
  const STARTUP_FAILURE_MARKER = "startup_registration_failed.json";
22
24
  const DEFAULT_OLLAMA_MODEL = "nomic-embed-text";
23
25
  const DEFAULT_OLLAMA_CHAT_MODEL = "phi3.5:3.8b";
@@ -120,6 +122,139 @@ async function waitForDaemonHealthy({ host, port, timeoutMs = DAEMON_HEALTH_TIME
120
122
  return false;
121
123
  }
122
124
 
125
+ async function waitForPortAvailable({
126
+ host,
127
+ port,
128
+ timeoutMs = DAEMON_PORT_RELEASE_TIMEOUT_MS
129
+ }) {
130
+ const deadline = Date.now() + timeoutMs;
131
+ while (Date.now() < deadline) {
132
+ if (await isPortAvailable(port, host)) {
133
+ return true;
134
+ }
135
+ await sleep(DAEMON_HEALTH_POLL_INTERVAL_MS);
136
+ }
137
+ return false;
138
+ }
139
+
140
+ function isPidRunning(pid) {
141
+ if (!Number.isFinite(pid) || pid <= 0) return false;
142
+ try {
143
+ process.kill(pid, 0);
144
+ return true;
145
+ } catch (err) {
146
+ return err?.code === "EPERM";
147
+ }
148
+ }
149
+
150
+ function readDaemonLockMetadataForPort(port) {
151
+ for (const lockPath of daemonLockPaths()) {
152
+ if (!lockPath || !fs.existsSync(lockPath)) continue;
153
+ try {
154
+ const raw = fs.readFileSync(lockPath, "utf8");
155
+ if (!raw.trim()) continue;
156
+ const payload = JSON.parse(raw);
157
+ const lockPort = Number(payload?.port);
158
+ const pid = Number(payload?.pid);
159
+ if (!Number.isFinite(lockPort) || lockPort !== port) continue;
160
+ return { pid, port: lockPort };
161
+ } catch {
162
+ continue;
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+
168
+ function normalizeVersion(value) {
169
+ return String(value || "")
170
+ .trim()
171
+ .replace(/^v/i, "");
172
+ }
173
+
174
+ function fetchDaemonInfo({ host, port, timeoutMs = DAEMON_HEALTH_REQUEST_TIMEOUT_MS }) {
175
+ return new Promise((resolve) => {
176
+ const req = http.request(
177
+ {
178
+ host,
179
+ port,
180
+ path: DAEMON_INFO_PATH,
181
+ method: "GET",
182
+ timeout: timeoutMs
183
+ },
184
+ (res) => {
185
+ let body = "";
186
+ res.setEncoding("utf8");
187
+ res.on("data", (chunk) => {
188
+ body += chunk;
189
+ });
190
+ res.on("end", () => {
191
+ if (res.statusCode !== 200) return resolve(null);
192
+ try {
193
+ const payload = JSON.parse(body);
194
+ resolve(payload);
195
+ } catch {
196
+ resolve(null);
197
+ }
198
+ });
199
+ }
200
+ );
201
+ req.on("timeout", () => {
202
+ req.destroy();
203
+ resolve(null);
204
+ });
205
+ req.on("error", () => resolve(null));
206
+ req.end();
207
+ });
208
+ }
209
+
210
+ function checkDocdexIdentity({ host, port, timeoutMs = DAEMON_HEALTH_REQUEST_TIMEOUT_MS }) {
211
+ return fetchDaemonInfo({ host, port, timeoutMs }).then((payload) => payload?.product === "Docdex");
212
+ }
213
+
214
+ async function resolveDaemonPortState({ host, port, logger, deps } = {}) {
215
+ const log = logger || console;
216
+ const helpers = {
217
+ isPortAvailable,
218
+ checkDaemonHealth,
219
+ checkDocdexIdentity,
220
+ stopDaemonService,
221
+ stopDaemonFromLock,
222
+ stopDaemonByName,
223
+ clearDaemonLocks,
224
+ sleep,
225
+ readDaemonLockMetadataForPort,
226
+ isPidRunning,
227
+ normalizeVersion
228
+ };
229
+ if (deps && typeof deps === "object") {
230
+ Object.assign(helpers, deps);
231
+ }
232
+
233
+ let available = await helpers.isPortAvailable(port, host);
234
+ if (available) return { available: true, reuseExisting: false, stopped: false };
235
+
236
+ helpers.stopDaemonService({ logger: log });
237
+ helpers.stopDaemonFromLock({ logger: log });
238
+ helpers.stopDaemonByName({ logger: log });
239
+ await helpers.sleep(DAEMON_HEALTH_POLL_INTERVAL_MS);
240
+
241
+ available = await helpers.isPortAvailable(port, host);
242
+ if (available) {
243
+ helpers.clearDaemonLocks();
244
+ return { available: true, reuseExisting: false, stopped: true };
245
+ }
246
+
247
+ const lockMeta = helpers.readDaemonLockMetadataForPort(port);
248
+ const lockRunning = lockMeta ? helpers.isPidRunning(lockMeta.pid) : false;
249
+ const healthy = await helpers.checkDaemonHealth({ host, port });
250
+ const identity = lockRunning ? true : await helpers.checkDocdexIdentity({ host, port });
251
+ const reuseExisting = Boolean(lockRunning || healthy || identity);
252
+ if (reuseExisting) {
253
+ log.warn?.(`[docdex] ${host}:${port} already in use by a running docdex daemon; reusing it.`);
254
+ }
255
+ return { available: false, reuseExisting, stopped: false };
256
+ }
257
+
123
258
  function parseServerBind(contents) {
124
259
  let inServer = false;
125
260
  const lines = contents.split(/\r?\n/);
@@ -2254,8 +2389,12 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
2254
2389
  existingConfig = fs.readFileSync(configPath, "utf8");
2255
2390
  }
2256
2391
  const port = DEFAULT_DAEMON_PORT;
2257
- const available = await isPortAvailable(port, DEFAULT_HOST);
2258
- if (!available) {
2392
+ const portState = await resolveDaemonPortState({
2393
+ host: DEFAULT_HOST,
2394
+ port,
2395
+ logger: log
2396
+ });
2397
+ if (!portState.available && !portState.reuseExisting) {
2259
2398
  log.error?.(
2260
2399
  `[docdex] ${DEFAULT_HOST}:${port} is already in use; docdex requires a fixed port. Stop the process using this port and re-run the install.`
2261
2400
  );
@@ -2268,19 +2407,42 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
2268
2407
  binaryPath: resolvedBinary,
2269
2408
  logger: log
2270
2409
  });
2271
- stopDaemonService({ logger: log });
2272
- stopDaemonFromLock({ logger: log });
2273
- stopDaemonByName({ logger: log });
2274
- clearDaemonLocks();
2275
- const result = await startDaemonWithHealthCheck({
2276
- binaryPath: startupBinaries.binaryPath,
2277
- port,
2278
- host: DEFAULT_HOST,
2279
- logger: log
2280
- });
2281
- if (!result.ok) {
2282
- log.warn?.(`[docdex] daemon failed to start on ${DEFAULT_HOST}:${port}.`);
2283
- throw new Error("docdex daemon failed to start");
2410
+ let reuseExisting = portState.reuseExisting;
2411
+ if (reuseExisting) {
2412
+ const daemonInfo = await fetchDaemonInfo({ host: DEFAULT_HOST, port });
2413
+ const daemonVersion = normalizeVersion(daemonInfo?.version);
2414
+ const packageVersion = normalizeVersion(resolvePackageVersion());
2415
+ if (daemonInfo?.product === "Docdex" && daemonVersion && packageVersion) {
2416
+ if (daemonVersion !== packageVersion) {
2417
+ log.warn?.(
2418
+ `[docdex] daemon version ${daemonVersion} differs from package ${packageVersion}; restarting daemon.`
2419
+ );
2420
+ stopDaemonService({ logger: log });
2421
+ stopDaemonFromLock({ logger: log });
2422
+ stopDaemonByName({ logger: log });
2423
+ clearDaemonLocks();
2424
+ const released = await waitForPortAvailable({
2425
+ host: DEFAULT_HOST,
2426
+ port
2427
+ });
2428
+ if (!released) {
2429
+ throw new Error("docdex daemon restart failed; port still in use");
2430
+ }
2431
+ reuseExisting = false;
2432
+ }
2433
+ }
2434
+ }
2435
+ if (!reuseExisting) {
2436
+ const result = await startDaemonWithHealthCheck({
2437
+ binaryPath: startupBinaries.binaryPath,
2438
+ port,
2439
+ host: DEFAULT_HOST,
2440
+ logger: log
2441
+ });
2442
+ if (!result.ok) {
2443
+ log.warn?.(`[docdex] daemon failed to start on ${DEFAULT_HOST}:${port}.`);
2444
+ throw new Error("docdex daemon failed to start");
2445
+ }
2284
2446
  }
2285
2447
 
2286
2448
  const httpBindAddr = `${DEFAULT_HOST}:${port}`;
@@ -2345,5 +2507,7 @@ module.exports = {
2345
2507
  shouldSkipSetup,
2346
2508
  launchSetupWizard,
2347
2509
  applyAgentInstructions,
2348
- buildDaemonEnv
2510
+ buildDaemonEnv,
2511
+ resolveDaemonPortState,
2512
+ normalizeVersion
2349
2513
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docdex",
3
- "version": "0.2.28",
3
+ "version": "0.2.29",
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": {