write.ts 4.8 KB

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