seekstone 0.20.3 → 0.21.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/README.md +2 -1
- package/dist/index.js +139 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Other MCP clients (Windsurf, Cline, …) take the Option 3 JSON block in their o
|
|
|
104
104
|
|
|
105
105
|
## Tools
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
22 tools: 12 read, 10 write.
|
|
108
108
|
|
|
109
109
|
### Read
|
|
110
110
|
|
|
@@ -114,6 +114,7 @@ Other MCP clients (Windsurf, Cline, …) take the Option 3 JSON block in their o
|
|
|
114
114
|
| `query_notes` | Structured metadata query. Filter by frontmatter key/value predicates (`eq`, `ne`, `contains`, `exists`, `missing`, `gt`/`gte`/`lt`/`lte`), tag, folder, modified time, and size; sort and select the fields you need. Returns compact rows, not note content. |
|
|
115
115
|
| `context_pack` | Answer-ready context for a natural-language question in one call, hard-capped at a byte budget (default 2 KB): ranked excerpts, linked neighbor notes with one-line summaries, and follow-up source paths — replaces a search → read → get_backlinks round-trip loop. Scopable by `folder`/`tag` and takes the same `mode` as `search`. |
|
|
116
116
|
| `read_note` | Read the full content of a note by vault-relative path. Supports returning a single section, block, or line range. |
|
|
117
|
+
| `read_notes` | Read up to 20 notes (or sections, blocks, line ranges) in one call under a total byte budget (default 16 KB). Over budget, each note is cut to a fair share and marked `truncated`; one bad path returns its own error without failing the batch. |
|
|
117
118
|
| `list_notes` | List notes, optionally filtered by folder prefix or tag. |
|
|
118
119
|
| `list_tags` | List all tags in the vault sorted by usage count (or alphabetically). |
|
|
119
120
|
| `outline_note` | Return a note's heading and block structure without its full content. |
|
package/dist/index.js
CHANGED
|
@@ -2694,19 +2694,77 @@ async function readNote(ctx2, input) {
|
|
|
2694
2694
|
};
|
|
2695
2695
|
}
|
|
2696
2696
|
|
|
2697
|
+
// src/tools/read_notes.ts
|
|
2698
|
+
import { z as z18 } from "zod";
|
|
2699
|
+
var DEFAULT_BUDGET_BYTES = 16384;
|
|
2700
|
+
var ReadNotesInput = z18.object({
|
|
2701
|
+
items: z18.array(ReadNoteInput).min(1).max(20).describe(
|
|
2702
|
+
"Notes to read, 1\u201320, each with the same options as read_note (path plus at most one of section, block, or lines)."
|
|
2703
|
+
),
|
|
2704
|
+
budgetBytes: z18.number().int().min(256).max(65536).default(DEFAULT_BUDGET_BYTES).describe(
|
|
2705
|
+
"Cap on the total note text returned across all items (UTF-8 bytes). When exceeded, each item is truncated to a fair share."
|
|
2706
|
+
)
|
|
2707
|
+
});
|
|
2708
|
+
async function readNotes(ctx2, input) {
|
|
2709
|
+
const budget = input.budgetBytes;
|
|
2710
|
+
const results = await Promise.all(
|
|
2711
|
+
input.items.map((item) => readNote(ctx2, item).catch((err) => toItemError(item.path, err)))
|
|
2712
|
+
);
|
|
2713
|
+
const entries = results.filter((r) => !("error" in r)).map((r) => ({ r, size: r.bytesReturned, alloc: 0 }));
|
|
2714
|
+
allocate(entries, budget);
|
|
2715
|
+
const cut = new Map(
|
|
2716
|
+
entries.map((e) => [e.r, truncateItem(e.r, e.alloc)])
|
|
2717
|
+
);
|
|
2718
|
+
const notes2 = results.map((r) => cut.get(r) ?? r);
|
|
2719
|
+
const bytesUsed = [...cut.values()].reduce((sum, r) => sum + r.bytesReturned, 0);
|
|
2720
|
+
return { notes: notes2, budgetBytes: budget, bytesUsed };
|
|
2721
|
+
}
|
|
2722
|
+
function toItemError(path2, err) {
|
|
2723
|
+
const { message, code } = err;
|
|
2724
|
+
if (message.startsWith("{")) return { path: path2, ...JSON.parse(message) };
|
|
2725
|
+
if (message.startsWith("Path outside vault")) return { path: path2, error: "outside_vault" };
|
|
2726
|
+
if (code === "ENOENT") return { path: path2, error: "not_found" };
|
|
2727
|
+
return { path: path2, error: "read_failed", message };
|
|
2728
|
+
}
|
|
2729
|
+
function allocate(entries, budget) {
|
|
2730
|
+
let pending = entries.filter((e) => e.size > 0);
|
|
2731
|
+
let remaining = budget;
|
|
2732
|
+
for (const e of entries) e.alloc = e.size;
|
|
2733
|
+
if (entries.reduce((sum, e) => sum + e.size, 0) <= budget) return;
|
|
2734
|
+
while (pending.length > 0) {
|
|
2735
|
+
const share = Math.floor(remaining / pending.length);
|
|
2736
|
+
const fits = pending.filter((e) => e.size <= share);
|
|
2737
|
+
if (fits.length === 0) {
|
|
2738
|
+
const extra = remaining - share * pending.length;
|
|
2739
|
+
for (const [k, e] of pending.entries()) e.alloc = share + (k < extra ? 1 : 0);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
for (const e of fits) remaining -= e.size;
|
|
2743
|
+
pending = pending.filter((e) => e.size > share);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
function truncateItem(r, maxBytes) {
|
|
2747
|
+
if (r.bytesReturned <= maxBytes) return r;
|
|
2748
|
+
const buf = Buffer.from(r.content, "utf8");
|
|
2749
|
+
let end = maxBytes;
|
|
2750
|
+
while (end > 0 && (buf.readUInt8(end) & 192) === 128) end--;
|
|
2751
|
+
const content = buf.toString("utf8", 0, end);
|
|
2752
|
+
return { ...r, content, bytesReturned: end, truncated: true };
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2697
2755
|
// src/tools/rename_heading.ts
|
|
2698
2756
|
import { readFile as readFile11 } from "fs/promises";
|
|
2699
2757
|
import { join as join5 } from "path";
|
|
2700
|
-
import { z as
|
|
2701
|
-
var RenameHeadingInput =
|
|
2702
|
-
path:
|
|
2703
|
-
oldHeading:
|
|
2758
|
+
import { z as z19 } from "zod";
|
|
2759
|
+
var RenameHeadingInput = z19.object({
|
|
2760
|
+
path: z19.string().describe("Vault-relative path of the note containing the heading."),
|
|
2761
|
+
oldHeading: z19.string().min(1).describe(
|
|
2704
2762
|
"Current heading text, without # markers. Matched case-insensitively; if the note has duplicate headings the first one wins, mirroring Obsidian link resolution."
|
|
2705
2763
|
),
|
|
2706
|
-
newHeading:
|
|
2764
|
+
newHeading: z19.string().min(1).refine((s) => !/[\n[\]|#]/.test(s), {
|
|
2707
2765
|
message: 'newHeading must not contain newlines, "[", "]", "|", or "#" \u2014 those break [[note#heading]] link syntax.'
|
|
2708
2766
|
}).describe("New heading text, without # markers."),
|
|
2709
|
-
prevHash:
|
|
2767
|
+
prevHash: z19.string().optional().describe(
|
|
2710
2768
|
"Optional compare-and-swap guard: the contentHash from a prior read of this note. Fails with hash_conflict if the note changed since."
|
|
2711
2769
|
)
|
|
2712
2770
|
});
|
|
@@ -2829,17 +2887,17 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2829
2887
|
|
|
2830
2888
|
// src/tools/replace_in_note.ts
|
|
2831
2889
|
import { readFile as readFile12 } from "fs/promises";
|
|
2832
|
-
import { z as
|
|
2833
|
-
var ReplaceInNoteInput =
|
|
2834
|
-
path:
|
|
2835
|
-
find:
|
|
2836
|
-
replace:
|
|
2837
|
-
regex:
|
|
2838
|
-
caseSensitive:
|
|
2839
|
-
wholeWord:
|
|
2840
|
-
limit:
|
|
2841
|
-
dryRun:
|
|
2842
|
-
prevHash:
|
|
2890
|
+
import { z as z20 } from "zod";
|
|
2891
|
+
var ReplaceInNoteInput = z20.object({
|
|
2892
|
+
path: z20.string().describe("Vault-relative path to the note."),
|
|
2893
|
+
find: z20.string().min(1).max(1e3).describe("Text or pattern to find (max 1000 chars; bounds regex-mode pattern size)."),
|
|
2894
|
+
replace: z20.string().describe("Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."),
|
|
2895
|
+
regex: z20.boolean().default(false).describe("Treat find as a regular expression."),
|
|
2896
|
+
caseSensitive: z20.boolean().default(false).describe("Case-sensitive matching."),
|
|
2897
|
+
wholeWord: z20.boolean().default(false).describe("Match whole words only (\\b word-boundary)."),
|
|
2898
|
+
limit: z20.number().int().min(1).optional().describe("Maximum number of replacements. Omit to replace all."),
|
|
2899
|
+
dryRun: z20.boolean().default(false).describe("If true, report matches without writing anything."),
|
|
2900
|
+
prevHash: z20.string().optional().describe(
|
|
2843
2901
|
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2844
2902
|
)
|
|
2845
2903
|
});
|
|
@@ -2924,12 +2982,12 @@ async function replaceInNote(ctx2, input) {
|
|
|
2924
2982
|
// src/tools/undo_write.ts
|
|
2925
2983
|
import { mkdir as mkdir6, readFile as readFile13, rm as rm3 } from "fs/promises";
|
|
2926
2984
|
import { dirname as dirname4 } from "path";
|
|
2927
|
-
import { z as
|
|
2928
|
-
var UndoWriteInput =
|
|
2929
|
-
seq:
|
|
2985
|
+
import { z as z21 } from "zod";
|
|
2986
|
+
var UndoWriteInput = z21.object({
|
|
2987
|
+
seq: z21.number().int().min(1).optional().describe(
|
|
2930
2988
|
"Journal entry to revert (from list_writes). Defaults to the most recent undoable write."
|
|
2931
2989
|
),
|
|
2932
|
-
force:
|
|
2990
|
+
force: z21.boolean().default(false).describe(
|
|
2933
2991
|
"Restore even if a file changed after the journaled write. The clobbered state is journaled first, so nothing is lost. Defaults to false \u2014 conflicts are refused."
|
|
2934
2992
|
)
|
|
2935
2993
|
});
|
|
@@ -3086,6 +3144,7 @@ function safeMeta(args) {
|
|
|
3086
3144
|
}
|
|
3087
3145
|
if (typeof a.query === "string") out.queryLen = a.query.length;
|
|
3088
3146
|
if (Array.isArray(a.where)) out.whereCount = a.where.length;
|
|
3147
|
+
if (Array.isArray(a.items)) out.itemsCount = a.items.length;
|
|
3089
3148
|
return out;
|
|
3090
3149
|
}
|
|
3091
3150
|
function resultBytes(r) {
|
|
@@ -3235,6 +3294,11 @@ async function run(ctx2, name, args) {
|
|
|
3235
3294
|
const result = await readNote(ctx2, input);
|
|
3236
3295
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
3237
3296
|
}
|
|
3297
|
+
case "read_notes": {
|
|
3298
|
+
const input = ReadNotesInput.parse(args);
|
|
3299
|
+
const result = await readNotes(ctx2, input);
|
|
3300
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
3301
|
+
}
|
|
3238
3302
|
case "list_notes": {
|
|
3239
3303
|
const input = ListNotesInput.parse(args);
|
|
3240
3304
|
const entries = listNotes(ctx2, input);
|
|
@@ -5121,6 +5185,59 @@ var ALL_TOOLS = [
|
|
|
5121
5185
|
required: ["path"]
|
|
5122
5186
|
}
|
|
5123
5187
|
},
|
|
5188
|
+
{
|
|
5189
|
+
name: "read_notes",
|
|
5190
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
5191
|
+
description: "Read up to 20 notes (or spans of them) in one call instead of repeated read_note calls, e.g. the top hits from search, query_notes, list_notes, or get_backlinks. Each item takes the same options as read_note and returns the same fields, in request order. budgetBytes (default 16384) caps the total note text: when the notes exceed it, each is cut to a fair share, marked truncated: true, and keeps noteBytes, so read the rest with read_note. A failing item (missing path, unknown section) returns its own error without failing the batch. contentHash is always the whole file's hash, usable as prevHash.",
|
|
5192
|
+
inputSchema: {
|
|
5193
|
+
type: "object",
|
|
5194
|
+
properties: {
|
|
5195
|
+
items: {
|
|
5196
|
+
type: "array",
|
|
5197
|
+
description: "Notes to read, 1\u201320. Same options as read_note.",
|
|
5198
|
+
items: {
|
|
5199
|
+
type: "object",
|
|
5200
|
+
properties: {
|
|
5201
|
+
path: {
|
|
5202
|
+
type: "string",
|
|
5203
|
+
description: 'Vault-relative path, e.g. "Daily Notes/2026-05-29.md".'
|
|
5204
|
+
},
|
|
5205
|
+
section: {
|
|
5206
|
+
type: "string",
|
|
5207
|
+
description: "Return only this heading section (heading text, # prefix optional)."
|
|
5208
|
+
},
|
|
5209
|
+
block: {
|
|
5210
|
+
type: "string",
|
|
5211
|
+
description: "Return only the line anchored by this block id (^ prefix optional)."
|
|
5212
|
+
},
|
|
5213
|
+
lines: {
|
|
5214
|
+
type: "object",
|
|
5215
|
+
description: "Return only this line range (1-indexed, inclusive).",
|
|
5216
|
+
properties: {
|
|
5217
|
+
from: { type: "number", description: "First line (1-indexed)." },
|
|
5218
|
+
to: {
|
|
5219
|
+
type: "number",
|
|
5220
|
+
description: "Last line (1-indexed, inclusive). Defaults to EOF."
|
|
5221
|
+
}
|
|
5222
|
+
},
|
|
5223
|
+
required: ["from"]
|
|
5224
|
+
},
|
|
5225
|
+
includeFrontmatter: {
|
|
5226
|
+
type: "boolean",
|
|
5227
|
+
description: "Prepend frontmatter to section/block span results. Default false for spans."
|
|
5228
|
+
}
|
|
5229
|
+
},
|
|
5230
|
+
required: ["path"]
|
|
5231
|
+
}
|
|
5232
|
+
},
|
|
5233
|
+
budgetBytes: {
|
|
5234
|
+
type: "number",
|
|
5235
|
+
description: "Cap on total note text returned across all items, in UTF-8 bytes (256\u201365536, default 16384)."
|
|
5236
|
+
}
|
|
5237
|
+
},
|
|
5238
|
+
required: ["items"]
|
|
5239
|
+
}
|
|
5240
|
+
},
|
|
5124
5241
|
{
|
|
5125
5242
|
name: "list_notes",
|
|
5126
5243
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
@@ -5628,7 +5745,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
5628
5745
|
}
|
|
5629
5746
|
|
|
5630
5747
|
// src/index.ts
|
|
5631
|
-
var VERSION = true ? "0.
|
|
5748
|
+
var VERSION = true ? "0.21.0" : "0.0.0-dev";
|
|
5632
5749
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
5633
5750
|
if (intent === "version") {
|
|
5634
5751
|
process.stdout.write(`${VERSION}
|