@doquflow/server 1.7.0 → 2.0.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.
@@ -1,198 +0,0 @@
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.listWiki = listWiki;
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
- const PLURAL_TO_SINGULAR = {
11
- entities: "entity",
12
- concepts: "concept",
13
- timelines: "timeline",
14
- syntheses: "synthesis",
15
- };
16
- const SINGULAR_TO_PLURAL = {
17
- entity: "entities",
18
- concept: "concepts",
19
- timeline: "timelines",
20
- synthesis: "syntheses",
21
- };
22
- const STALE_DAYS = 30;
23
- function unquote(s) {
24
- const t = s.trim();
25
- if (t.length >= 2) {
26
- const first = t[0];
27
- const last = t[t.length - 1];
28
- if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
29
- return t.slice(1, -1);
30
- }
31
- }
32
- return t;
33
- }
34
- /**
35
- * Parse YAML-ish frontmatter from markdown.
36
- *
37
- * Supports:
38
- * - Inline JSON/flow: key: ["a", "b"] or key: {x: 1}
39
- * - Block-style list: key:\n - "a"\n - "b"
40
- * - Scalar: key: value (values may contain ':' — split on FIRST ':' only)
41
- */
42
- function parseFrontmatter(content) {
43
- const match = content.match(/^---\n([\s\S]*?)\n---/);
44
- if (!match)
45
- return {};
46
- const lines = match[1].split("\n");
47
- const result = {};
48
- let i = 0;
49
- while (i < lines.length) {
50
- const line = lines[i];
51
- const trimmed = line.trim();
52
- if (!trimmed) {
53
- i++;
54
- continue;
55
- }
56
- // Block-list continuation handled inside the key branch below; top-level
57
- // dashes without a preceding key are ignored.
58
- if (trimmed.startsWith("- ")) {
59
- i++;
60
- continue;
61
- }
62
- const colonIdx = line.indexOf(":");
63
- if (colonIdx === -1) {
64
- i++;
65
- continue;
66
- }
67
- const key = line.slice(0, colonIdx).trim();
68
- const rawValue = line.slice(colonIdx + 1).trim();
69
- if (rawValue === "") {
70
- // Possibly a block-style list on subsequent lines: " - value"
71
- const items = [];
72
- let j = i + 1;
73
- while (j < lines.length) {
74
- const next = lines[j];
75
- if (!next.trim()) {
76
- j++;
77
- continue;
78
- }
79
- // A block-list item must be indented and start with "- ".
80
- const m = next.match(/^(\s+)-\s+(.*)$/);
81
- if (!m)
82
- break;
83
- items.push(unquote(m[2]));
84
- j++;
85
- }
86
- if (items.length > 0) {
87
- result[key] = items;
88
- }
89
- else {
90
- result[key] = "";
91
- }
92
- i = j;
93
- continue;
94
- }
95
- if (rawValue.startsWith("[") || rawValue.startsWith("{")) {
96
- try {
97
- result[key] = JSON.parse(rawValue);
98
- }
99
- catch {
100
- result[key] = rawValue;
101
- }
102
- }
103
- else {
104
- result[key] = unquote(rawValue);
105
- }
106
- i++;
107
- }
108
- return result;
109
- }
110
- function toStringArray(v) {
111
- if (!Array.isArray(v))
112
- return [];
113
- return v.filter((x) => typeof x === "string");
114
- }
115
- /**
116
- * Extract title from markdown
117
- */
118
- function extractTitle(content) {
119
- const match = content.match(/^#\s+(.+?)$/m);
120
- return match ? match[1].trim() : "Untitled";
121
- }
122
- async function listWiki(input) {
123
- try {
124
- const projectPath = node_path_1.default.resolve(input.project_path);
125
- const docuDir = node_path_1.default.join(projectPath, ".docuflow");
126
- const wikiDir = node_path_1.default.join(docuDir, "wiki");
127
- const pages = [];
128
- const categories = {};
129
- const now = Date.now();
130
- // Build list of categories to scan — always use correct plural directory names
131
- let categoriesToScan = ["entities", "concepts", "timelines", "syntheses"];
132
- if (input.category) {
133
- const pluralDir = SINGULAR_TO_PLURAL[input.category] ?? `${input.category}s`;
134
- categoriesToScan = [pluralDir];
135
- }
136
- // Scan each category
137
- for (const categoryDir of categoriesToScan) {
138
- const fullCategoryPath = node_path_1.default.join(wikiDir, categoryDir);
139
- const category = PLURAL_TO_SINGULAR[categoryDir] ?? categoryDir;
140
- try {
141
- const files = await promises_1.default.readdir(fullCategoryPath);
142
- let categoryCount = 0;
143
- for (const file of files) {
144
- if (!file.endsWith(".md"))
145
- continue;
146
- const filePath = node_path_1.default.join(fullCategoryPath, file);
147
- const read = await (0, filesystem_1.safeReadFile)(filePath);
148
- if (read.error || read.binary || !read.content)
149
- continue;
150
- const fm = parseFrontmatter(read.content);
151
- const title = extractTitle(read.content);
152
- const pageId = file.replace(".md", "");
153
- const updatedAt = fm.updated_at ?? new Date().toISOString();
154
- const updatedMs = new Date(updatedAt).getTime();
155
- const stale = !isNaN(updatedMs) && (now - updatedMs) > STALE_DAYS * 86_400_000;
156
- const outbound_links = toStringArray(fm.outbound_links);
157
- const inbound_links = toStringArray(fm.inbound_links);
158
- pages.push({
159
- id: pageId,
160
- title,
161
- category,
162
- path: node_path_1.default.relative(docuDir, filePath),
163
- created_at: fm.created_at ?? new Date().toISOString(),
164
- updated_at: updatedAt,
165
- sources: toStringArray(fm.sources),
166
- tags: toStringArray(fm.tags),
167
- stale,
168
- outbound_links,
169
- inbound_links,
170
- degree: outbound_links.length + inbound_links.length,
171
- });
172
- categoryCount++;
173
- }
174
- if (categoryCount > 0) {
175
- categories[category] = categoryCount;
176
- }
177
- }
178
- catch (e) {
179
- // Category directory may not exist yet
180
- }
181
- }
182
- return {
183
- total_pages: pages.length,
184
- stale_pages: pages.filter((p) => p.stale).length,
185
- pages: pages.sort((a, b) => a.title.localeCompare(b.title)),
186
- categories,
187
- };
188
- }
189
- catch (e) {
190
- return {
191
- total_pages: 0,
192
- stale_pages: 0,
193
- pages: [],
194
- categories: {},
195
- error: e?.message ?? String(e),
196
- };
197
- }
198
- }
@@ -1,228 +0,0 @@
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
- }
@@ -1,67 +0,0 @@
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
- }
@@ -1,53 +0,0 @@
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.readModule = readModule;
7
- const node_path_1 = __importDefault(require("node:path"));
8
- const filesystem_1 = require("../filesystem");
9
- const language_map_1 = require("../language-map");
10
- const extractor_1 = require("../extractor");
11
- const MAX_RAW = 8000;
12
- async function readModule(input) {
13
- const filePath = node_path_1.default.resolve(input.path);
14
- const language = (0, language_map_1.extensionToLanguage)(filePath);
15
- const read = await (0, filesystem_1.safeReadFile)(filePath);
16
- if (read.error) {
17
- return {
18
- path: filePath,
19
- language,
20
- size_bytes: 0,
21
- classes: [],
22
- functions: [],
23
- dependencies: [],
24
- db_tables: [],
25
- endpoints: [],
26
- config_refs: [],
27
- error: read.error,
28
- };
29
- }
30
- if (read.binary) {
31
- return {
32
- path: filePath,
33
- language,
34
- size_bytes: read.size,
35
- classes: [],
36
- functions: [],
37
- dependencies: [],
38
- db_tables: [],
39
- endpoints: [],
40
- config_refs: [],
41
- error: "binary file skipped",
42
- };
43
- }
44
- const content = read.content ?? "";
45
- const facts = (0, extractor_1.extract)(content);
46
- return {
47
- path: filePath,
48
- language,
49
- size_bytes: read.size,
50
- ...facts,
51
- raw_content: content.length > MAX_RAW ? content.slice(0, MAX_RAW) : content,
52
- };
53
- }
@@ -1,39 +0,0 @@
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.readSpecs = readSpecs;
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
- const STALE_DAYS = 30;
11
- async function readSpecs(input) {
12
- const projectPath = node_path_1.default.resolve(input.project_path);
13
- const docuDir = node_path_1.default.join(projectPath, ".docuflow");
14
- const indexPath = node_path_1.default.join(docuDir, "index.json");
15
- const index = await (0, filesystem_1.readJsonIfExists)(indexPath);
16
- if (!index || index.specs.length === 0) {
17
- return { specs_found: 0, specs: [] };
18
- }
19
- let entries = index.specs;
20
- if (input.module_name) {
21
- const needle = input.module_name.replace(/\.md$/i, "").toLowerCase();
22
- entries = entries.filter((s) => s.filename.replace(/\.md$/i, "").toLowerCase() === needle);
23
- }
24
- const now = Date.now();
25
- const specs = [];
26
- for (const entry of entries) {
27
- const filePath = node_path_1.default.join(docuDir, "specs", entry.filename);
28
- try {
29
- const content = await promises_1.default.readFile(filePath, "utf8");
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 });
33
- }
34
- catch {
35
- // spec entry exists in index but file missing — skip silently
36
- }
37
- }
38
- return { specs_found: specs.length, specs };
39
- }
@@ -1,91 +0,0 @@
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
- const category_dir_1 = require("../category-dir");
11
- async function saveAnswerAsPage(input) {
12
- try {
13
- const projectPath = node_path_1.default.resolve(input.project_path);
14
- const docuDir = node_path_1.default.join(projectPath, ".docuflow");
15
- const wikiDir = node_path_1.default.join(docuDir, "wiki");
16
- const category = input.category ?? "synthesis";
17
- const catDirPath = node_path_1.default.join(wikiDir, (0, category_dir_1.categoryDir)(category));
18
- await (0, filesystem_1.ensureDir)(catDirPath);
19
- const pageId = `query_${input.page_title
20
- .toLowerCase()
21
- .replace(/[^a-z0-9]/g, "_")
22
- .replace(/_+/g, "_")
23
- .substring(0, 50)}`;
24
- const now = new Date().toISOString();
25
- const sources = input.source_page_ids ?? [];
26
- const frontmatterYaml = `---
27
- created_at: ${now}
28
- updated_at: ${now}
29
- sources: ${JSON.stringify(sources)}
30
- tags: ["query_result","synthesis"]
31
- inbound_links: ${JSON.stringify([])}
32
- outbound_links: ${JSON.stringify(sources)}
33
- ---
34
- `;
35
- const pageContent = `${frontmatterYaml}
36
- # ${input.page_title}
37
-
38
- **Generated from query**: ${input.question}
39
-
40
- **Synthesis timestamp**: ${now}
41
-
42
- ## Answer
43
-
44
- ${input.answer}
45
-
46
- ## Related Pages
47
-
48
- ${sources.length > 0
49
- ? sources.map((s) => `- [\`${s}\`](../${(0, category_dir_1.categoryDir)(category)}/${s}.md)`).join("\n")
50
- : "No source pages linked."}
51
-
52
- ---
53
-
54
- *This page was generated by synthesizing answers from multiple wiki pages.*
55
- *To refine further, add more source documents and re-ingest.*
56
- `;
57
- const pageFile = node_path_1.default.join(catDirPath, `${pageId}.md`);
58
- await (0, filesystem_1.writeFileAtomic)(pageFile, pageContent);
59
- const logFile = node_path_1.default.join(docuDir, "log.md");
60
- try {
61
- let logContent = "";
62
- try {
63
- const logRead = await promises_1.default.readFile(logFile, "utf-8");
64
- logContent = logRead;
65
- }
66
- catch (e) {
67
- logContent = "# Operation Log\n\n";
68
- }
69
- const timestamp = now.split("T")[0];
70
- const logEntry = `## [${timestamp}] query-result | Saved answer as ${pageId}\n\n`;
71
- logContent += logEntry;
72
- await (0, filesystem_1.writeFileAtomic)(logFile, logContent);
73
- }
74
- catch (e) {
75
- // Log update failure is not critical
76
- }
77
- return {
78
- saved_page_id: pageId,
79
- saved_path: node_path_1.default.relative(docuDir, pageFile),
80
- category,
81
- };
82
- }
83
- catch (e) {
84
- return {
85
- saved_page_id: "",
86
- saved_path: "",
87
- category: input.category ?? "synthesis",
88
- error: e?.message ?? String(e),
89
- };
90
- }
91
- }