@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.
@@ -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
+ }
@@ -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 package first
65
+ // Try npm-installed studio MCP binary first
66
66
  try {
67
- return require.resolve("@doquflow/server/dist/index.js");
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, "..", "..", "server", "dist", "index.js");
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 `# DocuFlow AI Documentation Assistant
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
- - **read_module** Analyse a single source file. Returns language, classes, functions, dependencies, DB tables, endpoints, config refs, and raw content (first 8 KB).
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
- ## Wiki Pipeline Tools
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
- ## Health & Guidance Tools
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
- - **lint_wiki**Health check: orphan pages, broken refs, stale content, metadata gaps. Returns a 0–100 health score.
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
+ ## CLICore Commands
135
117
 
136
- ## Common Workflows
137
-
138
- ### First time — understand the codebase
139
118
  \`\`\`
140
- list_modules({ path: "${projectDir}" })
141
- read the language breakdown and dependency map
142
- write_spec each important module
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
- ### Ongoing — answer a question
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
- ### Maintenance check wiki health
133
+ ### Add new context
152
134
  \`\`\`
153
- lint_wiki({ project_path: "${projectDir}" })
154
- fix orphans and broken refs
135
+ # drop a markdown file in .docuflow/sources/
136
+ ingest_source({ project_path: "${projectDir}", source_filename: "auth-design.md" })
155
137
  \`\`\`
156
138
 
157
- ### Maintenance — re-ingest with updated rules
158
- \`\`\`
159
- docuflow rewiki --dry-run # preview cleanup
160
- docuflow rewiki # apply (backs up wiki first)
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("DocuFlow")) {
186
- // Already has DocuFlow section replace it
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
- // Append to existing CLAUDE.md
187
+ // No DocuFlow section yet — append
192
188
  await promises_1.default.appendFile(claudeMdPath, "\n\n" + newSection, "utf8");
193
189
  }
194
190
  }