@doquflow/server 0.2.0 → 0.4.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,228 @@
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
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ async function countWikiPages(wikiDir) {
10
+ let total = 0;
11
+ for (const cat of ["entities", "concepts", "timelines", "syntheses"]) {
12
+ try {
13
+ const files = await promises_1.default.readdir(node_path_1.default.join(wikiDir, cat));
14
+ total += files.filter((f) => f.endsWith(".md")).length;
15
+ }
16
+ catch {
17
+ // directory may not exist
18
+ }
19
+ }
20
+ return total;
21
+ }
22
+ async function getSourceFileSize(sourcesDir, filename) {
23
+ try {
24
+ const stat = await promises_1.default.stat(node_path_1.default.join(sourcesDir, filename));
25
+ return stat.size;
26
+ }
27
+ catch {
28
+ return 0;
29
+ }
30
+ }
31
+ function predictPageCount(fileSizeBytes) {
32
+ // Rough heuristic: ~1 wiki page per 800 bytes of source
33
+ const low = Math.max(1, Math.floor(fileSizeBytes / 1200));
34
+ const high = Math.max(2, Math.ceil(fileSizeBytes / 600));
35
+ return `${low}–${high}`;
36
+ }
37
+ /**
38
+ * preview_generation
39
+ *
40
+ * Shows what a tool will generate before it actually runs.
41
+ * Removes black-box feeling by providing transparency.
42
+ *
43
+ * Input:
44
+ * - tool_name: string (the tool you want to run)
45
+ * - project_path: string
46
+ * - params: Record<string, any> (the parameters you'd pass to the tool)
47
+ *
48
+ * Output:
49
+ * - Predicted actions and outputs
50
+ * - Files that will be affected
51
+ * - Impact level (low/medium/high)
52
+ * - Recommendation on whether to proceed
53
+ */
54
+ async function previewGeneration(input) {
55
+ const projectPath = node_path_1.default.resolve(input.project_path);
56
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
57
+ const wikiDir = node_path_1.default.join(docuDir, "wiki");
58
+ const sourcesDir = node_path_1.default.join(docuDir, "sources");
59
+ const toolName = input.tool_name;
60
+ const params = input.params;
61
+ // Read real wiki state upfront
62
+ const existingPageCount = await countWikiPages(wikiDir);
63
+ if (toolName === "ingest_source") {
64
+ const filename = params.source_filename ?? "";
65
+ const fileSize = await getSourceFileSize(sourcesDir, filename);
66
+ const predictedNew = predictPageCount(fileSize);
67
+ const sizeLabel = fileSize > 0 ? `${Math.round(fileSize / 1024)}KB` : "unknown size";
68
+ return {
69
+ tool_name: "ingest_source",
70
+ tool_description: "Process a new source and integrate it into the wiki",
71
+ input_provided: params,
72
+ predicted_actions: [
73
+ `✓ Read ${filename} (${sizeLabel})`,
74
+ "✓ Extract entities and concepts from markdown",
75
+ "✓ Generate wiki pages (one per entity/concept found)",
76
+ "✓ Create cross-references between pages",
77
+ "✓ Update index.md with new entries",
78
+ "✓ Append entry to log.md",
79
+ ],
80
+ predicted_outputs: [
81
+ {
82
+ type: "Wiki Pages",
83
+ description: `Estimated ${predictedNew} new pages (wiki currently has ${existingPageCount})`,
84
+ example: "entities/ServiceName.md, concepts/Pattern.md, syntheses/source_name.md",
85
+ },
86
+ {
87
+ type: "Index Update",
88
+ description: "index.md gets new page entries with metadata",
89
+ example: "index.md entry with source, date, page count",
90
+ },
91
+ {
92
+ type: "Log Entry",
93
+ description: "log.md records this ingest",
94
+ example: `[${new Date().toISOString().slice(0, 10)}] ingest | ${filename} → N pages created`,
95
+ },
96
+ ],
97
+ data_modified: true,
98
+ files_affected: [
99
+ ".docuflow/wiki/entities/*.md",
100
+ ".docuflow/wiki/concepts/*.md",
101
+ ".docuflow/wiki/syntheses/*.md",
102
+ ".docuflow/index.md",
103
+ ".docuflow/log.md",
104
+ ],
105
+ estimated_impact: "high",
106
+ proceed_recommendation: "✓ Safe to proceed. Source will be integrated and wiki will grow. This is expected behavior.",
107
+ };
108
+ }
109
+ if (toolName === "query_wiki") {
110
+ return {
111
+ tool_name: "query_wiki",
112
+ tool_description: "Search and synthesize answers from the wiki",
113
+ input_provided: params,
114
+ predicted_actions: [
115
+ "✓ Search index.md for relevant pages",
116
+ "✓ Read matching wiki pages",
117
+ "✓ Synthesize answer with citations",
118
+ "✓ Return answer with source pages",
119
+ ],
120
+ predicted_outputs: [
121
+ {
122
+ type: "Answer",
123
+ description: `Synthesized answer from up to ${existingPageCount} wiki pages`,
124
+ example: "The system uses MCP protocol to communicate with tools...",
125
+ },
126
+ {
127
+ type: "Source Pages",
128
+ description: "Wiki pages used to create the answer",
129
+ example: "concepts/MCP_Protocol.md, entities/Server.md",
130
+ },
131
+ {
132
+ type: "Confidence",
133
+ description: "How well the question was answered (0-100)",
134
+ example: existingPageCount > 10 ? "85 (good coverage)" : "50 (sparse wiki — add more sources)",
135
+ },
136
+ ],
137
+ data_modified: false,
138
+ files_affected: [],
139
+ estimated_impact: "low",
140
+ proceed_recommendation: existingPageCount === 0
141
+ ? "⚠ Wiki is empty — ingest sources first for useful answers."
142
+ : "✓ Safe to proceed. This is a read-only operation. No wiki data will be changed.",
143
+ };
144
+ }
145
+ if (toolName === "lint_wiki") {
146
+ return {
147
+ tool_name: "lint_wiki",
148
+ tool_description: "Health check the wiki for issues",
149
+ input_provided: params,
150
+ predicted_actions: [
151
+ "✓ Scan for orphan pages (no incoming links)",
152
+ "✓ Find stale pages (30+ days old)",
153
+ "✓ Check for broken cross-references",
154
+ "✓ Detect contradictions in content",
155
+ "✓ Look for metadata gaps",
156
+ "✓ Calculate overall health score",
157
+ ],
158
+ predicted_outputs: [
159
+ {
160
+ type: "Issues",
161
+ description: `Scanning ${existingPageCount} wiki pages for problems`,
162
+ example: "N orphan pages, N stale (>30 days), N broken references",
163
+ },
164
+ {
165
+ type: "Health Score",
166
+ description: "Overall wiki health (0-100)",
167
+ example: existingPageCount > 0 ? "Score will vary — run to find out" : "N/A (no pages yet)",
168
+ },
169
+ {
170
+ type: "Recommendations",
171
+ description: "Actionable suggestions to improve wiki",
172
+ example: "Delete orphans, update stale pages, add missing cross-refs",
173
+ },
174
+ ],
175
+ data_modified: false,
176
+ files_affected: [],
177
+ estimated_impact: "low",
178
+ proceed_recommendation: existingPageCount === 0
179
+ ? "⚠ Wiki is empty — nothing to lint yet. Ingest sources first."
180
+ : "✓ Safe to proceed. This is a read-only diagnostic. It will report issues but not fix them.",
181
+ };
182
+ }
183
+ if (toolName === "save_answer_as_page") {
184
+ return {
185
+ tool_name: "save_answer_as_page",
186
+ tool_description: "Save an answer as a new wiki page (enables knowledge compounding)",
187
+ input_provided: params,
188
+ predicted_actions: [
189
+ "✓ Format the answer as markdown with frontmatter",
190
+ "✓ Place in appropriate category (synthesis or concept)",
191
+ "✓ Update index.md with new page",
192
+ "✓ Add log entry",
193
+ ],
194
+ predicted_outputs: [
195
+ {
196
+ type: "New Wiki Page",
197
+ description: `New markdown file — wiki will have ${existingPageCount + 1} pages`,
198
+ example: ".docuflow/wiki/syntheses/query_result_<date>.md",
199
+ },
200
+ {
201
+ type: "Metadata",
202
+ description: "YAML frontmatter with creation date, tags, sources",
203
+ example: `created_at: ${new Date().toISOString().slice(0, 10)}, tags: [synthesis]`,
204
+ },
205
+ ],
206
+ data_modified: true,
207
+ files_affected: [
208
+ ".docuflow/wiki/syntheses/*.md or concepts/*.md",
209
+ ".docuflow/index.md",
210
+ ".docuflow/log.md",
211
+ ],
212
+ estimated_impact: "medium",
213
+ proceed_recommendation: "✓ This compounds knowledge in your wiki! Proceed to file interesting discoveries.",
214
+ };
215
+ }
216
+ // Unknown tool fallback
217
+ return {
218
+ tool_name: toolName,
219
+ tool_description: "Unknown tool",
220
+ input_provided: params,
221
+ predicted_actions: ["❌ Tool not found"],
222
+ predicted_outputs: [],
223
+ data_modified: false,
224
+ files_affected: [],
225
+ estimated_impact: "low",
226
+ proceed_recommendation: "❌ Unknown tool. Check tool name and try again.",
227
+ };
228
+ }
@@ -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
+ }
@@ -7,6 +7,7 @@ exports.readSpecs = readSpecs;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const promises_1 = __importDefault(require("node:fs/promises"));
9
9
  const filesystem_1 = require("../filesystem");
10
+ const STALE_DAYS = 30;
10
11
  async function readSpecs(input) {
11
12
  const projectPath = node_path_1.default.resolve(input.project_path);
12
13
  const docuDir = node_path_1.default.join(projectPath, ".docuflow");
@@ -20,12 +21,15 @@ async function readSpecs(input) {
20
21
  const needle = input.module_name.replace(/\.md$/i, "").toLowerCase();
21
22
  entries = entries.filter((s) => s.filename.replace(/\.md$/i, "").toLowerCase() === needle);
22
23
  }
24
+ const now = Date.now();
23
25
  const specs = [];
24
26
  for (const entry of entries) {
25
27
  const filePath = node_path_1.default.join(docuDir, "specs", entry.filename);
26
28
  try {
27
29
  const content = await promises_1.default.readFile(filePath, "utf8");
28
- specs.push({ filename: entry.filename, written_at: entry.written_at, content });
30
+ const writtenMs = entry.written_at ? new Date(entry.written_at).getTime() : NaN;
31
+ const stale = !isNaN(writtenMs) && (now - writtenMs) > STALE_DAYS * 86_400_000;
32
+ specs.push({ filename: entry.filename, written_at: entry.written_at, stale, content });
29
33
  }
30
34
  catch {
31
35
  // spec entry exists in index but file missing — skip silently
@@ -0,0 +1,102 @@
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
+ const CATEGORY_DIR = {
38
+ synthesis: "syntheses",
39
+ entity: "entities",
40
+ concept: "concepts",
41
+ timeline: "timelines",
42
+ };
43
+ // Build page content
44
+ const pageContent = `${frontmatterYaml}
45
+ # ${input.page_title}
46
+
47
+ **Generated from query**: ${input.question}
48
+
49
+ **Synthesis timestamp**: ${now}
50
+
51
+ ## Answer
52
+
53
+ ${input.answer}
54
+
55
+ ## Related Pages
56
+
57
+ ${sources.length > 0
58
+ ? sources.map((s) => `- [\`${s}\`](../${CATEGORY_DIR[category] ?? category + "s"}/${s}.md)`).join("\n")
59
+ : "No source pages linked."}
60
+
61
+ ---
62
+
63
+ *This page was generated by synthesizing answers from multiple wiki pages.*
64
+ *To refine further, add more source documents and re-ingest.*
65
+ `;
66
+ // Write the page file
67
+ const pageFile = node_path_1.default.join(categoryDir, `${pageId}.md`);
68
+ const bytes = await (0, filesystem_1.writeFileAtomic)(pageFile, pageContent);
69
+ // Also update log.md to record this
70
+ const logFile = node_path_1.default.join(docuDir, "log.md");
71
+ try {
72
+ let logContent = "";
73
+ try {
74
+ const logRead = await promises_1.default.readFile(logFile, "utf-8");
75
+ logContent = logRead;
76
+ }
77
+ catch (e) {
78
+ logContent = "# Operation Log\n\n";
79
+ }
80
+ const timestamp = now.split("T")[0]; // YYYY-MM-DD
81
+ const logEntry = `## [${timestamp}] query-result | Saved answer as ${pageId}\n\n`;
82
+ logContent += logEntry;
83
+ await (0, filesystem_1.writeFileAtomic)(logFile, logContent);
84
+ }
85
+ catch (e) {
86
+ // Log update failure is not critical
87
+ }
88
+ return {
89
+ saved_page_id: pageId,
90
+ saved_path: node_path_1.default.relative(docuDir, pageFile),
91
+ category,
92
+ };
93
+ }
94
+ catch (e) {
95
+ return {
96
+ saved_page_id: "",
97
+ saved_path: "",
98
+ category: input.category ?? "synthesis",
99
+ error: e?.message ?? String(e),
100
+ };
101
+ }
102
+ }
@@ -0,0 +1,157 @@
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
+ const PLURAL_TO_SINGULAR = {
62
+ entities: "entity",
63
+ concepts: "concept",
64
+ timelines: "timeline",
65
+ syntheses: "synthesis",
66
+ };
67
+ for (const category of categories) {
68
+ const categoryDir = node_path_1.default.join(wikiDir, category);
69
+ try {
70
+ const files = await promises_1.default.readdir(categoryDir);
71
+ for (const file of files) {
72
+ if (!file.endsWith(".md"))
73
+ continue;
74
+ const filePath = node_path_1.default.join(categoryDir, file);
75
+ const read = await (0, filesystem_1.safeReadFile)(filePath);
76
+ if (read.error || read.binary || !read.content)
77
+ continue;
78
+ const fm = parseFrontmatter(read.content);
79
+ const title = extractTitle(read.content);
80
+ const pageId = file.replace(".md", "");
81
+ entries.push({
82
+ id: pageId,
83
+ title,
84
+ category: PLURAL_TO_SINGULAR[category] ?? category.replace(/s$/, ""),
85
+ path: node_path_1.default.relative(docuDir, filePath),
86
+ created_at: fm.created_at ?? new Date().toISOString(),
87
+ });
88
+ }
89
+ }
90
+ catch (e) {
91
+ // Category dir may not exist yet
92
+ }
93
+ }
94
+ // Generate index.md content
95
+ const now = new Date().toISOString();
96
+ let indexContent = `# Wiki Index
97
+
98
+ Generated: ${now}
99
+
100
+ ## Overview
101
+
102
+ Total pages: ${entries.length}
103
+
104
+ ## By Category
105
+
106
+ `;
107
+ // Group by category
108
+ const byCategory = {};
109
+ for (const entry of entries) {
110
+ if (!byCategory[entry.category])
111
+ byCategory[entry.category] = [];
112
+ byCategory[entry.category].push(entry);
113
+ }
114
+ // Add each category section
115
+ for (const category of ["entity", "concept", "timeline", "synthesis"]) {
116
+ const categoryEntries = byCategory[category] || [];
117
+ if (categoryEntries.length === 0)
118
+ continue;
119
+ indexContent += `### ${category.charAt(0).toUpperCase() + category.slice(1)} Pages (${categoryEntries.length})\n\n`;
120
+ for (const entry of categoryEntries.sort((a, b) => a.title.localeCompare(b.title))) {
121
+ indexContent += `- [\`${entry.id}\`](./${entry.path}) — **${entry.title}**\n`;
122
+ }
123
+ indexContent += "\n";
124
+ }
125
+ // Write index.md
126
+ await (0, filesystem_1.ensureDir)(docuDir);
127
+ await (0, filesystem_1.writeFileAtomic)(indexFile, indexContent);
128
+ // Append to log.md
129
+ let logUpdated = false;
130
+ try {
131
+ const logRead = await (0, filesystem_1.safeReadFile)(logFile);
132
+ let logContent = logRead.content ?? "# Operation Log\n\n";
133
+ // Add entry
134
+ const timestamp = now.split("T")[0]; // YYYY-MM-DD
135
+ const logEntry = `## [${timestamp}] index-update | ${entries.length} pages indexed\n\n`;
136
+ logContent += logEntry;
137
+ await (0, filesystem_1.writeFileAtomic)(logFile, logContent);
138
+ logUpdated = true;
139
+ }
140
+ catch (e) {
141
+ // Log may not exist, that's ok
142
+ }
143
+ return {
144
+ entries_indexed: entries.length,
145
+ index_file: indexFile,
146
+ log_appended: logUpdated,
147
+ };
148
+ }
149
+ catch (e) {
150
+ return {
151
+ entries_indexed: 0,
152
+ index_file: "",
153
+ log_appended: false,
154
+ error: e?.message ?? String(e),
155
+ };
156
+ }
157
+ }