@ksm0709/context 0.0.33 → 0.0.35
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 +14 -1
- package/dist/cli/index.js +105 -46
- package/dist/index.js +4 -5
- package/dist/mcp.js +491 -85
- package/dist/omc/session-start-hook.js +9 -7
- package/dist/omx/index.mjs +86 -28
- package/package.json +4 -5
package/dist/mcp.js
CHANGED
|
@@ -32638,8 +32638,462 @@ 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/mcp-server.ts
|
|
32643
33097
|
function startMcpServer() {
|
|
32644
33098
|
const server = new McpServer({
|
|
32645
33099
|
name: "context-mcp-server",
|
|
@@ -32648,62 +33102,20 @@ function startMcpServer() {
|
|
|
32648
33102
|
capabilities: {}
|
|
32649
33103
|
});
|
|
32650
33104
|
server.registerTool("search_knowledge", {
|
|
32651
|
-
description: "Search
|
|
33105
|
+
description: "Search knowledge notes in docs/ and .context/ using weighted metadata/body matching and ranked results",
|
|
32652
33106
|
inputSchema: {
|
|
32653
|
-
query: exports_external.string().describe("The
|
|
33107
|
+
query: exports_external.string().describe("The search query to match against titles, descriptions, tags, and note content"),
|
|
32654
33108
|
limit: exports_external.number().optional().describe("Maximum number of results to return (default: 50)")
|
|
32655
33109
|
}
|
|
32656
33110
|
}, async ({ query, limit = 50 }) => {
|
|
32657
|
-
const searchDirs = ["docs", ".context"];
|
|
32658
|
-
const results = [];
|
|
32659
|
-
const maxResults = limit;
|
|
32660
|
-
const snippetLength = 100;
|
|
32661
33111
|
try {
|
|
32662
|
-
const
|
|
32663
|
-
|
|
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
|
-
}
|
|
33112
|
+
const notes = await loadKnowledgeNotes(process.cwd());
|
|
33113
|
+
const results = searchKnowledgeNotes(notes, query, limit);
|
|
32699
33114
|
return {
|
|
32700
33115
|
content: [
|
|
32701
33116
|
{
|
|
32702
33117
|
type: "text",
|
|
32703
|
-
text: results
|
|
32704
|
-
Snippet: ${r.snippet}`).join(`
|
|
32705
|
-
|
|
32706
|
-
`) : "No matches found."
|
|
33118
|
+
text: buildSearchKnowledgeResponse(results)
|
|
32707
33119
|
}
|
|
32708
33120
|
]
|
|
32709
33121
|
};
|
|
@@ -32720,25 +33132,19 @@ Snippet: ${r.snippet}`).join(`
|
|
|
32720
33132
|
}
|
|
32721
33133
|
});
|
|
32722
33134
|
server.registerTool("read_knowledge", {
|
|
32723
|
-
description: "Read
|
|
33135
|
+
description: "Read a specific knowledge note and append linked-note metadata to help agents explore related notes",
|
|
32724
33136
|
inputSchema: {
|
|
32725
33137
|
path: exports_external.string().describe("The relative path to the file (e.g., docs/architecture.md)")
|
|
32726
33138
|
}
|
|
32727
33139
|
}, async ({ path: filePath }) => {
|
|
32728
33140
|
try {
|
|
32729
|
-
const normalizedPath =
|
|
32730
|
-
|
|
32731
|
-
|
|
32732
|
-
|
|
32733
|
-
|
|
32734
|
-
|
|
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;
|
|
33141
|
+
const normalizedPath = normalizeKnowledgePath(filePath);
|
|
33142
|
+
const fullPath = path2.resolve(process.cwd(), normalizedPath);
|
|
33143
|
+
const notes = await loadKnowledgeNotes(process.cwd());
|
|
33144
|
+
const note = notes.find((entry) => entry.file === normalizedPath);
|
|
33145
|
+
const content = note?.content ?? await fs2.readFile(fullPath, "utf-8");
|
|
33146
|
+
const relatedNotesSection = note ? formatRelatedNotesSection(resolveRelatedKnowledgeLinks(notes, note.file, note.links)) : "";
|
|
33147
|
+
const truncatedContent = buildReadKnowledgeResponse(content, relatedNotesSection);
|
|
32742
33148
|
return {
|
|
32743
33149
|
content: [
|
|
32744
33150
|
{
|
|
@@ -32775,15 +33181,15 @@ Snippet: ${r.snippet}`).join(`
|
|
|
32775
33181
|
const seconds = String(date6.getSeconds()).padStart(2, "0");
|
|
32776
33182
|
const dateString = `${year}-${month}-${day}`;
|
|
32777
33183
|
const timestamp = `[${year}-${month}-${day} ${hours}:${minutes}:${seconds}]`;
|
|
32778
|
-
const dirPath =
|
|
32779
|
-
const filePath =
|
|
32780
|
-
await
|
|
33184
|
+
const dirPath = path2.resolve(process.cwd(), ".context/memory/daily");
|
|
33185
|
+
const filePath = path2.join(dirPath, `${dateString}.md`);
|
|
33186
|
+
await fs2.mkdir(dirPath, { recursive: true });
|
|
32781
33187
|
let textToAppend = content;
|
|
32782
33188
|
if (!content.startsWith(`[${year}-${month}-${day}`)) {
|
|
32783
33189
|
textToAppend = `${timestamp} ${content}`;
|
|
32784
33190
|
}
|
|
32785
33191
|
try {
|
|
32786
|
-
const existingContent = await
|
|
33192
|
+
const existingContent = await fs2.readFile(filePath, "utf-8");
|
|
32787
33193
|
if (existingContent.length > 0 && !existingContent.endsWith(`
|
|
32788
33194
|
`)) {
|
|
32789
33195
|
textToAppend = `
|
|
@@ -32795,7 +33201,7 @@ Snippet: ${r.snippet}`).join(`
|
|
|
32795
33201
|
textToAppend += `
|
|
32796
33202
|
`;
|
|
32797
33203
|
}
|
|
32798
|
-
await
|
|
33204
|
+
await fs2.appendFile(filePath, textToAppend, "utf-8");
|
|
32799
33205
|
return {
|
|
32800
33206
|
content: [
|
|
32801
33207
|
{
|
|
@@ -32834,10 +33240,10 @@ Snippet: ${r.snippet}`).join(`
|
|
|
32834
33240
|
const month = String(date6.getMonth() + 1).padStart(2, "0");
|
|
32835
33241
|
const day = String(date6.getDate()).padStart(2, "0");
|
|
32836
33242
|
const dateString = `${year}-${month}-${day}`;
|
|
32837
|
-
const filePath =
|
|
33243
|
+
const filePath = path2.resolve(process.cwd(), ".context/memory/daily", `${dateString}.md`);
|
|
32838
33244
|
let fileContent;
|
|
32839
33245
|
try {
|
|
32840
|
-
fileContent = await
|
|
33246
|
+
fileContent = await fs2.readFile(filePath, "utf-8");
|
|
32841
33247
|
} catch (err) {
|
|
32842
33248
|
if (err.code === "ENOENT") {
|
|
32843
33249
|
return {
|
|
@@ -32888,15 +33294,15 @@ Snippet: ${r.snippet}`).join(`
|
|
|
32888
33294
|
}, async ({ title, content, tags, linked_notes, template }) => {
|
|
32889
33295
|
try {
|
|
32890
33296
|
const filename = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") + ".md";
|
|
32891
|
-
const dirPath =
|
|
32892
|
-
const filePath =
|
|
32893
|
-
await
|
|
33297
|
+
const dirPath = path2.resolve(process.cwd(), ".context/memory");
|
|
33298
|
+
const filePath = path2.join(dirPath, filename);
|
|
33299
|
+
await fs2.mkdir(dirPath, { recursive: true });
|
|
32894
33300
|
const date6 = new Date().toISOString().split("T")[0];
|
|
32895
33301
|
let fileContent = "";
|
|
32896
33302
|
if (template) {
|
|
32897
|
-
const templatePath =
|
|
33303
|
+
const templatePath = path2.resolve(process.cwd(), `.context/templates/${template}.md`);
|
|
32898
33304
|
try {
|
|
32899
|
-
fileContent = await
|
|
33305
|
+
fileContent = await fs2.readFile(templatePath, "utf-8");
|
|
32900
33306
|
fileContent = fileContent.replace(/\[\uC81C\uBAA9\]/g, title);
|
|
32901
33307
|
fileContent += `
|
|
32902
33308
|
|
|
@@ -32937,7 +33343,7 @@ ${tags.map((t) => ` - ${t}`).join(`
|
|
|
32937
33343
|
`) + `
|
|
32938
33344
|
`;
|
|
32939
33345
|
}
|
|
32940
|
-
await
|
|
33346
|
+
await fs2.writeFile(filePath, fileContent, "utf-8");
|
|
32941
33347
|
return {
|
|
32942
33348
|
content: [
|
|
32943
33349
|
{
|
|
@@ -32967,19 +33373,19 @@ ${tags.map((t) => ` - ${t}`).join(`
|
|
|
32967
33373
|
}
|
|
32968
33374
|
}, async ({ path: filePath, content, mode }) => {
|
|
32969
33375
|
try {
|
|
32970
|
-
const normalizedPath =
|
|
32971
|
-
if (normalizedPath.startsWith("..") ||
|
|
33376
|
+
const normalizedPath = path2.normalize(filePath);
|
|
33377
|
+
if (normalizedPath.startsWith("..") || path2.isAbsolute(normalizedPath)) {
|
|
32972
33378
|
throw new Error("Invalid path: Directory traversal is not allowed");
|
|
32973
33379
|
}
|
|
32974
33380
|
if (!normalizedPath.startsWith("docs/") && !normalizedPath.startsWith(".context/")) {
|
|
32975
33381
|
throw new Error("Invalid path: Only files in docs/ or .context/ are allowed");
|
|
32976
33382
|
}
|
|
32977
|
-
const fullPath =
|
|
32978
|
-
await
|
|
33383
|
+
const fullPath = path2.resolve(process.cwd(), normalizedPath);
|
|
33384
|
+
await fs2.mkdir(path2.dirname(fullPath), { recursive: true });
|
|
32979
33385
|
if (mode === "append") {
|
|
32980
33386
|
let textToAppend = content;
|
|
32981
33387
|
try {
|
|
32982
|
-
const existingContent = await
|
|
33388
|
+
const existingContent = await fs2.readFile(fullPath, "utf-8");
|
|
32983
33389
|
if (existingContent.length > 0 && !existingContent.endsWith(`
|
|
32984
33390
|
`)) {
|
|
32985
33391
|
textToAppend = `
|
|
@@ -32991,9 +33397,9 @@ ${tags.map((t) => ` - ${t}`).join(`
|
|
|
32991
33397
|
textToAppend += `
|
|
32992
33398
|
`;
|
|
32993
33399
|
}
|
|
32994
|
-
await
|
|
33400
|
+
await fs2.appendFile(fullPath, textToAppend, "utf-8");
|
|
32995
33401
|
} else {
|
|
32996
|
-
await
|
|
33402
|
+
await fs2.writeFile(fullPath, content, "utf-8");
|
|
32997
33403
|
}
|
|
32998
33404
|
return {
|
|
32999
33405
|
content: [
|
|
@@ -33061,12 +33467,12 @@ ${tags.map((t) => ` - ${t}`).join(`
|
|
|
33061
33467
|
};
|
|
33062
33468
|
}
|
|
33063
33469
|
try {
|
|
33064
|
-
const dirPath =
|
|
33065
|
-
const filePath =
|
|
33066
|
-
await
|
|
33470
|
+
const dirPath = path2.resolve(process.cwd(), ".context");
|
|
33471
|
+
const filePath = path2.join(dirPath, ".work-complete");
|
|
33472
|
+
await fs2.mkdir(dirPath, { recursive: true });
|
|
33067
33473
|
const content = `timestamp=${Date.now()}
|
|
33068
33474
|
`;
|
|
33069
|
-
await
|
|
33475
|
+
await fs2.writeFile(filePath, content, "utf-8");
|
|
33070
33476
|
return {
|
|
33071
33477
|
content: [
|
|
33072
33478
|
{
|