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 +49 -20
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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 (!
|
|
993
|
+
if (!isPlainObject2(filter)) {
|
|
966
994
|
throw invalidFilter("filter must be a JSON object");
|
|
967
995
|
}
|
|
968
|
-
|
|
996
|
+
walk3(filter, []);
|
|
969
997
|
}
|
|
970
|
-
function
|
|
998
|
+
function walk3(value, pathSegments) {
|
|
971
999
|
if (Array.isArray(value)) {
|
|
972
|
-
value.forEach((entry, idx) =>
|
|
1000
|
+
value.forEach((entry, idx) => walk3(entry, [...pathSegments, `[${idx}]`]));
|
|
973
1001
|
return;
|
|
974
1002
|
}
|
|
975
|
-
if (!
|
|
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
|
-
|
|
1009
|
+
walk3(child, [...pathSegments, key]);
|
|
982
1010
|
}
|
|
983
1011
|
}
|
|
984
|
-
function
|
|
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(
|
|
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: () =>
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|