@doquflow/cli 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.
- package/dist/commands/doctor.js +270 -0
- package/dist/commands/help.js +38 -0
- package/dist/commands/ingest.js +118 -0
- package/dist/commands/init.js +46 -50
- package/dist/commands/query.js +144 -0
- package/dist/commands/recent.js +72 -205
- package/dist/commands/rewiki.js +6 -3
- package/dist/commands/sync.js +10 -5
- package/dist/commands/ui.js +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/commands/watch.js +8 -5
- package/dist/index.js +154 -153
- package/package.json +4 -3
- package/ui-dist/assets/index-BMnRdqwa.js +44 -0
- package/ui-dist/assets/index-Cnq2PhDd.js +44 -0
- package/ui-dist/index.html +1 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* docuflow doctor
|
|
4
|
+
*
|
|
5
|
+
* Diagnostic command that reports installed DocuFlow packages, MCP server
|
|
6
|
+
* registrations, detected workflow, and actionable recommendations.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* docuflow doctor # human-readable report
|
|
10
|
+
* docuflow doctor --json # machine-readable JSON
|
|
11
|
+
* docuflow doctor --quiet # recommendations only
|
|
12
|
+
*/
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.run = run;
|
|
18
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
21
|
+
// ── Package version resolution ────────────────────────────────────────────────
|
|
22
|
+
function resolvePackageVersion(pkgName) {
|
|
23
|
+
const candidates = [
|
|
24
|
+
() => require.resolve(`${pkgName}/package.json`),
|
|
25
|
+
() => node_path_1.default.resolve(__dirname, "../../../", pkgName.replace("@doquflow/", ""), "package.json"),
|
|
26
|
+
() => node_path_1.default.resolve(__dirname, "../../", pkgName.replace("@doquflow/", ""), "package.json"),
|
|
27
|
+
];
|
|
28
|
+
for (const c of candidates) {
|
|
29
|
+
try {
|
|
30
|
+
const pkgPath = c();
|
|
31
|
+
const raw = node_fs_1.default.readFileSync(pkgPath, "utf8");
|
|
32
|
+
return JSON.parse(raw).version;
|
|
33
|
+
}
|
|
34
|
+
catch { /* try next */ }
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function getInstalledPackages() {
|
|
39
|
+
const packages = ["@doquflow/cli", "@doquflow/core", "@doquflow/studio", "@doquflow/server"];
|
|
40
|
+
return packages.map(name => ({ name, version: resolvePackageVersion(name) }));
|
|
41
|
+
}
|
|
42
|
+
function getClaudeDesktopConfigPath() {
|
|
43
|
+
const p = process.platform;
|
|
44
|
+
if (p === "darwin")
|
|
45
|
+
return node_path_1.default.join(node_os_1.default.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
46
|
+
if (p === "win32")
|
|
47
|
+
return node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Roaming", "Claude", "claude_desktop_config.json");
|
|
48
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
49
|
+
}
|
|
50
|
+
function getMcpRegistrations(projectPath) {
|
|
51
|
+
const results = [];
|
|
52
|
+
// Project-level .mcp.json
|
|
53
|
+
const projectMcp = node_path_1.default.join(projectPath, ".mcp.json");
|
|
54
|
+
try {
|
|
55
|
+
const cfg = JSON.parse(node_fs_1.default.readFileSync(projectMcp, "utf8"));
|
|
56
|
+
const entry = cfg.mcpServers?.docuflow;
|
|
57
|
+
results.push({ source: ".mcp.json", registered: !!entry, command: entry?.command, args: entry?.args });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
results.push({ source: ".mcp.json", registered: false });
|
|
61
|
+
}
|
|
62
|
+
// Claude Desktop
|
|
63
|
+
try {
|
|
64
|
+
const cfg = JSON.parse(node_fs_1.default.readFileSync(getClaudeDesktopConfigPath(), "utf8"));
|
|
65
|
+
const entry = cfg.mcpServers?.docuflow;
|
|
66
|
+
results.push({ source: "Claude Desktop", registered: !!entry, command: entry?.command, args: entry?.args });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
results.push({ source: "Claude Desktop", registered: false });
|
|
70
|
+
}
|
|
71
|
+
// VS Code user MCP
|
|
72
|
+
const vscodePath = process.platform === "darwin"
|
|
73
|
+
? node_path_1.default.join(node_os_1.default.homedir(), "Library", "Application Support", "Code", "User", "mcp.json")
|
|
74
|
+
: process.platform === "win32"
|
|
75
|
+
? node_path_1.default.join(node_os_1.default.homedir(), "AppData", "Roaming", "Code", "User", "mcp.json")
|
|
76
|
+
: node_path_1.default.join(node_os_1.default.homedir(), ".config", "Code", "User", "mcp.json");
|
|
77
|
+
try {
|
|
78
|
+
const cfg = JSON.parse(node_fs_1.default.readFileSync(vscodePath, "utf8"));
|
|
79
|
+
const entry = cfg.servers?.docuflow;
|
|
80
|
+
results.push({ source: "VS Code (user)", registered: !!entry, command: entry?.command, args: entry?.args });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
results.push({ source: "VS Code (user)", registered: false });
|
|
84
|
+
}
|
|
85
|
+
// Copilot CLI
|
|
86
|
+
const copilotPath = node_path_1.default.join(node_os_1.default.homedir(), ".copilot", "mcp-config.json");
|
|
87
|
+
try {
|
|
88
|
+
const cfg = JSON.parse(node_fs_1.default.readFileSync(copilotPath, "utf8"));
|
|
89
|
+
const entry = cfg.mcpServers?.docuflow;
|
|
90
|
+
results.push({ source: "Copilot CLI", registered: !!entry, command: entry?.command, args: entry?.args });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
results.push({ source: "Copilot CLI", registered: false });
|
|
94
|
+
}
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
function detectWorkflow(projectPath) {
|
|
98
|
+
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
99
|
+
const hasDocuflow = node_fs_1.default.existsSync(docuDir);
|
|
100
|
+
let hasSources = false;
|
|
101
|
+
let hasSpecs = false;
|
|
102
|
+
let hasQueryUsage = false;
|
|
103
|
+
let watchRunning = false;
|
|
104
|
+
if (hasDocuflow) {
|
|
105
|
+
try {
|
|
106
|
+
hasSources = node_fs_1.default.readdirSync(node_path_1.default.join(docuDir, "sources")).some(f => f.endsWith(".md"));
|
|
107
|
+
}
|
|
108
|
+
catch { /* */ }
|
|
109
|
+
try {
|
|
110
|
+
hasSpecs = node_fs_1.default.readdirSync(node_path_1.default.join(docuDir, "specs")).some(f => f.endsWith(".md"));
|
|
111
|
+
}
|
|
112
|
+
catch { /* */ }
|
|
113
|
+
try {
|
|
114
|
+
const log = node_fs_1.default.readFileSync(node_path_1.default.join(docuDir, "log.md"), "utf8");
|
|
115
|
+
hasQueryUsage = /query-wiki|queryWiki/i.test(log);
|
|
116
|
+
}
|
|
117
|
+
catch { /* */ }
|
|
118
|
+
// Check watch daemon PID file
|
|
119
|
+
const pidFile = node_path_1.default.join(docuDir, "watch.pid.json");
|
|
120
|
+
try {
|
|
121
|
+
const data = JSON.parse(node_fs_1.default.readFileSync(pidFile, "utf8"));
|
|
122
|
+
try {
|
|
123
|
+
process.kill(data.pid, 0);
|
|
124
|
+
watchRunning = true;
|
|
125
|
+
}
|
|
126
|
+
catch { /* not running */ }
|
|
127
|
+
}
|
|
128
|
+
catch { /* no pid file */ }
|
|
129
|
+
}
|
|
130
|
+
return { hasDocuflow, hasSources, hasSpecs, hasQueryUsage, watchRunning };
|
|
131
|
+
}
|
|
132
|
+
function getHealthSummary(projectPath) {
|
|
133
|
+
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
134
|
+
const byCategory = { entities: 0, concepts: 0, timelines: 0, syntheses: 0 };
|
|
135
|
+
for (const cat of Object.keys(byCategory)) {
|
|
136
|
+
try {
|
|
137
|
+
const files = node_fs_1.default.readdirSync(node_path_1.default.join(docuDir, "wiki", cat));
|
|
138
|
+
byCategory[cat] = files.filter(f => f.endsWith(".md")).length;
|
|
139
|
+
}
|
|
140
|
+
catch { /* dir may not exist */ }
|
|
141
|
+
}
|
|
142
|
+
const totalPages = Object.values(byCategory).reduce((a, b) => a + b, 0);
|
|
143
|
+
let lastUpdate = "never";
|
|
144
|
+
try {
|
|
145
|
+
const log = node_fs_1.default.readFileSync(node_path_1.default.join(docuDir, "log.md"), "utf8");
|
|
146
|
+
const match = log.match(/\[(\d{4}-\d{2}-\d{2}[^\]]*)\]/);
|
|
147
|
+
if (match) {
|
|
148
|
+
const d = new Date(match[1]);
|
|
149
|
+
if (!isNaN(d.getTime())) {
|
|
150
|
+
const diffH = Math.floor((Date.now() - d.getTime()) / 3_600_000);
|
|
151
|
+
lastUpdate = diffH < 1 ? "just now" : diffH < 24 ? `${diffH}h ago` : `${Math.floor(diffH / 24)}d ago`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch { /* */ }
|
|
156
|
+
return { totalPages, byCategory, lastUpdate, healthScore: null };
|
|
157
|
+
}
|
|
158
|
+
function buildRecommendations(packages, registrations, workflow, health) {
|
|
159
|
+
const recs = [];
|
|
160
|
+
const pkgMap = Object.fromEntries(packages.map(p => [p.name, p.version]));
|
|
161
|
+
// Core/Studio installed?
|
|
162
|
+
if (!pkgMap["@doquflow/core"] && !pkgMap["@doquflow/studio"]) {
|
|
163
|
+
if (pkgMap["@doquflow/cli"]) {
|
|
164
|
+
// CLI installs core+studio transitively — this just means they weren't directly required
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
recs.push({ severity: "error", message: "No DocuFlow packages detected", action: 'Run: npm i -g @doquflow/cli' });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Suggest minimal install if only using query/ingest
|
|
171
|
+
const hasCli = !!pkgMap["@doquflow/cli"];
|
|
172
|
+
const hasCore = !!pkgMap["@doquflow/core"];
|
|
173
|
+
const hasStudio = !!pkgMap["@doquflow/studio"];
|
|
174
|
+
if (hasCli && !workflow.watchRunning && !workflow.hasQueryUsage && hasCore && !hasStudio) {
|
|
175
|
+
recs.push({ severity: "info", message: "You only use core tools (query/ingest)", action: "Consider: npm i -g @doquflow/core for a smaller install" });
|
|
176
|
+
}
|
|
177
|
+
// Not initialised
|
|
178
|
+
if (!workflow.hasDocuflow) {
|
|
179
|
+
recs.push({ severity: "error", message: ".docuflow/ not found in this directory", action: "Run: docuflow init" });
|
|
180
|
+
return recs; // no point checking further
|
|
181
|
+
}
|
|
182
|
+
// No MCP registration anywhere
|
|
183
|
+
const anyRegistered = registrations.some(r => r.registered);
|
|
184
|
+
if (!anyRegistered) {
|
|
185
|
+
recs.push({ severity: "error", message: "DocuFlow is not registered with any MCP host", action: "Run: docuflow init (re-run is safe)" });
|
|
186
|
+
}
|
|
187
|
+
// No sources
|
|
188
|
+
if (!workflow.hasSources) {
|
|
189
|
+
recs.push({ severity: "warn", message: "No source documents in .docuflow/sources/", action: "Drop a markdown file there, then run: docuflow ingest" });
|
|
190
|
+
}
|
|
191
|
+
// Empty wiki
|
|
192
|
+
if (health.totalPages === 0 && workflow.hasSources) {
|
|
193
|
+
recs.push({ severity: "warn", message: "Wiki is empty but sources exist", action: "Run: docuflow rewiki" });
|
|
194
|
+
}
|
|
195
|
+
// Old @doquflow/server pointing at dist/index.js instead of dist/mcp/index.js
|
|
196
|
+
for (const reg of registrations.filter(r => r.registered)) {
|
|
197
|
+
const argStr = reg.args?.join(" ") ?? "";
|
|
198
|
+
if (argStr.includes("@doquflow/server") && argStr.includes("dist/index.js")) {
|
|
199
|
+
recs.push({ severity: "info", message: `${reg.source}: MCP registration uses old server path`, action: "Run: docuflow init to refresh to @doquflow/studio/dist/mcp/index.js" });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (recs.length === 0) {
|
|
203
|
+
recs.push({ severity: "info", message: "Everything looks good", action: "No action needed" });
|
|
204
|
+
}
|
|
205
|
+
return recs;
|
|
206
|
+
}
|
|
207
|
+
// ── Output formatters ─────────────────────────────────────────────────────────
|
|
208
|
+
function icon(severity) {
|
|
209
|
+
return severity === "error" ? "🔴" : severity === "warn" ? "🟡" : "🟢";
|
|
210
|
+
}
|
|
211
|
+
function printHuman(packages, registrations, workflow, health, recs, quiet) {
|
|
212
|
+
if (!quiet) {
|
|
213
|
+
// 1. Installed packages
|
|
214
|
+
console.log("\n── 1. Installed packages ────────────────────────────────");
|
|
215
|
+
for (const pkg of packages) {
|
|
216
|
+
const mark = pkg.version ? "✓" : "✗";
|
|
217
|
+
const ver = pkg.version ?? "not found";
|
|
218
|
+
console.log(` ${mark} ${pkg.name.padEnd(22)} ${ver}`);
|
|
219
|
+
}
|
|
220
|
+
// 2. MCP registrations
|
|
221
|
+
console.log("\n── 2. MCP server registrations ──────────────────────────");
|
|
222
|
+
for (const reg of registrations) {
|
|
223
|
+
const mark = reg.registered ? "✓" : "·";
|
|
224
|
+
const detail = reg.registered && reg.command
|
|
225
|
+
? ` ${reg.command} ${(reg.args ?? []).slice(-1)[0] ?? ""}`
|
|
226
|
+
: "";
|
|
227
|
+
console.log(` ${mark} ${reg.source.padEnd(18)}${reg.registered ? "registered" : "not registered"}${detail}`);
|
|
228
|
+
}
|
|
229
|
+
// 3. Workflow detection
|
|
230
|
+
console.log("\n── 3. Workflow detection ─────────────────────────────────");
|
|
231
|
+
console.log(` ${workflow.hasDocuflow ? "✓" : "✗"} .docuflow/ initialised`);
|
|
232
|
+
console.log(` ${workflow.hasSources ? "✓" : "·"} source documents present`);
|
|
233
|
+
console.log(` ${workflow.hasSpecs ? "✓" : "·"} specs written`);
|
|
234
|
+
console.log(` ${workflow.hasQueryUsage ? "✓" : "·"} query_wiki usage in logs`);
|
|
235
|
+
console.log(` ${workflow.watchRunning ? "✓" : "·"} watch daemon running`);
|
|
236
|
+
// 4. Health summary
|
|
237
|
+
console.log("\n── 5. Wiki health summary ───────────────────────────────");
|
|
238
|
+
console.log(` Pages total: ${health.totalPages}`);
|
|
239
|
+
if (health.totalPages > 0) {
|
|
240
|
+
for (const [cat, n] of Object.entries(health.byCategory)) {
|
|
241
|
+
if (n > 0)
|
|
242
|
+
console.log(` ${cat.padEnd(14)} ${n}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
console.log(` Last updated: ${health.lastUpdate}`);
|
|
246
|
+
console.log("");
|
|
247
|
+
}
|
|
248
|
+
// 4. Recommendations (always shown)
|
|
249
|
+
if (!quiet)
|
|
250
|
+
console.log("── 4. Recommendations ───────────────────────────────────");
|
|
251
|
+
for (const rec of recs) {
|
|
252
|
+
console.log(` ${icon(rec.severity)} ${rec.message}`);
|
|
253
|
+
console.log(` → ${rec.action}`);
|
|
254
|
+
}
|
|
255
|
+
console.log("");
|
|
256
|
+
}
|
|
257
|
+
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
258
|
+
async function run(opts = {}) {
|
|
259
|
+
const projectPath = process.cwd();
|
|
260
|
+
const packages = getInstalledPackages();
|
|
261
|
+
const registrations = getMcpRegistrations(projectPath);
|
|
262
|
+
const workflow = detectWorkflow(projectPath);
|
|
263
|
+
const health = getHealthSummary(projectPath);
|
|
264
|
+
const recs = buildRecommendations(packages, registrations, workflow, health);
|
|
265
|
+
if (opts.json) {
|
|
266
|
+
console.log(JSON.stringify({ packages, registrations, workflow, health, recommendations: recs }, null, 2));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
printHuman(packages, registrations, workflow, health, recs, opts.quiet ?? false);
|
|
270
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Help text for `docuflow --help` and `docuflow advanced --help`.
|
|
3
|
+
// No external help library — plain console.log for zero-dependency output.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.printCoreHelp = printCoreHelp;
|
|
6
|
+
exports.printAdvancedHelp = printAdvancedHelp;
|
|
7
|
+
function printCoreHelp() {
|
|
8
|
+
console.log('DocuFlow — preserve decision context for AI agents.');
|
|
9
|
+
console.log('');
|
|
10
|
+
console.log('CORE');
|
|
11
|
+
console.log(' docuflow init Initialise .docuflow/ in this project');
|
|
12
|
+
console.log(' docuflow ingest <file> Add a source document to the wiki');
|
|
13
|
+
console.log(' docuflow query "<question>" Ask the wiki — answer with citations');
|
|
14
|
+
console.log(' docuflow status Show wiki health and counts');
|
|
15
|
+
console.log(' docuflow rewiki Migrate / re-ingest with current rules');
|
|
16
|
+
console.log(' docuflow doctor Diagnose install, MCP registration, and wiki health');
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log('ADVANCED');
|
|
19
|
+
console.log(' docuflow advanced --help See watch / sync / ui / review / recent / suggest / update');
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(' docuflow --version Print version');
|
|
22
|
+
console.log(' docuflow --help Show this help');
|
|
23
|
+
}
|
|
24
|
+
function printAdvancedHelp() {
|
|
25
|
+
console.log('DocuFlow — advanced surface (sync daemons, UI, audit commands).');
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log(' docuflow advanced watch [stop|status|restart] Auto-sync daemon');
|
|
28
|
+
console.log(' docuflow advanced sync [--ai] One-shot sync for CI / git hooks');
|
|
29
|
+
console.log(' docuflow advanced ui [--port N] Launch web UI dashboard');
|
|
30
|
+
console.log(' docuflow advanced start Alias for ui');
|
|
31
|
+
console.log(' docuflow advanced review Review uncommitted changes');
|
|
32
|
+
console.log(' docuflow advanced recent Recent work dashboard');
|
|
33
|
+
console.log(' docuflow advanced suggest First-steps guidance');
|
|
34
|
+
console.log(' docuflow advanced update Self-upgrade DocuFlow');
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log('Note: every "advanced" command also works at its old top-level path');
|
|
37
|
+
console.log('(e.g. `docuflow watch` is still valid). The "advanced" prefix is optional.');
|
|
38
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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.run = run;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const c = {
|
|
10
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
11
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
12
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
13
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
14
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
15
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
16
|
+
};
|
|
17
|
+
function loadServerTool(toolFile) {
|
|
18
|
+
const candidates = [
|
|
19
|
+
() => require(`@doquflow/core/dist/tools/${toolFile}`),
|
|
20
|
+
() => require(node_path_1.default.resolve(__dirname, "../../../core/dist/tools", toolFile)),
|
|
21
|
+
() => require(node_path_1.default.resolve(__dirname, "../../core/dist/tools", toolFile)),
|
|
22
|
+
() => require(`@doquflow/studio/dist/tools/${toolFile}`),
|
|
23
|
+
() => require(node_path_1.default.resolve(__dirname, "../../../studio/dist/tools", toolFile)),
|
|
24
|
+
() => require(node_path_1.default.resolve(__dirname, "../../studio/dist/tools", toolFile)),
|
|
25
|
+
];
|
|
26
|
+
for (const attempt of candidates) {
|
|
27
|
+
try {
|
|
28
|
+
return attempt();
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Cannot load server tool "${toolFile}". Run "npm run build" first.`);
|
|
33
|
+
}
|
|
34
|
+
async function run(options) {
|
|
35
|
+
const { sourceFile, all = false, dryRun = false, quiet = false } = options;
|
|
36
|
+
const projectPath = node_path_1.default.resolve(process.cwd());
|
|
37
|
+
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
38
|
+
const sourcesDir = node_path_1.default.join(docuDir, "sources");
|
|
39
|
+
function info(msg) { if (!quiet)
|
|
40
|
+
process.stdout.write(msg + "\n"); }
|
|
41
|
+
if (!all && !sourceFile) {
|
|
42
|
+
console.error(c.red(" ✗ Provide a source filename or pass --all."));
|
|
43
|
+
console.error(` Usage: docuflow ingest <source.md>`);
|
|
44
|
+
console.error(` docuflow ingest --all`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
if (!node_fs_1.default.existsSync(docuDir)) {
|
|
48
|
+
console.error(c.red(` ✗ .docuflow/ not found at ${projectPath}`));
|
|
49
|
+
console.error(` Run "docuflow init" first.`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
if (dryRun) {
|
|
53
|
+
info(c.yellow(" dry-run not supported for ingest_source; use `docuflow rewiki --dry-run` for a full simulation"));
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
let ingestSource;
|
|
57
|
+
try {
|
|
58
|
+
({ ingestSource } = loadServerTool("ingest-source"));
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
console.error(c.red(` ✗ ${e.message}`));
|
|
62
|
+
process.exit(2);
|
|
63
|
+
}
|
|
64
|
+
// Collect files to ingest
|
|
65
|
+
let files;
|
|
66
|
+
if (all) {
|
|
67
|
+
if (!node_fs_1.default.existsSync(sourcesDir)) {
|
|
68
|
+
console.error(c.yellow(" No .docuflow/sources/ directory found — nothing to ingest."));
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
files = node_fs_1.default.readdirSync(sourcesDir).filter(f => f.endsWith(".md"));
|
|
72
|
+
if (files.length === 0) {
|
|
73
|
+
info(c.yellow(" No source files found in .docuflow/sources/"));
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Single file — verify it exists
|
|
79
|
+
const filename = sourceFile;
|
|
80
|
+
const fullPath = node_path_1.default.join(sourcesDir, filename);
|
|
81
|
+
if (!node_fs_1.default.existsSync(fullPath)) {
|
|
82
|
+
console.error(c.red(` ✗ Source file not found: .docuflow/sources/${filename}`));
|
|
83
|
+
process.exit(3);
|
|
84
|
+
}
|
|
85
|
+
files = [filename];
|
|
86
|
+
}
|
|
87
|
+
info(c.bold(`DocuFlow ingest`));
|
|
88
|
+
info(` Project : ${projectPath}`);
|
|
89
|
+
info(` Files : ${files.length}`);
|
|
90
|
+
info("");
|
|
91
|
+
let totalCreated = 0;
|
|
92
|
+
let totalUpdated = 0;
|
|
93
|
+
let errored = 0;
|
|
94
|
+
for (const filename of files) {
|
|
95
|
+
info(c.dim(` Ingesting ${filename}…`));
|
|
96
|
+
try {
|
|
97
|
+
const result = await ingestSource({ project_path: projectPath, source_filename: filename });
|
|
98
|
+
totalCreated += result.pages_created ?? 0;
|
|
99
|
+
totalUpdated += result.pages_updated ?? 0;
|
|
100
|
+
info(` ${c.green("✓")} ${filename}` +
|
|
101
|
+
` ${c.dim(`+${result.pages_created ?? 0} created, ~${result.pages_updated ?? 0} updated`)}`);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
errored++;
|
|
105
|
+
console.error(c.red(` ✗ ${filename}: ${e.message}`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
info("");
|
|
109
|
+
if (errored === 0) {
|
|
110
|
+
info(c.green(" ✓ Done.") +
|
|
111
|
+
` Pages created: ${totalCreated} updated: ${totalUpdated}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
info(c.yellow(` Done with ${errored} error(s).`) +
|
|
115
|
+
` Pages created: ${totalCreated} updated: ${totalUpdated}`);
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -62,13 +62,13 @@ function getCodexConfigPath() {
|
|
|
62
62
|
return node_path_1.default.join(node_os_1.default.homedir(), ".codex", "config.toml");
|
|
63
63
|
}
|
|
64
64
|
function resolveServerBin() {
|
|
65
|
-
// Try npm-installed
|
|
65
|
+
// Try npm-installed studio MCP binary first
|
|
66
66
|
try {
|
|
67
|
-
return require.resolve("@doquflow/
|
|
67
|
+
return require.resolve("@doquflow/studio/dist/mcp/index.js");
|
|
68
68
|
}
|
|
69
69
|
catch {
|
|
70
70
|
// Fallback: monorepo sibling path (dev environment)
|
|
71
|
-
return node_path_1.default.resolve(__dirname, "..", "..", "
|
|
71
|
+
return node_path_1.default.resolve(__dirname, "..", "..", "studio", "dist", "mcp", "index.js");
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
async function copyTemplateFile(templateName, destPath) {
|
|
@@ -101,64 +101,55 @@ async function copyTemplateFile(templateName, destPath) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
function buildClaudeMd(projectDir) {
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
DocuFlow is an MCP server that gives you structured access to this codebase and maintains a living wiki.
|
|
107
|
-
It is registered in your Claude Desktop config and available as MCP tools in every session.
|
|
108
|
-
|
|
109
|
-
## Codebase Scanner Tools
|
|
104
|
+
return `<!-- BEGIN DOCUFLOW -->
|
|
105
|
+
# DocuFlow — AI Documentation Assistant
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
- Example: \`read_module({ path: "src/UserService.cs" })\`
|
|
113
|
-
- **list_modules** — Walk a directory and extract facts for every non-binary file. Use this to understand the full project in one call.
|
|
114
|
-
- Example: \`list_modules({ path: "${projectDir}" })\`
|
|
115
|
-
- **write_spec** — Persist a markdown spec to \`.docuflow/specs/<filename>.md\` and update the index.
|
|
116
|
-
- Example: \`write_spec({ project_path: "${projectDir}", filename: "UserService", content: "# UserService\\n..." })\`
|
|
117
|
-
- **read_specs** — Read previously written specs, optionally filtered by name.
|
|
118
|
-
- Example: \`read_specs({ project_path: "${projectDir}" })\`
|
|
107
|
+
DocuFlow preserves decision context for AI agents. Intent in, value out.
|
|
119
108
|
|
|
120
|
-
##
|
|
121
|
-
|
|
122
|
-
- **ingest_source** — Ingest a markdown file from \`.docuflow/sources/\` and generate wiki pages (entities, concepts).
|
|
123
|
-
- **update_index** — Rebuild \`.docuflow/index.md\` from all wiki pages.
|
|
124
|
-
- **list_wiki** — List all wiki pages, optionally filtered by category (entity/concept/timeline/synthesis).
|
|
125
|
-
- **wiki_search** — BM25 search across all wiki pages. Returns ranked results with previews.
|
|
126
|
-
- **query_wiki** — One-stop Q&A: searches wiki, synthesises an answer, returns source citations.
|
|
127
|
-
- **synthesize_answer** — Generate a markdown synthesis from a list of specific wiki page IDs.
|
|
128
|
-
- **save_answer_as_page** — Persist a synthesised answer back into the wiki (knowledge compounding).
|
|
109
|
+
## Core tools (use these first)
|
|
129
110
|
|
|
130
|
-
|
|
111
|
+
- **query_wiki({ project_path, question })** — Ask the wiki. Returns an answer with citations.
|
|
112
|
+
- **ingest_source({ project_path, source_filename })** — Fold a markdown source into the wiki.
|
|
113
|
+
- **wiki_search({ project_path, query })** — BM25 search across all pages.
|
|
114
|
+
- **read_module({ path })** — Read and extract facts from a single source file.
|
|
131
115
|
|
|
132
|
-
|
|
133
|
-
- **get_schema_guidance** — Analyse what wiki pages should exist based on the schema and current state.
|
|
134
|
-
- **preview_generation** — Preview what a tool will do before running it.
|
|
116
|
+
## CLI — Core Commands
|
|
135
117
|
|
|
136
|
-
## Common Workflows
|
|
137
|
-
|
|
138
|
-
### First time — understand the codebase
|
|
139
118
|
\`\`\`
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
119
|
+
docuflow query "<question>" # ask the wiki from the shell
|
|
120
|
+
docuflow ingest <source.md> # add a source doc to the wiki
|
|
121
|
+
docuflow status # wiki health and counts
|
|
122
|
+
docuflow rewiki # re-ingest with current rules
|
|
123
|
+
docuflow init # initialise .docuflow/ in this project
|
|
143
124
|
\`\`\`
|
|
144
125
|
|
|
145
|
-
|
|
126
|
+
## Workflows
|
|
127
|
+
|
|
128
|
+
### Answer a question
|
|
146
129
|
\`\`\`
|
|
147
130
|
query_wiki({ project_path: "${projectDir}", question: "How does authentication work?" })
|
|
148
|
-
→ save_answer_as_page if the answer is worth keeping
|
|
149
131
|
\`\`\`
|
|
150
132
|
|
|
151
|
-
###
|
|
133
|
+
### Add new context
|
|
152
134
|
\`\`\`
|
|
153
|
-
|
|
154
|
-
|
|
135
|
+
# drop a markdown file in .docuflow/sources/
|
|
136
|
+
ingest_source({ project_path: "${projectDir}", source_filename: "auth-design.md" })
|
|
155
137
|
\`\`\`
|
|
156
138
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
139
|
+
## Advanced tools
|
|
140
|
+
|
|
141
|
+
Use when the core tools don't cover the workflow. Each has more parameters and side effects.
|
|
142
|
+
|
|
143
|
+
- **list_modules** — Walk a directory tree and extract facts in bulk
|
|
144
|
+
- **list_wiki** — Inventory pages by category, with staleness flags
|
|
145
|
+
- **write_spec / read_specs** — Persistent agent-written specs
|
|
146
|
+
- **save_answer_as_page** — Promote a synthesised answer into the wiki
|
|
147
|
+
- **synthesize_answer** — Combine multiple pages into a markdown synthesis
|
|
148
|
+
- **update_index** — Rebuild \`.docuflow/index.md\`
|
|
149
|
+
- **lint_wiki** — Health checks: orphans, broken refs, stale content
|
|
150
|
+
- **get_schema_guidance** — Recommend what pages should exist
|
|
151
|
+
- **preview_generation** — Show what a tool will do before running
|
|
152
|
+
- **generate_dependency_graph** — Build the import/shared-table graph
|
|
162
153
|
|
|
163
154
|
## Storage Layout
|
|
164
155
|
|
|
@@ -175,20 +166,25 @@ docuflow rewiki # apply (backs up wiki first)
|
|
|
175
166
|
├── index.md Auto-maintained catalog
|
|
176
167
|
└── log.md Operation log
|
|
177
168
|
\`\`\`
|
|
178
|
-
|
|
169
|
+
<!-- END DOCUFLOW -->`;
|
|
179
170
|
}
|
|
180
171
|
async function writeClaudeMd(projectDir) {
|
|
181
172
|
const claudeMdPath = node_path_1.default.join(projectDir, "CLAUDE.md");
|
|
182
173
|
const newSection = buildClaudeMd(projectDir);
|
|
183
174
|
if (node_fs_1.default.existsSync(claudeMdPath)) {
|
|
184
175
|
const existing = await promises_1.default.readFile(claudeMdPath, "utf8");
|
|
185
|
-
if (existing.includes("
|
|
186
|
-
//
|
|
176
|
+
if (existing.includes("<!-- BEGIN DOCUFLOW -->") && existing.includes("<!-- END DOCUFLOW -->")) {
|
|
177
|
+
// Marker-based replacement — idempotent re-runs preserve surrounding content
|
|
178
|
+
const replaced = existing.replace(/<!-- BEGIN DOCUFLOW -->[\s\S]*?<!-- END DOCUFLOW -->/, newSection.trimEnd());
|
|
179
|
+
await promises_1.default.writeFile(claudeMdPath, replaced, "utf8");
|
|
180
|
+
}
|
|
181
|
+
else if (existing.includes("DocuFlow")) {
|
|
182
|
+
// Old format without markers — replace old DocuFlow section, add markers this time
|
|
187
183
|
const withoutDocuflow = existing.replace(/\n?# DocuFlow[\s\S]*/, "").trimEnd();
|
|
188
184
|
await promises_1.default.writeFile(claudeMdPath, withoutDocuflow + "\n\n" + newSection, "utf8");
|
|
189
185
|
}
|
|
190
186
|
else {
|
|
191
|
-
//
|
|
187
|
+
// No DocuFlow section yet — append
|
|
192
188
|
await promises_1.default.appendFile(claudeMdPath, "\n\n" + newSection, "utf8");
|
|
193
189
|
}
|
|
194
190
|
}
|