docdex 0.2.59 → 0.2.61

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.61
4
+ - 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.
5
+ - 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.
6
+ - Harden installer-managed client config rewrites so whitespace-padded Docdex keys and section names are normalized instead of duplicated on reinstall.
7
+
8
+ ## 0.2.60
9
+ - 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.
10
+ - Update the packaged daemon dependency set to remove the vulnerable `rustls-webpki` chain that caused the nightly security audit failure.
11
+
3
12
  ## 0.2.58
4
13
  - Export Docdex delegation savings in hourly mswarm telemetry packages and expose matching runtime/admin mswarm summaries for frontend visibility.
5
14
 
package/assets/agents.md CHANGED
@@ -1,4 +1,4 @@
1
- ---- START OF DOCDEX INFO V0.2.59 ----
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`).
@@ -666,8 +666,17 @@ function stripLegacyDocdexBodySegment(segment, body) {
666
666
  const normalizedSegment = String(segment || "").replace(/\r\n/g, "\n");
667
667
  const normalizedBody = String(body || "").replace(/\r\n/g, "\n");
668
668
  if (!normalizedBody.trim()) return normalizedSegment;
669
- const re = new RegExp(`\\n?${escapeRegExp(normalizedBody)}\\n?`, "g");
670
- return normalizedSegment.replace(re, "\n").replace(/\n{3,}/g, "\n\n");
669
+ let result = normalizedSegment;
670
+ let index = result.indexOf(normalizedBody);
671
+ while (index !== -1) {
672
+ let start = index;
673
+ let end = index + normalizedBody.length;
674
+ if (start > 0 && result[start - 1] === "\n") start -= 1;
675
+ if (end < result.length && result[end] === "\n") end += 1;
676
+ result = `${result.slice(0, start)}\n${result.slice(end)}`;
677
+ index = result.indexOf(normalizedBody);
678
+ }
679
+ return result.replace(/\n{3,}/g, "\n\n");
671
680
  }
672
681
 
673
682
  function stripLegacyDocdexBody(text, body) {
@@ -1082,6 +1091,7 @@ function upsertMcpServerJson(pathname, url, options = {}) {
1082
1091
  const { value } = readJson(pathname);
1083
1092
  if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
1084
1093
  const root = value;
1094
+ const before = JSON.stringify(root);
1085
1095
  const extra =
1086
1096
  options &&
1087
1097
  typeof options === "object" &&
@@ -1090,9 +1100,44 @@ function upsertMcpServerJson(pathname, url, options = {}) {
1090
1100
  !Array.isArray(options.extra)
1091
1101
  ? options.extra
1092
1102
  : {};
1093
- const extraEntries = Object.entries(extra);
1094
- const matchesExtras = (entry) =>
1095
- extraEntries.every(([key, value]) => entry && entry[key] === value);
1103
+ const isPlainObject = (entry) =>
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
+ };
1116
+ const removeDocdexFromSection = (key) => {
1117
+ const section = root[key];
1118
+ if (Array.isArray(section)) {
1119
+ const filtered = section.filter(
1120
+ (entry) => normalizeManagedServerName(entry?.name) !== "docdex"
1121
+ );
1122
+ if (filtered.length !== section.length) {
1123
+ root[key] = filtered;
1124
+ }
1125
+ return;
1126
+ }
1127
+ if (!isPlainObject(section)) {
1128
+ return;
1129
+ }
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) {
1138
+ delete root[key];
1139
+ }
1140
+ };
1096
1141
  const pickSection = () => {
1097
1142
  if (root.mcpServers && typeof root.mcpServers === "object" && !Array.isArray(root.mcpServers)) {
1098
1143
  return { key: "mcpServers", section: root.mcpServers };
@@ -1103,29 +1148,42 @@ function upsertMcpServerJson(pathname, url, options = {}) {
1103
1148
  return null;
1104
1149
  };
1105
1150
  if (Array.isArray(root.mcpServers)) {
1106
- const idx = root.mcpServers.findIndex((entry) => entry && entry.name === "docdex");
1107
- if (idx >= 0) {
1108
- const current = root.mcpServers[idx] || {};
1109
- if (current.url === url && matchesExtras(current)) return false;
1110
- root.mcpServers[idx] = { ...current, ...extra, url, name: "docdex" };
1111
- writeJson(pathname, root);
1112
- return true;
1151
+ const nextEntries = [];
1152
+ let insertIndex = -1;
1153
+ let current = {};
1154
+ for (const entry of root.mcpServers) {
1155
+ if (normalizeManagedServerName(entry?.name) === "docdex") {
1156
+ if (insertIndex === -1) {
1157
+ insertIndex = nextEntries.length;
1158
+ current = isPlainObject(entry) ? { ...entry } : {};
1159
+ }
1160
+ continue;
1161
+ }
1162
+ nextEntries.push(entry);
1113
1163
  }
1114
- root.mcpServers.push({ ...extra, url, name: "docdex" });
1115
- writeJson(pathname, root);
1116
- return true;
1117
- }
1118
-
1119
- const picked = pickSection();
1120
- if (!picked) {
1121
- root.mcpServers = {};
1164
+ const nextEntry = { ...current, ...extra, url, name: "docdex" };
1165
+ if (insertIndex === -1) {
1166
+ nextEntries.push(nextEntry);
1167
+ } else {
1168
+ nextEntries.splice(insertIndex, 0, nextEntry);
1169
+ }
1170
+ root.mcpServers = nextEntries;
1171
+ removeDocdexFromSection("mcp_servers");
1172
+ } else {
1173
+ const picked = pickSection();
1174
+ const sectionKey = picked ? picked.key : "mcpServers";
1175
+ if (!picked) {
1176
+ root[sectionKey] = {};
1177
+ }
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 };
1184
+ removeDocdexFromSection(sectionKey === "mcpServers" ? "mcp_servers" : "mcpServers");
1122
1185
  }
1123
- const section = picked ? picked.section : root.mcpServers;
1124
- const current = section.docdex;
1125
- if (current && current.url === url && matchesExtras(current)) return false;
1126
- const base =
1127
- current && typeof current === "object" && !Array.isArray(current) ? current : {};
1128
- section.docdex = { ...base, ...extra, url };
1186
+ if (JSON.stringify(root) === before) return false;
1129
1187
  writeJson(pathname, root);
1130
1188
  return true;
1131
1189
  }
@@ -1137,7 +1195,15 @@ function upsertZedConfig(pathname, url) {
1137
1195
  if (!root.experimental_mcp_servers || typeof root.experimental_mcp_servers !== "object" || Array.isArray(root.experimental_mcp_servers)) {
1138
1196
  root.experimental_mcp_servers = {};
1139
1197
  }
1140
- 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
+ }
1141
1207
  if (current && current.url === url) return false;
1142
1208
  root.experimental_mcp_servers.docdex = { url };
1143
1209
  writeJson(pathname, root);
@@ -1146,16 +1212,52 @@ function upsertZedConfig(pathname, url) {
1146
1212
 
1147
1213
  function upsertCodexConfig(pathname, url) {
1148
1214
  const codexTimeoutSec = 300;
1149
- const hasSection = (contents, section) =>
1150
- new RegExp(`^\\s*\\[${section}\\]\\s*$`, "m").test(contents);
1151
- const hasNestedMcpServers = (contents) =>
1152
- /^\s*\[mcp_servers\.[^\]]+\]\s*$/m.test(contents);
1153
1215
  const legacyInstructionPath = "~/.docdex/agents.md";
1154
1216
  const parseTomlString = (value) => {
1155
1217
  const trimmed = value.trim();
1156
1218
  const quoted = trimmed.match(/^"(.*)"$/) || trimmed.match(/^'(.*)'$/);
1157
1219
  return quoted ? quoted[1] : trimmed;
1158
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";
1159
1261
  const migrateLegacyMcpServers = (contents) => {
1160
1262
  if (!/\[\[mcp_servers\]\]/m.test(contents)) {
1161
1263
  return { contents, migrated: false };
@@ -1220,8 +1322,7 @@ function upsertCodexConfig(pathname, url) {
1220
1322
  ["startup_timeout_sec", `${codexTimeoutSec}`]
1221
1323
  ];
1222
1324
  const lines = contents.split(/\r?\n/);
1223
- const headerRe = /^\s*\[mcp_servers\.docdex\]\s*$/;
1224
- let start = lines.findIndex((line) => headerRe.test(line));
1325
+ let start = lines.findIndex((line) => isDocdexNestedMcpServerSectionLine(line));
1225
1326
  if (start === -1) {
1226
1327
  if (lines.length && lines[lines.length - 1].trim()) lines.push("");
1227
1328
  lines.push("[mcp_servers.docdex]");
@@ -1231,7 +1332,7 @@ function upsertCodexConfig(pathname, url) {
1231
1332
  return { contents: lines.join("\n"), updated: true };
1232
1333
  }
1233
1334
  let end = start + 1;
1234
- while (end < lines.length && !/^\s*\[.+\]\s*$/.test(lines[end])) {
1335
+ while (end < lines.length && !isTomlSectionLine(lines[end])) {
1235
1336
  end += 1;
1236
1337
  }
1237
1338
  let updated = false;
@@ -1239,7 +1340,7 @@ function upsertCodexConfig(pathname, url) {
1239
1340
  const lineValue = `${key} = ${value}`;
1240
1341
  let keyIndex = -1;
1241
1342
  for (let i = start + 1; i < end; i += 1) {
1242
- if (new RegExp(`^\\s*${key}\\s*=`).test(lines[i])) {
1343
+ if (parseTomlAssignmentKey(lines[i]) === key) {
1243
1344
  keyIndex = i;
1244
1345
  break;
1245
1346
  }
@@ -1260,8 +1361,7 @@ function upsertCodexConfig(pathname, url) {
1260
1361
  const entryLine =
1261
1362
  `docdex = { url = "${urlValue}", tool_timeout_sec = ${codexTimeoutSec}, startup_timeout_sec = ${codexTimeoutSec} }`;
1262
1363
  const lines = contents.split(/\r?\n/);
1263
- const headerRe = /^\s*\[mcp_servers\]\s*$/;
1264
- const start = lines.findIndex((line) => headerRe.test(line));
1364
+ const start = lines.findIndex((line) => isMcpServersSectionLine(line));
1265
1365
  if (start === -1) {
1266
1366
  if (lines.length && lines[lines.length - 1].trim()) lines.push("");
1267
1367
  lines.push("[mcp_servers]");
@@ -1269,23 +1369,29 @@ function upsertCodexConfig(pathname, url) {
1269
1369
  return { contents: lines.join("\n"), updated: true };
1270
1370
  }
1271
1371
  let end = start + 1;
1272
- while (end < lines.length && !/^\s*\[.+\]\s*$/.test(lines[end])) {
1372
+ while (end < lines.length && !isTomlSectionLine(lines[end])) {
1273
1373
  end += 1;
1274
1374
  }
1275
1375
  let updated = false;
1276
- let docdexLine = -1;
1376
+ const docdexLines = [];
1277
1377
  for (let i = start + 1; i < end; i += 1) {
1278
- if (/^\s*docdex\s*=/.test(lines[i])) {
1279
- docdexLine = i;
1280
- break;
1378
+ if (isDocdexAssignmentLine(lines[i])) {
1379
+ docdexLines.push(i);
1281
1380
  }
1282
1381
  }
1283
- if (docdexLine === -1) {
1382
+ if (docdexLines.length === 0) {
1284
1383
  lines.splice(end, 0, entryLine);
1285
1384
  updated = true;
1286
- } else if (lines[docdexLine].trim() !== entryLine) {
1287
- lines[docdexLine] = entryLine;
1288
- updated = true;
1385
+ } else {
1386
+ const [firstDocdexLine, ...extraDocdexLines] = docdexLines;
1387
+ if (lines[firstDocdexLine].trim() !== entryLine) {
1388
+ lines[firstDocdexLine] = entryLine;
1389
+ updated = true;
1390
+ }
1391
+ for (let i = extraDocdexLines.length - 1; i >= 0; i -= 1) {
1392
+ lines.splice(extraDocdexLines[i], 1);
1393
+ updated = true;
1394
+ }
1289
1395
  }
1290
1396
  return { contents: lines.join("\n"), updated };
1291
1397
  };
@@ -1307,10 +1413,10 @@ function upsertCodexConfig(pathname, url) {
1307
1413
  };
1308
1414
 
1309
1415
  for (const line of lines) {
1310
- const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
1416
+ const section = parseTomlHeaderParts(line);
1311
1417
  if (section) {
1312
1418
  flushFeatures();
1313
- if (section[1].trim() === "features") {
1419
+ if (section.length === 1 && section[0] === "features") {
1314
1420
  inFeatures = true;
1315
1421
  buffer = [line];
1316
1422
  continue;
@@ -1336,6 +1442,73 @@ function upsertCodexConfig(pathname, url) {
1336
1442
  return { contents: output.join("\n"), updated };
1337
1443
  };
1338
1444
 
1445
+ const removeRootDocdexEntries = (text) => {
1446
+ const lines = text.split(/\r?\n/);
1447
+ const output = [];
1448
+ let inRootSection = false;
1449
+ let updated = false;
1450
+ for (const line of lines) {
1451
+ const section = parseTomlHeaderParts(line);
1452
+ if (section) {
1453
+ inRootSection = section.length === 1 && section[0] === "mcp_servers";
1454
+ output.push(line);
1455
+ continue;
1456
+ }
1457
+ if (inRootSection && isDocdexAssignmentLine(line)) {
1458
+ updated = true;
1459
+ continue;
1460
+ }
1461
+ output.push(line);
1462
+ }
1463
+ return { contents: output.join("\n"), updated };
1464
+ };
1465
+
1466
+ const countRootDocdexEntries = (text) => {
1467
+ const lines = text.split(/\r?\n/);
1468
+ let inRootSection = false;
1469
+ let count = 0;
1470
+ for (const line of lines) {
1471
+ const section = parseTomlHeaderParts(line);
1472
+ if (section) {
1473
+ inRootSection = section.length === 1 && section[0] === "mcp_servers";
1474
+ continue;
1475
+ }
1476
+ if (inRootSection && isDocdexAssignmentLine(line)) {
1477
+ count += 1;
1478
+ }
1479
+ }
1480
+ return count;
1481
+ };
1482
+
1483
+ const removeNestedDocdexSections = (text) => {
1484
+ const lines = text.split(/\r?\n/);
1485
+ const output = [];
1486
+ let skipping = false;
1487
+ let updated = false;
1488
+ for (const line of lines) {
1489
+ const isSection = isTomlSectionLine(line);
1490
+ if (skipping) {
1491
+ if (isSection) {
1492
+ skipping = false;
1493
+ } else {
1494
+ continue;
1495
+ }
1496
+ }
1497
+ if (isDocdexNestedMcpServerSectionLine(line)) {
1498
+ skipping = true;
1499
+ updated = true;
1500
+ continue;
1501
+ }
1502
+ output.push(line);
1503
+ }
1504
+ return { contents: output.join("\n"), updated };
1505
+ };
1506
+
1507
+ const countNestedDocdexSections = (text) =>
1508
+ text
1509
+ .split(/\r?\n/)
1510
+ .filter((line) => isDocdexNestedMcpServerSectionLine(line)).length;
1511
+
1339
1512
  let contents = "";
1340
1513
  if (fs.existsSync(pathname)) {
1341
1514
  contents = fs.readFileSync(pathname, "utf8");
@@ -1351,7 +1524,23 @@ function upsertCodexConfig(pathname, url) {
1351
1524
  contents = cleaned.contents;
1352
1525
  updated = updated || cleaned.updated;
1353
1526
 
1354
- if (hasNestedMcpServers(contents)) {
1527
+ const preferNested = hasNestedMcpServers(contents);
1528
+ const rootDocdexCount = countRootDocdexEntries(contents);
1529
+ const nestedDocdexCount = countNestedDocdexSections(contents);
1530
+
1531
+ if (preferNested && rootDocdexCount > 0) {
1532
+ const prunedRoot = removeRootDocdexEntries(contents);
1533
+ contents = prunedRoot.contents;
1534
+ updated = updated || prunedRoot.updated;
1535
+ }
1536
+
1537
+ if ((!preferNested && nestedDocdexCount > 0) || (preferNested && nestedDocdexCount > 1)) {
1538
+ const prunedNested = removeNestedDocdexSections(contents);
1539
+ contents = prunedNested.contents;
1540
+ updated = updated || prunedNested.updated;
1541
+ }
1542
+
1543
+ if (preferNested) {
1355
1544
  const nested = upsertDocdexNested(contents, url);
1356
1545
  contents = nested.contents;
1357
1546
  updated = updated || nested.updated;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docdex",
3
- "version": "0.2.59",
3
+ "version": "0.2.61",
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": {