neuro-vault-mcp 4.1.0 → 4.2.0

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/dist/cli.js CHANGED
@@ -944,6 +944,33 @@ function extractTags(frontmatter) {
944
944
  return out;
945
945
  }
946
946
 
947
+ // src/lib/obsidian/query/default-regex-options.ts
948
+ var REGEX_KEY = "$regex";
949
+ var OPTIONS_KEY = "$options";
950
+ var DEFAULT_OPTIONS = "i";
951
+ function applyDefaultRegexOptions(filter) {
952
+ return walk2(filter);
953
+ }
954
+ function walk2(value) {
955
+ if (Array.isArray(value)) {
956
+ return value.map(walk2);
957
+ }
958
+ if (!isPlainObject(value)) {
959
+ return value;
960
+ }
961
+ const out = {};
962
+ for (const [key, child] of Object.entries(value)) {
963
+ out[key] = walk2(child);
964
+ }
965
+ if (REGEX_KEY in out && !(OPTIONS_KEY in out)) {
966
+ out[OPTIONS_KEY] = DEFAULT_OPTIONS;
967
+ }
968
+ return out;
969
+ }
970
+ function isPlainObject(value) {
971
+ return typeof value === "object" && value !== null && !Array.isArray(value);
972
+ }
973
+
947
974
  // src/lib/obsidian/query/whitelist.ts
948
975
  var ALLOWED_OPERATORS = /* @__PURE__ */ new Set([
949
976
  "$eq",
@@ -956,32 +983,33 @@ var ALLOWED_OPERATORS = /* @__PURE__ */ new Set([
956
983
  "$lte",
957
984
  "$exists",
958
985
  "$regex",
986
+ "$options",
959
987
  "$and",
960
988
  "$or",
961
989
  "$nor",
962
990
  "$not"
963
991
  ]);
964
992
  function validateFilter(filter) {
965
- if (!isPlainObject(filter)) {
993
+ if (!isPlainObject2(filter)) {
966
994
  throw invalidFilter("filter must be a JSON object");
967
995
  }
968
- walk2(filter, []);
996
+ walk3(filter, []);
969
997
  }
970
- function walk2(value, pathSegments) {
998
+ function walk3(value, pathSegments) {
971
999
  if (Array.isArray(value)) {
972
- value.forEach((entry, idx) => walk2(entry, [...pathSegments, `[${idx}]`]));
1000
+ value.forEach((entry, idx) => walk3(entry, [...pathSegments, `[${idx}]`]));
973
1001
  return;
974
1002
  }
975
- if (!isPlainObject(value)) return;
1003
+ if (!isPlainObject2(value)) return;
976
1004
  for (const [key, child] of Object.entries(value)) {
977
1005
  if (key.startsWith("$") && !ALLOWED_OPERATORS.has(key)) {
978
1006
  const where = pathSegments.length === 0 ? key : `${pathSegments.join(".")}.${key}`;
979
1007
  throw invalidFilter(`operator ${key} is not allowed (at ${where})`);
980
1008
  }
981
- walk2(child, [...pathSegments, key]);
1009
+ walk3(child, [...pathSegments, key]);
982
1010
  }
983
1011
  }
984
- function isPlainObject(value) {
1012
+ function isPlainObject2(value) {
985
1013
  return typeof value === "object" && value !== null && !Array.isArray(value);
986
1014
  }
987
1015
  function invalidFilter(message) {
@@ -997,6 +1025,7 @@ var READ_BATCH_SIZE2 = 32;
997
1025
  async function runQueryNotes(input, reader, graph) {
998
1026
  const validated = validateInput(input);
999
1027
  validateFilter(validated.filter);
1028
+ const effectiveFilter = applyDefaultRegexOptions(validated.filter);
1000
1029
  if (graph) {
1001
1030
  await graph.ensureFresh();
1002
1031
  }
@@ -1016,7 +1045,7 @@ async function runQueryNotes(input, reader, graph) {
1016
1045
  }
1017
1046
  let matcher;
1018
1047
  try {
1019
- matcher = sift(validated.filter);
1048
+ matcher = sift(effectiveFilter);
1020
1049
  } catch (err) {
1021
1050
  const message = err instanceof Error ? err.message : String(err);
1022
1051
  throw new ToolHandlerError("INVALID_FILTER", `filter rejected by sift: ${message}`);
@@ -1811,7 +1840,7 @@ __export(util_exports, {
1811
1840
  getSizableOrigin: () => getSizableOrigin,
1812
1841
  hexToUint8Array: () => hexToUint8Array,
1813
1842
  isObject: () => isObject,
1814
- isPlainObject: () => isPlainObject2,
1843
+ isPlainObject: () => isPlainObject3,
1815
1844
  issue: () => issue,
1816
1845
  joinValues: () => joinValues,
1817
1846
  jsonStringifyReplacer: () => jsonStringifyReplacer,
@@ -1995,7 +2024,7 @@ var allowsEval = cached(() => {
1995
2024
  return false;
1996
2025
  }
1997
2026
  });
1998
- function isPlainObject2(o) {
2027
+ function isPlainObject3(o) {
1999
2028
  if (isObject(o) === false)
2000
2029
  return false;
2001
2030
  const ctor = o.constructor;
@@ -2012,7 +2041,7 @@ function isPlainObject2(o) {
2012
2041
  return true;
2013
2042
  }
2014
2043
  function shallowClone(o) {
2015
- if (isPlainObject2(o))
2044
+ if (isPlainObject3(o))
2016
2045
  return { ...o };
2017
2046
  if (Array.isArray(o))
2018
2047
  return [...o];
@@ -2205,7 +2234,7 @@ function omit(schema, mask) {
2205
2234
  return clone(schema, def);
2206
2235
  }
2207
2236
  function extend(schema, shape) {
2208
- if (!isPlainObject2(shape)) {
2237
+ if (!isPlainObject3(shape)) {
2209
2238
  throw new Error("Invalid input to extend: expected a plain object");
2210
2239
  }
2211
2240
  const checks = schema._zod.def.checks;
@@ -2228,7 +2257,7 @@ function extend(schema, shape) {
2228
2257
  return clone(schema, def);
2229
2258
  }
2230
2259
  function safeExtend(schema, shape) {
2231
- if (!isPlainObject2(shape)) {
2260
+ if (!isPlainObject3(shape)) {
2232
2261
  throw new Error("Invalid input to safeExtend: expected a plain object");
2233
2262
  }
2234
2263
  const def = mergeDefs(schema._zod.def, {
@@ -4503,7 +4532,7 @@ function mergeValues(a, b) {
4503
4532
  if (a instanceof Date && b instanceof Date && +a === +b) {
4504
4533
  return { valid: true, data: a };
4505
4534
  }
4506
- if (isPlainObject2(a) && isPlainObject2(b)) {
4535
+ if (isPlainObject3(a) && isPlainObject3(b)) {
4507
4536
  const bKeys = Object.keys(b);
4508
4537
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
4509
4538
  const newObj = { ...a, ...b };
@@ -4657,7 +4686,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4657
4686
  $ZodType.init(inst, def);
4658
4687
  inst._zod.parse = (payload, ctx) => {
4659
4688
  const input = payload.value;
4660
- if (!isPlainObject2(input)) {
4689
+ if (!isPlainObject3(input)) {
4661
4690
  payload.issues.push({
4662
4691
  expected: "record",
4663
4692
  code: "invalid_type",
@@ -14969,7 +14998,7 @@ function unwrap(schema) {
14969
14998
  }
14970
14999
  return current;
14971
15000
  }
14972
- function isPlainObject3(value) {
15001
+ function isPlainObject4(value) {
14973
15002
  return typeof value === "object" && value !== null && !Array.isArray(value);
14974
15003
  }
14975
15004
  function describeJsonShape(parsed) {
@@ -15018,7 +15047,7 @@ function coerceFieldValue(schema, value, fieldName = "value") {
15018
15047
  return value;
15019
15048
  }
15020
15049
  if (inner instanceof external_exports.ZodObject || inner instanceof external_exports.ZodRecord) {
15021
- if (isPlainObject3(value)) return value;
15050
+ if (isPlainObject4(value)) return value;
15022
15051
  if (typeof value === "string") {
15023
15052
  let parsed;
15024
15053
  try {
@@ -15029,7 +15058,7 @@ function coerceFieldValue(schema, value, fieldName = "value") {
15029
15058
  `expected object or JSON-string of one, failed to parse: ${JSON.stringify(value)}`
15030
15059
  );
15031
15060
  }
15032
- if (isPlainObject3(parsed)) return parsed;
15061
+ if (isPlainObject4(parsed)) return parsed;
15033
15062
  throw new CoerceError(
15034
15063
  fieldName,
15035
15064
  `expected object, parsed JSON resolved to ${describeJsonShape(parsed)}`
@@ -16311,7 +16340,7 @@ function buildQueryNotesTool(deps) {
16311
16340
  return {
16312
16341
  name: "query_notes",
16313
16342
  title: "Query Notes",
16314
- description: 'Run a structured MongoDB-style query against the vault\'s frontmatter, tags, and wikilink graph. `filter` is a sift/MongoDB filter object evaluated against `NoteRecord` shape `{ path, frontmatter, tags, backlink_count }` \u2014 `tags` is an array of strings (no leading `#`) extracted from the `tags:` frontmatter field; `backlink_count` is the number of vault-wide wikilinks (and `![[embeds]]`) that point at the note. Reference frontmatter keys with the dotted prefix `frontmatter.<key>`. Supported operators: `$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, `$regex`, `$and`, `$or`, `$nor`, `$not`. Optional `path_prefix` restricts the scan to a vault subtree (vault-relative POSIX, no leading slash). Optional `sort` is `{ field, order }` \u2014 `field` must be `"path"`, `"backlink_count"`, or start with `"frontmatter."`. Optional `limit` defaults to 100, max 1000. Optional `include_content` (default false) \u2014 when true, each result also carries `content` (note body). Returns `{ results, count, truncated }`; each result item carries `backlink_count`. `truncated` is true when more notes matched than `limit` allowed. Reads directly from disk and does not require Obsidian to be running.',
16343
+ description: 'Run a structured MongoDB-style query against the vault\'s frontmatter, tags, and wikilink graph. `filter` is a sift/MongoDB filter object evaluated against `NoteRecord` shape `{ path, frontmatter, tags, backlink_count }` \u2014 `tags` is an array of strings (no leading `#`) extracted from the `tags:` frontmatter field; `backlink_count` is the number of vault-wide wikilinks (and `![[embeds]]`) that point at the note. Reference frontmatter keys with the dotted prefix `frontmatter.<key>`. Supported operators: `$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, `$regex`, `$options`, `$and`, `$or`, `$nor`, `$not`. `$regex` is case-insensitive by default; pass `$options` (e.g. `\'\'` for case-sensitive, `\'m\'` for multiline-only) to override. Optional `path_prefix` restricts the scan to a vault subtree (vault-relative POSIX, no leading slash). Optional `sort` is `{ field, order }` \u2014 `field` must be `"path"`, `"backlink_count"`, or start with `"frontmatter."`. Optional `limit` defaults to 100, max 1000. Optional `include_content` (default false) \u2014 when true, each result also carries `content` (note body). Returns `{ results, count, truncated }`; each result item carries `backlink_count`. `truncated` is true when more notes matched than `limit` allowed. Reads directly from disk and does not require Obsidian to be running.',
16315
16344
  inputSchema: inputSchema11,
16316
16345
  handler: async (input) => {
16317
16346
  return runQueryNotes(input, reader, graph);
@@ -16531,7 +16560,7 @@ Use \`read_notes\`, \`create_note\`, \`edit_note\`, \`read_daily\`, \`append_dai
16531
16560
 
16532
16561
  ### Structured queries
16533
16562
 
16534
- Use \`query_notes\` for multi-criteria questions that combine tags, frontmatter properties, and ranges \u2014 for example "active projects with #ai", "todo tasks created this week", "notes with deadline set", "all notes tagged X". The \`filter\` is a MongoDB-style object evaluated against \`{ path, frontmatter, tags, backlink_count }\` \u2014 reference frontmatter keys as \`frontmatter.<key>\`, tags via the top-level \`tags\` field (no leading \`#\`), and \`backlink_count\` as a top-level scalar. Supported operators: \`$eq\`, \`$ne\`, \`$in\`, \`$nin\`, \`$gt\`, \`$gte\`, \`$lt\`, \`$lte\`, \`$exists\`, \`$regex\`, \`$and\`, \`$or\`, \`$nor\`, \`$not\`. To list notes by a single tag use \`{ filter: { tags: 'X' } }\`. \`backlink_count\` is filterable (\`{ backlink_count: { $gte: 5 } }\`) and sortable (\`sort: { field: 'backlink_count', order: 'desc' }\`). The result \`{ results, count, truncated }\` includes \`frontmatter\` and \`backlink_count\` always; pass \`include_content: true\` only when bodies are needed up-front (it can grow the response a lot). Reads directly from disk; does not need Obsidian running. \`limit\` defaults to 100 and is capped at 1000.
16563
+ Use \`query_notes\` for multi-criteria questions that combine tags, frontmatter properties, and ranges \u2014 for example "active projects with #ai", "todo tasks created this week", "notes with deadline set", "all notes tagged X". The \`filter\` is a MongoDB-style object evaluated against \`{ path, frontmatter, tags, backlink_count }\` \u2014 reference frontmatter keys as \`frontmatter.<key>\`, tags via the top-level \`tags\` field (no leading \`#\`), and \`backlink_count\` as a top-level scalar. Supported operators: \`$eq\`, \`$ne\`, \`$in\`, \`$nin\`, \`$gt\`, \`$gte\`, \`$lt\`, \`$lte\`, \`$exists\`, \`$regex\`, \`$options\`, \`$and\`, \`$or\`, \`$nor\`, \`$not\`. \`$regex\` is case-insensitive by default; pass \`$options\` (e.g. \`''\` for case-sensitive, \`'m'\` for multiline-only) to override. To list notes by a single tag use \`{ filter: { tags: 'X' } }\`. \`backlink_count\` is filterable (\`{ backlink_count: { $gte: 5 } }\`) and sortable (\`sort: { field: 'backlink_count', order: 'desc' }\`). The result \`{ results, count, truncated }\` includes \`frontmatter\` and \`backlink_count\` always; pass \`include_content: true\` only when bodies are needed up-front (it can grow the response a lot). Reads directly from disk; does not need Obsidian running. \`limit\` defaults to 100 and is capped at 1000.
16535
16564
 
16536
16565
  ### Frontmatter properties
16537
16566