@doquflow/cli 0.4.5 → 0.5.1

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.
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildClaudeMd = buildClaudeMd;
7
+ exports.buildAgentsMd = buildAgentsMd;
7
8
  exports.run = run;
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const promises_1 = __importDefault(require("node:fs/promises"));
@@ -32,6 +33,9 @@ function getVSCodeMcpConfigPath() {
32
33
  function getCopilotCliMcpConfigPath() {
33
34
  return node_path_1.default.join(node_os_1.default.homedir(), ".copilot", "mcp-config.json");
34
35
  }
36
+ function getCodexConfigPath() {
37
+ return node_path_1.default.join(node_os_1.default.homedir(), ".codex", "config.toml");
38
+ }
35
39
  function resolveServerBin() {
36
40
  // Try npm-installed package first
37
41
  try {
@@ -161,10 +165,98 @@ async function writeClaudeMd(projectDir) {
161
165
  await promises_1.default.writeFile(claudeMdPath, newSection, "utf8");
162
166
  }
163
167
  }
168
+ function buildAgentsMd(projectDir) {
169
+ return `# DocuFlow — AI Documentation Assistant
170
+
171
+ DocuFlow is an MCP server that provides structured access to this codebase and maintains a living wiki.
172
+ It is registered via \`.codex/config.toml\` and available as MCP tools in every Codex session.
173
+
174
+ ## Available MCP Tools
175
+
176
+ ### Codebase Scanner
177
+ - **read_module** — Analyse a single file: language, classes, functions, dependencies, DB tables, endpoints, config refs, raw content.
178
+ - \`read_module({ path: "src/UserService.cs" })\`
179
+ - **list_modules** — Walk a directory, extract facts for every file. One call to understand the whole project.
180
+ - \`list_modules({ path: "${projectDir}" })\`
181
+ - **write_spec** — Save a markdown spec to \`.docuflow/specs/<name>.md\`.
182
+ - \`write_spec({ project_path: "${projectDir}", filename: "UserService", content: "..." })\`
183
+ - **read_specs** — Read saved specs, optionally filtered by name.
184
+ - \`read_specs({ project_path: "${projectDir}" })\`
185
+
186
+ ### Wiki Pipeline
187
+ - **ingest_source** — Ingest a markdown file from \`.docuflow/sources/\` into the wiki (entities, concepts).
188
+ - **update_index** — Rebuild \`.docuflow/index.md\` from all wiki pages.
189
+ - **list_wiki** — List all wiki pages by category (entity/concept/timeline/synthesis).
190
+ - **wiki_search** — BM25 search across all wiki pages.
191
+ - **query_wiki** — Q&A: searches wiki, synthesises an answer, returns citations.
192
+ - \`query_wiki({ project_path: "${projectDir}", question: "How does auth work?" })\`
193
+ - **synthesize_answer** — Generate a markdown synthesis from a list of page IDs.
194
+ - **save_answer_as_page** — Persist a synthesis as a wiki page.
195
+
196
+ ### Health & Guidance
197
+ - **lint_wiki** — Health check: orphan pages, broken refs, stale content. Returns a 0–100 health score.
198
+ - **get_schema_guidance** — Recommend what wiki pages should exist based on schema + current state.
199
+ - **preview_generation** — Preview what a tool will generate before running it.
200
+
201
+ ## Common Workflows
202
+
203
+ Start here — understand the codebase:
204
+ \`\`\`
205
+ list_modules({ path: "${projectDir}" })
206
+ → write_spec for important modules
207
+ \`\`\`
208
+
209
+ Answer a question:
210
+ \`\`\`
211
+ query_wiki({ project_path: "${projectDir}", question: "..." })
212
+ \`\`\`
213
+
214
+ Maintain wiki health:
215
+ \`\`\`
216
+ lint_wiki({ project_path: "${projectDir}" })
217
+ \`\`\`
218
+
219
+ ## Storage Layout
220
+
221
+ \`\`\`
222
+ .docuflow/
223
+ ├── specs/ Code specs written by write_spec
224
+ ├── wiki/ LLM-generated wiki pages
225
+ │ ├── entities/
226
+ │ ├── concepts/
227
+ │ ├── timelines/
228
+ │ └── syntheses/
229
+ ├── sources/ Raw markdown docs to ingest
230
+ ├── schema.md Wiki configuration (edit to customise)
231
+ ├── index.md Auto-maintained catalog
232
+ └── log.md Operation log
233
+ \`\`\`
234
+ `;
235
+ }
236
+ async function writeAgentsMd(projectDir) {
237
+ const agentsMdPath = node_path_1.default.join(projectDir, "AGENTS.md");
238
+ const newSection = buildAgentsMd(projectDir);
239
+ if (node_fs_1.default.existsSync(agentsMdPath)) {
240
+ const existing = await promises_1.default.readFile(agentsMdPath, "utf8");
241
+ if (existing.includes("DocuFlow")) {
242
+ // Replace existing DocuFlow section
243
+ const withoutDocuflow = existing.replace(/\n?# DocuFlow[\s\S]*/, "").trimEnd();
244
+ await promises_1.default.writeFile(agentsMdPath, withoutDocuflow + "\n\n" + newSection, "utf8");
245
+ }
246
+ else {
247
+ // Append to existing AGENTS.md
248
+ await promises_1.default.appendFile(agentsMdPath, "\n\n" + newSection, "utf8");
249
+ }
250
+ }
251
+ else {
252
+ await promises_1.default.writeFile(agentsMdPath, newSection, "utf8");
253
+ }
254
+ }
164
255
  async function run() {
165
256
  const configPath = getClaudeDesktopConfigPath();
166
257
  const vscodeConfigPath = getVSCodeMcpConfigPath();
167
258
  const copilotCliConfigPath = getCopilotCliMcpConfigPath();
259
+ const codexConfigPath = getCodexConfigPath();
168
260
  const serverBin = resolveServerBin();
169
261
  const nodeBin = process.execPath;
170
262
  // Register in Claude Desktop config
@@ -223,6 +315,24 @@ async function run() {
223
315
  catch {
224
316
  // Copilot CLI not installed — skip silently
225
317
  }
318
+ // Register in OpenAI Codex CLI (~/.codex/config.toml in TOML format)
319
+ let codexCliRegistered = false;
320
+ try {
321
+ await promises_1.default.mkdir(node_path_1.default.dirname(codexConfigPath), { recursive: true });
322
+ let tomlContent = "";
323
+ try {
324
+ tomlContent = await promises_1.default.readFile(codexConfigPath, "utf8");
325
+ }
326
+ catch { /* new file */ }
327
+ if (!tomlContent.includes("[mcp_servers.docuflow]")) {
328
+ const entry = `\n[mcp_servers.docuflow]\ncommand = "${nodeBin}"\nargs = [${JSON.stringify(serverBin)}]\n`;
329
+ await promises_1.default.writeFile(codexConfigPath, tomlContent + entry, "utf8");
330
+ }
331
+ codexCliRegistered = true;
332
+ }
333
+ catch {
334
+ // Codex CLI not installed — skip silently
335
+ }
226
336
  // Create .docuflow/ directory structure
227
337
  const projectDir = process.cwd();
228
338
  const docuflowDir = node_path_1.default.join(projectDir, ".docuflow");
@@ -245,6 +355,8 @@ async function run() {
245
355
  await copyTemplateFile("log.md", node_path_1.default.join(docuflowDir, "log.md"));
246
356
  // Generate CLAUDE.md so Claude Code picks up DocuFlow automatically
247
357
  await writeClaudeMd(projectDir);
358
+ // Generate AGENTS.md so OpenAI Codex picks up DocuFlow automatically
359
+ await writeAgentsMd(projectDir);
248
360
  // Write .vscode/mcp.json for project-level workspace MCP config (shareable via git)
249
361
  // Uses npx so it works on any machine — safe to commit
250
362
  const vscodeDirPath = node_path_1.default.join(projectDir, ".vscode");
@@ -274,6 +386,8 @@ async function run() {
274
386
  await promises_1.default.appendFile(gitignorePath, "\n# Docuflow\n.docuflow/\n");
275
387
  }
276
388
  }
389
+ // Install git post-commit hook (auto-sync on every commit)
390
+ await installGitHook(projectDir);
277
391
  console.log("\u2713 DocuFlow initialised successfully.");
278
392
  console.log("");
279
393
  console.log("\ud83d\udcc1 Structure created:");
@@ -289,18 +403,75 @@ async function run() {
289
403
  console.log(` \u251c\u2500\u2500 index.md (auto-maintained catalog)`);
290
404
  console.log(` \u2514\u2500\u2500 log.md (operation log)`);
291
405
  console.log("");
292
- console.log("\ud83d\udcdd CLAUDE.md:");
293
- console.log(` Generated at: ${node_path_1.default.join(projectDir, "CLAUDE.md")}`);
294
- console.log(` Claude Code reads DocuFlow tool instructions automatically.`);
406
+ console.log("\ud83d\udcdd Instruction files:");
407
+ console.log(` CLAUDE.md ✓ ${node_path_1.default.join(projectDir, "CLAUDE.md")}`);
408
+ console.log(` AGENTS.md ✓ ${node_path_1.default.join(projectDir, "AGENTS.md")}`);
295
409
  console.log("");
296
410
  console.log("\ud83d\udd27 MCP Registration:");
297
411
  console.log(` Claude Desktop: \u2713 registered`);
298
412
  console.log(` VS Code Copilot: ${vscodeRegistered ? "\u2713 registered (user-level)" : "\u2014 not detected"}`);
299
413
  console.log(` Copilot CLI: ${copilotCliRegistered ? "\u2713 registered (~/.copilot/mcp-config.json)" : "\u2014 not detected"}`);
414
+ console.log(` Codex CLI: ${codexCliRegistered ? "\u2713 registered (~/.codex/config.toml)" : "\u2014 not detected"}`);
300
415
  console.log(` Workspace: \u2713 .vscode/mcp.json written (commit to share with team)`);
301
416
  console.log("");
302
417
  console.log("\ud83d\udcd6 Next steps:");
303
418
  console.log(" 1. Edit .docuflow/schema.md to customize your wiki domain");
304
419
  console.log(" 2. Add markdown docs to .docuflow/sources/ then ingest them");
305
420
  console.log(" 3. Restart Claude Desktop / reload VS Code / restart Copilot CLI");
421
+ console.log("");
422
+ console.log("\u26a1 Auto-sync options:");
423
+ console.log(" docuflow watch # background daemon (watches for file changes)");
424
+ console.log(" docuflow watch --ai # + Claude/Codex documents code changes automatically");
425
+ console.log(" docuflow sync # one-shot sync (good for CI/CD)");
426
+ console.log(" docuflow sync --ai # + AI generates docs from last git commit");
427
+ console.log("");
428
+ console.log(" A git post-commit hook was installed at .git/hooks/post-commit");
429
+ console.log(" It runs \"docuflow sync --ai --quiet\" after every commit automatically.");
430
+ }
431
+ /**
432
+ * Install a git post-commit hook that runs `docuflow sync` automatically.
433
+ * Idempotent — does not overwrite a hook that already has DocuFlow content.
434
+ */
435
+ async function installGitHook(projectDir) {
436
+ const gitDir = node_path_1.default.join(projectDir, ".git");
437
+ if (!node_fs_1.default.existsSync(gitDir))
438
+ return; // not a git repo
439
+ const hooksDir = node_path_1.default.join(gitDir, "hooks");
440
+ await promises_1.default.mkdir(hooksDir, { recursive: true });
441
+ const hookPath = node_path_1.default.join(hooksDir, "post-commit");
442
+ const hookMarker = "# docuflow-auto-sync";
443
+ // Don't overwrite if already installed
444
+ if (node_fs_1.default.existsSync(hookPath)) {
445
+ const existing = await promises_1.default.readFile(hookPath, "utf8");
446
+ if (existing.includes(hookMarker))
447
+ return;
448
+ // Append to existing hook
449
+ const appendContent = [
450
+ "",
451
+ hookMarker,
452
+ `# Auto-generated by docuflow init — runs wiki sync after every commit`,
453
+ `# Requires Claude CLI, Codex CLI, or ANTHROPIC_API_KEY for AI-powered doc generation`,
454
+ `# Remove the lines below to disable auto-sync`,
455
+ `if command -v docuflow &> /dev/null; then`,
456
+ ` docuflow sync --ai --quiet &`,
457
+ `fi`,
458
+ ].join("\n");
459
+ await promises_1.default.appendFile(hookPath, appendContent + "\n");
460
+ return;
461
+ }
462
+ // Create new hook file
463
+ const hookContent = [
464
+ `#!/bin/sh`,
465
+ hookMarker,
466
+ `# Auto-generated by docuflow init`,
467
+ `# Syncs the DocuFlow wiki after every commit using Claude / Codex / Anthropic API`,
468
+ `# AI bridge priority: Claude CLI > Codex CLI > ANTHROPIC_API_KEY`,
469
+ `# Run in background (&) so it never delays your git workflow`,
470
+ `if command -v docuflow &> /dev/null; then`,
471
+ ` docuflow sync --ai --quiet &`,
472
+ `fi`,
473
+ ].join("\n");
474
+ await promises_1.default.writeFile(hookPath, hookContent + "\n", "utf8");
475
+ // Make executable
476
+ node_fs_1.default.chmodSync(hookPath, 0o755);
306
477
  }
@@ -0,0 +1,361 @@
1
+ "use strict";
2
+ /**
3
+ * docuflow sync
4
+ *
5
+ * One-shot synchronisation — re-ingests all sources, rebuilds the index,
6
+ * and runs a lint health check. Designed for CI/CD pipelines and git hooks.
7
+ *
8
+ * Usage:
9
+ * docuflow sync # sync all sources, rebuild index, lint
10
+ * docuflow sync --ai # also call Claude/Codex for changed code
11
+ * docuflow sync --since-commit HEAD~1 # only process files changed in last commit
12
+ * docuflow sync --source myfile.md # sync a single source file
13
+ * docuflow sync --no-lint # skip health check (faster for CI)
14
+ *
15
+ * Exit codes:
16
+ * 0 — success, wiki is healthy (score ≥ 70)
17
+ * 1 — wiki has issues (score < 70) — use to fail CI
18
+ * 2 — fatal error (server tools not found, .docuflow missing)
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.run = run;
25
+ const node_fs_1 = __importDefault(require("node:fs"));
26
+ const promises_1 = __importDefault(require("node:fs/promises"));
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const node_child_process_1 = require("node:child_process");
29
+ const watch_1 = require("./watch");
30
+ // ─── Colour helpers ────────────────────────────────────────────────────────────
31
+ const c = {
32
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
33
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
34
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
35
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
36
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
37
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
38
+ };
39
+ // ─── Dynamic server tool loader ────────────────────────────────────────────────
40
+ function loadServerTool(toolFile) {
41
+ const candidates = [
42
+ () => require(`@doquflow/server/dist/tools/${toolFile}`),
43
+ () => require(node_path_1.default.resolve(__dirname, "../../../server/dist/tools", toolFile)),
44
+ () => require(node_path_1.default.resolve(__dirname, "../../server/dist/tools", toolFile)),
45
+ ];
46
+ for (const attempt of candidates) {
47
+ try {
48
+ return attempt();
49
+ }
50
+ catch { }
51
+ }
52
+ throw new Error(`Cannot load server tool "${toolFile}". Run "npm run build" first.`);
53
+ }
54
+ // ─── Git helpers ───────────────────────────────────────────────────────────────
55
+ /**
56
+ * Get list of source files changed since a given git ref.
57
+ * Returns paths relative to projectPath.
58
+ */
59
+ function getGitChangedFiles(projectPath, sinceRef) {
60
+ try {
61
+ const output = (0, node_child_process_1.execSync)(`git diff --name-only ${sinceRef} HEAD`, {
62
+ cwd: projectPath,
63
+ encoding: "utf8",
64
+ }).trim();
65
+ return output ? output.split("\n").filter(Boolean) : [];
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ }
71
+ /**
72
+ * Get files staged in the current commit (for post-commit hook use).
73
+ */
74
+ function getLastCommitFiles(projectPath) {
75
+ try {
76
+ const output = (0, node_child_process_1.execSync)("git diff-tree --no-commit-id -r --name-only HEAD", {
77
+ cwd: projectPath,
78
+ encoding: "utf8",
79
+ }).trim();
80
+ return output ? output.split("\n").filter(Boolean) : [];
81
+ }
82
+ catch {
83
+ return [];
84
+ }
85
+ }
86
+ // ─── AI bridge for sync (uses watch.ts bridges) ───────────────────────────────
87
+ function buildSyncPrompt(projectPath, changedFiles) {
88
+ const fileList = changedFiles.length > 0
89
+ ? changedFiles.slice(0, 10).map(f => `- ${f}`).join("\n")
90
+ : "(full sync — no specific changed files)";
91
+ return [
92
+ `You are the DocuFlow wiki maintainer for: ${projectPath}`,
93
+ ``,
94
+ `Files recently committed/changed:`,
95
+ fileList,
96
+ ``,
97
+ `Use docuflow MCP tools in order:`,
98
+ `1. list_wiki({ project_path: "${projectPath}" }) — note page count`,
99
+ `2. ingest_source for each .docuflow/sources/ file relevant to the changes`,
100
+ ` (if unsure, ingest all source files)`,
101
+ `3. update_index({ project_path: "${projectPath}" })`,
102
+ `4. lint_wiki({ project_path: "${projectPath}", check_type: "all" })`,
103
+ `5. Report: pages before/after, health score, high-severity issues.`,
104
+ ``,
105
+ `Be concise. Just execute and report.`,
106
+ ].join("\n");
107
+ }
108
+ /**
109
+ * Run Copilot CLI — directly calls DocuFlow MCP tools.
110
+ * Returns the assistant's text response or null.
111
+ */
112
+ function runCopilotSync(prompt) {
113
+ const result = (0, node_child_process_1.spawnSync)("copilot", ["--prompt", prompt, "--allow-all-tools", "--allow-all-paths", "--no-ask-user", "--output-format", "json"], { encoding: "utf8", timeout: 180_000 });
114
+ if (result.error || result.status !== 0)
115
+ return null;
116
+ let lastMessage = null;
117
+ for (const line of (result.stdout ?? "").split("\n")) {
118
+ try {
119
+ const obj = JSON.parse(line.trim());
120
+ if (obj.type === "assistant.message" && obj.data?.content)
121
+ lastMessage = obj.data.content;
122
+ }
123
+ catch { }
124
+ }
125
+ return lastMessage;
126
+ }
127
+ /**
128
+ * Run Claude CLI — directly calls DocuFlow MCP tools.
129
+ * Pass allowDangerousPermissions=true (via --allow-dangerous-permissions CLI flag)
130
+ * to skip interactive permission prompts for non-interactive use.
131
+ * Without it Claude CLI may prompt for tool access and time out in a daemon context.
132
+ * Injects docuflow MCP config explicitly via --mcp-config.
133
+ */
134
+ function runClaudeSync(prompt, projectPath, allowDangerousPermissions = false) {
135
+ // Build the MCP config pointing to the local server binary
136
+ let serverBin;
137
+ try {
138
+ serverBin = require.resolve("@doquflow/server/dist/index.js");
139
+ }
140
+ catch {
141
+ serverBin = node_path_1.default.resolve(__dirname, "../../server/dist/index.js");
142
+ }
143
+ const mcpConfig = JSON.stringify({
144
+ mcpServers: {
145
+ docuflow: { type: "stdio", command: process.execPath, args: [serverBin] }
146
+ }
147
+ });
148
+ const env = { ...process.env };
149
+ const claudeArgs = ["--print", "--mcp-config", mcpConfig];
150
+ if (allowDangerousPermissions) {
151
+ claudeArgs.splice(1, 0, "--dangerously-skip-permissions");
152
+ }
153
+ const result = (0, node_child_process_1.spawnSync)("claude", claudeArgs, { input: prompt, encoding: "utf8", timeout: 180_000, env });
154
+ const stdout = result.stdout?.trim() ?? "";
155
+ // Filter out Claude auth errors
156
+ if (!stdout || stdout.includes("Invalid API key") || stdout.includes("authentication"))
157
+ return null;
158
+ return stdout || null;
159
+ }
160
+ /**
161
+ * Fallback for codex/api: generate markdown doc from changed files, then ingest.
162
+ */
163
+ async function generateAndIngest(projectPath, changedFiles, bridge, info) {
164
+ const fileList = changedFiles.map(f => `- ${f}`).join("\n");
165
+ const prompt = [
166
+ `You are maintaining documentation for a software project.`,
167
+ `These files changed:\n${fileList}`,
168
+ ``,
169
+ `Write a concise markdown document (200-500 words) capturing what changed.`,
170
+ `Markdown only. Start with # heading. No preamble.`,
171
+ ].join("\n");
172
+ let docContent = null;
173
+ if (bridge === "codex") {
174
+ const r = (0, node_child_process_1.spawnSync)("codex", [prompt], { encoding: "utf8", timeout: 90_000 });
175
+ docContent = r.status === 0 ? r.stdout?.trim() ?? null : null;
176
+ }
177
+ else if (bridge === "api" && process.env.ANTHROPIC_API_KEY) {
178
+ const https = require("https");
179
+ const body = JSON.stringify({ model: "claude-3-5-haiku-20241022", max_tokens: 1024, messages: [{ role: "user", content: prompt }] });
180
+ docContent = await new Promise((resolve) => {
181
+ const req = https.request({ hostname: "api.anthropic.com", path: "/v1/messages", method: "POST",
182
+ headers: { "Content-Type": "application/json", "x-api-key": process.env.ANTHROPIC_API_KEY, "anthropic-version": "2023-06-01", "Content-Length": Buffer.byteLength(body) } }, (res) => { let d = ""; res.on("data", (ch) => d += ch); res.on("end", () => { try {
183
+ resolve(JSON.parse(d)?.content?.[0]?.text ?? null);
184
+ }
185
+ catch {
186
+ resolve(null);
187
+ } }); });
188
+ req.on("error", () => resolve(null));
189
+ req.setTimeout(90_000, () => { req.destroy(); resolve(null); });
190
+ req.write(body);
191
+ req.end();
192
+ });
193
+ }
194
+ if (!docContent) {
195
+ info(" ⚠ AI returned no content — skipping AI doc generation");
196
+ return;
197
+ }
198
+ const sourcesDir = node_path_1.default.join(projectPath, ".docuflow", "sources");
199
+ await promises_1.default.mkdir(sourcesDir, { recursive: true });
200
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
201
+ const autoFilename = `auto_sync_${timestamp}.md`;
202
+ await promises_1.default.writeFile(node_path_1.default.join(sourcesDir, autoFilename), docContent, "utf8");
203
+ info(` ✅ AI doc saved → ${autoFilename}`);
204
+ }
205
+ async function run(options = {}) {
206
+ const projectPath = node_path_1.default.resolve(options.projectPath ?? process.cwd());
207
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
208
+ const sourcesDir = node_path_1.default.join(docuDir, "sources");
209
+ const quiet = options.quiet ?? false;
210
+ const failThreshold = options.failOnScore ?? 70;
211
+ function info(msg) {
212
+ if (!quiet)
213
+ console.log(msg);
214
+ }
215
+ if (!node_fs_1.default.existsSync(docuDir)) {
216
+ console.error(c.red(` ✗ .docuflow/ not found at ${projectPath}`));
217
+ console.error(` Run "docuflow init" first.`);
218
+ process.exit(2);
219
+ }
220
+ info(c.bold("\n 🔄 DocuFlow Sync\n"));
221
+ const startTime = Date.now();
222
+ let skipManualSync = false; // set true when Copilot/Claude handle everything
223
+ // ── Step 1: Determine which source files to process ─────────────────────────
224
+ let sourceFilesToProcess = [];
225
+ if (options.sourceFile) {
226
+ // Single file mode
227
+ sourceFilesToProcess = [options.sourceFile];
228
+ info(` 📄 Single file mode: ${c.cyan(options.sourceFile)}`);
229
+ }
230
+ else {
231
+ // Scan all .md files in sources/
232
+ try {
233
+ const all = await promises_1.default.readdir(sourcesDir);
234
+ sourceFilesToProcess = all.filter((f) => f.endsWith(".md"));
235
+ }
236
+ catch {
237
+ info(c.yellow(` ⚠ No sources/ directory found — nothing to ingest`));
238
+ sourceFilesToProcess = [];
239
+ }
240
+ info(` 📚 Found ${sourceFilesToProcess.length} source file(s) to ingest`);
241
+ }
242
+ // ── Step 2: AI-powered sync for code changes ─────────────────────────────
243
+ if (options.ai) {
244
+ const codeExts = /\.(ts|tsx|js|jsx|mjs|py|go|rb|java|cs|php|rs|kt|swift)$/;
245
+ const changedCodeFiles = options.sinceCommit
246
+ ? getGitChangedFiles(projectPath, options.sinceCommit).filter(f => !f.startsWith(".docuflow") && codeExts.test(f))
247
+ : getLastCommitFiles(projectPath).filter(f => !f.startsWith(".docuflow") && codeExts.test(f));
248
+ const bridge = (0, watch_1.detectBridge)({ useAI: true, forceCopilot: options.forceCopilot, forceClaude: options.forceClaude, forceCodex: options.forceCodex });
249
+ const sinceLabel = options.sinceCommit ?? "last commit";
250
+ if (changedCodeFiles.length > 0) {
251
+ info(`\n 🤖 ${changedCodeFiles.length} code file(s) changed (${sinceLabel})`);
252
+ for (const f of changedCodeFiles.slice(0, 5))
253
+ info(c.dim(` ${f}`));
254
+ if (bridge === "copilot") {
255
+ info(` ⚡ Copilot will directly call DocuFlow MCP tools (ingest + index + lint)...`);
256
+ const prompt = buildSyncPrompt(projectPath, changedCodeFiles);
257
+ const result = runCopilotSync(prompt);
258
+ if (result) {
259
+ info(c.green(` ✅ Copilot completed wiki sync via MCP tools`));
260
+ info(c.dim(` ${result.replace(/\n/g, "\n ")}`));
261
+ // Copilot handled everything — skip manual steps below
262
+ skipManualSync = true;
263
+ }
264
+ else {
265
+ info(c.yellow(` ⚠ Copilot returned no result — continuing with direct sync`));
266
+ }
267
+ }
268
+ else if (bridge === "claude") {
269
+ info(` ⚡ Claude will directly call DocuFlow MCP tools (ingest + index + lint)...`);
270
+ const prompt = buildSyncPrompt(projectPath, changedCodeFiles);
271
+ const result = runClaudeSync(prompt, projectPath, options.allowDangerousPermissions);
272
+ if (result) {
273
+ info(c.green(` ✅ Claude completed wiki sync via MCP tools`));
274
+ info(c.dim(` ${result.replace(/\n/g, "\n ")}`));
275
+ skipManualSync = true;
276
+ }
277
+ else {
278
+ info(c.yellow(` ⚠ Claude returned no result — continuing with direct sync`));
279
+ }
280
+ }
281
+ else if (bridge !== "none") {
282
+ // codex or api: generate doc → add to sources
283
+ await generateAndIngest(projectPath, changedCodeFiles, bridge, info);
284
+ const sourcesAgain = await promises_1.default.readdir(sourcesDir).catch(() => []);
285
+ sourceFilesToProcess = sourcesAgain.filter(f => f.endsWith(".md"));
286
+ }
287
+ }
288
+ else {
289
+ info(c.dim(` No code files changed since ${sinceLabel}`));
290
+ }
291
+ }
292
+ // ── Step 3: Ingest + Step 4: Rebuild (skip if Copilot/Claude handled via MCP) ──
293
+ let totalPagesCreated = 0;
294
+ let ingestErrors = 0;
295
+ if (!skipManualSync) {
296
+ const { ingestSource } = loadServerTool("ingest-source");
297
+ const { updateIndex } = loadServerTool("update-index");
298
+ if (sourceFilesToProcess.length > 0) {
299
+ info(`\n 📥 Ingesting sources...`);
300
+ }
301
+ for (const filename of sourceFilesToProcess) {
302
+ try {
303
+ const result = await ingestSource({ project_path: projectPath, source_filename: filename });
304
+ const created = result.pages_created?.length ?? 0;
305
+ totalPagesCreated += created;
306
+ info(` ${created > 0 ? c.green("✓") : c.yellow("~")} ${filename} → ${created} page(s)`);
307
+ }
308
+ catch (e) {
309
+ info(c.red(` ✗ ${filename}: ${e.message}`));
310
+ ingestErrors++;
311
+ }
312
+ }
313
+ info(`\n 📋 Rebuilding index...`);
314
+ const indexResult = await updateIndex({ project_path: projectPath });
315
+ info(c.green(` ✅ Index rebuilt — ${indexResult.entries_indexed} entries`));
316
+ }
317
+ else {
318
+ info(c.dim(` ℹ️ Ingest/index handled by Copilot/Claude via MCP tools directly`));
319
+ }
320
+ // ── Step 5: Lint ─────────────────────────────────────────────────────────────
321
+ let healthScore = 100;
322
+ if (!options.noLint) {
323
+ info(`\n 🔍 Running wiki health check...`);
324
+ const { lintWiki } = loadServerTool("lint-wiki");
325
+ const lintResult = await lintWiki({ project_path: projectPath, check_type: "all" });
326
+ healthScore = lintResult.health_score ?? 100;
327
+ const scoreLabel = healthScore >= 90
328
+ ? c.green(`${healthScore}/100`)
329
+ : healthScore >= 70
330
+ ? c.yellow(`${healthScore}/100`)
331
+ : c.red(`${healthScore}/100`);
332
+ info(` 📊 Wiki health: ${scoreLabel}`);
333
+ if (lintResult.issues_found?.length > 0) {
334
+ const high = lintResult.issues_found.filter((i) => i.severity === "high").length;
335
+ const med = lintResult.issues_found.filter((i) => i.severity === "medium").length;
336
+ const low = lintResult.issues_found.filter((i) => i.severity === "low").length;
337
+ info(` 🔴 High: ${high} 🟡 Medium: ${med} 🟢 Low: ${low}`);
338
+ }
339
+ for (const rec of lintResult.recommendations?.slice(0, 3) ?? []) {
340
+ info(c.dim(` → ${rec}`));
341
+ }
342
+ }
343
+ // ── Summary ──────────────────────────────────────────────────────────────────
344
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
345
+ info(`\n ─────────────────────────────────────────────`);
346
+ info(` Sources processed: ${sourceFilesToProcess.length}`);
347
+ info(` Pages created: ${totalPagesCreated}`);
348
+ info(` Ingest errors: ${ingestErrors > 0 ? c.red(String(ingestErrors)) : "0"}`);
349
+ if (!options.noLint) {
350
+ info(` Health score: ${healthScore}/100`);
351
+ }
352
+ info(` Duration: ${elapsed}s`);
353
+ info(` ─────────────────────────────────────────────\n`);
354
+ // Exit with code 1 if health is below threshold (useful for CI)
355
+ if (!options.noLint && healthScore < failThreshold) {
356
+ process.exit(1);
357
+ }
358
+ if (ingestErrors > 0) {
359
+ process.exit(1);
360
+ }
361
+ }