doc-syncer 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrii Orlov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # doc-sync
2
+
3
+ AI-powered documentation sync using Claude Code or Codex.
4
+
5
+ ```
6
+ Code changes (PR/branch) → Agent analyzes → Docs updated
7
+ ```
8
+
9
+ ## Install
10
+
11
+ ### Via npm (recommended)
12
+
13
+ ```bash
14
+ npm install -g doc-syncer
15
+ ```
16
+
17
+ ### From source
18
+
19
+ ```bash
20
+ git clone https://github.com/orlan0045/doc-syncer.git
21
+ cd doc-syncer
22
+ bun install
23
+ ```
24
+
25
+ ## Prerequisites
26
+
27
+ You need one of these AI agents installed:
28
+
29
+ ```bash
30
+ # Option 1: Claude Code CLI (agent: claude)
31
+ npm install -g @anthropic-ai/claude-code
32
+ claude # authenticate once
33
+
34
+ # Option 2: Codex CLI (agent: codex)
35
+ # Follow Codex CLI installation instructions
36
+ ```
37
+
38
+ ## Setup
39
+
40
+ Download the example config and customize it:
41
+
42
+ ```bash
43
+ # Download example config
44
+ curl -o doc-syncer.config.yml https://raw.githubusercontent.com/orlan0045/doc-syncer/main/doc-syncer.config.example.yml
45
+
46
+ # Edit with your repo paths
47
+ nano doc-syncer.config.yml
48
+ ```
49
+
50
+ Example configuration:
51
+
52
+ ```yaml
53
+ agent: claude
54
+ base_branch: main
55
+
56
+ modes:
57
+ frontend:
58
+ default: true
59
+ code_repo: /path/to/your/code-repo
60
+ docs_repo: /path/to/your/docs-repo
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ### Option 1: Config file (recommended)
66
+
67
+ Run:
68
+
69
+ ```bash
70
+ doc-syncer sync # run it
71
+ doc-syncer sync --dry-run # preview only
72
+ doc-syncer sync --mode backend # use specific mode from config
73
+ ```
74
+
75
+ ### Option 2: CLI flags
76
+
77
+ ```bash
78
+ doc-syncer sync --code ~/dev/my-app --docs ~/dev/my-app-docs
79
+ doc-syncer sync --code ~/dev/my-app --docs ~/dev/my-app-docs --branch feature/xyz
80
+ ```
81
+
82
+ ## What Happens
83
+
84
+ 1. Gets git diff from your feature branch
85
+ 2. Passes diff + docs repo access to the selected agent
86
+ 3. Agent explores docs, understands style, updates what's relevant
87
+ 4. You review with `git diff`
88
+
89
+ ## Options
90
+
91
+ ```
92
+ -m, --mode Mode preset to use (from config file)
93
+ -c, --code-repo Path to code repository
94
+ -d, --docs-repo Path to documentation repository
95
+ -b, --branch Feature branch (default: current)
96
+ --base Base branch (default: main)
97
+ --config Config file (default: doc-syncer.config.yml)
98
+ --dry-run Preview without running
99
+ --agent AI agent to run (claude | codex)
100
+ -h, --help Show help
101
+ ```
102
+
103
+ ## After Running
104
+
105
+ ```bash
106
+ cd /path/to/docs-repo
107
+ git diff # review changes
108
+ git add -A && git commit -m "docs: sync with feature/xyz"
109
+ ```
@@ -0,0 +1,28 @@
1
+ # doc-syncer configuration example
2
+ # Copy this file to doc-syncer.config.yml and customize for your repos
3
+
4
+ # Which AI agent to run: claude | codex
5
+ agent: claude
6
+
7
+ # Branch to compare against
8
+ base_branch: main
9
+
10
+ # Tool permissions for the agent (default: Read, Write, Edit)
11
+ permissions:
12
+ - Read
13
+ - Write
14
+ - Edit
15
+
16
+ # Modes allow you to configure multiple code/docs repo pairs
17
+ # Use --mode <name> to select which mode to run
18
+ modes:
19
+ # Example mode 1: Frontend app
20
+ frontend:
21
+ default: true # Use this mode by default if --mode not specified
22
+ code_repo: /path/to/your/code-repo
23
+ docs_repo: /path/to/your/docs-repo
24
+
25
+ # Example mode 2: Backend API
26
+ backend:
27
+ code_repo: /path/to/your/api-repo
28
+ docs_repo: /path/to/your/api-docs-repo
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "doc-syncer",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered documentation sync using Claude Code or Codex",
5
+ "type": "module",
6
+ "author": "Andrii Orlov",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "documentation",
10
+ "sync",
11
+ "ai",
12
+ "claude",
13
+ "codex",
14
+ "docs",
15
+ "git",
16
+ "automation"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/orlan0045/doc-syncer.git"
21
+ },
22
+ "homepage": "https://github.com/orlan0045/doc-syncer#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/orlan0045/doc-syncer/issues"
25
+ },
26
+ "bin": {
27
+ "doc-syncer": "src/cli.ts"
28
+ },
29
+ "files": [
30
+ "src/",
31
+ "doc-syncer.config.example.yml",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "init": "cp doc-syncer.config.example.yml doc-syncer.config.yml",
37
+ "sync": "bun src/doc-sync.ts",
38
+ "sync:dry": "bun src/doc-sync.ts --dry-run",
39
+ "prepublishOnly": "echo 'Ready to publish'"
40
+ },
41
+ "dependencies": {
42
+ "yaml": "^2.6.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=20.0.0"
46
+ }
47
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * doc-syncer CLI entry point
5
+ *
6
+ * Usage:
7
+ * doc-syncer sync [options]
8
+ * doc-syncer --help
9
+ */
10
+
11
+ const args = Bun.argv.slice(2);
12
+ const command = args[0];
13
+
14
+ if (!command || command === "--help" || command === "-h") {
15
+ printHelp();
16
+ process.exit(0);
17
+ }
18
+
19
+ if (command === "sync") {
20
+ // Remove 'sync' from args and run the sync script
21
+ Bun.argv.splice(2, 1);
22
+ await import("./doc-sync.ts");
23
+ } else {
24
+ console.error(`\n❌ Unknown command: ${command}\n`);
25
+ printHelp();
26
+ process.exit(1);
27
+ }
28
+
29
+ function printHelp() {
30
+ console.log(`
31
+ doc-syncer: AI-powered documentation sync
32
+
33
+ Usage:
34
+ doc-syncer sync [options]
35
+
36
+ Commands:
37
+ sync Sync documentation based on code changes
38
+
39
+ Options:
40
+ -m, --mode Mode preset to use (from config file)
41
+ -c, --code-repo Path to code repository
42
+ -d, --docs-repo Path to documentation repository
43
+ -b, --branch Feature branch to analyze (default: current)
44
+ --base Base branch to diff against (default: main)
45
+ --config Path to YAML config file (default: doc-syncer.config.yml)
46
+ --dry-run Preview without running the agent
47
+ --agent AI agent to run (claude | codex)
48
+ -h, --help Show this help
49
+
50
+ Examples:
51
+ doc-syncer sync # Use default mode
52
+ doc-syncer sync --mode esign # Use specific mode
53
+ doc-syncer sync --mode esign --dry-run # Preview mode
54
+ doc-syncer sync --code ~/dev/myapp --docs ~/dev/myapp-docs
55
+ `);
56
+ }
@@ -0,0 +1,475 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * doc-sync: AI-powered documentation sync using Claude Code or Codex
5
+ *
6
+ * Usage:
7
+ * bun sync --code ~/dev/app --docs ~/dev/app-docs
8
+ * bun sync --config doc-sync.yml
9
+ * bun sync --dry-run
10
+ */
11
+
12
+ import { $ } from "bun";
13
+ import { parseArgs } from "util";
14
+ import { resolve } from "path";
15
+ import { parse as parseYaml } from "yaml";
16
+
17
+ // ============ TYPES ============
18
+
19
+ interface Config {
20
+ codeRepo: string;
21
+ docsRepo: string;
22
+ baseBranch: string;
23
+ featureBranch?: string;
24
+ dryRun: boolean;
25
+ agent: "claude" | "codex";
26
+ permissions: string[];
27
+ mode?: string;
28
+ }
29
+
30
+ // ============ CONFIG ============
31
+
32
+ async function loadConfig(): Promise<Config> {
33
+ const { values } = parseArgs({
34
+ args: Bun.argv.slice(2),
35
+ options: {
36
+ "code-repo": { type: "string", short: "c" },
37
+ "code": { type: "string" },
38
+ "docs-repo": { type: "string", short: "d" },
39
+ "docs": { type: "string" },
40
+ "branch": { type: "string", short: "b" },
41
+ "base": { type: "string" },
42
+ "config": { type: "string" },
43
+ "dry-run": { type: "boolean" },
44
+ "agent": { type: "string" },
45
+ "mode": { type: "string", short: "m" },
46
+ "help": { type: "boolean", short: "h" },
47
+ },
48
+ allowPositionals: true,
49
+ });
50
+
51
+ if (values.help) {
52
+ printHelp();
53
+ process.exit(0);
54
+ }
55
+
56
+ // Try config file first
57
+ const configPath = values.config || "doc-syncer.config.yml";
58
+ const configFile = Bun.file(configPath);
59
+
60
+ if (await configFile.exists()) {
61
+ const content = await configFile.text();
62
+ const yaml = parseYaml(content);
63
+
64
+ // Check if modes exist
65
+ if (yaml.modes && typeof yaml.modes === "object") {
66
+ const modeName = values.mode || findDefaultMode(yaml.modes);
67
+
68
+ if (!modeName) {
69
+ console.error("\n❌ No default mode found and no --mode specified.");
70
+ console.error(`\nAvailable modes: ${Object.keys(yaml.modes).join(", ")}`);
71
+ console.error(`\nSet default: true on a mode or use --mode <name>\n`);
72
+ process.exit(1);
73
+ }
74
+
75
+ const mode = yaml.modes[modeName];
76
+ if (!mode) {
77
+ console.error(`\n❌ Mode "${modeName}" not found in config.`);
78
+ console.error(`\nAvailable modes: ${Object.keys(yaml.modes).join(", ")}\n`);
79
+ process.exit(1);
80
+ }
81
+
82
+ // Merge mode config with top-level defaults
83
+ const agent = parseAgent(values.agent ?? mode.agent ?? yaml.agent);
84
+ const permissions = parsePermissions(mode.permissions ?? yaml.permissions);
85
+
86
+ return {
87
+ codeRepo: resolve(mode.code_repo || mode.codeRepo),
88
+ docsRepo: resolve(mode.docs_repo || mode.docsRepo),
89
+ baseBranch: mode.base_branch || mode.baseBranch || yaml.base_branch || yaml.baseBranch || "main",
90
+ featureBranch: values.branch || mode.feature_branch || mode.featureBranch,
91
+ dryRun: values["dry-run"] || false,
92
+ agent,
93
+ permissions,
94
+ mode: modeName,
95
+ };
96
+ }
97
+
98
+ // Fallback: no modes, use top-level config (backward compatibility)
99
+ const agent = parseAgent(values.agent ?? yaml.agent);
100
+ const permissions = parsePermissions(yaml.permissions);
101
+ return {
102
+ codeRepo: resolve(yaml.code_repo || yaml.codeRepo),
103
+ docsRepo: resolve(yaml.docs_repo || yaml.docsRepo),
104
+ baseBranch: yaml.base_branch || yaml.baseBranch || "main",
105
+ featureBranch: values.branch || yaml.feature_branch || yaml.featureBranch,
106
+ dryRun: values["dry-run"] || false,
107
+ agent,
108
+ permissions,
109
+ };
110
+ }
111
+
112
+ // CLI args
113
+ const codeRepo = values["code-repo"] || values.code;
114
+ const docsRepo = values["docs-repo"] || values.docs;
115
+ const agent = parseAgent(values.agent);
116
+
117
+ if (!codeRepo || !docsRepo) {
118
+ printHelp();
119
+ process.exit(1);
120
+ }
121
+
122
+ return {
123
+ codeRepo: resolve(codeRepo),
124
+ docsRepo: resolve(docsRepo),
125
+ baseBranch: values.base || "main",
126
+ featureBranch: values.branch,
127
+ dryRun: values["dry-run"] || false,
128
+ agent,
129
+ permissions: parsePermissions(undefined),
130
+ };
131
+ }
132
+
133
+ function findDefaultMode(modes: Record<string, any>): string | null {
134
+ for (const [name, config] of Object.entries(modes)) {
135
+ if (config.default === true) {
136
+ return name;
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+
142
+ function parseAgent(value: unknown): "claude" | "codex" {
143
+ if (!value) return "claude";
144
+ const normalized = String(value).toLowerCase();
145
+ if (normalized === "claude" || normalized === "codex") {
146
+ return normalized;
147
+ }
148
+ console.error(`\n❌ Invalid agent: ${value}. Use "claude" or "codex".`);
149
+ process.exit(1);
150
+ }
151
+
152
+ function parsePermissions(value: unknown): string[] {
153
+ const defaultPerms = ["Read", "Write", "Edit"];
154
+ if (!value) return defaultPerms;
155
+ if (Array.isArray(value)) return value.map(String);
156
+ return defaultPerms;
157
+ }
158
+
159
+ function printHelp() {
160
+ console.log(`
161
+ doc-sync: AI-powered documentation sync
162
+
163
+ Usage:
164
+ bun sync [options]
165
+ bun sync --mode <name> [options]
166
+ bun sync --code <path> --docs <path> [options]
167
+
168
+ Options:
169
+ -m, --mode Mode preset to use (from config file)
170
+ -c, --code-repo Path to code repository
171
+ -d, --docs-repo Path to documentation repository
172
+ -b, --branch Feature branch to analyze (default: current)
173
+ --base Base branch to diff against (default: main)
174
+ --config Path to YAML config file (default: doc-syncer.config.yml)
175
+ --dry-run Preview without running the agent
176
+ --agent AI agent to run (claude | codex)
177
+ -h, --help Show this help
178
+
179
+ Examples:
180
+ bun sync # Use default mode from config
181
+ bun sync --mode esign # Use specific mode
182
+ bun sync --mode esign --dry-run # Preview mode
183
+ bun sync --code ~/dev/app --docs ~/dev/app-docs
184
+ `);
185
+ }
186
+
187
+ // ============ GIT HELPERS ============
188
+
189
+ async function gitDiff(repoPath: string, base: string, branch: string): Promise<string> {
190
+ try {
191
+ const result = await $`git -C ${repoPath} diff ${base}...${branch}`.text();
192
+ return result;
193
+ } catch {
194
+ // Fallback: diff against base directly
195
+ const result = await $`git -C ${repoPath} diff ${base}`.text();
196
+ return result;
197
+ }
198
+ }
199
+
200
+ async function gitCurrentBranch(repoPath: string): Promise<string> {
201
+ const result = await $`git -C ${repoPath} branch --show-current`.text();
202
+ return result.trim();
203
+ }
204
+
205
+ async function gitChangedFiles(repoPath: string, base: string, branch: string): Promise<string[]> {
206
+ try {
207
+ const result = await $`git -C ${repoPath} diff --name-only ${base}...${branch}`.text();
208
+ return result.trim().split("\n").filter(Boolean);
209
+ } catch {
210
+ const result = await $`git -C ${repoPath} diff --name-only ${base}`.text();
211
+ return result.trim().split("\n").filter(Boolean);
212
+ }
213
+ }
214
+
215
+ async function checkIncomingChanges(repoPath: string): Promise<boolean> {
216
+ try {
217
+ // Fetch latest changes
218
+ await $`git -C ${repoPath} fetch --quiet`.quiet();
219
+
220
+ // Get current branch
221
+ const branch = await gitCurrentBranch(repoPath);
222
+
223
+ // Check if remote tracking branch exists
224
+ const hasRemote = await $`git -C ${repoPath} rev-parse --verify origin/${branch}`.quiet().nothrow();
225
+ if (hasRemote.exitCode !== 0) {
226
+ return false; // No remote branch, no incoming changes
227
+ }
228
+
229
+ // Count incoming commits
230
+ const result = await $`git -C ${repoPath} rev-list HEAD..origin/${branch} --count`.text();
231
+ const count = parseInt(result.trim(), 10);
232
+ return count > 0;
233
+ } catch {
234
+ return false; // On any error, assume no incoming changes
235
+ }
236
+ }
237
+
238
+ async function promptUser(question: string, options: string[]): Promise<string> {
239
+ console.log(`\n${question}`);
240
+ options.forEach((opt, i) => console.log(` ${i + 1}. ${opt}`));
241
+
242
+ const readline = require("readline").createInterface({
243
+ input: process.stdin,
244
+ output: process.stdout,
245
+ });
246
+
247
+ return new Promise((resolve) => {
248
+ const askQuestion = () => {
249
+ readline.question("\nYour choice (1-2): ", (answer: string) => {
250
+ const num = parseInt(answer.trim(), 10);
251
+ if (num >= 1 && num <= options.length) {
252
+ readline.close();
253
+ resolve(options[num - 1]);
254
+ } else {
255
+ process.stdout.write(`Invalid choice. Please enter 1-${options.length}.`);
256
+ askQuestion();
257
+ }
258
+ });
259
+ };
260
+ askQuestion();
261
+ });
262
+ }
263
+
264
+ // ============ AGENT ============
265
+
266
+ async function runAgent(prompt: string, cwd: string, agent: Config["agent"], permissions: string[]): Promise<string> {
267
+ let command: string[];
268
+ let useStdin = false;
269
+
270
+ if (agent === "claude") {
271
+ command = ["claude", "--print"];
272
+ for (const perm of permissions) {
273
+ command.push("--allowedTools", perm);
274
+ }
275
+ useStdin = true; // Claude accepts prompt via stdin
276
+ } else {
277
+ command = ["codex", "exec", prompt];
278
+ }
279
+
280
+ const proc = Bun.spawn(command, {
281
+ cwd,
282
+ stdin: useStdin ? "pipe" : undefined,
283
+ stdout: "pipe",
284
+ stderr: "pipe",
285
+ });
286
+
287
+ // Send prompt via stdin for claude
288
+ if (useStdin && proc.stdin) {
289
+ proc.stdin.write(prompt);
290
+ proc.stdin.end();
291
+ }
292
+
293
+ const output = await new Response(proc.stdout).text();
294
+ const exitCode = await proc.exited;
295
+
296
+ if (exitCode !== 0) {
297
+ const stderr = await new Response(proc.stderr).text();
298
+ throw new Error(`Agent exited with code ${exitCode}: ${stderr}`);
299
+ }
300
+
301
+ return output;
302
+ }
303
+
304
+ // ============ MAIN ============
305
+
306
+ async function main() {
307
+ const config = await loadConfig();
308
+ const agentLabel = config.agent === "claude" ? "Claude Code" : "Codex";
309
+
310
+ console.log(`
311
+ ╔══════════════════════════════════════════════════════════════╗
312
+ ║ doc-sync ║
313
+ ╚══════════════════════════════════════════════════════════════╝
314
+ `);
315
+ if (config.mode) {
316
+ console.log(`📋 Mode: ${config.mode}`);
317
+ }
318
+ console.log(`📁 Code: ${config.codeRepo}`);
319
+ console.log(`📄 Docs: ${config.docsRepo}`);
320
+ console.log(`🎯 Base: ${config.baseBranch}`);
321
+ console.log(`🏃 Dry run: ${config.dryRun ? "yes" : "no"}`);
322
+ console.log(`🤖 Agent: ${agentLabel}`);
323
+ console.log(`🔑 Tools: ${config.permissions.join(", ")}`);
324
+
325
+ // Validate paths
326
+ const codeExists = await Bun.file(`${config.codeRepo}/.git/HEAD`).exists();
327
+ const docsExists = await Bun.file(`${config.docsRepo}/.git/HEAD`).exists();
328
+
329
+ if (!codeExists) {
330
+ console.error(`\n❌ Code repo not found: ${config.codeRepo}`);
331
+ process.exit(1);
332
+ }
333
+ if (!docsExists) {
334
+ console.error(`\n❌ Docs repo not found: ${config.docsRepo}`);
335
+ process.exit(1);
336
+ }
337
+
338
+ // Check for incoming changes in docs repo
339
+ console.log("\n🔍 Checking for incoming changes in docs repo...");
340
+ const hasIncoming = await checkIncomingChanges(config.docsRepo);
341
+
342
+ if (hasIncoming) {
343
+ console.log("⚠️ Warning: Docs repo has incoming changes from remote!\n");
344
+ const choice = await promptUser(
345
+ "What would you like to do?",
346
+ ["Ignore and proceed anyway", "Stop and pull latest changes first"]
347
+ );
348
+
349
+ if (choice === "Stop and pull latest changes first") {
350
+ console.log("\n❌ Sync stopped.");
351
+ console.log(`\nPlease pull the latest changes first:`);
352
+ console.log(` cd ${config.docsRepo}`);
353
+ console.log(` git pull`);
354
+ console.log(`\nThen run doc-syncer again.`);
355
+ process.exit(0);
356
+ }
357
+
358
+ console.log("\n⚠️ Proceeding with local changes...\n");
359
+ } else {
360
+ console.log("✅ Docs repo is up to date\n");
361
+ }
362
+
363
+ // Get branch
364
+ const branch = config.featureBranch || await gitCurrentBranch(config.codeRepo);
365
+ console.log(`🌿 Branch: ${branch}\n`);
366
+
367
+ // Get diff
368
+ console.log("📊 Getting diff...");
369
+ const diff = await gitDiff(config.codeRepo, config.baseBranch, branch);
370
+
371
+ if (!diff.trim()) {
372
+ console.log("✅ No changes found. Nothing to document.");
373
+ process.exit(0);
374
+ }
375
+
376
+ const changedFiles = await gitChangedFiles(config.codeRepo, config.baseBranch, branch);
377
+ console.log(` ${changedFiles.length} files changed\n`);
378
+
379
+ // Build prompt
380
+ const prompt = buildPrompt(config, branch, diff, changedFiles);
381
+
382
+ if (config.dryRun) {
383
+ console.log("─".repeat(60));
384
+ console.log("DRY RUN — Would send this prompt to the agent:\n");
385
+ console.log(prompt.slice(0, 2000));
386
+ if (prompt.length > 2000) console.log(`\n... [${prompt.length} chars total]`);
387
+ console.log("─".repeat(60));
388
+ console.log("\nRun without --dry-run to execute.");
389
+ process.exit(0);
390
+ }
391
+
392
+ // Run agent
393
+ console.log(`🤖 Running ${agentLabel}...`);
394
+
395
+ const startTime = Date.now();
396
+ let timerInterval: Timer | null = null;
397
+
398
+ try {
399
+ // Start live timer
400
+ timerInterval = setInterval(() => {
401
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
402
+ const minutes = Math.floor(elapsed / 60);
403
+ const seconds = elapsed % 60;
404
+ const timeStr = minutes > 0
405
+ ? `${minutes}m ${seconds}s`
406
+ : `${seconds}s`;
407
+ process.stdout.write(`\r⏱️ Running... ${timeStr}`);
408
+ }, 1000);
409
+
410
+ const result = await runAgent(prompt, config.docsRepo, config.agent, config.permissions);
411
+
412
+ // Clear timer
413
+ if (timerInterval) clearInterval(timerInterval);
414
+ const totalTime = Math.floor((Date.now() - startTime) / 1000);
415
+ const minutes = Math.floor(totalTime / 60);
416
+ const seconds = totalTime % 60;
417
+ const timeStr = minutes > 0
418
+ ? `${minutes}m ${seconds}s`
419
+ : `${seconds}s`;
420
+
421
+ process.stdout.write(`\r✅ Completed in ${timeStr}\n\n`);
422
+ console.log("─".repeat(60));
423
+ console.log(result);
424
+ console.log("─".repeat(60));
425
+ console.log("\n✅ Done. Review changes:");
426
+ console.log(` cd ${config.docsRepo}`);
427
+ console.log(" git diff");
428
+ } catch (err) {
429
+ if (timerInterval) clearInterval(timerInterval);
430
+ console.error(`\n\n❌ ${(err as Error).message}`);
431
+ process.exit(1);
432
+ }
433
+ }
434
+
435
+ function buildPrompt(
436
+ config: Config,
437
+ branch: string,
438
+ diff: string,
439
+ changedFiles: string[]
440
+ ): string {
441
+ const maxDiff = 80000;
442
+ const truncated = diff.length > maxDiff
443
+ ? diff.slice(0, maxDiff) + `\n\n... [truncated, ${diff.length} chars total]`
444
+ : diff;
445
+
446
+ return `You are a documentation expert. Update the docs in this repository based on code changes.
447
+
448
+ ## Context
449
+ - Code repo: ${config.codeRepo}
450
+ - Docs repo: ${config.docsRepo} (you are here)
451
+ - Branch: ${branch}
452
+ - Base: ${config.baseBranch}
453
+
454
+ ## Changed Files
455
+ ${changedFiles.map(f => `- ${f}`).join("\n")}
456
+
457
+ ## Diff
458
+ \`\`\`diff
459
+ ${truncated}
460
+ \`\`\`
461
+
462
+ ## Do the minimum necessary to update the docs!
463
+
464
+ ## Task
465
+ 1. Explore this docs repo — understand structure and style
466
+ 2. Analyze the code changes — what was added/changed/removed
467
+ 3. Update relevant documentation — match existing style
468
+ 4. Report what you changed
469
+
470
+
471
+ You have full user's permissions to write into DOCs directory
472
+ Only update docs affected by the changes. Preserve formatting. If nothing needs updating, say so.`;
473
+ }
474
+
475
+ main().catch(console.error);