@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/README.md +14 -1
- package/dist/cli/index.js +107 -46
- package/dist/index.js +6 -5
- package/dist/mcp.js +629 -99
- package/dist/omc/session-start-hook.js +11 -7
- package/dist/omx/index.mjs +88 -28
- package/package.json +4 -5
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
|
|
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
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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 =
|
|
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;
|
|
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 =
|
|
32779
|
-
const filePath =
|
|
32780
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
33337
|
+
const filePath = path2.resolve(process.cwd(), ".context/memory/daily", `${dateString}.md`);
|
|
32838
33338
|
let fileContent;
|
|
32839
33339
|
try {
|
|
32840
|
-
fileContent = await
|
|
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.
|
|
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
|
|
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 =
|
|
32892
|
-
const filePath =
|
|
32893
|
-
await
|
|
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
|
-
|
|
32898
|
-
|
|
32899
|
-
|
|
32900
|
-
|
|
32901
|
-
|
|
32902
|
-
|
|
32903
|
-
|
|
32904
|
-
|
|
32905
|
-
|
|
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
|
|
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 =
|
|
32971
|
-
if (normalizedPath.startsWith("..") ||
|
|
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 =
|
|
32978
|
-
await
|
|
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
|
|
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
|
|
33524
|
+
await fs2.appendFile(fullPath, textToAppend, "utf-8");
|
|
32995
33525
|
} else {
|
|
32996
|
-
await
|
|
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 =
|
|
33065
|
-
const filePath =
|
|
33066
|
-
await
|
|
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
|
|
33599
|
+
await fs2.writeFile(filePath, content, "utf-8");
|
|
33070
33600
|
return {
|
|
33071
33601
|
content: [
|
|
33072
33602
|
{
|