@doquflow/server 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.previewGeneration = previewGeneration;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ /**
9
+ * preview_generation
10
+ *
11
+ * Shows what a tool will generate before it actually runs.
12
+ * Removes black-box feeling by providing transparency.
13
+ *
14
+ * Input:
15
+ * - tool_name: string (the tool you want to run)
16
+ * - project_path: string
17
+ * - params: Record<string, any> (the parameters you'd pass to the tool)
18
+ *
19
+ * Output:
20
+ * - Predicted actions and outputs
21
+ * - Files that will be affected
22
+ * - Impact level (low/medium/high)
23
+ * - Recommendation on whether to proceed
24
+ */
25
+ async function previewGeneration(input) {
26
+ const projectPath = node_path_1.default.resolve(input.project_path);
27
+ const toolName = input.tool_name;
28
+ const params = input.params;
29
+ // Define preview for each tool
30
+ const previews = {
31
+ ingest_source: (projectPath, params) => ({
32
+ tool_name: "ingest_source",
33
+ tool_description: "Process a new source and integrate it into the wiki",
34
+ input_provided: params,
35
+ predicted_actions: [
36
+ "✓ Read the source file(s)",
37
+ "✓ Extract entities and concepts",
38
+ "✓ Generate or update wiki pages",
39
+ "✓ Create cross-references",
40
+ "✓ Update index.md with new entities",
41
+ "✓ Append entry to log.md",
42
+ ],
43
+ predicted_outputs: [
44
+ {
45
+ type: "Wiki Pages",
46
+ description: "5-20 new or updated wiki pages depending on source size",
47
+ example: "entities/ServiceName.md, concepts/Pattern.md, syntheses/Integration.md",
48
+ },
49
+ {
50
+ type: "Index Update",
51
+ description: "Updated index.md with new pages and metadata",
52
+ example: "index.md entry with source, date, page count",
53
+ },
54
+ {
55
+ type: "Log Entry",
56
+ description: "New line in log.md documenting the ingest",
57
+ example: "[2026-04-17 14:23] ingest | source_name.md → 12 pages created, 5 updated",
58
+ },
59
+ ],
60
+ data_modified: true,
61
+ files_affected: [
62
+ ".docuflow/wiki/entities/*.md",
63
+ ".docuflow/wiki/concepts/*.md",
64
+ ".docuflow/wiki/syntheses/*.md",
65
+ ".docuflow/index.md",
66
+ ".docuflow/log.md",
67
+ ],
68
+ estimated_impact: "high",
69
+ proceed_recommendation: "✓ Safe to proceed. Source will be integrated and wiki will grow. This is expected behavior.",
70
+ }),
71
+ query_wiki: (projectPath, params) => ({
72
+ tool_name: "query_wiki",
73
+ tool_description: "Search and synthesize answers from the wiki",
74
+ input_provided: params,
75
+ predicted_actions: [
76
+ "✓ Search index.md for relevant pages",
77
+ "✓ Read matching wiki pages",
78
+ "✓ Synthesize answer with citations",
79
+ "✓ Return answer with source pages",
80
+ ],
81
+ predicted_outputs: [
82
+ {
83
+ type: "Answer",
84
+ description: "Synthesized answer to your question",
85
+ example: 'The system uses MCP protocol to communicate with tools...',
86
+ },
87
+ {
88
+ type: "Source Pages",
89
+ description: "Wiki pages used to create the answer",
90
+ example: "concepts/MCP_Protocol.md, entities/Server.md",
91
+ },
92
+ {
93
+ type: "Confidence",
94
+ description: "How well the question was answered (0-100)",
95
+ example: "85 (good coverage, some gaps possible)",
96
+ },
97
+ ],
98
+ data_modified: false,
99
+ files_affected: [],
100
+ estimated_impact: "low",
101
+ proceed_recommendation: "✓ Safe to proceed. This is a read-only operation. No wiki data will be changed.",
102
+ }),
103
+ lint_wiki: (projectPath, params) => ({
104
+ tool_name: "lint_wiki",
105
+ tool_description: "Health check the wiki for issues",
106
+ input_provided: params,
107
+ predicted_actions: [
108
+ "✓ Scan for orphan pages (no incoming links)",
109
+ "✓ Find stale pages (30+ days old)",
110
+ "✓ Check for broken cross-references",
111
+ "✓ Detect contradictions in content",
112
+ "✓ Look for metadata gaps",
113
+ "✓ Calculate overall health score",
114
+ ],
115
+ predicted_outputs: [
116
+ {
117
+ type: "Issues",
118
+ description: "Potential problems found in the wiki",
119
+ example: "5 orphan pages, 2 stale (>30 days), 3 broken references",
120
+ },
121
+ {
122
+ type: "Health Score",
123
+ description: "Overall wiki health (0-100)",
124
+ example: "78/100 (Good, some maintenance recommended)",
125
+ },
126
+ {
127
+ type: "Recommendations",
128
+ description: "Actionable suggestions to improve wiki",
129
+ example: "Delete orphans, update stale pages, add missing cross-refs",
130
+ },
131
+ ],
132
+ data_modified: false,
133
+ files_affected: [],
134
+ estimated_impact: "low",
135
+ proceed_recommendation: "✓ Safe to proceed. This is a read-only diagnostic. It will report issues but not fix them.",
136
+ }),
137
+ save_answer_as_page: (projectPath, params) => ({
138
+ tool_name: "save_answer_as_page",
139
+ tool_description: "Save an answer as a new wiki page (enables knowledge compounding)",
140
+ input_provided: params,
141
+ predicted_actions: [
142
+ "✓ Extract the answer from conversation",
143
+ "✓ Create markdown file with frontmatter metadata",
144
+ "✓ Place in appropriate category (synthesis or concept)",
145
+ "✓ Update index.md with new page",
146
+ "✓ Add log entry",
147
+ ],
148
+ predicted_outputs: [
149
+ {
150
+ type: "New Wiki Page",
151
+ description: "New markdown file in wiki/syntheses/ or wiki/concepts/",
152
+ example: ".docuflow/wiki/syntheses/query_result_20260417.md",
153
+ },
154
+ {
155
+ type: "Metadata",
156
+ description: "YAML frontmatter with creation date, tags, sources",
157
+ example: "created_at: 2026-04-17, sources: [5 pages], tags: [architecture]",
158
+ },
159
+ ],
160
+ data_modified: true,
161
+ files_affected: [
162
+ ".docuflow/wiki/syntheses/*.md or concepts/*.md",
163
+ ".docuflow/index.md",
164
+ ".docuflow/log.md",
165
+ ],
166
+ estimated_impact: "medium",
167
+ proceed_recommendation: "✓ This compounds knowledge in your wiki! Proceed to file interesting discoveries.",
168
+ }),
169
+ lint_wiki_with_check: (projectPath, params) => ({
170
+ tool_name: "lint_wiki",
171
+ tool_description: "Run specific lint check on the wiki",
172
+ input_provided: params,
173
+ predicted_actions: [
174
+ `✓ Run ${params.check_type || "all"} lint checks`,
175
+ "✓ Analyze wiki for specific issues",
176
+ "✓ Generate targeted recommendations",
177
+ ],
178
+ predicted_outputs: [
179
+ {
180
+ type: "Check Results",
181
+ description: `Results for ${params.check_type || "all checks"}`,
182
+ example: "5 issues found of this type",
183
+ },
184
+ {
185
+ type: "Recommendations",
186
+ description: "How to fix the issues",
187
+ example: "Delete orphan pages, add missing references",
188
+ },
189
+ ],
190
+ data_modified: false,
191
+ files_affected: [],
192
+ estimated_impact: "low",
193
+ proceed_recommendation: "✓ Safe to proceed. This is diagnostic and won't modify wiki data.",
194
+ }),
195
+ };
196
+ // Get preview based on tool name
197
+ const previewFn = previews[toolName];
198
+ if (!previewFn) {
199
+ return {
200
+ tool_name: toolName,
201
+ tool_description: "Unknown tool",
202
+ input_provided: params,
203
+ predicted_actions: ["❌ Tool not found"],
204
+ predicted_outputs: [],
205
+ data_modified: false,
206
+ files_affected: [],
207
+ estimated_impact: "low",
208
+ proceed_recommendation: "❌ Unknown tool. Check tool name and try again.",
209
+ };
210
+ }
211
+ return previewFn(projectPath, params);
212
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.queryWiki = queryWiki;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const wiki_search_1 = require("./wiki-search");
9
+ const answer_synthesis_1 = require("./answer-synthesis");
10
+ async function queryWiki(input) {
11
+ try {
12
+ const projectPath = node_path_1.default.resolve(input.project_path);
13
+ const maxSources = input.max_sources ?? 5;
14
+ // Step 1: Search the wiki for relevant pages
15
+ const searchResult = await (0, wiki_search_1.wikiSearch)({
16
+ project_path: projectPath,
17
+ query: input.question,
18
+ limit: Math.max(10, maxSources * 2), // Get extra to filter
19
+ });
20
+ if (searchResult.error || !searchResult.results.length) {
21
+ return {
22
+ question: input.question,
23
+ answer: `No relevant wiki pages found for: ${input.question}`,
24
+ source_pages: [],
25
+ search_results: 0,
26
+ confidence: 0,
27
+ error: searchResult.error,
28
+ };
29
+ }
30
+ // Step 2: Select top N results for synthesis
31
+ const topResults = searchResult.results.slice(0, maxSources);
32
+ const sourcePageIds = topResults.map((r) => r.page_id);
33
+ // Step 3: Synthesize answer from selected pages
34
+ const synthesisResult = await (0, answer_synthesis_1.synthesizeAnswer)({
35
+ project_path: projectPath,
36
+ query: input.question,
37
+ source_page_ids: sourcePageIds,
38
+ });
39
+ if (synthesisResult.error) {
40
+ return {
41
+ question: input.question,
42
+ answer: `Error synthesizing answer: ${synthesisResult.error}`,
43
+ source_pages: synthesisResult.source_pages,
44
+ search_results: searchResult.total_results,
45
+ confidence: 0,
46
+ error: synthesisResult.error,
47
+ };
48
+ }
49
+ return {
50
+ question: input.question,
51
+ answer: synthesisResult.answer,
52
+ source_pages: synthesisResult.source_pages,
53
+ search_results: searchResult.total_results,
54
+ confidence: synthesisResult.confidence,
55
+ };
56
+ }
57
+ catch (e) {
58
+ return {
59
+ question: input.question,
60
+ answer: `Query failed: ${e?.message ?? String(e)}`,
61
+ source_pages: [],
62
+ search_results: 0,
63
+ confidence: 0,
64
+ error: e?.message ?? String(e),
65
+ };
66
+ }
67
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.saveAnswerAsPage = saveAnswerAsPage;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const filesystem_1 = require("../filesystem");
10
+ async function saveAnswerAsPage(input) {
11
+ try {
12
+ const projectPath = node_path_1.default.resolve(input.project_path);
13
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
14
+ const wikiDir = node_path_1.default.join(docuDir, "wiki");
15
+ // Use provided category or default to synthesis
16
+ const category = input.category ?? "synthesis";
17
+ const categoryDir = node_path_1.default.join(wikiDir, category + "s");
18
+ await (0, filesystem_1.ensureDir)(categoryDir);
19
+ // Generate page ID from title
20
+ const pageId = `query_${input.page_title
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]/g, "_")
23
+ .replace(/_+/g, "_")
24
+ .substring(0, 50)}`;
25
+ // Generate frontmatter
26
+ const now = new Date().toISOString();
27
+ const sources = input.source_page_ids ?? [];
28
+ const frontmatterYaml = `---
29
+ created_at: ${now}
30
+ updated_at: ${now}
31
+ sources: ${JSON.stringify(sources)}
32
+ tags: ["query_result","synthesis"]
33
+ inbound_links: ${JSON.stringify([])}
34
+ outbound_links: ${JSON.stringify(sources)}
35
+ ---
36
+ `;
37
+ // Build page content
38
+ const pageContent = `${frontmatterYaml}
39
+ # ${input.page_title}
40
+
41
+ **Generated from query**: ${input.question}
42
+
43
+ **Synthesis timestamp**: ${now}
44
+
45
+ ## Answer
46
+
47
+ ${input.answer}
48
+
49
+ ## Related Pages
50
+
51
+ ${sources.length > 0
52
+ ? sources.map((s) => `- [\`${s}\`](../CATEGORY/${s}.md)`).join("\n")
53
+ : "No source pages linked."}
54
+
55
+ ---
56
+
57
+ *This page was generated by synthesizing answers from multiple wiki pages.*
58
+ *To refine further, add more source documents and re-ingest.*
59
+ `;
60
+ // Write the page file
61
+ const pageFile = node_path_1.default.join(categoryDir, `${pageId}.md`);
62
+ const bytes = await (0, filesystem_1.writeFileAtomic)(pageFile, pageContent);
63
+ // Also update log.md to record this
64
+ const logFile = node_path_1.default.join(docuDir, "log.md");
65
+ try {
66
+ let logContent = "";
67
+ try {
68
+ const logRead = await promises_1.default.readFile(logFile, "utf-8");
69
+ logContent = logRead;
70
+ }
71
+ catch (e) {
72
+ logContent = "# Operation Log\n\n";
73
+ }
74
+ const timestamp = now.split("T")[0]; // YYYY-MM-DD
75
+ const logEntry = `## [${timestamp}] query-result | Saved answer as ${pageId}\n\n`;
76
+ logContent += logEntry;
77
+ await (0, filesystem_1.writeFileAtomic)(logFile, logContent);
78
+ }
79
+ catch (e) {
80
+ // Log update failure is not critical
81
+ }
82
+ return {
83
+ saved_page_id: pageId,
84
+ saved_path: node_path_1.default.relative(docuDir, pageFile),
85
+ category,
86
+ };
87
+ }
88
+ catch (e) {
89
+ return {
90
+ saved_page_id: "",
91
+ saved_path: "",
92
+ category: input.category ?? "synthesis",
93
+ error: e?.message ?? String(e),
94
+ };
95
+ }
96
+ }
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.updateIndex = updateIndex;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const filesystem_1 = require("../filesystem");
10
+ /**
11
+ * Parse frontmatter from a markdown file.
12
+ * Expects YAML format between --- markers.
13
+ */
14
+ function parseFrontmatter(content) {
15
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
16
+ if (!match)
17
+ return {};
18
+ const yaml = match[1];
19
+ const result = {};
20
+ for (const line of yaml.split("\n")) {
21
+ if (!line.trim())
22
+ continue;
23
+ const [key, ...valueParts] = line.split(":");
24
+ const value = valueParts.join(":").trim();
25
+ try {
26
+ // Try to parse as JSON (arrays, objects)
27
+ if (value.startsWith("[") || value.startsWith("{")) {
28
+ result[key.trim()] = JSON.parse(value);
29
+ }
30
+ else {
31
+ result[key.trim()] = value;
32
+ }
33
+ }
34
+ catch {
35
+ result[key.trim()] = value;
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+ /**
41
+ * Extract title from markdown content.
42
+ * Looks for first H1 header.
43
+ */
44
+ function extractTitle(content) {
45
+ const match = content.match(/^#\s+(.+?)$/m);
46
+ return match ? match[1].trim() : "Untitled";
47
+ }
48
+ /**
49
+ * Scan all wiki pages and regenerate index.md
50
+ */
51
+ async function updateIndex(input) {
52
+ try {
53
+ const projectPath = node_path_1.default.resolve(input.project_path);
54
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
55
+ const wikiDir = node_path_1.default.join(docuDir, "wiki");
56
+ const indexFile = node_path_1.default.join(docuDir, "index.md");
57
+ const logFile = node_path_1.default.join(docuDir, "log.md");
58
+ // Scan all wiki pages
59
+ const entries = [];
60
+ const categories = ["entities", "concepts", "timelines", "syntheses"];
61
+ for (const category of categories) {
62
+ const categoryDir = node_path_1.default.join(wikiDir, category);
63
+ try {
64
+ const files = await promises_1.default.readdir(categoryDir);
65
+ for (const file of files) {
66
+ if (!file.endsWith(".md"))
67
+ continue;
68
+ const filePath = node_path_1.default.join(categoryDir, file);
69
+ const read = await (0, filesystem_1.safeReadFile)(filePath);
70
+ if (read.error || read.binary || !read.content)
71
+ continue;
72
+ const fm = parseFrontmatter(read.content);
73
+ const title = extractTitle(read.content);
74
+ const pageId = file.replace(".md", "");
75
+ entries.push({
76
+ id: pageId,
77
+ title,
78
+ category: category.replace("s", ""), // entities → entity
79
+ path: node_path_1.default.relative(docuDir, filePath),
80
+ created_at: fm.created_at ?? new Date().toISOString(),
81
+ });
82
+ }
83
+ }
84
+ catch (e) {
85
+ // Category dir may not exist yet
86
+ }
87
+ }
88
+ // Generate index.md content
89
+ const now = new Date().toISOString();
90
+ let indexContent = `# Wiki Index
91
+
92
+ Generated: ${now}
93
+
94
+ ## Overview
95
+
96
+ Total pages: ${entries.length}
97
+
98
+ ## By Category
99
+
100
+ `;
101
+ // Group by category
102
+ const byCategory = {};
103
+ for (const entry of entries) {
104
+ if (!byCategory[entry.category])
105
+ byCategory[entry.category] = [];
106
+ byCategory[entry.category].push(entry);
107
+ }
108
+ // Add each category section
109
+ for (const category of ["entity", "concept", "timeline", "synthesis"]) {
110
+ const categoryEntries = byCategory[category] || [];
111
+ if (categoryEntries.length === 0)
112
+ continue;
113
+ indexContent += `### ${category.charAt(0).toUpperCase() + category.slice(1)} Pages (${categoryEntries.length})\n\n`;
114
+ for (const entry of categoryEntries.sort((a, b) => a.title.localeCompare(b.title))) {
115
+ indexContent += `- [\`${entry.id}\`](./${entry.path}) — **${entry.title}**\n`;
116
+ }
117
+ indexContent += "\n";
118
+ }
119
+ // Write index.md
120
+ await (0, filesystem_1.ensureDir)(docuDir);
121
+ await (0, filesystem_1.writeFileAtomic)(indexFile, indexContent);
122
+ // Append to log.md
123
+ let logUpdated = false;
124
+ try {
125
+ const logRead = await (0, filesystem_1.safeReadFile)(logFile);
126
+ let logContent = logRead.content ?? "# Operation Log\n\n";
127
+ // Add entry
128
+ const timestamp = now.split("T")[0]; // YYYY-MM-DD
129
+ const logEntry = `## [${timestamp}] index-update | ${entries.length} pages indexed\n\n`;
130
+ logContent += logEntry;
131
+ await (0, filesystem_1.writeFileAtomic)(logFile, logContent);
132
+ logUpdated = true;
133
+ }
134
+ catch (e) {
135
+ // Log may not exist, that's ok
136
+ }
137
+ return {
138
+ entries_indexed: entries.length,
139
+ index_file: indexFile,
140
+ log_appended: logUpdated,
141
+ };
142
+ }
143
+ catch (e) {
144
+ return {
145
+ entries_indexed: 0,
146
+ index_file: "",
147
+ log_appended: false,
148
+ error: e?.message ?? String(e),
149
+ };
150
+ }
151
+ }
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.wikiSearch = wikiSearch;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const filesystem_1 = require("../filesystem");
10
+ /**
11
+ * Simple BM25-inspired scoring with term frequency and document frequency.
12
+ * Weights entity pages more heavily than concepts, titles match higher.
13
+ */
14
+ function scoreMatch(query, content, title, category, totalPages) {
15
+ const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
16
+ let score = 0;
17
+ const matched_terms = new Set();
18
+ for (const term of queryTerms) {
19
+ // Title match (highest weight)
20
+ if (title.toLowerCase().includes(term)) {
21
+ score += 50;
22
+ matched_terms.add(term);
23
+ }
24
+ // Content match with term frequency (TF)
25
+ const contentLower = content.toLowerCase();
26
+ const termRegex = new RegExp(`\\b${term}\\b`, "gi");
27
+ const matches = contentLower.match(termRegex);
28
+ if (matches) {
29
+ const tf = matches.length;
30
+ // Inverse document frequency approximation
31
+ const idf = Math.log(totalPages / Math.max(1, matches.length));
32
+ score += tf * idf * 2;
33
+ matched_terms.add(term);
34
+ }
35
+ }
36
+ // Category boost: entities score higher than concepts
37
+ if (category === "entity") {
38
+ score *= 1.3;
39
+ }
40
+ else if (category === "synthesis") {
41
+ score *= 1.1;
42
+ }
43
+ return { score, matched_terms: Array.from(matched_terms) };
44
+ }
45
+ /**
46
+ * Extract a preview snippet from content around matched terms
47
+ */
48
+ function extractPreview(content, matchedTerms, maxLength = 150) {
49
+ if (!matchedTerms.length) {
50
+ // Return first non-empty paragraph
51
+ const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#") && !l.startsWith("---"));
52
+ return lines.slice(0, 2).join(" ").substring(0, maxLength);
53
+ }
54
+ // Find first occurrence of any matched term
55
+ const contentLower = content.toLowerCase();
56
+ const term = matchedTerms[0];
57
+ const index = contentLower.indexOf(term);
58
+ if (index === -1) {
59
+ const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
60
+ return lines.slice(0, 2).join(" ").substring(0, maxLength);
61
+ }
62
+ const start = Math.max(0, index - 60);
63
+ const end = Math.min(content.length, index + 150);
64
+ let preview = content.substring(start, end);
65
+ // Trim to word boundary
66
+ const lastSpace = preview.lastIndexOf(" ");
67
+ if (lastSpace > 0) {
68
+ preview = preview.substring(0, lastSpace) + "...";
69
+ }
70
+ return preview;
71
+ }
72
+ async function wikiSearch(input) {
73
+ try {
74
+ const projectPath = node_path_1.default.resolve(input.project_path);
75
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
76
+ const wikiDir = node_path_1.default.join(docuDir, "wiki");
77
+ const limit = input.limit ?? 10;
78
+ const allResults = [];
79
+ // Build list of categories to search
80
+ let categoriesToScan = ["entities", "concepts", "timelines", "syntheses"];
81
+ if (input.category) {
82
+ categoriesToScan = [`${input.category}s`];
83
+ }
84
+ // First pass: count total pages (for IDF calculation)
85
+ let totalPages = 0;
86
+ for (const categoryDir of categoriesToScan) {
87
+ const fullCategoryPath = node_path_1.default.join(wikiDir, categoryDir);
88
+ try {
89
+ const files = await promises_1.default.readdir(fullCategoryPath);
90
+ totalPages += files.filter((f) => f.endsWith(".md")).length;
91
+ }
92
+ catch (e) {
93
+ // Category may not exist
94
+ }
95
+ }
96
+ // Second pass: search all pages
97
+ for (const categoryDir of categoriesToScan) {
98
+ const fullCategoryPath = node_path_1.default.join(wikiDir, categoryDir);
99
+ const category = categoryDir.replace("s", ""); // entities → entity
100
+ try {
101
+ const files = await promises_1.default.readdir(fullCategoryPath);
102
+ for (const file of files) {
103
+ if (!file.endsWith(".md"))
104
+ continue;
105
+ const filePath = node_path_1.default.join(fullCategoryPath, file);
106
+ const read = await (0, filesystem_1.safeReadFile)(filePath);
107
+ if (read.error || read.binary || !read.content)
108
+ continue;
109
+ // Extract title from content (first H1)
110
+ const titleMatch = read.content.match(/^#\s+(.+?)$/m);
111
+ const title = titleMatch ? titleMatch[1].trim() : file.replace(".md", "");
112
+ const pageId = file.replace(".md", "");
113
+ // Score the match
114
+ const { score, matched_terms } = scoreMatch(input.query, read.content, title, category, totalPages);
115
+ // Only include if there's at least one match
116
+ if (score > 0 && matched_terms.length > 0) {
117
+ const preview = extractPreview(read.content, matched_terms);
118
+ allResults.push({
119
+ page_id: pageId,
120
+ title,
121
+ category,
122
+ path: node_path_1.default.relative(docuDir, filePath),
123
+ relevance_score: Math.round(score * 10) / 10, // Round to 1 decimal
124
+ preview,
125
+ matched_terms,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ catch (e) {
131
+ // Category directory may not exist
132
+ }
133
+ }
134
+ // Sort by relevance score (descending) and return top N
135
+ allResults.sort((a, b) => b.relevance_score - a.relevance_score);
136
+ const results = allResults.slice(0, limit);
137
+ return {
138
+ query: input.query,
139
+ results,
140
+ total_results: allResults.length,
141
+ };
142
+ }
143
+ catch (e) {
144
+ return {
145
+ query: input.query,
146
+ results: [],
147
+ total_results: 0,
148
+ error: e?.message ?? String(e),
149
+ };
150
+ }
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doquflow/server",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Docuflow MCP server — lets AI agents read codebases and persist living specs",
5
5
  "author": "Docuflow <hello@doquflows.dev>",
6
6
  "license": "MIT",