@doquflow/server 1.6.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,157 +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.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
- }
@@ -1,157 +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.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
- const PLURAL_TO_SINGULAR = {
97
- entities: "entity",
98
- concepts: "concept",
99
- timelines: "timeline",
100
- syntheses: "synthesis",
101
- };
102
- // Second pass: search all pages
103
- for (const categoryDir of categoriesToScan) {
104
- const fullCategoryPath = node_path_1.default.join(wikiDir, categoryDir);
105
- const category = PLURAL_TO_SINGULAR[categoryDir] ?? categoryDir.replace(/s$/, "");
106
- try {
107
- const files = await promises_1.default.readdir(fullCategoryPath);
108
- for (const file of files) {
109
- if (!file.endsWith(".md"))
110
- continue;
111
- const filePath = node_path_1.default.join(fullCategoryPath, file);
112
- const read = await (0, filesystem_1.safeReadFile)(filePath);
113
- if (read.error || read.binary || !read.content)
114
- continue;
115
- // Extract title from content (first H1)
116
- const titleMatch = read.content.match(/^#\s+(.+?)$/m);
117
- const title = titleMatch ? titleMatch[1].trim() : file.replace(".md", "");
118
- const pageId = file.replace(".md", "");
119
- // Score the match
120
- const { score, matched_terms } = scoreMatch(input.query, read.content, title, category, totalPages);
121
- // Only include if there's at least one match
122
- if (score > 0 && matched_terms.length > 0) {
123
- const preview = extractPreview(read.content, matched_terms);
124
- allResults.push({
125
- page_id: pageId,
126
- title,
127
- category,
128
- path: node_path_1.default.relative(docuDir, filePath),
129
- relevance_score: Math.round(score * 10) / 10, // Round to 1 decimal
130
- preview,
131
- matched_terms,
132
- });
133
- }
134
- }
135
- }
136
- catch (e) {
137
- // Category directory may not exist
138
- }
139
- }
140
- // Sort by relevance score (descending) and return top N
141
- allResults.sort((a, b) => b.relevance_score - a.relevance_score);
142
- const results = allResults.slice(0, limit);
143
- return {
144
- query: input.query,
145
- results,
146
- total_results: allResults.length,
147
- };
148
- }
149
- catch (e) {
150
- return {
151
- query: input.query,
152
- results: [],
153
- total_results: 0,
154
- error: e?.message ?? String(e),
155
- };
156
- }
157
- }
@@ -1,55 +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.writeSpec = writeSpec;
7
- const node_path_1 = __importDefault(require("node:path"));
8
- const filesystem_1 = require("../filesystem");
9
- const promises_1 = __importDefault(require("node:fs/promises"));
10
- // Per-project write lock: maps projectPath → promise chain so that concurrent
11
- // calls to writeSpec on the same project are always serialised. This prevents
12
- // the read-modify-write on index.json from racing when the agent issues several
13
- // write_spec calls in parallel (e.g. writing one spec per module at once).
14
- const indexLocks = new Map();
15
- function withLock(key, fn) {
16
- const prev = indexLocks.get(key) ?? Promise.resolve();
17
- const next = prev.then(fn).catch(() => { });
18
- indexLocks.set(key, next);
19
- return next;
20
- }
21
- async function writeSpec(input) {
22
- const projectPath = node_path_1.default.resolve(input.project_path);
23
- const docuDir = node_path_1.default.join(projectPath, ".docuflow");
24
- const specsDir = node_path_1.default.join(docuDir, "specs");
25
- await (0, filesystem_1.ensureDir)(specsDir);
26
- const cleanName = input.filename.replace(/\.md$/i, "");
27
- const targetFile = node_path_1.default.join(specsDir, `${cleanName}.md`);
28
- // Write the markdown file immediately — each spec file is independent so
29
- // parallel writes to different filenames are safe without locking.
30
- const bytes = await (0, filesystem_1.writeFileAtomic)(targetFile, input.content);
31
- // Serialise index updates per project to avoid read-modify-write races.
32
- const indexPath = node_path_1.default.join(docuDir, "index.json");
33
- let indexUpdated = false;
34
- let writeError;
35
- await withLock(projectPath, async () => {
36
- try {
37
- const existing = (await (0, filesystem_1.readJsonIfExists)(indexPath)) ?? { specs: [] };
38
- const now = new Date().toISOString();
39
- const idx = existing.specs.findIndex((s) => s.filename === `${cleanName}.md`);
40
- if (idx >= 0)
41
- existing.specs[idx].written_at = now;
42
- else
43
- existing.specs.push({ filename: `${cleanName}.md`, written_at: now });
44
- await promises_1.default.writeFile(indexPath, JSON.stringify(existing, null, 2), "utf8");
45
- indexUpdated = true;
46
- }
47
- catch (e) {
48
- writeError = e?.message ?? String(e);
49
- }
50
- });
51
- if (writeError) {
52
- return { written_to: targetFile, bytes_written: bytes, index_updated: false, error: writeError };
53
- }
54
- return { written_to: targetFile, bytes_written: bytes, index_updated: indexUpdated };
55
- }
package/dist/types.js DELETED
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });