write.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import { Command } from "commander";
  2. import { PipelineRunner, StateManager } from "@actalk/inkos-core";
  3. import { readdir, unlink } from "node:fs/promises";
  4. import { join } from "node:path";
  5. import { loadConfig, createClient, findProjectRoot, log, logError } from "../utils.js";
  6. export const writeCommand = new Command("write")
  7. .description("Write chapters");
  8. writeCommand
  9. .command("next")
  10. .description("Write the next chapter for a book")
  11. .argument("<book-id>", "Book ID")
  12. .option("--count <n>", "Number of chapters to write", "1")
  13. .action(async (bookId: string, opts) => {
  14. try {
  15. const config = await loadConfig();
  16. const client = createClient(config);
  17. const root = findProjectRoot();
  18. const pipeline = new PipelineRunner({
  19. client,
  20. model: config.llm.model,
  21. projectRoot: root,
  22. notifyChannels: config.notify,
  23. });
  24. const count = parseInt(opts.count, 10);
  25. for (let i = 0; i < count; i++) {
  26. log(`Writing chapter for "${bookId}"...`);
  27. const result = await pipeline.writeNextChapter(bookId);
  28. log(` Chapter ${result.chapterNumber}: ${result.title}`);
  29. log(` Words: ${result.wordCount}`);
  30. log(` Audit: ${result.auditResult.passed ? "PASSED" : "NEEDS REVIEW"}`);
  31. if (result.revised) {
  32. log(" Auto-revised: YES (critical issues were fixed)");
  33. }
  34. log(` Status: ${result.status}`);
  35. if (result.auditResult.issues.length > 0) {
  36. log(" Issues:");
  37. for (const issue of result.auditResult.issues) {
  38. log(` [${issue.severity}] ${issue.category}: ${issue.description}`);
  39. }
  40. }
  41. log("");
  42. }
  43. log("Done.");
  44. } catch (e) {
  45. logError(`Failed to write chapter: ${e}`);
  46. process.exit(1);
  47. }
  48. });
  49. writeCommand
  50. .command("rewrite")
  51. .description("Re-generate a specific chapter (removes it and writes fresh)")
  52. .argument("<book-id>", "Book ID")
  53. .argument("<chapter>", "Chapter number to rewrite")
  54. .action(async (bookId: string, chapterStr: string) => {
  55. try {
  56. const config = await loadConfig();
  57. const client = createClient(config);
  58. const root = findProjectRoot();
  59. const chapterNum = parseInt(chapterStr, 10);
  60. const state = new StateManager(root);
  61. const bookDir = state.bookDir(bookId);
  62. const chaptersDir = join(bookDir, "chapters");
  63. // Remove existing chapter file
  64. const files = await readdir(chaptersDir);
  65. const paddedNum = String(chapterNum).padStart(4, "0");
  66. const existing = files.filter((f) => f.startsWith(paddedNum) && f.endsWith(".md"));
  67. for (const f of existing) {
  68. await unlink(join(chaptersDir, f));
  69. log(`Removed: ${f}`);
  70. }
  71. // Remove from index (and all chapters after it)
  72. const index = await state.loadChapterIndex(bookId);
  73. const trimmed = index.filter((ch) => ch.number < chapterNum);
  74. await state.saveChapterIndex(bookId, trimmed);
  75. // Also remove later chapter files since state will be rolled back
  76. const laterFiles = files.filter((f) => {
  77. const num = parseInt(f.slice(0, 4), 10);
  78. return num > chapterNum && f.endsWith(".md");
  79. });
  80. for (const f of laterFiles) {
  81. await unlink(join(chaptersDir, f));
  82. log(`Removed later chapter: ${f}`);
  83. }
  84. // Restore state to previous chapter's end-state
  85. if (chapterNum > 1) {
  86. const restored = await state.restoreState(bookId, chapterNum - 1);
  87. if (restored) {
  88. log(`State restored from chapter ${chapterNum - 1} snapshot.`);
  89. } else {
  90. log(`Warning: no snapshot for chapter ${chapterNum - 1}. Using current state.`);
  91. }
  92. }
  93. log(`Regenerating chapter ${chapterNum}...`);
  94. const pipeline = new PipelineRunner({
  95. client,
  96. model: config.llm.model,
  97. projectRoot: root,
  98. notifyChannels: config.notify,
  99. });
  100. const result = await pipeline.writeNextChapter(bookId);
  101. log(` Chapter ${result.chapterNumber}: ${result.title}`);
  102. log(` Words: ${result.wordCount}`);
  103. log(` Audit: ${result.auditResult.passed ? "PASSED" : "NEEDS REVIEW"}`);
  104. log(` Status: ${result.status}`);
  105. } catch (e) {
  106. logError(`Failed to rewrite chapter: ${e}`);
  107. process.exit(1);
  108. }
  109. });