@ksm0709/context 0.0.34 → 0.0.36

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/mcp.js CHANGED
@@ -32638,8 +32638,556 @@ class StdioServerTransport {
32638
32638
  }
32639
32639
 
32640
32640
  // src/lib/mcp-server.ts
32641
+ import * as fs2 from "fs/promises";
32642
+ import * as path2 from "path";
32643
+
32644
+ // src/lib/knowledge-search.ts
32641
32645
  import * as fs from "fs/promises";
32642
32646
  import * as path from "path";
32647
+ var SEARCH_DIRECTORIES = ["docs", ".context"];
32648
+ var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
32649
+ var HEADING_REGEX = /^#\s+(.+)$/m;
32650
+ var WIKI_LINK_REGEX = /\[\[([^[\]]+?)\]\]/g;
32651
+ var SNIPPET_LENGTH = 160;
32652
+ var KNOWLEDGE_READ_MAX_LENGTH = 32 * 1024;
32653
+ var RELATED_NOTES_SECTION_MAX_LENGTH = 6 * 1024;
32654
+ var STOP_WORDS = new Set([
32655
+ "a",
32656
+ "an",
32657
+ "and",
32658
+ "are",
32659
+ "as",
32660
+ "at",
32661
+ "be",
32662
+ "but",
32663
+ "by",
32664
+ "for",
32665
+ "from",
32666
+ "how",
32667
+ "i",
32668
+ "in",
32669
+ "is",
32670
+ "it",
32671
+ "of",
32672
+ "on",
32673
+ "or",
32674
+ "that",
32675
+ "the",
32676
+ "this",
32677
+ "to",
32678
+ "what",
32679
+ "when",
32680
+ "where",
32681
+ "which",
32682
+ "with",
32683
+ "\uAC00",
32684
+ "\uB294",
32685
+ "\uB3C4",
32686
+ "\uB97C",
32687
+ "\uC5D0",
32688
+ "\uC640",
32689
+ "\uC744",
32690
+ "\uC758",
32691
+ "\uC774",
32692
+ "\uC880",
32693
+ "\uD560",
32694
+ "\uB54C"
32695
+ ]);
32696
+ function normalizeKnowledgePath(filePath) {
32697
+ const normalizedPath = path.normalize(filePath);
32698
+ if (normalizedPath.startsWith("..") || path.isAbsolute(normalizedPath)) {
32699
+ throw new Error("Invalid path: Directory traversal is not allowed");
32700
+ }
32701
+ const relativePath = toPosixPath(normalizedPath);
32702
+ if (!SEARCH_DIRECTORIES.some((dir) => relativePath.startsWith(`${dir}/`))) {
32703
+ throw new Error("Invalid path: Only files in docs/ or .context/ are allowed");
32704
+ }
32705
+ return relativePath;
32706
+ }
32707
+ async function loadKnowledgeNotes(rootDir) {
32708
+ const notes = [];
32709
+ for (const dir of SEARCH_DIRECTORIES) {
32710
+ const fullDirPath = path.resolve(rootDir, dir);
32711
+ try {
32712
+ const files = await fs.readdir(fullDirPath, { recursive: true });
32713
+ for (const file2 of files) {
32714
+ if (typeof file2 !== "string" || !file2.endsWith(".md")) {
32715
+ continue;
32716
+ }
32717
+ const fullPath = path.join(fullDirPath, file2);
32718
+ const content = await fs.readFile(fullPath, "utf-8");
32719
+ const relativePath = toPosixPath(path.relative(rootDir, fullPath));
32720
+ notes.push(parseKnowledgeNote(relativePath, content));
32721
+ }
32722
+ } catch (error45) {
32723
+ if (error45.code !== "ENOENT") {
32724
+ throw error45;
32725
+ }
32726
+ }
32727
+ }
32728
+ return notes.sort((left, right) => left.file.localeCompare(right.file));
32729
+ }
32730
+ function buildSearchKnowledgeResponse(results) {
32731
+ if (results.length === 0) {
32732
+ return "No matches found.";
32733
+ }
32734
+ return [
32735
+ ...results.map((result, index) => [
32736
+ `Result ${index + 1}`,
32737
+ `Path: ${result.file}`,
32738
+ `Title: ${result.title}`,
32739
+ `Description: ${result.description || "(none)"}`,
32740
+ `Tags: ${result.tags.length > 0 ? result.tags.join(", ") : "(none)"}`,
32741
+ `Score: ${result.score}`,
32742
+ `Match reasons: ${result.matchReasons.length > 0 ? result.matchReasons.join(", ") : "(general overlap)"}`,
32743
+ `Snippet: ${result.snippet}`
32744
+ ].join(`
32745
+ `)),
32746
+ "Open a relevant note with read_knowledge to inspect the full content and linked-note metadata."
32747
+ ].join(`
32748
+
32749
+ `);
32750
+ }
32751
+ function formatRelatedNotesSection(relatedNotes) {
32752
+ if (relatedNotes.resolved.length === 0 && relatedNotes.unresolved.length === 0) {
32753
+ return "";
32754
+ }
32755
+ const lines = ["## Related Notes", ""];
32756
+ if (relatedNotes.resolved.length > 0) {
32757
+ for (const note of relatedNotes.resolved) {
32758
+ lines.push(`- Title: ${note.title}`);
32759
+ lines.push(` Path: ${note.file}`);
32760
+ lines.push(` Description: ${note.description || "(none)"}`);
32761
+ lines.push(` Tags: ${note.tags.length > 0 ? note.tags.join(", ") : "(none)"}`);
32762
+ }
32763
+ lines.push("");
32764
+ }
32765
+ if (relatedNotes.unresolved.length > 0) {
32766
+ lines.push(`Unresolved links: ${relatedNotes.unresolved.join(", ")}`);
32767
+ lines.push("");
32768
+ }
32769
+ lines.push("If one of these related notes looks relevant, open it with `read_knowledge` to continue exploring.");
32770
+ return lines.join(`
32771
+ `).trim();
32772
+ }
32773
+ function buildReadKnowledgeResponse(content, relatedNotesSection) {
32774
+ if (!relatedNotesSection) {
32775
+ return content.length > KNOWLEDGE_READ_MAX_LENGTH ? `${content.substring(0, KNOWLEDGE_READ_MAX_LENGTH)}
32776
+
32777
+ ... (content truncated due to size limit)` : content;
32778
+ }
32779
+ const truncatedRelatedSection = relatedNotesSection.length > RELATED_NOTES_SECTION_MAX_LENGTH ? `${relatedNotesSection.slice(0, RELATED_NOTES_SECTION_MAX_LENGTH)}
32780
+ ... (related notes truncated)` : relatedNotesSection;
32781
+ const reservedLength = truncatedRelatedSection.length + 2;
32782
+ const mainBudget = Math.max(0, KNOWLEDGE_READ_MAX_LENGTH - reservedLength);
32783
+ const truncatedMainContent = content.length > mainBudget ? `${content.substring(0, mainBudget)}
32784
+
32785
+ ... (content truncated due to size limit)` : content;
32786
+ return `${truncatedMainContent}
32787
+
32788
+ ${truncatedRelatedSection}`;
32789
+ }
32790
+ function parseKnowledgeNote(file2, content) {
32791
+ const { body, metadata } = parseFrontmatter(content);
32792
+ const title = resolveTitle(file2, body, metadata);
32793
+ const description = resolveDescription(body, metadata);
32794
+ const tags = resolveTags(metadata);
32795
+ return {
32796
+ body,
32797
+ content,
32798
+ description,
32799
+ file: file2,
32800
+ links: extractWikiLinks(content),
32801
+ pathStem: normalizeSearchText(stripExtension(file2)),
32802
+ tags,
32803
+ title
32804
+ };
32805
+ }
32806
+ function searchKnowledgeNotes(notes, query, limit) {
32807
+ const normalizedQuery = normalizeSearchText(query);
32808
+ const queryTokens = tokenizeQuery(query);
32809
+ if (normalizedQuery.length === 0 && queryTokens.length === 0) {
32810
+ return [];
32811
+ }
32812
+ return notes.map((note) => scoreKnowledgeNote(note, normalizedQuery, queryTokens)).filter((match) => match.score > 0).sort((left, right) => {
32813
+ if (right.score !== left.score) {
32814
+ return right.score - left.score;
32815
+ }
32816
+ return left.file.localeCompare(right.file);
32817
+ }).slice(0, limit);
32818
+ }
32819
+ function resolveRelatedKnowledgeLinks(notes, currentFile, links) {
32820
+ const lookup = new Map;
32821
+ for (const note of notes) {
32822
+ const keys = [normalizeSearchText(note.title), note.pathStem];
32823
+ for (const key of keys) {
32824
+ if (!key || lookup.has(key)) {
32825
+ continue;
32826
+ }
32827
+ lookup.set(key, note);
32828
+ }
32829
+ }
32830
+ const resolved = [];
32831
+ const unresolved = [];
32832
+ const seenFiles = new Set;
32833
+ for (const rawLink of links) {
32834
+ const normalizedLink = normalizeSearchText(rawLink);
32835
+ const note = lookup.get(normalizedLink);
32836
+ if (!note || note.file === currentFile || seenFiles.has(note.file)) {
32837
+ if (!note) {
32838
+ unresolved.push(rawLink);
32839
+ }
32840
+ continue;
32841
+ }
32842
+ resolved.push(note);
32843
+ seenFiles.add(note.file);
32844
+ }
32845
+ return { resolved, unresolved };
32846
+ }
32847
+ function scoreKnowledgeNote(note, normalizedQuery, queryTokens) {
32848
+ const matchReasons = [];
32849
+ let score = 0;
32850
+ score += scoreTextField({
32851
+ exactPhraseWeight: 80,
32852
+ label: "title",
32853
+ matchReasons,
32854
+ queryTokens,
32855
+ text: note.title,
32856
+ tokenWeight: 24,
32857
+ normalizedQuery
32858
+ });
32859
+ score += scoreTags(note.tags, queryTokens, matchReasons);
32860
+ score += scoreTextField({
32861
+ exactPhraseWeight: 40,
32862
+ label: "description",
32863
+ matchReasons,
32864
+ queryTokens,
32865
+ text: note.description,
32866
+ tokenWeight: 14,
32867
+ normalizedQuery
32868
+ });
32869
+ score += scoreTextField({
32870
+ exactPhraseWeight: 20,
32871
+ label: "body",
32872
+ matchReasons,
32873
+ queryTokens,
32874
+ text: note.body,
32875
+ tokenWeight: 6,
32876
+ normalizedQuery
32877
+ });
32878
+ score += scoreTextField({
32879
+ exactPhraseWeight: 12,
32880
+ label: "path",
32881
+ matchReasons,
32882
+ queryTokens,
32883
+ text: note.file,
32884
+ tokenWeight: 4,
32885
+ normalizedQuery
32886
+ });
32887
+ return {
32888
+ description: note.description,
32889
+ file: note.file,
32890
+ matchReasons,
32891
+ score,
32892
+ snippet: buildSnippet(note, queryTokens),
32893
+ tags: note.tags,
32894
+ title: note.title
32895
+ };
32896
+ }
32897
+ function scoreTextField(params) {
32898
+ const normalizedText = normalizeSearchText(params.text);
32899
+ if (!normalizedText) {
32900
+ return 0;
32901
+ }
32902
+ const tokenSet = new Set(tokenizeQuery(params.text));
32903
+ const matchedTokenCount = params.queryTokens.filter((token) => tokenSet.has(token)).length;
32904
+ let score = matchedTokenCount * params.tokenWeight;
32905
+ if (matchedTokenCount > 0) {
32906
+ params.matchReasons.push(`${params.label}:${matchedTokenCount}`);
32907
+ }
32908
+ if (params.normalizedQuery && normalizedText.includes(params.normalizedQuery)) {
32909
+ score += params.exactPhraseWeight;
32910
+ params.matchReasons.push(`${params.label}:phrase`);
32911
+ }
32912
+ return score;
32913
+ }
32914
+ function scoreTags(tags, queryTokens, matchReasons) {
32915
+ const normalizedTags = tags.map((tag) => normalizeSearchText(tag)).filter(Boolean);
32916
+ let score = 0;
32917
+ let matched = 0;
32918
+ for (const token of queryTokens) {
32919
+ const hasMatch = normalizedTags.some((tag) => tag === token || tag.includes(token));
32920
+ if (!hasMatch) {
32921
+ continue;
32922
+ }
32923
+ matched += 1;
32924
+ score += 20;
32925
+ }
32926
+ if (matched > 0) {
32927
+ matchReasons.push(`tags:${matched}`);
32928
+ }
32929
+ return score;
32930
+ }
32931
+ function buildSnippet(note, queryTokens) {
32932
+ const preferredSources = [note.description, note.body, note.title];
32933
+ for (const source of preferredSources) {
32934
+ const snippet = buildSnippetFromText(source, queryTokens);
32935
+ if (snippet) {
32936
+ return snippet;
32937
+ }
32938
+ }
32939
+ return truncateInlineText(note.title || note.file, SNIPPET_LENGTH);
32940
+ }
32941
+ function buildSnippetFromText(text, queryTokens) {
32942
+ const inlineText = text.replace(/\s+/g, " ").trim();
32943
+ if (!inlineText) {
32944
+ return "";
32945
+ }
32946
+ const lowerText = inlineText.toLowerCase();
32947
+ let firstIndex = Number.POSITIVE_INFINITY;
32948
+ for (const token of queryTokens) {
32949
+ const matchIndex = lowerText.indexOf(token.toLowerCase());
32950
+ if (matchIndex !== -1 && matchIndex < firstIndex) {
32951
+ firstIndex = matchIndex;
32952
+ }
32953
+ }
32954
+ if (!Number.isFinite(firstIndex)) {
32955
+ return truncateInlineText(inlineText, SNIPPET_LENGTH);
32956
+ }
32957
+ const start = Math.max(0, firstIndex - Math.floor(SNIPPET_LENGTH / 3));
32958
+ const end = Math.min(inlineText.length, start + SNIPPET_LENGTH);
32959
+ let snippet = inlineText.slice(start, end);
32960
+ if (start > 0) {
32961
+ snippet = `...${snippet}`;
32962
+ }
32963
+ if (end < inlineText.length) {
32964
+ snippet = `${snippet}...`;
32965
+ }
32966
+ return snippet;
32967
+ }
32968
+ function truncateInlineText(text, maxLength) {
32969
+ if (text.length <= maxLength) {
32970
+ return text;
32971
+ }
32972
+ return `${text.slice(0, maxLength - 3)}...`;
32973
+ }
32974
+ function parseFrontmatter(content) {
32975
+ const match = FRONTMATTER_REGEX.exec(content);
32976
+ if (!match) {
32977
+ return {
32978
+ body: content,
32979
+ metadata: {}
32980
+ };
32981
+ }
32982
+ return {
32983
+ body: content.slice(match[0].length),
32984
+ metadata: parseFrontmatterBlock(match[1])
32985
+ };
32986
+ }
32987
+ function parseFrontmatterBlock(block) {
32988
+ const metadata = {};
32989
+ const lines = block.split(/\r?\n/);
32990
+ let currentArrayKey = null;
32991
+ for (const rawLine of lines) {
32992
+ const line = rawLine.trimEnd();
32993
+ const trimmed = line.trim();
32994
+ if (!trimmed || trimmed.startsWith("#")) {
32995
+ continue;
32996
+ }
32997
+ if (currentArrayKey && /^\s*-\s+/.test(line)) {
32998
+ const arrayValue = normalizeScalarValue(trimmed.replace(/^-+\s*/, ""));
32999
+ const existing = metadata[currentArrayKey];
33000
+ if (Array.isArray(existing)) {
33001
+ existing.push(arrayValue);
33002
+ } else {
33003
+ metadata[currentArrayKey] = [arrayValue];
33004
+ }
33005
+ continue;
33006
+ }
33007
+ currentArrayKey = null;
33008
+ const separatorIndex = line.indexOf(":");
33009
+ if (separatorIndex === -1) {
33010
+ continue;
33011
+ }
33012
+ const key = line.slice(0, separatorIndex).trim().toLowerCase();
33013
+ const value = line.slice(separatorIndex + 1).trim();
33014
+ if (!value) {
33015
+ currentArrayKey = key;
33016
+ metadata[key] = [];
33017
+ continue;
33018
+ }
33019
+ if (value.startsWith("[") && value.endsWith("]")) {
33020
+ metadata[key] = value.slice(1, -1).split(",").map((item) => normalizeScalarValue(item)).filter(Boolean);
33021
+ continue;
33022
+ }
33023
+ metadata[key] = normalizeScalarValue(value);
33024
+ }
33025
+ return metadata;
33026
+ }
33027
+ function resolveTitle(file2, body, metadata) {
33028
+ const frontmatterTitle = getMetadataString(metadata, "title");
33029
+ if (frontmatterTitle) {
33030
+ return frontmatterTitle;
33031
+ }
33032
+ const headingMatch = HEADING_REGEX.exec(body);
33033
+ if (headingMatch) {
33034
+ return headingMatch[1].trim();
33035
+ }
33036
+ return humanizeSlug(path.basename(file2, ".md"));
33037
+ }
33038
+ function resolveDescription(body, metadata) {
33039
+ const frontmatterDescription = getMetadataString(metadata, "description") ?? getMetadataString(metadata, "summary");
33040
+ if (frontmatterDescription) {
33041
+ return frontmatterDescription;
33042
+ }
33043
+ const lines = body.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
33044
+ const paragraph = lines.find((line) => !line.startsWith("- ") && !line.startsWith("* "));
33045
+ return paragraph ? truncateInlineText(paragraph, 200) : "";
33046
+ }
33047
+ function resolveTags(metadata) {
33048
+ const tags = metadata.tags;
33049
+ if (Array.isArray(tags)) {
33050
+ return tags.map((tag) => tag.trim()).filter(Boolean);
33051
+ }
33052
+ if (typeof tags === "string") {
33053
+ return tags.split(",").map((tag) => tag.trim()).filter(Boolean);
33054
+ }
33055
+ return [];
33056
+ }
33057
+ function getMetadataString(metadata, key) {
33058
+ const value = metadata[key];
33059
+ if (typeof value === "string" && value.trim()) {
33060
+ return value.trim();
33061
+ }
33062
+ return null;
33063
+ }
33064
+ function normalizeScalarValue(value) {
33065
+ return value.replace(/^['"]|['"]$/g, "").trim();
33066
+ }
33067
+ function extractWikiLinks(content) {
33068
+ const links = new Set;
33069
+ for (const match of content.matchAll(WIKI_LINK_REGEX)) {
33070
+ const rawTarget = match[1].split("|")[0].split("#")[0].trim();
33071
+ if (rawTarget) {
33072
+ links.add(rawTarget);
33073
+ }
33074
+ }
33075
+ return [...links];
33076
+ }
33077
+ function tokenizeQuery(text) {
33078
+ const rawTokens = text.toLowerCase().split(/[^\p{L}\p{N}]+/u).map((token) => token.trim()).filter(Boolean);
33079
+ const filteredTokens = rawTokens.filter((token) => !STOP_WORDS.has(token));
33080
+ return [...new Set(filteredTokens.length > 0 ? filteredTokens : rawTokens)];
33081
+ }
33082
+ function normalizeSearchText(text) {
33083
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
33084
+ }
33085
+ function toPosixPath(filePath) {
33086
+ return filePath.replace(/\\/g, "/");
33087
+ }
33088
+ function humanizeSlug(slug) {
33089
+ const words = slug.split(/[-_]+/).filter(Boolean).map((word) => word.length > 0 ? word[0].toUpperCase() + word.slice(1) : word);
33090
+ return words.join(" ") || slug;
33091
+ }
33092
+ function stripExtension(file2) {
33093
+ return file2.endsWith(".md") ? file2.slice(0, -3) : file2;
33094
+ }
33095
+
33096
+ // src/lib/knowledge-note-template-validation.ts
33097
+ var HEADING_REGEX2 = /^(#{1,3})\s+(.+)$/;
33098
+ var ADDITIONAL_FORBIDDEN_SNIPPETS = ["[\uC81C\uBAA9]", "[\uAC04\uB2E8\uD55C \uC124\uBA85]", "TODO"];
33099
+ var RELATED_NOTES_TITLES = ["\uAD00\uB828 \uB178\uD2B8", "Related Notes"];
33100
+ function escapeRegExp(value) {
33101
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33102
+ }
33103
+ function parseMarkdownHeadings(content) {
33104
+ const lines = content.split(`
33105
+ `);
33106
+ return lines.flatMap((line, index) => {
33107
+ const match = line.match(HEADING_REGEX2);
33108
+ if (!match) {
33109
+ return [];
33110
+ }
33111
+ return [
33112
+ {
33113
+ level: match[1].length,
33114
+ line: index,
33115
+ raw: line.trim(),
33116
+ text: match[2].trim()
33117
+ }
33118
+ ];
33119
+ });
33120
+ }
33121
+ function buildHeadingPattern(templateText) {
33122
+ const escaped = escapeRegExp(templateText).replace(/\\\[[^\]]+\\\]/g, "(.+)");
33123
+ return new RegExp(`^${escaped}$`);
33124
+ }
33125
+ function headingMatches(template, candidate) {
33126
+ if (template.level !== candidate.level) {
33127
+ return false;
33128
+ }
33129
+ return buildHeadingPattern(template.text).test(candidate.text);
33130
+ }
33131
+ function collectForbiddenSnippets(templateContent) {
33132
+ const snippets = new Set(ADDITIONAL_FORBIDDEN_SNIPPETS);
33133
+ for (const line of templateContent.split(`
33134
+ `)) {
33135
+ const trimmed = line.trim();
33136
+ if (!trimmed) {
33137
+ continue;
33138
+ }
33139
+ const headingMatch = trimmed.match(HEADING_REGEX2);
33140
+ if (headingMatch) {
33141
+ if (trimmed.includes("[") || trimmed.includes("...") || trimmed.includes("TODO")) {
33142
+ snippets.add(trimmed);
33143
+ }
33144
+ continue;
33145
+ }
33146
+ snippets.add(trimmed);
33147
+ }
33148
+ return [...snippets];
33149
+ }
33150
+ function isRelatedNotesHeading(heading) {
33151
+ return RELATED_NOTES_TITLES.includes(heading.text);
33152
+ }
33153
+ function validateTemplatedKnowledgeNoteContent(templateContent, content) {
33154
+ const templateHeadings = parseMarkdownHeadings(templateContent);
33155
+ const contentHeadings = parseMarkdownHeadings(content);
33156
+ const contentLines = content.split(`
33157
+ `);
33158
+ const errors3 = [];
33159
+ const matchedHeadings = [];
33160
+ let searchStart = 0;
33161
+ for (const templateHeading of templateHeadings) {
33162
+ const matchIndex = contentHeadings.findIndex((candidate, index) => index >= searchStart && headingMatches(templateHeading, candidate));
33163
+ if (matchIndex === -1) {
33164
+ errors3.push(`Missing required heading: ${templateHeading.raw}`);
33165
+ continue;
33166
+ }
33167
+ matchedHeadings.push(contentHeadings[matchIndex]);
33168
+ searchStart = matchIndex + 1;
33169
+ }
33170
+ for (const forbiddenSnippet of collectForbiddenSnippets(templateContent)) {
33171
+ if (content.includes(forbiddenSnippet)) {
33172
+ errors3.push(`Template placeholder was not replaced: ${forbiddenSnippet}`);
33173
+ }
33174
+ }
33175
+ matchedHeadings.forEach((heading, index) => {
33176
+ const nextHeadingLine = matchedHeadings[index + 1]?.line ?? contentLines.length;
33177
+ const sectionBody = contentLines.slice(heading.line + 1, nextHeadingLine).join(`
33178
+ `).trim();
33179
+ if (heading.level > 1 && !sectionBody) {
33180
+ errors3.push(`Section is empty: ${heading.raw}`);
33181
+ return;
33182
+ }
33183
+ if (isRelatedNotesHeading(heading) && !/\[\[[^[\]]+\]\]/.test(sectionBody)) {
33184
+ errors3.push(`Related notes section must include at least one wikilink: ${heading.raw}`);
33185
+ }
33186
+ });
33187
+ return { errors: errors3 };
33188
+ }
33189
+
33190
+ // src/lib/mcp-server.ts
32643
33191
  function startMcpServer() {
32644
33192
  const server = new McpServer({
32645
33193
  name: "context-mcp-server",
@@ -32648,62 +33196,20 @@ function startMcpServer() {
32648
33196
  capabilities: {}
32649
33197
  });
32650
33198
  server.registerTool("search_knowledge", {
32651
- description: "Search .md files in docs/ and .context/ directories for a keyword or regex",
33199
+ description: "Search knowledge notes in docs/ and .context/ using weighted metadata/body matching and ranked results",
32652
33200
  inputSchema: {
32653
- query: exports_external.string().describe("The keyword or regex to search for"),
33201
+ query: exports_external.string().describe("The search query to match against titles, descriptions, tags, and note content"),
32654
33202
  limit: exports_external.number().optional().describe("Maximum number of results to return (default: 50)")
32655
33203
  }
32656
33204
  }, async ({ query, limit = 50 }) => {
32657
- const searchDirs = ["docs", ".context"];
32658
- const results = [];
32659
- const maxResults = limit;
32660
- const snippetLength = 100;
32661
33205
  try {
32662
- const regex = new RegExp(query, "i");
32663
- for (const dir of searchDirs) {
32664
- const fullDirPath = path.resolve(process.cwd(), dir);
32665
- try {
32666
- const files = await fs.readdir(fullDirPath, { recursive: true });
32667
- for (const file2 of files) {
32668
- if (typeof file2 === "string" && file2.endsWith(".md")) {
32669
- const filePath = path.join(fullDirPath, file2);
32670
- const content = await fs.readFile(filePath, "utf-8");
32671
- const match = regex.exec(content);
32672
- if (match) {
32673
- const start = Math.max(0, match.index - snippetLength / 2);
32674
- const end = Math.min(content.length, match.index + match[0].length + snippetLength / 2);
32675
- let snippet = content.substring(start, end).replace(/\n/g, " ");
32676
- if (start > 0)
32677
- snippet = "..." + snippet;
32678
- if (end < content.length)
32679
- snippet = snippet + "...";
32680
- results.push({
32681
- file: path.relative(process.cwd(), filePath),
32682
- snippet
32683
- });
32684
- if (results.length >= maxResults) {
32685
- break;
32686
- }
32687
- }
32688
- }
32689
- }
32690
- } catch (err) {
32691
- if (err.code !== "ENOENT") {
32692
- console.error(`Error reading directory ${dir}:`, err);
32693
- }
32694
- }
32695
- if (results.length >= maxResults) {
32696
- break;
32697
- }
32698
- }
33206
+ const notes = await loadKnowledgeNotes(process.cwd());
33207
+ const results = searchKnowledgeNotes(notes, query, limit);
32699
33208
  return {
32700
33209
  content: [
32701
33210
  {
32702
33211
  type: "text",
32703
- text: results.length > 0 ? results.map((r) => `File: ${r.file}
32704
- Snippet: ${r.snippet}`).join(`
32705
-
32706
- `) : "No matches found."
33212
+ text: buildSearchKnowledgeResponse(results)
32707
33213
  }
32708
33214
  ]
32709
33215
  };
@@ -32720,25 +33226,19 @@ Snippet: ${r.snippet}`).join(`
32720
33226
  }
32721
33227
  });
32722
33228
  server.registerTool("read_knowledge", {
32723
- description: "Read the content of a specific .md file in docs/ or .context/ directories",
33229
+ description: "Read a specific knowledge note and append linked-note metadata to help agents explore related notes",
32724
33230
  inputSchema: {
32725
33231
  path: exports_external.string().describe("The relative path to the file (e.g., docs/architecture.md)")
32726
33232
  }
32727
33233
  }, async ({ path: filePath }) => {
32728
33234
  try {
32729
- const normalizedPath = path.normalize(filePath);
32730
- if (normalizedPath.startsWith("..") || path.isAbsolute(normalizedPath)) {
32731
- throw new Error("Invalid path: Directory traversal is not allowed");
32732
- }
32733
- if (!normalizedPath.startsWith("docs/") && !normalizedPath.startsWith(".context/")) {
32734
- throw new Error("Invalid path: Only files in docs/ or .context/ are allowed");
32735
- }
32736
- const fullPath = path.resolve(process.cwd(), normalizedPath);
32737
- const content = await fs.readFile(fullPath, "utf-8");
32738
- const MAX_LENGTH = 32 * 1024;
32739
- const truncatedContent = content.length > MAX_LENGTH ? content.substring(0, MAX_LENGTH) + `
32740
-
32741
- ... (content truncated due to size limit)` : content;
33235
+ const normalizedPath = normalizeKnowledgePath(filePath);
33236
+ const fullPath = path2.resolve(process.cwd(), normalizedPath);
33237
+ const notes = await loadKnowledgeNotes(process.cwd());
33238
+ const note = notes.find((entry) => entry.file === normalizedPath);
33239
+ const content = note?.content ?? await fs2.readFile(fullPath, "utf-8");
33240
+ const relatedNotesSection = note ? formatRelatedNotesSection(resolveRelatedKnowledgeLinks(notes, note.file, note.links)) : "";
33241
+ const truncatedContent = buildReadKnowledgeResponse(content, relatedNotesSection);
32742
33242
  return {
32743
33243
  content: [
32744
33244
  {
@@ -32775,15 +33275,15 @@ Snippet: ${r.snippet}`).join(`
32775
33275
  const seconds = String(date6.getSeconds()).padStart(2, "0");
32776
33276
  const dateString = `${year}-${month}-${day}`;
32777
33277
  const timestamp = `[${year}-${month}-${day} ${hours}:${minutes}:${seconds}]`;
32778
- const dirPath = path.resolve(process.cwd(), ".context/memory/daily");
32779
- const filePath = path.join(dirPath, `${dateString}.md`);
32780
- await fs.mkdir(dirPath, { recursive: true });
33278
+ const dirPath = path2.resolve(process.cwd(), ".context/memory/daily");
33279
+ const filePath = path2.join(dirPath, `${dateString}.md`);
33280
+ await fs2.mkdir(dirPath, { recursive: true });
32781
33281
  let textToAppend = content;
32782
33282
  if (!content.startsWith(`[${year}-${month}-${day}`)) {
32783
33283
  textToAppend = `${timestamp} ${content}`;
32784
33284
  }
32785
33285
  try {
32786
- const existingContent = await fs.readFile(filePath, "utf-8");
33286
+ const existingContent = await fs2.readFile(filePath, "utf-8");
32787
33287
  if (existingContent.length > 0 && !existingContent.endsWith(`
32788
33288
  `)) {
32789
33289
  textToAppend = `
@@ -32795,7 +33295,7 @@ Snippet: ${r.snippet}`).join(`
32795
33295
  textToAppend += `
32796
33296
  `;
32797
33297
  }
32798
- await fs.appendFile(filePath, textToAppend, "utf-8");
33298
+ await fs2.appendFile(filePath, textToAppend, "utf-8");
32799
33299
  return {
32800
33300
  content: [
32801
33301
  {
@@ -32834,10 +33334,10 @@ Snippet: ${r.snippet}`).join(`
32834
33334
  const month = String(date6.getMonth() + 1).padStart(2, "0");
32835
33335
  const day = String(date6.getDate()).padStart(2, "0");
32836
33336
  const dateString = `${year}-${month}-${day}`;
32837
- const filePath = path.resolve(process.cwd(), ".context/memory/daily", `${dateString}.md`);
33337
+ const filePath = path2.resolve(process.cwd(), ".context/memory/daily", `${dateString}.md`);
32838
33338
  let fileContent;
32839
33339
  try {
32840
- fileContent = await fs.readFile(filePath, "utf-8");
33340
+ fileContent = await fs2.readFile(filePath, "utf-8");
32841
33341
  } catch (err) {
32842
33342
  if (err.code === "ENOENT") {
32843
33343
  return {
@@ -32877,35 +33377,65 @@ Snippet: ${r.snippet}`).join(`
32877
33377
  }
32878
33378
  });
32879
33379
  server.registerTool("create_knowledge_note", {
32880
- description: "Create a new Zettelkasten knowledge note with frontmatter and wikilinks. You can optionally use a template by providing the `template` parameter. Available templates: adr (Architecture Decision Records), pattern (Design patterns), bug (Bug reports and analysis), gotcha (Pitfalls and gotchas), decision (General decisions), context (General context and background), runbook (Procedures and runbooks), insight (Insights and learnings).",
33380
+ description: "Create a new Zettelkasten knowledge note with frontmatter and wikilinks. When you provide `template`, first read `.context/templates/<template>.md` and pass fully completed markdown in `content`. Available templates: adr (Architecture Decision Records), pattern (Design patterns), bug (Bug reports and analysis), gotcha (Pitfalls and gotchas), decision (General decisions), context (General context and background), runbook (Procedures and runbooks), insight (Insights and learnings).",
32881
33381
  inputSchema: {
32882
33382
  title: exports_external.string().describe("The title of the note"),
32883
- content: exports_external.string().describe("The main content of the note"),
32884
- tags: exports_external.array(exports_external.string()).optional().describe("Optional tags for the note"),
32885
- linked_notes: exports_external.array(exports_external.string()).optional().describe("Optional list of related note titles to link to"),
32886
- template: exports_external.enum(["adr", "pattern", "bug", "gotcha", "decision", "context", "runbook", "insight"]).optional().describe("Optional template to use for the note")
33383
+ content: exports_external.string().describe("The main content of the note. When `template` is set, this must be the complete markdown document that already follows the template."),
33384
+ tags: exports_external.array(exports_external.string()).optional().describe("Optional tags for the note. Not supported when `template` is set."),
33385
+ linked_notes: exports_external.array(exports_external.string()).optional().describe("Optional list of related note titles to link to. Not supported when `template` is set; include related notes directly in the markdown content instead."),
33386
+ template: exports_external.enum(["adr", "pattern", "bug", "gotcha", "decision", "context", "runbook", "insight"]).optional().describe("Optional template to validate against. Read the template file first and pass fully completed markdown in `content`.")
32887
33387
  }
32888
33388
  }, async ({ title, content, tags, linked_notes, template }) => {
32889
33389
  try {
32890
33390
  const filename = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") + ".md";
32891
- const dirPath = path.resolve(process.cwd(), ".context/memory");
32892
- const filePath = path.join(dirPath, filename);
32893
- await fs.mkdir(dirPath, { recursive: true });
33391
+ const dirPath = path2.resolve(process.cwd(), ".context/memory");
33392
+ const filePath = path2.join(dirPath, filename);
33393
+ await fs2.mkdir(dirPath, { recursive: true });
32894
33394
  const date6 = new Date().toISOString().split("T")[0];
32895
33395
  let fileContent = "";
32896
33396
  if (template) {
32897
- const templatePath = path.resolve(process.cwd(), `.context/templates/${template}.md`);
32898
- try {
32899
- fileContent = await fs.readFile(templatePath, "utf-8");
32900
- fileContent = fileContent.replace(/\[\uC81C\uBAA9\]/g, title);
32901
- fileContent += `
32902
-
32903
- ${content}`;
32904
- } catch (err) {
32905
- fileContent = `Error loading template: ${err instanceof Error ? err.message : String(err)}
32906
-
32907
- ${content}`;
33397
+ if (tags && tags.length > 0) {
33398
+ return {
33399
+ content: [
33400
+ {
33401
+ type: "text",
33402
+ text: "Error creating knowledge note: `tags` is not supported in template mode. Read the template and include any frontmatter or metadata directly in the markdown content."
33403
+ }
33404
+ ],
33405
+ isError: true
33406
+ };
32908
33407
  }
33408
+ if (linked_notes && linked_notes.length > 0) {
33409
+ return {
33410
+ content: [
33411
+ {
33412
+ type: "text",
33413
+ text: "Error creating knowledge note: `linked_notes` is not supported in template mode. Read the template and include related notes directly in the markdown content."
33414
+ }
33415
+ ],
33416
+ isError: true
33417
+ };
33418
+ }
33419
+ const templatePath = path2.resolve(process.cwd(), `.context/templates/${template}.md`);
33420
+ const templateContent = await fs2.readFile(templatePath, "utf-8");
33421
+ const validation = validateTemplatedKnowledgeNoteContent(templateContent, content);
33422
+ if (validation.errors.length > 0) {
33423
+ return {
33424
+ content: [
33425
+ {
33426
+ type: "text",
33427
+ text: `Error creating knowledge note: template content is invalid.
33428
+ - ${validation.errors.join(`
33429
+ - `)}
33430
+ Read the template and provide the fully completed markdown document in \`content\`.`
33431
+ }
33432
+ ],
33433
+ isError: true
33434
+ };
33435
+ }
33436
+ fileContent = content.endsWith(`
33437
+ `) ? content : `${content}
33438
+ `;
32909
33439
  } else {
32910
33440
  fileContent = `---
32911
33441
  `;
@@ -32937,7 +33467,7 @@ ${tags.map((t) => ` - ${t}`).join(`
32937
33467
  `) + `
32938
33468
  `;
32939
33469
  }
32940
- await fs.writeFile(filePath, fileContent, "utf-8");
33470
+ await fs2.writeFile(filePath, fileContent, "utf-8");
32941
33471
  return {
32942
33472
  content: [
32943
33473
  {
@@ -32967,19 +33497,19 @@ ${tags.map((t) => ` - ${t}`).join(`
32967
33497
  }
32968
33498
  }, async ({ path: filePath, content, mode }) => {
32969
33499
  try {
32970
- const normalizedPath = path.normalize(filePath);
32971
- if (normalizedPath.startsWith("..") || path.isAbsolute(normalizedPath)) {
33500
+ const normalizedPath = path2.normalize(filePath);
33501
+ if (normalizedPath.startsWith("..") || path2.isAbsolute(normalizedPath)) {
32972
33502
  throw new Error("Invalid path: Directory traversal is not allowed");
32973
33503
  }
32974
33504
  if (!normalizedPath.startsWith("docs/") && !normalizedPath.startsWith(".context/")) {
32975
33505
  throw new Error("Invalid path: Only files in docs/ or .context/ are allowed");
32976
33506
  }
32977
- const fullPath = path.resolve(process.cwd(), normalizedPath);
32978
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
33507
+ const fullPath = path2.resolve(process.cwd(), normalizedPath);
33508
+ await fs2.mkdir(path2.dirname(fullPath), { recursive: true });
32979
33509
  if (mode === "append") {
32980
33510
  let textToAppend = content;
32981
33511
  try {
32982
- const existingContent = await fs.readFile(fullPath, "utf-8");
33512
+ const existingContent = await fs2.readFile(fullPath, "utf-8");
32983
33513
  if (existingContent.length > 0 && !existingContent.endsWith(`
32984
33514
  `)) {
32985
33515
  textToAppend = `
@@ -32991,9 +33521,9 @@ ${tags.map((t) => ` - ${t}`).join(`
32991
33521
  textToAppend += `
32992
33522
  `;
32993
33523
  }
32994
- await fs.appendFile(fullPath, textToAppend, "utf-8");
33524
+ await fs2.appendFile(fullPath, textToAppend, "utf-8");
32995
33525
  } else {
32996
- await fs.writeFile(fullPath, content, "utf-8");
33526
+ await fs2.writeFile(fullPath, content, "utf-8");
32997
33527
  }
32998
33528
  return {
32999
33529
  content: [
@@ -33061,12 +33591,12 @@ ${tags.map((t) => ` - ${t}`).join(`
33061
33591
  };
33062
33592
  }
33063
33593
  try {
33064
- const dirPath = path.resolve(process.cwd(), ".context");
33065
- const filePath = path.join(dirPath, ".work-complete");
33066
- await fs.mkdir(dirPath, { recursive: true });
33594
+ const dirPath = path2.resolve(process.cwd(), ".context");
33595
+ const filePath = path2.join(dirPath, ".work-complete");
33596
+ await fs2.mkdir(dirPath, { recursive: true });
33067
33597
  const content = `timestamp=${Date.now()}
33068
33598
  `;
33069
- await fs.writeFile(filePath, content, "utf-8");
33599
+ await fs2.writeFile(filePath, content, "utf-8");
33070
33600
  return {
33071
33601
  content: [
33072
33602
  {