docdex 0.2.15 → 0.2.17

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.
@@ -19,6 +19,7 @@ const DEFAULT_OLLAMA_MODEL = "nomic-embed-text";
19
19
  const DEFAULT_OLLAMA_CHAT_MODEL = "phi3.5:3.8b";
20
20
  const DEFAULT_OLLAMA_CHAT_MODEL_SIZE_GIB = 2.2;
21
21
  const SETUP_PENDING_MARKER = "setup_pending.json";
22
+ const AGENTS_DOC_FILENAME = "agents.md";
22
23
 
23
24
  function defaultConfigPath() {
24
25
  return path.join(os.homedir(), ".docdex", "config.toml");
@@ -153,6 +154,126 @@ function writeJson(pathname, value) {
153
154
  fs.writeFileSync(pathname, JSON.stringify(value, null, 2) + "\n");
154
155
  }
155
156
 
157
+ function agentsDocSourcePath() {
158
+ return path.join(__dirname, "..", "assets", AGENTS_DOC_FILENAME);
159
+ }
160
+
161
+ function loadAgentInstructions() {
162
+ const sourcePath = agentsDocSourcePath();
163
+ if (!fs.existsSync(sourcePath)) return "";
164
+ try {
165
+ return fs.readFileSync(sourcePath, "utf8");
166
+ } catch {
167
+ return "";
168
+ }
169
+ }
170
+
171
+ function normalizeInstructionText(value) {
172
+ return String(value || "").trim();
173
+ }
174
+
175
+ function mergeInstructionText(existing, instructions) {
176
+ const next = normalizeInstructionText(instructions);
177
+ if (!next) return normalizeInstructionText(existing);
178
+ const current = normalizeInstructionText(existing);
179
+ if (!current) return next;
180
+ if (current.includes(next)) return current;
181
+ return `${current}\n\n${next}`;
182
+ }
183
+
184
+ function writeTextFile(pathname, contents) {
185
+ const next = contents.endsWith("\n") ? contents : `${contents}\n`;
186
+ let current = "";
187
+ if (fs.existsSync(pathname)) {
188
+ current = fs.readFileSync(pathname, "utf8");
189
+ if (current === next) return false;
190
+ }
191
+ fs.mkdirSync(path.dirname(pathname), { recursive: true });
192
+ fs.writeFileSync(pathname, next);
193
+ return true;
194
+ }
195
+
196
+ function upsertPromptFile(pathname, instructions, { prepend = false } = {}) {
197
+ const next = normalizeInstructionText(instructions);
198
+ if (!next) return false;
199
+ let current = "";
200
+ if (fs.existsSync(pathname)) {
201
+ current = fs.readFileSync(pathname, "utf8");
202
+ if (current.includes(next)) return false;
203
+ }
204
+ const currentTrimmed = normalizeInstructionText(current);
205
+ let merged = next;
206
+ if (currentTrimmed) {
207
+ merged = prepend ? `${next}\n\n${currentTrimmed}` : `${currentTrimmed}\n\n${next}`;
208
+ }
209
+ return writeTextFile(pathname, merged);
210
+ }
211
+
212
+ function escapeRegExp(value) {
213
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
214
+ }
215
+
216
+ function upsertYamlInstruction(pathname, key, instructions) {
217
+ const next = normalizeInstructionText(instructions);
218
+ if (!next) return false;
219
+ let current = "";
220
+ if (fs.existsSync(pathname)) {
221
+ current = fs.readFileSync(pathname, "utf8");
222
+ }
223
+ const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:`, "m");
224
+ if (keyRe.test(current)) {
225
+ if (current.includes(next)) return false;
226
+ return false;
227
+ }
228
+ const lines = next.split(/\r?\n/).map((line) => ` ${line}`);
229
+ const block = `${key}: |\n${lines.join("\n")}`;
230
+ const merged = current.trim() ? `${current.trim()}\n\n${block}` : block;
231
+ return writeTextFile(pathname, merged);
232
+ }
233
+
234
+ function upsertClaudeInstructions(pathname, instructions) {
235
+ const { value } = readJson(pathname);
236
+ if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
237
+ const merged = mergeInstructionText(value.instructions, instructions);
238
+ if (!merged || merged === value.instructions) return false;
239
+ value.instructions = merged;
240
+ writeJson(pathname, value);
241
+ return true;
242
+ }
243
+
244
+ function upsertContinueInstructions(pathname, instructions) {
245
+ const { value } = readJson(pathname);
246
+ if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
247
+ const merged = mergeInstructionText(value.systemMessage, instructions);
248
+ if (!merged || merged === value.systemMessage) return false;
249
+ value.systemMessage = merged;
250
+ writeJson(pathname, value);
251
+ return true;
252
+ }
253
+
254
+ function upsertZedInstructions(pathname, instructions) {
255
+ const { value } = readJson(pathname);
256
+ if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
257
+ if (!value.assistant || typeof value.assistant !== "object" || Array.isArray(value.assistant)) {
258
+ value.assistant = {};
259
+ }
260
+ const merged = mergeInstructionText(value.assistant.system_prompt, instructions);
261
+ if (!merged || merged === value.assistant.system_prompt) return false;
262
+ value.assistant.system_prompt = merged;
263
+ writeJson(pathname, value);
264
+ return true;
265
+ }
266
+
267
+ function upsertVsCodeInstructions(pathname, instructionsPath) {
268
+ const { value } = readJson(pathname);
269
+ if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
270
+ const key = "copilot.chat.codeGeneration.instructions";
271
+ if (value[key] === instructionsPath) return false;
272
+ value[key] = instructionsPath;
273
+ writeJson(pathname, value);
274
+ return true;
275
+ }
276
+
156
277
  function upsertMcpServerJson(pathname, url) {
157
278
  const { value } = readJson(pathname);
158
279
  if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
@@ -210,6 +331,7 @@ function upsertCodexConfig(pathname, url) {
210
331
  new RegExp(`^\\s*\\[${section}\\]\\s*$`, "m").test(contents);
211
332
  const hasNestedMcpServers = (contents) =>
212
333
  /^\s*\[mcp_servers\.[^\]]+\]\s*$/m.test(contents);
334
+ const legacyInstructionPath = "~/.docdex/agents.md";
213
335
  const parseTomlString = (value) => {
214
336
  const trimmed = value.trim();
215
337
  const quoted = trimmed.match(/^"(.*)"$/) || trimmed.match(/^'(.*)'$/);
@@ -336,6 +458,52 @@ function upsertCodexConfig(pathname, url) {
336
458
  return { contents: lines.join("\n"), updated };
337
459
  };
338
460
 
461
+ const removeLegacyInstructions = (text) => {
462
+ const lines = text.split(/\r?\n/);
463
+ const output = [];
464
+ let inFeatures = false;
465
+ let featuresHasEntries = false;
466
+ let buffer = [];
467
+ let updated = false;
468
+
469
+ const flushFeatures = () => {
470
+ if (!inFeatures) return;
471
+ if (featuresHasEntries) output.push(...buffer);
472
+ inFeatures = false;
473
+ featuresHasEntries = false;
474
+ buffer = [];
475
+ };
476
+
477
+ for (const line of lines) {
478
+ const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
479
+ if (section) {
480
+ flushFeatures();
481
+ if (section[1].trim() === "features") {
482
+ inFeatures = true;
483
+ buffer = [line];
484
+ continue;
485
+ }
486
+ output.push(line);
487
+ continue;
488
+ }
489
+ if (inFeatures) {
490
+ const match = line.match(/^\s*experimental_instructions_file\s*=\s*(.+?)\s*$/);
491
+ if (match && parseTomlString(match[1]) === legacyInstructionPath) {
492
+ updated = true;
493
+ continue;
494
+ }
495
+ if (line.trim() && !line.trim().startsWith("#") && /=/.test(line)) {
496
+ featuresHasEntries = true;
497
+ }
498
+ buffer.push(line);
499
+ continue;
500
+ }
501
+ output.push(line);
502
+ }
503
+ flushFeatures();
504
+ return { contents: output.join("\n"), updated };
505
+ };
506
+
339
507
  let contents = "";
340
508
  if (fs.existsSync(pathname)) {
341
509
  contents = fs.readFileSync(pathname, "utf8");
@@ -347,6 +515,10 @@ function upsertCodexConfig(pathname, url) {
347
515
  updated = updated || migrated.migrated;
348
516
  }
349
517
 
518
+ const cleaned = removeLegacyInstructions(contents);
519
+ contents = cleaned.contents;
520
+ updated = updated || cleaned.updated;
521
+
350
522
  if (hasNestedMcpServers(contents)) {
351
523
  const nested = upsertDocdexNested(contents, url);
352
524
  contents = nested.contents;
@@ -422,6 +594,67 @@ function clientConfigPaths() {
422
594
  }
423
595
  }
424
596
 
597
+ function clientInstructionPaths() {
598
+ const home = os.homedir();
599
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
600
+ const userProfile = process.env.USERPROFILE || home;
601
+ const vscodeGlobalInstructions = path.join(home, ".vscode", "global_instructions.md");
602
+ const windsurfGlobalRules = path.join(userProfile, ".codeium", "windsurf", "memories", "global_rules.md");
603
+ const rooRules = path.join(home, ".roo", "rules", "docdex.md");
604
+ const pearaiAgent = path.join(home, ".config", "pearai", "agent.md");
605
+ const aiderConfig = path.join(home, ".aider.conf.yml");
606
+ const gooseConfig = path.join(home, ".config", "goose", "config.yaml");
607
+ const openInterpreterConfig = path.join(home, ".openinterpreter", "profiles", "default.yaml");
608
+ const codexAgents = path.join(userProfile, ".codex", "AGENTS.md");
609
+ switch (process.platform) {
610
+ case "win32":
611
+ return {
612
+ claude: path.join(appData, "Claude", "claude_desktop_config.json"),
613
+ continue: path.join(userProfile, ".continue", "config.json"),
614
+ zed: path.join(appData, "Zed", "settings.json"),
615
+ vscodeSettings: path.join(appData, "Code", "User", "settings.json"),
616
+ vscodeGlobalInstructions,
617
+ windsurfGlobalRules,
618
+ rooRules,
619
+ pearaiAgent,
620
+ aiderConfig,
621
+ gooseConfig,
622
+ openInterpreterConfig,
623
+ codexAgents
624
+ };
625
+ case "darwin":
626
+ return {
627
+ claude: path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
628
+ continue: path.join(home, ".continue", "config.json"),
629
+ zed: path.join(home, ".config", "zed", "settings.json"),
630
+ vscodeSettings: path.join(home, "Library", "Application Support", "Code", "User", "settings.json"),
631
+ vscodeGlobalInstructions,
632
+ windsurfGlobalRules,
633
+ rooRules,
634
+ pearaiAgent,
635
+ aiderConfig,
636
+ gooseConfig,
637
+ openInterpreterConfig,
638
+ codexAgents
639
+ };
640
+ default:
641
+ return {
642
+ claude: path.join(home, ".config", "Claude", "claude_desktop_config.json"),
643
+ continue: path.join(home, ".continue", "config.json"),
644
+ zed: path.join(home, ".config", "zed", "settings.json"),
645
+ vscodeSettings: path.join(home, ".config", "Code", "User", "settings.json"),
646
+ vscodeGlobalInstructions,
647
+ windsurfGlobalRules,
648
+ rooRules,
649
+ pearaiAgent,
650
+ aiderConfig,
651
+ gooseConfig,
652
+ openInterpreterConfig,
653
+ codexAgents
654
+ };
655
+ }
656
+ }
657
+
425
658
  function resolveBinaryPath({ binaryPath } = {}) {
426
659
  if (binaryPath && fs.existsSync(binaryPath)) return binaryPath;
427
660
  try {
@@ -434,6 +667,66 @@ function resolveBinaryPath({ binaryPath } = {}) {
434
667
  return null;
435
668
  }
436
669
 
670
+ function applyAgentInstructions({ logger } = {}) {
671
+ const instructions = loadAgentInstructions();
672
+ if (!normalizeInstructionText(instructions)) return { ok: false, reason: "missing_instructions" };
673
+ const paths = clientInstructionPaths();
674
+ let updated = false;
675
+ const safeApply = (label, fn) => {
676
+ try {
677
+ const didUpdate = fn();
678
+ if (didUpdate) updated = true;
679
+ return didUpdate;
680
+ } catch (err) {
681
+ logger?.warn?.(`[docdex] agent instructions update failed for ${label}: ${err?.message || err}`);
682
+ return false;
683
+ }
684
+ };
685
+
686
+ if (paths.vscodeGlobalInstructions) {
687
+ safeApply("vscode-global", () =>
688
+ upsertPromptFile(paths.vscodeGlobalInstructions, instructions, { prepend: true })
689
+ );
690
+ }
691
+ if (paths.vscodeSettings && paths.vscodeGlobalInstructions) {
692
+ safeApply("vscode-settings", () => upsertVsCodeInstructions(paths.vscodeSettings, paths.vscodeGlobalInstructions));
693
+ }
694
+ if (paths.windsurfGlobalRules) {
695
+ safeApply("windsurf", () => upsertPromptFile(paths.windsurfGlobalRules, instructions, { prepend: true }));
696
+ }
697
+ if (paths.rooRules) {
698
+ safeApply("roo", () => upsertPromptFile(paths.rooRules, instructions));
699
+ }
700
+ if (paths.pearaiAgent) {
701
+ safeApply("pearai", () => upsertPromptFile(paths.pearaiAgent, instructions, { prepend: true }));
702
+ }
703
+ if (paths.claude) {
704
+ safeApply("claude", () => upsertClaudeInstructions(paths.claude, instructions));
705
+ }
706
+ if (paths.continue) {
707
+ safeApply("continue", () => upsertContinueInstructions(paths.continue, instructions));
708
+ }
709
+ if (paths.zed) {
710
+ safeApply("zed", () => upsertZedInstructions(paths.zed, instructions));
711
+ }
712
+ if (paths.aiderConfig) {
713
+ safeApply("aider", () => upsertYamlInstruction(paths.aiderConfig, "system-prompt", instructions));
714
+ }
715
+ if (paths.gooseConfig) {
716
+ safeApply("goose", () => upsertYamlInstruction(paths.gooseConfig, "instructions", instructions));
717
+ }
718
+ if (paths.openInterpreterConfig) {
719
+ safeApply("open-interpreter", () =>
720
+ upsertYamlInstruction(paths.openInterpreterConfig, "system_message", instructions)
721
+ );
722
+ }
723
+ if (paths.codexAgents) {
724
+ safeApply("codex", () => upsertPromptFile(paths.codexAgents, instructions));
725
+ }
726
+
727
+ return { ok: true, updated };
728
+ }
729
+
437
730
  function resolveMcpBinaryPath(binaryPath) {
438
731
  if (!binaryPath) return null;
439
732
  const dir = path.dirname(binaryPath);
@@ -604,6 +897,14 @@ function normalizeModelName(name) {
604
897
  return String(name || "").trim();
605
898
  }
606
899
 
900
+ function isEmbeddingModelName(name) {
901
+ const normalized = normalizeModelName(name).toLowerCase();
902
+ if (!normalized) return false;
903
+ const base = normalized.split(":")[0];
904
+ const embed = DEFAULT_OLLAMA_MODEL.toLowerCase();
905
+ return base === embed || base.startsWith(`${embed}-`);
906
+ }
907
+
607
908
  function readLlmDefaultModel(contents) {
608
909
  let inLlm = false;
609
910
  const lines = String(contents || "").split(/\r?\n/);
@@ -861,6 +1162,10 @@ async function maybePromptOllamaModel({
861
1162
 
862
1163
  const forced = normalizeModelName(env.DOCDEX_OLLAMA_MODEL);
863
1164
  if (forced) {
1165
+ if (isEmbeddingModelName(forced)) {
1166
+ logger?.warn?.(`[docdex] ${forced} is an embedding-only model; choose a chat model.`);
1167
+ return { status: "skipped", reason: "embedding_only" };
1168
+ }
864
1169
  const installed = listOllamaModels() || [];
865
1170
  const forcedLower = forced.toLowerCase();
866
1171
  const hasForced = installed.some((model) => normalizeModelName(model).toLowerCase() === forcedLower);
@@ -915,7 +1220,7 @@ async function maybePromptOllamaModel({
915
1220
 
916
1221
  const normalizedInstalled = installed.map(normalizeModelName);
917
1222
  const displayModels = normalizedInstalled.map((model) => {
918
- const selectable = model.toLowerCase() !== DEFAULT_OLLAMA_MODEL.toLowerCase();
1223
+ const selectable = !isEmbeddingModelName(model);
919
1224
  return {
920
1225
  model,
921
1226
  label: selectable ? model : `${model} (embedding only)`,
@@ -971,6 +1276,11 @@ async function maybePromptOllamaModel({
971
1276
  updateDefaultModelConfig(configPath, phiModel, logger);
972
1277
  return { status: "installed", model: phiModel };
973
1278
  }
1279
+ if (isEmbeddingModelName(normalizedAnswer)) {
1280
+ const modelName = normalizedAnswer || DEFAULT_OLLAMA_MODEL;
1281
+ logger?.warn?.(`[docdex] ${modelName} is an embedding-only model; choose a chat model.`);
1282
+ return { status: "skipped", reason: "embedding_only" };
1283
+ }
974
1284
  const numeric = Number.parseInt(answerLower, 10);
975
1285
  if (Number.isFinite(numeric) && numeric >= 1 && numeric <= displayModels.length) {
976
1286
  const selected = displayModels[numeric - 1];
@@ -1189,6 +1499,7 @@ function commandExists(cmd, spawnSyncFn) {
1189
1499
  function launchMacTerminal({ binaryPath, args, spawnSyncFn, logger }) {
1190
1500
  const command = [
1191
1501
  "DOCDEX_SETUP_AUTO=1",
1502
+ "DOCDEX_SETUP_MODE=auto",
1192
1503
  `"${binaryPath}"`,
1193
1504
  ...args.map((arg) => `"${arg}"`)
1194
1505
  ].join(" ");
@@ -1209,7 +1520,7 @@ function launchMacTerminal({ binaryPath, args, spawnSyncFn, logger }) {
1209
1520
  }
1210
1521
 
1211
1522
  function launchLinuxTerminal({ binaryPath, args, spawnFn, spawnSyncFn }) {
1212
- const envArgs = ["env", "DOCDEX_SETUP_AUTO=1", binaryPath, ...args];
1523
+ const envArgs = ["env", "DOCDEX_SETUP_AUTO=1", "DOCDEX_SETUP_MODE=auto", binaryPath, ...args];
1213
1524
  const candidates = [
1214
1525
  { cmd: "x-terminal-emulator", args: ["-e", ...envArgs] },
1215
1526
  { cmd: "gnome-terminal", args: ["--", ...envArgs] },
@@ -1248,7 +1559,7 @@ function launchSetupWizard({
1248
1559
  if (!binaryPath) return { ok: false, reason: "missing_binary" };
1249
1560
  if (shouldSkipSetup(env)) return { ok: false, reason: "skipped" };
1250
1561
 
1251
- const args = ["setup"];
1562
+ const args = ["setup", "--auto"];
1252
1563
  if (platform === "linux" || platform === "darwin") {
1253
1564
  if (!canPrompt(stdin, stdout)) {
1254
1565
  return { ok: false, reason: "non_interactive" };
@@ -1265,7 +1576,7 @@ function launchSetupWizard({
1265
1576
 
1266
1577
  if (platform === "win32") {
1267
1578
  const quoted = `"${binaryPath}" ${args.map((arg) => `"${arg}"`).join(" ")}`;
1268
- const cmdline = `set DOCDEX_SETUP_AUTO=1 && ${quoted}`;
1579
+ const cmdline = `set DOCDEX_SETUP_AUTO=1 && set DOCDEX_SETUP_MODE=auto && ${quoted}`;
1269
1580
  const result = spawnSyncFn("cmd", ["/c", "start", "", "cmd", "/c", cmdline]);
1270
1581
  if (result.status === 0) return { ok: true };
1271
1582
  logger?.warn?.(`[docdex] cmd start failed: ${result.stderr || "unknown error"}`);
@@ -1323,6 +1634,7 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
1323
1634
  upsertZedConfig(paths.zed, url);
1324
1635
  }
1325
1636
  upsertCodexConfig(paths.codex, codexUrl);
1637
+ applyAgentInstructions({ logger: log });
1326
1638
 
1327
1639
  const daemonRoot = ensureDaemonRoot();
1328
1640
  const resolvedBinary = resolveBinaryPath({ binaryPath });
@@ -1375,5 +1687,6 @@ module.exports = {
1375
1687
  hasInteractiveTty,
1376
1688
  canPromptWithTty,
1377
1689
  shouldSkipSetup,
1378
- launchSetupWizard
1690
+ launchSetupWizard,
1691
+ applyAgentInstructions
1379
1692
  };
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "docdex",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
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
7
  "docdex": "bin/docdex.js",
8
- "docdexd": "bin/docdex.js"
8
+ "docdexd": "bin/docdex.js",
9
+ "docdex-mcp-server": "bin/docdex-mcp-server.js"
9
10
  },
10
11
  "files": [
11
12
  "bin",
12
13
  "lib",
14
+ "assets",
13
15
  "README.md",
14
16
  "CHANGELOG.md",
15
17
  "LICENSE"
@@ -60,6 +62,7 @@
60
62
  "access": "public"
61
63
  },
62
64
  "dependencies": {
65
+ "playwright": "^1.49.0",
63
66
  "tar": "^6.2.1"
64
67
  }
65
68
  }