docdex 0.2.60 → 0.2.62

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,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.62
4
+ - Expand the packaged personal-preferences surface with claim, snapshot, feedback, and mind-clone flows across HTTP, CLI, and MCP, including review/override/forget controls and clone regression coverage.
5
+ - Harden the packaged installer's local-binary path selection by validating `docdexd --version` output before using explicit or fallback local binaries and by avoiding stale repo-local binaries during registry installs.
6
+
7
+ ## 0.2.61
8
+ - Add the optional personal-preferences memory subsystem with local capture, background digestion, and packaged HTTP/CLI/MCP controls for status, search, processing, export, redaction, deletion, and purge flows.
9
+ - Align the packaged personal-preferences surface with the top-level `[personal_preferences]` config, richer lineage/materialization, review and retention controls, bounded chat-context injection, and supported-client transcript scanning.
10
+ - Harden installer-managed client config rewrites so whitespace-padded Docdex keys and section names are normalized instead of duplicated on reinstall.
11
+
3
12
  ## 0.2.60
4
13
  - Deduplicate installer-managed Docdex client config on reinstall: JSON client configs now collapse stale `docdex` entries, Codex TOML converges to one canonical Docdex entry, and packaged Docdex instruction blocks replace older Codex/Gemini/Claude prompt blocks instead of duplicating them.
5
14
  - Update the packaged daemon dependency set to remove the vulnerable `rustls-webpki` chain that caused the nightly security audit failure.
package/assets/agents.md CHANGED
@@ -1,4 +1,4 @@
1
- ---- START OF DOCDEX INFO V0.2.60 ----
1
+ ---- START OF DOCDEX INFO V0.2.61 ----
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`).
package/lib/install.js CHANGED
@@ -219,6 +219,12 @@ function getVersion() {
219
219
  return version;
220
220
  }
221
221
 
222
+ function normalizeVersion(value) {
223
+ return String(value || "")
224
+ .trim()
225
+ .replace(/^v/i, "");
226
+ }
227
+
222
228
  function requestOptions() {
223
229
  const headers = { "User-Agent": USER_AGENT };
224
230
  const token = process.env.DOCDEX_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
@@ -665,11 +671,7 @@ function shouldPreferLocalInstall({ env, localBinaryPath, pathModule, localRepoR
665
671
  if (parseEnvBool(env?.[LOCAL_FALLBACK_ENV]) === false) return false;
666
672
  if (env?.[LOCAL_BINARY_ENV]) return true;
667
673
  if (env?.npm_lifecycle_event !== "postinstall") return false;
668
- if (isLocalInstallRequest({ env, pathModule })) return true;
669
- if (!env?.INIT_CWD || !localRepoRoot) return false;
670
- const initCwd = pathModule.resolve(env.INIT_CWD);
671
- const repoRoot = pathModule.resolve(localRepoRoot);
672
- return initCwd === repoRoot || initCwd.startsWith(`${repoRoot}${pathModule.sep}`);
674
+ return isLocalInstallRequest({ env, pathModule });
673
675
  }
674
676
 
675
677
  function resolveLocalBinaryCandidate({
@@ -746,6 +748,100 @@ async function installFromLocalBinary({
746
748
  return { binaryPath: destPath, outcome: "local", outcomeCode: "local" };
747
749
  }
748
750
 
751
+ function parseBinaryVersionOutput(output) {
752
+ const text = String(output || "").trim();
753
+ if (!text) return null;
754
+ const taggedMatch = text.match(/\bdocdexd\s+v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z._-]+)?)\b/i);
755
+ if (taggedMatch?.[1]) return normalizeVersion(taggedMatch[1]);
756
+ const genericMatch = text.match(/\bv?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z._-]+)?)\b/);
757
+ if (genericMatch?.[1]) return normalizeVersion(genericMatch[1]);
758
+ return null;
759
+ }
760
+
761
+ function probeBinaryVersion({
762
+ binaryPath,
763
+ spawnSyncFn = spawnSync
764
+ }) {
765
+ if (!binaryPath) {
766
+ return { version: null, raw: "", error: "missing_binary" };
767
+ }
768
+ const result = spawnSyncFn(binaryPath, ["--version"], {
769
+ encoding: "utf8"
770
+ });
771
+ const raw = [result?.stdout, result?.stderr].filter(Boolean).join("\n").trim();
772
+ if (result?.error) {
773
+ return {
774
+ version: null,
775
+ raw,
776
+ error: result.error?.message || String(result.error)
777
+ };
778
+ }
779
+ if (typeof result?.status === "number" && result.status !== 0) {
780
+ return {
781
+ version: null,
782
+ raw,
783
+ error: raw || `exit_${result.status}`
784
+ };
785
+ }
786
+ const version = parseBinaryVersionOutput(raw);
787
+ if (!version) {
788
+ return { version: null, raw, error: "version_unparseable" };
789
+ }
790
+ return { version, raw, error: null };
791
+ }
792
+
793
+ function validateLocalBinaryVersion({
794
+ binaryPath,
795
+ expectedVersion,
796
+ spawnSyncFn = spawnSync
797
+ }) {
798
+ const probed = probeBinaryVersion({ binaryPath, spawnSyncFn });
799
+ if (!probed.version) {
800
+ return {
801
+ ok: false,
802
+ reason: probed.error || "version_probe_failed",
803
+ expectedVersion,
804
+ detectedVersion: null,
805
+ raw: probed.raw || ""
806
+ };
807
+ }
808
+ if (normalizeVersion(probed.version) !== normalizeVersion(expectedVersion)) {
809
+ return {
810
+ ok: false,
811
+ reason: "version_mismatch",
812
+ expectedVersion,
813
+ detectedVersion: probed.version,
814
+ raw: probed.raw || ""
815
+ };
816
+ }
817
+ return {
818
+ ok: true,
819
+ reason: "matched",
820
+ expectedVersion,
821
+ detectedVersion: probed.version,
822
+ raw: probed.raw || ""
823
+ };
824
+ }
825
+
826
+ function buildLocalBinaryVersionError({ binaryPath, validation, explicitEnvOverride = false } = {}) {
827
+ const expected = validation?.expectedVersion || "unknown";
828
+ const detected = validation?.detectedVersion || "unknown";
829
+ const sourceName = explicitEnvOverride ? "DOCDEX_LOCAL_BINARY" : "local Docdex binary";
830
+ const probeHint = validation?.reason === "version_mismatch"
831
+ ? `expected ${expected} but found ${detected}`
832
+ : `version probe failed (${validation?.reason || "unknown"})`;
833
+ return new InstallerConfigError(
834
+ `${sourceName} is stale or invalid: ${probeHint}`,
835
+ {
836
+ expectedVersion: expected,
837
+ detectedVersion: validation?.detectedVersion || null,
838
+ binaryPath: binaryPath || null,
839
+ explicitEnvOverride,
840
+ probeOutput: validation?.raw || null
841
+ }
842
+ );
843
+ }
844
+
749
845
  async function maybeInstallLocalFallback({
750
846
  err,
751
847
  env,
@@ -761,7 +857,8 @@ async function maybeInstallLocalFallback({
761
857
  writeJsonFileAtomicFn,
762
858
  logger,
763
859
  localRepoRoot,
764
- localBinaryPath
860
+ localBinaryPath,
861
+ spawnSyncFn = spawnSync
765
862
  }) {
766
863
  if (!err || err.code !== "DOCDEX_CHECKSUM_UNUSABLE") return null;
767
864
  const allowFallback = parseEnvBool(env[LOCAL_FALLBACK_ENV]);
@@ -778,6 +875,18 @@ async function maybeInstallLocalFallback({
778
875
  });
779
876
  if (!candidate) return null;
780
877
 
878
+ const validation = validateLocalBinaryVersion({
879
+ binaryPath: candidate,
880
+ expectedVersion: version,
881
+ spawnSyncFn
882
+ });
883
+ if (!validation.ok) {
884
+ logger?.warn?.(
885
+ `[docdex] local fallback skipped for ${candidate}: expected ${version}, detected ${validation.detectedVersion || "unknown"} (${validation.reason}).`
886
+ );
887
+ return null;
888
+ }
889
+
781
890
  return installFromLocalBinary({
782
891
  fsModule,
783
892
  pathModule,
@@ -1780,6 +1889,7 @@ async function runInstaller(options) {
1780
1889
  const artifactNameFn = opts.artifactNameFn || artifactName;
1781
1890
  const assetPatternForPlatformKeyFn = opts.assetPatternForPlatformKeyFn || assetPatternForPlatformKey;
1782
1891
  const sha256FileFn = opts.sha256FileFn || sha256File;
1892
+ const spawnSyncFn = opts.spawnSyncFn || spawnSync;
1783
1893
  const writeJsonFileAtomicFn = opts.writeJsonFileAtomicFn || writeJsonFileAtomic;
1784
1894
  const restartFn = opts.restartFn;
1785
1895
  const localRepoRoot =
@@ -1853,6 +1963,18 @@ async function runInstaller(options) {
1853
1963
 
1854
1964
  const forceLocalBinary = Boolean(env?.[LOCAL_BINARY_ENV]);
1855
1965
  if (forceLocalBinary && localBinaryPath) {
1966
+ const validation = validateLocalBinaryVersion({
1967
+ binaryPath: localBinaryPath,
1968
+ expectedVersion: version,
1969
+ spawnSyncFn
1970
+ });
1971
+ if (!validation.ok) {
1972
+ throw buildLocalBinaryVersionError({
1973
+ binaryPath: localBinaryPath,
1974
+ validation,
1975
+ explicitEnvOverride: true
1976
+ });
1977
+ }
1856
1978
  const localInstall = await installFromLocalBinary({
1857
1979
  fsModule,
1858
1980
  pathModule,
@@ -1871,6 +1993,18 @@ async function runInstaller(options) {
1871
1993
  }
1872
1994
 
1873
1995
  if (preferLocal) {
1996
+ const validation = validateLocalBinaryVersion({
1997
+ binaryPath: localBinaryPath,
1998
+ expectedVersion: version,
1999
+ spawnSyncFn
2000
+ });
2001
+ if (!validation.ok) {
2002
+ throw buildLocalBinaryVersionError({
2003
+ binaryPath: localBinaryPath,
2004
+ validation,
2005
+ explicitEnvOverride: false
2006
+ });
2007
+ }
1874
2008
  const localInstall = await installFromLocalBinary({
1875
2009
  fsModule,
1876
2010
  pathModule,
@@ -1951,7 +2085,8 @@ async function runInstaller(options) {
1951
2085
  writeJsonFileAtomicFn,
1952
2086
  logger,
1953
2087
  localRepoRoot,
1954
- localBinaryPath
2088
+ localBinaryPath,
2089
+ spawnSyncFn
1955
2090
  });
1956
2091
  if (fallback) {
1957
2092
  return fallback;
@@ -2666,5 +2801,8 @@ module.exports = {
2666
2801
  ChecksumResolutionError,
2667
2802
  runInstaller,
2668
2803
  describeFatalError,
2669
- handleFatal
2804
+ handleFatal,
2805
+ parseBinaryVersionOutput,
2806
+ probeBinaryVersion,
2807
+ validateLocalBinaryVersion
2670
2808
  };
@@ -1102,20 +1102,39 @@ function upsertMcpServerJson(pathname, url, options = {}) {
1102
1102
  : {};
1103
1103
  const isPlainObject = (entry) =>
1104
1104
  typeof entry === "object" && entry != null && !Array.isArray(entry);
1105
+ const normalizeManagedServerName = (value) =>
1106
+ typeof value === "string" ? value.trim().toLowerCase() : null;
1107
+ const collectDocdexSectionEntry = (section) => {
1108
+ if (!isPlainObject(section)) return {};
1109
+ for (const [key, value] of Object.entries(section)) {
1110
+ if (normalizeManagedServerName(key) === "docdex" && isPlainObject(value)) {
1111
+ return { ...value };
1112
+ }
1113
+ }
1114
+ return {};
1115
+ };
1105
1116
  const removeDocdexFromSection = (key) => {
1106
1117
  const section = root[key];
1107
1118
  if (Array.isArray(section)) {
1108
- const filtered = section.filter((entry) => !(entry && entry.name === "docdex"));
1119
+ const filtered = section.filter(
1120
+ (entry) => normalizeManagedServerName(entry?.name) !== "docdex"
1121
+ );
1109
1122
  if (filtered.length !== section.length) {
1110
1123
  root[key] = filtered;
1111
1124
  }
1112
1125
  return;
1113
1126
  }
1114
- if (!isPlainObject(section) || !Object.prototype.hasOwnProperty.call(section, "docdex")) {
1127
+ if (!isPlainObject(section)) {
1115
1128
  return;
1116
1129
  }
1117
- delete section.docdex;
1118
- if (Object.keys(section).length === 0) {
1130
+ let removed = false;
1131
+ for (const configKey of Object.keys(section)) {
1132
+ if (normalizeManagedServerName(configKey) === "docdex") {
1133
+ delete section[configKey];
1134
+ removed = true;
1135
+ }
1136
+ }
1137
+ if (removed && Object.keys(section).length === 0) {
1119
1138
  delete root[key];
1120
1139
  }
1121
1140
  };
@@ -1133,7 +1152,7 @@ function upsertMcpServerJson(pathname, url, options = {}) {
1133
1152
  let insertIndex = -1;
1134
1153
  let current = {};
1135
1154
  for (const entry of root.mcpServers) {
1136
- if (entry && entry.name === "docdex") {
1155
+ if (normalizeManagedServerName(entry?.name) === "docdex") {
1137
1156
  if (insertIndex === -1) {
1138
1157
  insertIndex = nextEntries.length;
1139
1158
  current = isPlainObject(entry) ? { ...entry } : {};
@@ -1156,9 +1175,12 @@ function upsertMcpServerJson(pathname, url, options = {}) {
1156
1175
  if (!picked) {
1157
1176
  root[sectionKey] = {};
1158
1177
  }
1159
- const section = root[sectionKey];
1160
- const current = isPlainObject(section.docdex) ? section.docdex : {};
1161
- section.docdex = { ...current, ...extra, url };
1178
+ const current = collectDocdexSectionEntry(root[sectionKey]);
1179
+ removeDocdexFromSection(sectionKey);
1180
+ if (!root[sectionKey] || !isPlainObject(root[sectionKey])) {
1181
+ root[sectionKey] = {};
1182
+ }
1183
+ root[sectionKey].docdex = { ...current, ...extra, url };
1162
1184
  removeDocdexFromSection(sectionKey === "mcpServers" ? "mcp_servers" : "mcpServers");
1163
1185
  }
1164
1186
  if (JSON.stringify(root) === before) return false;
@@ -1173,7 +1195,15 @@ function upsertZedConfig(pathname, url) {
1173
1195
  if (!root.experimental_mcp_servers || typeof root.experimental_mcp_servers !== "object" || Array.isArray(root.experimental_mcp_servers)) {
1174
1196
  root.experimental_mcp_servers = {};
1175
1197
  }
1176
- const current = root.experimental_mcp_servers.docdex;
1198
+ const normalizeManagedServerName = (value) =>
1199
+ typeof value === "string" ? value.trim().toLowerCase() : null;
1200
+ let current = root.experimental_mcp_servers.docdex;
1201
+ for (const key of Object.keys(root.experimental_mcp_servers)) {
1202
+ if (normalizeManagedServerName(key) === "docdex" && key !== "docdex") {
1203
+ if (current == null) current = root.experimental_mcp_servers[key];
1204
+ delete root.experimental_mcp_servers[key];
1205
+ }
1206
+ }
1177
1207
  if (current && current.url === url) return false;
1178
1208
  root.experimental_mcp_servers.docdex = { url };
1179
1209
  writeJson(pathname, root);
@@ -1182,16 +1212,52 @@ function upsertZedConfig(pathname, url) {
1182
1212
 
1183
1213
  function upsertCodexConfig(pathname, url) {
1184
1214
  const codexTimeoutSec = 300;
1185
- const hasSection = (contents, section) =>
1186
- new RegExp(`^\\s*\\[${section}\\]\\s*$`, "m").test(contents);
1187
- const hasNestedMcpServers = (contents) =>
1188
- /^\s*\[mcp_servers\.[^\]]+\]\s*$/m.test(contents);
1189
1215
  const legacyInstructionPath = "~/.docdex/agents.md";
1190
1216
  const parseTomlString = (value) => {
1191
1217
  const trimmed = value.trim();
1192
1218
  const quoted = trimmed.match(/^"(.*)"$/) || trimmed.match(/^'(.*)'$/);
1193
1219
  return quoted ? quoted[1] : trimmed;
1194
1220
  };
1221
+ const normalizeTomlKeyPart = (value) => parseTomlString(value).trim();
1222
+ const parseTomlHeaderParts = (line) => {
1223
+ const match = line.match(/^\s*\[([^\]]+)\]\s*$/);
1224
+ if (!match) return null;
1225
+ return match[1].split(".").map(normalizeTomlKeyPart).filter((part) => part.length > 0);
1226
+ };
1227
+ const hasSection = (contents, section) => {
1228
+ const expectedParts = section.split(".").map(normalizeTomlKeyPart);
1229
+ return contents
1230
+ .split(/\r?\n/)
1231
+ .some((line) => {
1232
+ const parts = parseTomlHeaderParts(line);
1233
+ return (
1234
+ parts &&
1235
+ parts.length === expectedParts.length &&
1236
+ parts.every((part, index) => part === expectedParts[index])
1237
+ );
1238
+ });
1239
+ };
1240
+ const isTomlSectionLine = (line) => parseTomlHeaderParts(line) != null;
1241
+ const isMcpServersSectionLine = (line) => {
1242
+ const parts = parseTomlHeaderParts(line);
1243
+ return !!parts && parts.length === 1 && parts[0] === "mcp_servers";
1244
+ };
1245
+ const isDocdexNestedMcpServerSectionLine = (line) => {
1246
+ const parts = parseTomlHeaderParts(line);
1247
+ return !!parts && parts.length === 2 && parts[0] === "mcp_servers" && parts[1] === "docdex";
1248
+ };
1249
+ const hasNestedMcpServers = (contents) =>
1250
+ contents
1251
+ .split(/\r?\n/)
1252
+ .some((line) => {
1253
+ const parts = parseTomlHeaderParts(line);
1254
+ return !!parts && parts.length > 1 && parts[0] === "mcp_servers";
1255
+ });
1256
+ const parseTomlAssignmentKey = (line) => {
1257
+ const match = line.match(/^\s*([^=]+?)\s*=/);
1258
+ return match ? normalizeTomlKeyPart(match[1]) : null;
1259
+ };
1260
+ const isDocdexAssignmentLine = (line) => parseTomlAssignmentKey(line) === "docdex";
1195
1261
  const migrateLegacyMcpServers = (contents) => {
1196
1262
  if (!/\[\[mcp_servers\]\]/m.test(contents)) {
1197
1263
  return { contents, migrated: false };
@@ -1256,8 +1322,7 @@ function upsertCodexConfig(pathname, url) {
1256
1322
  ["startup_timeout_sec", `${codexTimeoutSec}`]
1257
1323
  ];
1258
1324
  const lines = contents.split(/\r?\n/);
1259
- const headerRe = /^\s*\[mcp_servers\.docdex\]\s*$/;
1260
- let start = lines.findIndex((line) => headerRe.test(line));
1325
+ let start = lines.findIndex((line) => isDocdexNestedMcpServerSectionLine(line));
1261
1326
  if (start === -1) {
1262
1327
  if (lines.length && lines[lines.length - 1].trim()) lines.push("");
1263
1328
  lines.push("[mcp_servers.docdex]");
@@ -1267,7 +1332,7 @@ function upsertCodexConfig(pathname, url) {
1267
1332
  return { contents: lines.join("\n"), updated: true };
1268
1333
  }
1269
1334
  let end = start + 1;
1270
- while (end < lines.length && !/^\s*\[.+\]\s*$/.test(lines[end])) {
1335
+ while (end < lines.length && !isTomlSectionLine(lines[end])) {
1271
1336
  end += 1;
1272
1337
  }
1273
1338
  let updated = false;
@@ -1275,7 +1340,7 @@ function upsertCodexConfig(pathname, url) {
1275
1340
  const lineValue = `${key} = ${value}`;
1276
1341
  let keyIndex = -1;
1277
1342
  for (let i = start + 1; i < end; i += 1) {
1278
- if (new RegExp(`^\\s*${key}\\s*=`).test(lines[i])) {
1343
+ if (parseTomlAssignmentKey(lines[i]) === key) {
1279
1344
  keyIndex = i;
1280
1345
  break;
1281
1346
  }
@@ -1296,8 +1361,7 @@ function upsertCodexConfig(pathname, url) {
1296
1361
  const entryLine =
1297
1362
  `docdex = { url = "${urlValue}", tool_timeout_sec = ${codexTimeoutSec}, startup_timeout_sec = ${codexTimeoutSec} }`;
1298
1363
  const lines = contents.split(/\r?\n/);
1299
- const headerRe = /^\s*\[mcp_servers\]\s*$/;
1300
- const start = lines.findIndex((line) => headerRe.test(line));
1364
+ const start = lines.findIndex((line) => isMcpServersSectionLine(line));
1301
1365
  if (start === -1) {
1302
1366
  if (lines.length && lines[lines.length - 1].trim()) lines.push("");
1303
1367
  lines.push("[mcp_servers]");
@@ -1305,13 +1369,13 @@ function upsertCodexConfig(pathname, url) {
1305
1369
  return { contents: lines.join("\n"), updated: true };
1306
1370
  }
1307
1371
  let end = start + 1;
1308
- while (end < lines.length && !/^\s*\[.+\]\s*$/.test(lines[end])) {
1372
+ while (end < lines.length && !isTomlSectionLine(lines[end])) {
1309
1373
  end += 1;
1310
1374
  }
1311
1375
  let updated = false;
1312
1376
  const docdexLines = [];
1313
1377
  for (let i = start + 1; i < end; i += 1) {
1314
- if (/^\s*docdex\s*=/.test(lines[i])) {
1378
+ if (isDocdexAssignmentLine(lines[i])) {
1315
1379
  docdexLines.push(i);
1316
1380
  }
1317
1381
  }
@@ -1349,10 +1413,10 @@ function upsertCodexConfig(pathname, url) {
1349
1413
  };
1350
1414
 
1351
1415
  for (const line of lines) {
1352
- const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
1416
+ const section = parseTomlHeaderParts(line);
1353
1417
  if (section) {
1354
1418
  flushFeatures();
1355
- if (section[1].trim() === "features") {
1419
+ if (section.length === 1 && section[0] === "features") {
1356
1420
  inFeatures = true;
1357
1421
  buffer = [line];
1358
1422
  continue;
@@ -1384,13 +1448,13 @@ function upsertCodexConfig(pathname, url) {
1384
1448
  let inRootSection = false;
1385
1449
  let updated = false;
1386
1450
  for (const line of lines) {
1387
- const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
1451
+ const section = parseTomlHeaderParts(line);
1388
1452
  if (section) {
1389
- inRootSection = section[1].trim() === "mcp_servers";
1453
+ inRootSection = section.length === 1 && section[0] === "mcp_servers";
1390
1454
  output.push(line);
1391
1455
  continue;
1392
1456
  }
1393
- if (inRootSection && /^\s*docdex\s*=/.test(line)) {
1457
+ if (inRootSection && isDocdexAssignmentLine(line)) {
1394
1458
  updated = true;
1395
1459
  continue;
1396
1460
  }
@@ -1404,12 +1468,12 @@ function upsertCodexConfig(pathname, url) {
1404
1468
  let inRootSection = false;
1405
1469
  let count = 0;
1406
1470
  for (const line of lines) {
1407
- const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
1471
+ const section = parseTomlHeaderParts(line);
1408
1472
  if (section) {
1409
- inRootSection = section[1].trim() === "mcp_servers";
1473
+ inRootSection = section.length === 1 && section[0] === "mcp_servers";
1410
1474
  continue;
1411
1475
  }
1412
- if (inRootSection && /^\s*docdex\s*=/.test(line)) {
1476
+ if (inRootSection && isDocdexAssignmentLine(line)) {
1413
1477
  count += 1;
1414
1478
  }
1415
1479
  }
@@ -1422,7 +1486,7 @@ function upsertCodexConfig(pathname, url) {
1422
1486
  let skipping = false;
1423
1487
  let updated = false;
1424
1488
  for (const line of lines) {
1425
- const isSection = /^\s*\[.+\]\s*$/.test(line);
1489
+ const isSection = isTomlSectionLine(line);
1426
1490
  if (skipping) {
1427
1491
  if (isSection) {
1428
1492
  skipping = false;
@@ -1430,7 +1494,7 @@ function upsertCodexConfig(pathname, url) {
1430
1494
  continue;
1431
1495
  }
1432
1496
  }
1433
- if (/^\s*\[mcp_servers\.docdex\]\s*$/.test(line)) {
1497
+ if (isDocdexNestedMcpServerSectionLine(line)) {
1434
1498
  skipping = true;
1435
1499
  updated = true;
1436
1500
  continue;
@@ -1441,7 +1505,9 @@ function upsertCodexConfig(pathname, url) {
1441
1505
  };
1442
1506
 
1443
1507
  const countNestedDocdexSections = (text) =>
1444
- (text.match(/^\s*\[mcp_servers\.docdex\]\s*$/gm) || []).length;
1508
+ text
1509
+ .split(/\r?\n/)
1510
+ .filter((line) => isDocdexNestedMcpServerSectionLine(line)).length;
1445
1511
 
1446
1512
  let contents = "";
1447
1513
  if (fs.existsSync(pathname)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docdex",
3
- "version": "0.2.60",
3
+ "version": "0.2.62",
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": {