docdex 0.2.8 → 0.2.10

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
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.8
3
+ ## 0.2.10
4
4
  - Added glama support
5
5
 
6
6
  ## 0.1.10
package/lib/install.js CHANGED
@@ -427,6 +427,15 @@ async function installFromLocalBinary({
427
427
  if (!isWin32) {
428
428
  await fsModule.promises.chmod(destPath, 0o755).catch(() => {});
429
429
  }
430
+ const mcpName = isWin32 ? "docdex-mcp-server.exe" : "docdex-mcp-server";
431
+ const mcpSource = pathModule.join(pathModule.dirname(binaryPath), mcpName);
432
+ if (fsModule.existsSync(mcpSource)) {
433
+ const mcpDest = pathModule.join(distDir, mcpName);
434
+ await fsModule.promises.copyFile(mcpSource, mcpDest);
435
+ if (!isWin32) {
436
+ await fsModule.promises.chmod(mcpDest, 0o755).catch(() => {});
437
+ }
438
+ }
430
439
  const binarySha256 = await sha256FileFn(destPath);
431
440
  const metadata = {
432
441
  schemaVersion: INSTALL_METADATA_SCHEMA_VERSION,
@@ -40,6 +40,10 @@ function configUrlForPort(port) {
40
40
  return `http://localhost:${port}/sse`;
41
41
  }
42
42
 
43
+ function configStreamableUrlForPort(port) {
44
+ return `http://localhost:${port}/v1/mcp`;
45
+ }
46
+
43
47
  function isPortAvailable(port, host) {
44
48
  return new Promise((resolve) => {
45
49
  const server = net.createServer();
@@ -837,11 +841,21 @@ async function maybePromptOllamaModel({
837
841
  }
838
842
 
839
843
  const normalizedInstalled = installed.map(normalizeModelName);
844
+ const displayModels = normalizedInstalled.map((model) => {
845
+ const selectable = model.toLowerCase() !== DEFAULT_OLLAMA_MODEL.toLowerCase();
846
+ return {
847
+ model,
848
+ label: selectable ? model : `${model} (embedding only)`,
849
+ selectable
850
+ };
851
+ });
852
+ const selectableModels = displayModels.filter((item) => item.selectable).map((item) => item.model);
840
853
  const installedLower = normalizedInstalled.map((model) => model.toLowerCase());
841
854
  const hasPhi = installedLower.includes(phiModel.toLowerCase());
842
- const selectionDefault = defaultChoice && installedLower.includes(defaultChoice.toLowerCase())
855
+ const defaultLower = defaultChoice ? defaultChoice.toLowerCase() : null;
856
+ const selectionDefault = defaultLower && selectableModels.some((model) => model.toLowerCase() === defaultLower)
843
857
  ? defaultChoice
844
- : normalizedInstalled[0];
858
+ : selectableModels[0];
845
859
 
846
860
  if (decision.mode === "auto") {
847
861
  if (selectionDefault) {
@@ -852,24 +866,28 @@ async function maybePromptOllamaModel({
852
866
  }
853
867
 
854
868
  stdout.write("[docdex] Ollama models detected:\n");
855
- normalizedInstalled.forEach((model, idx) => {
856
- const marker = model === selectionDefault ? " (default)" : "";
857
- stdout.write(` ${idx + 1}) ${model}${marker}\n`);
869
+ displayModels.forEach((item, idx) => {
870
+ const marker = item.model === selectionDefault ? " (default)" : "";
871
+ stdout.write(` ${idx + 1}) ${item.label}${marker}\n`);
858
872
  });
859
873
  if (!hasPhi) {
860
874
  stdout.write(` I) Install ${phiModel} (~${sizeText}, free ${freeText})\n`);
861
875
  }
862
876
  stdout.write(" S) Skip\n");
863
877
 
878
+ const defaultHint = selectionDefault ? ` [${selectionDefault}]` : "";
864
879
  const answer = await promptInput(
865
- `[docdex] Select default model [${selectionDefault}]: `,
880
+ `[docdex] Select default model${defaultHint}: `,
866
881
  { stdin, stdout }
867
882
  );
868
883
  const normalizedAnswer = normalizeModelName(answer);
869
884
  const answerLower = normalizedAnswer.toLowerCase();
870
885
  if (!answer) {
871
- updateDefaultModelConfig(configPath, selectionDefault, logger);
872
- return { status: "selected", model: selectionDefault };
886
+ if (selectionDefault) {
887
+ updateDefaultModelConfig(configPath, selectionDefault, logger);
888
+ return { status: "selected", model: selectionDefault };
889
+ }
890
+ return { status: "skipped", reason: "no_models" };
873
891
  }
874
892
  if (answerLower === "s" || answerLower === "skip") {
875
893
  return { status: "skipped", reason: "user_skip" };
@@ -881,16 +899,24 @@ async function maybePromptOllamaModel({
881
899
  return { status: "installed", model: phiModel };
882
900
  }
883
901
  const numeric = Number.parseInt(answerLower, 10);
884
- if (Number.isFinite(numeric) && numeric >= 1 && numeric <= normalizedInstalled.length) {
885
- const selected = normalizedInstalled[numeric - 1];
886
- updateDefaultModelConfig(configPath, selected, logger);
887
- return { status: "selected", model: selected };
902
+ if (Number.isFinite(numeric) && numeric >= 1 && numeric <= displayModels.length) {
903
+ const selected = displayModels[numeric - 1];
904
+ if (!selected.selectable) {
905
+ logger?.warn?.(`[docdex] ${selected.model} is an embedding-only model; choose a chat model.`);
906
+ return { status: "skipped", reason: "embedding_only" };
907
+ }
908
+ updateDefaultModelConfig(configPath, selected.model, logger);
909
+ return { status: "selected", model: selected.model };
888
910
  }
889
911
  const matchedIndex = installedLower.indexOf(answerLower);
890
912
  if (matchedIndex !== -1) {
891
- const selected = normalizedInstalled[matchedIndex];
892
- updateDefaultModelConfig(configPath, selected, logger);
893
- return { status: "selected", model: selected };
913
+ const selected = displayModels[matchedIndex];
914
+ if (!selected.selectable) {
915
+ logger?.warn?.(`[docdex] ${selected.model} is an embedding-only model; choose a chat model.`);
916
+ return { status: "skipped", reason: "embedding_only" };
917
+ }
918
+ updateDefaultModelConfig(configPath, selected.model, logger);
919
+ return { status: "selected", model: selected.model };
894
920
  }
895
921
  logger?.warn?.("[docdex] Unrecognized selection; skipping model update.");
896
922
  return { status: "skipped", reason: "invalid_selection" };
@@ -1063,6 +1089,55 @@ function shouldSkipSetup(env = process.env) {
1063
1089
  return parseEnvBool(env.DOCDEX_SETUP_SKIP) === true;
1064
1090
  }
1065
1091
 
1092
+ function commandExists(cmd, spawnSyncFn) {
1093
+ const result = spawnSyncFn(cmd, ["--version"], { stdio: "ignore" });
1094
+ if (result?.error?.code === "ENOENT") return false;
1095
+ return true;
1096
+ }
1097
+
1098
+ function launchMacTerminal({ binaryPath, args, spawnSyncFn, logger }) {
1099
+ const command = [binaryPath, ...args].join(" ");
1100
+ const script = [
1101
+ 'tell application "Terminal"',
1102
+ 'if not (exists window 1) then',
1103
+ `do script ${JSON.stringify(command)}`,
1104
+ "else",
1105
+ `do script ${JSON.stringify(command)} in window 1`,
1106
+ "end if",
1107
+ "activate",
1108
+ "end tell"
1109
+ ].join("\n");
1110
+ const result = spawnSyncFn("osascript", ["-e", script]);
1111
+ if (result.status === 0) return true;
1112
+ logger?.warn?.(`[docdex] osascript launch failed: ${result.stderr || "unknown error"}`);
1113
+ return false;
1114
+ }
1115
+
1116
+ function launchLinuxTerminal({ binaryPath, args, spawnFn, spawnSyncFn }) {
1117
+ const candidates = [
1118
+ { cmd: "x-terminal-emulator", args: ["-e", binaryPath, ...args] },
1119
+ { cmd: "gnome-terminal", args: ["--", binaryPath, ...args] },
1120
+ { cmd: "konsole", args: ["-e", binaryPath, ...args] },
1121
+ { cmd: "xfce4-terminal", args: ["-e", binaryPath, ...args] },
1122
+ { cmd: "xterm", args: ["-e", binaryPath, ...args] },
1123
+ { cmd: "kitty", args: ["-e", binaryPath, ...args] },
1124
+ { cmd: "alacritty", args: ["-e", binaryPath, ...args] },
1125
+ { cmd: "wezterm", args: ["start", "--", binaryPath, ...args] }
1126
+ ];
1127
+ for (const candidate of candidates) {
1128
+ if (!commandExists(candidate.cmd, spawnSyncFn)) continue;
1129
+ const child = spawnFn(candidate.cmd, candidate.args, {
1130
+ stdio: "ignore",
1131
+ detached: true
1132
+ });
1133
+ if (child?.pid) {
1134
+ child.unref?.();
1135
+ return true;
1136
+ }
1137
+ }
1138
+ return false;
1139
+ }
1140
+
1066
1141
  function launchSetupWizard({
1067
1142
  binaryPath,
1068
1143
  logger,
@@ -1082,9 +1157,14 @@ function launchSetupWizard({
1082
1157
  if (!canPrompt(stdin, stdout)) {
1083
1158
  return { ok: false, reason: "non_interactive" };
1084
1159
  }
1085
- const child = spawnFn(binaryPath, args, { stdio: "inherit" });
1086
- if (child.pid) return { ok: true };
1087
- return { ok: false, reason: "spawn_failed" };
1160
+ if (platform === "darwin") {
1161
+ return launchMacTerminal({ binaryPath, args, spawnSyncFn, logger })
1162
+ ? { ok: true }
1163
+ : { ok: false, reason: "terminal_launch_failed" };
1164
+ }
1165
+ return launchLinuxTerminal({ binaryPath, args, spawnFn, spawnSyncFn })
1166
+ ? { ok: true }
1167
+ : { ok: false, reason: "terminal_launch_failed" };
1088
1168
  }
1089
1169
 
1090
1170
  if (platform === "win32") {
@@ -1125,10 +1205,11 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
1125
1205
  }
1126
1206
 
1127
1207
  const url = configUrlForPort(port);
1208
+ const codexUrl = configStreamableUrlForPort(port);
1128
1209
  const paths = clientConfigPaths();
1129
1210
  upsertMcpServerJson(paths.claude, url);
1130
1211
  upsertMcpServerJson(paths.cursor, url);
1131
- upsertCodexConfig(paths.codex, url);
1212
+ upsertCodexConfig(paths.codex, codexUrl);
1132
1213
 
1133
1214
  const daemonRoot = ensureDaemonRoot();
1134
1215
  const resolvedBinary = resolveBinaryPath({ binaryPath });
@@ -1160,6 +1241,7 @@ module.exports = {
1160
1241
  upsertCodexConfig,
1161
1242
  pickAvailablePort,
1162
1243
  configUrlForPort,
1244
+ configStreamableUrlForPort,
1163
1245
  parseEnvBool,
1164
1246
  resolveOllamaInstallMode,
1165
1247
  resolveOllamaModelPromptMode,
package/lib/uninstall.js CHANGED
@@ -206,9 +206,68 @@ function removeCodexConfig(pathname, name = "docdex") {
206
206
  return output.join("\n");
207
207
  };
208
208
 
209
+ const removeLooseEntry = (text) => {
210
+ const lines = text.split(/\r?\n/);
211
+ const output = [];
212
+ let changed = false;
213
+ for (const line of lines) {
214
+ if (/^\s*docdex\s*=/.test(line)) {
215
+ changed = true;
216
+ continue;
217
+ }
218
+ output.push(line);
219
+ }
220
+ return { text: output.join("\n"), changed };
221
+ };
222
+
223
+ const removeEmptyMcpServersTable = (text) => {
224
+ const lines = text.split(/\r?\n/);
225
+ const output = [];
226
+ let inTable = false;
227
+ let tableHasEntries = false;
228
+ let buffer = [];
229
+
230
+ const flushTable = () => {
231
+ if (!inTable) return;
232
+ if (tableHasEntries) output.push(...buffer);
233
+ inTable = false;
234
+ tableHasEntries = false;
235
+ buffer = [];
236
+ };
237
+
238
+ for (const line of lines) {
239
+ const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
240
+ if (section) {
241
+ flushTable();
242
+ if (section[1].trim() === "mcp_servers") {
243
+ inTable = true;
244
+ buffer = [line];
245
+ continue;
246
+ }
247
+ output.push(line);
248
+ continue;
249
+ }
250
+
251
+ if (inTable) {
252
+ if (line.trim() && !line.trim().startsWith("#")) {
253
+ if (/=/.test(line)) tableHasEntries = true;
254
+ }
255
+ buffer.push(line);
256
+ continue;
257
+ }
258
+
259
+ output.push(line);
260
+ }
261
+ flushTable();
262
+ return output.join("\n");
263
+ };
264
+
209
265
  contents = removeArrayBlocks(contents);
210
266
  contents = removeNestedSection(contents);
211
267
  contents = removeTableEntry(contents);
268
+ const loose = removeLooseEntry(contents);
269
+ contents = loose.text;
270
+ contents = removeEmptyMcpServersTable(contents);
212
271
 
213
272
  if (contents !== original) {
214
273
  fs.writeFileSync(pathname, contents.endsWith("\n") ? contents : `${contents}\n`);
@@ -321,6 +380,16 @@ function stopDaemonByName() {
321
380
  return true;
322
381
  }
323
382
 
383
+ function stopMcpByName() {
384
+ if (process.platform === "win32") {
385
+ spawnSync("taskkill", ["/IM", "docdex-mcp-server.exe", "/T", "/F"]);
386
+ return true;
387
+ }
388
+ spawnSync("pkill", ["-TERM", "-x", "docdex-mcp-server"]);
389
+ spawnSync("pkill", ["-TERM", "-f", "docdex-mcp-server"]);
390
+ return true;
391
+ }
392
+
324
393
  function unregisterStartup() {
325
394
  if (process.platform === "darwin") {
326
395
  const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", "com.docdex.daemon.plist");
@@ -399,6 +468,7 @@ async function main() {
399
468
  if (!stopped) {
400
469
  stopDaemonByName();
401
470
  }
471
+ stopMcpByName();
402
472
  unregisterStartup();
403
473
  removeClientConfigs();
404
474
  clearStartupFailure();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docdex",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "mcpName": "io.github.bekirdag/docdex",
5
5
  "description": "Docdex CLI as an npm-installable binary wrapper.",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "postinstall": "node ./lib/install.js",
19
+ "preuninstall": "node ./lib/uninstall.js",
19
20
  "postuninstall": "node ./lib/uninstall.js",
20
21
  "test": "node --test",
21
22
  "pack:verify": "node --test test/packaging_guardrails.test.js"