Просмотр исходного кода

feat: add draft, audit, revise, agent CLI commands

Atomic commands with --json output for external agent integration.
Agent command wraps runAgentLoop for natural language mode.
Add resolveContext helper for --context/--context-file/stdin.
majx_mac 8 месяцев назад
Родитель
Сommit
7ed2360429

+ 65 - 0
packages/cli/src/commands/agent.ts

@@ -0,0 +1,65 @@
+import { Command } from "commander";
+import { runAgentLoop } from "@actalk/inkos-core";
+import { loadConfig, createClient, findProjectRoot, resolveContext, log, logError } from "../utils.js";
+
+export const agentCommand = new Command("agent")
+  .description("Natural language agent mode (LLM orchestrates via tool-use)")
+  .argument("<instruction>", "Natural language instruction")
+  .option("--context <text>", "Additional context (natural language)")
+  .option("--context-file <path>", "Read additional context from file")
+  .option("--max-turns <n>", "Maximum agent turns", "20")
+  .option("--json", "Output JSON (suppress progress messages)")
+  .option("--quiet", "Suppress tool call logs")
+  .action(async (instruction: string, opts) => {
+    try {
+      const config = await loadConfig();
+      const client = createClient(config);
+      const root = findProjectRoot();
+      const context = await resolveContext(opts);
+
+      const fullInstruction = context
+        ? `${instruction}\n\n补充信息:${context}`
+        : instruction;
+
+      const maxTurns = parseInt(opts.maxTurns, 10);
+
+      const result = await runAgentLoop(
+        {
+          client,
+          model: config.llm.model,
+          projectRoot: root,
+        },
+        fullInstruction,
+        {
+          maxTurns,
+          onToolCall: opts.quiet || opts.json
+            ? undefined
+            : (name, args) => {
+                log(`  [tool] ${name}(${JSON.stringify(args)})`);
+              },
+          onToolResult: opts.quiet || opts.json
+            ? undefined
+            : (name, result) => {
+                const preview = result.length > 200 ? `${result.slice(0, 200)}...` : result;
+                log(`  [result] ${name} → ${preview}`);
+              },
+          onMessage: opts.json
+            ? undefined
+            : (content) => {
+                log(`\n${content}`);
+              },
+        },
+      );
+
+      if (opts.json) {
+        log(JSON.stringify({ result }));
+      }
+    } catch (e) {
+      if (opts.json) {
+        log(JSON.stringify({ error: String(e) }));
+      } else {
+        logError(`Agent failed: ${e}`);
+      }
+      process.exit(1);
+    }
+  });

+ 47 - 0
packages/cli/src/commands/audit.ts

@@ -0,0 +1,47 @@
+import { Command } from "commander";
+import { PipelineRunner } from "@actalk/inkos-core";
+import { loadConfig, createClient, findProjectRoot, log, logError } from "../utils.js";
+
+export const auditCommand = new Command("audit")
+  .description("Audit a chapter for continuity issues")
+  .argument("<book-id>", "Book ID")
+  .argument("[chapter]", "Chapter number (defaults to latest)")
+  .option("--json", "Output JSON")
+  .action(async (bookId: string, chapterStr: string | undefined, opts) => {
+    try {
+      const config = await loadConfig();
+      const client = createClient(config);
+      const root = findProjectRoot();
+
+      const pipeline = new PipelineRunner({
+        client,
+        model: config.llm.model,
+        projectRoot: root,
+      });
+
+      const chapterNumber = chapterStr ? parseInt(chapterStr, 10) : undefined;
+      if (!opts.json) log(`Auditing "${bookId}"${chapterNumber ? ` chapter ${chapterNumber}` : " (latest)"}...`);
+
+      const result = await pipeline.auditDraft(bookId, chapterNumber);
+
+      if (opts.json) {
+        log(JSON.stringify(result, null, 2));
+      } else {
+        log(`  Chapter ${result.chapterNumber}: ${result.passed ? "PASSED" : "FAILED"}`);
+        log(`  Summary: ${result.summary}`);
+        if (result.issues.length > 0) {
+          log("  Issues:");
+          for (const issue of result.issues) {
+            log(`    [${issue.severity}] ${issue.category}: ${issue.description}`);
+          }
+        }
+      }
+    } catch (e) {
+      if (opts.json) {
+        log(JSON.stringify({ error: String(e) }));
+      } else {
+        logError(`Audit failed: ${e}`);
+      }
+      process.exit(1);
+    }
+  });

+ 43 - 0
packages/cli/src/commands/draft.ts

@@ -0,0 +1,43 @@
+import { Command } from "commander";
+import { PipelineRunner } from "@actalk/inkos-core";
+import { loadConfig, createClient, findProjectRoot, resolveContext, log, logError } from "../utils.js";
+
+export const draftCommand = new Command("draft")
+  .description("Write a draft chapter (no audit/revise)")
+  .argument("<book-id>", "Book ID")
+  .option("--context <text>", "Creative guidance (natural language)")
+  .option("--context-file <path>", "Read guidance from file")
+  .option("--json", "Output JSON")
+  .action(async (bookId: string, opts) => {
+    try {
+      const config = await loadConfig();
+      const client = createClient(config);
+      const root = findProjectRoot();
+      const context = await resolveContext(opts);
+
+      const pipeline = new PipelineRunner({
+        client,
+        model: config.llm.model,
+        projectRoot: root,
+      });
+
+      if (!opts.json) log(`Writing draft for "${bookId}"...`);
+
+      const result = await pipeline.writeDraft(bookId, context);
+
+      if (opts.json) {
+        log(JSON.stringify(result, null, 2));
+      } else {
+        log(`  Chapter ${result.chapterNumber}: ${result.title}`);
+        log(`  Words: ${result.wordCount}`);
+        log(`  File: ${result.filePath}`);
+      }
+    } catch (e) {
+      if (opts.json) {
+        log(JSON.stringify({ error: String(e) }));
+      } else {
+        logError(`Failed to write draft: ${e}`);
+      }
+      process.exit(1);
+    }
+  });

+ 47 - 0
packages/cli/src/commands/revise.ts

@@ -0,0 +1,47 @@
+import { Command } from "commander";
+import { PipelineRunner } from "@actalk/inkos-core";
+import { loadConfig, createClient, findProjectRoot, log, logError } from "../utils.js";
+
+export const reviseCommand = new Command("revise")
+  .description("Revise a chapter based on audit issues")
+  .argument("<book-id>", "Book ID")
+  .argument("[chapter]", "Chapter number (defaults to latest)")
+  .option("--json", "Output JSON")
+  .action(async (bookId: string, chapterStr: string | undefined, opts) => {
+    try {
+      const config = await loadConfig();
+      const client = createClient(config);
+      const root = findProjectRoot();
+
+      const pipeline = new PipelineRunner({
+        client,
+        model: config.llm.model,
+        projectRoot: root,
+      });
+
+      const chapterNumber = chapterStr ? parseInt(chapterStr, 10) : undefined;
+      if (!opts.json) log(`Revising "${bookId}"${chapterNumber ? ` chapter ${chapterNumber}` : " (latest)"}...`);
+
+      const result = await pipeline.reviseDraft(bookId, chapterNumber);
+
+      if (opts.json) {
+        log(JSON.stringify(result, null, 2));
+      } else {
+        log(`  Chapter ${result.chapterNumber} revised`);
+        log(`  Words: ${result.wordCount}`);
+        if (result.fixedIssues.length > 0) {
+          log("  Fixed:");
+          for (const fix of result.fixedIssues) {
+            log(`    - ${fix}`);
+          }
+        }
+      }
+    } catch (e) {
+      if (opts.json) {
+        log(JSON.stringify({ error: String(e) }));
+      } else {
+        logError(`Revise failed: ${e}`);
+      }
+      process.exit(1);
+    }
+  });

+ 9 - 1
packages/cli/src/index.ts

@@ -11,13 +11,17 @@ import { radarCommand } from "./commands/radar.js";
 import { upCommand, downCommand } from "./commands/daemon.js";
 import { doctorCommand } from "./commands/doctor.js";
 import { exportCommand } from "./commands/export.js";
+import { draftCommand } from "./commands/draft.js";
+import { auditCommand } from "./commands/audit.js";
+import { reviseCommand } from "./commands/revise.js";
+import { agentCommand } from "./commands/agent.js";
 
 const program = new Command();
 
 program
   .name("inkos")
   .description("InkOS — Multi-agent novel production system")
-  .version("0.1.0");
+  .version("0.2.0");
 
 program.addCommand(initCommand);
 program.addCommand(configCommand);
@@ -30,5 +34,9 @@ program.addCommand(upCommand);
 program.addCommand(downCommand);
 program.addCommand(doctorCommand);
 program.addCommand(exportCommand);
+program.addCommand(draftCommand);
+program.addCommand(auditCommand);
+program.addCommand(reviseCommand);
+program.addCommand(agentCommand);
 
 program.parse();

+ 20 - 0
packages/cli/src/utils.ts

@@ -3,6 +3,26 @@ import { join, resolve } from "node:path";
 import { config as loadEnv } from "dotenv";
 import { createLLMClient, type ProjectConfig, ProjectConfigSchema } from "@actalk/inkos-core";
 
+export async function resolveContext(opts: {
+  readonly context?: string;
+  readonly contextFile?: string;
+}): Promise<string | undefined> {
+  if (opts.context) return opts.context;
+  if (opts.contextFile) {
+    return readFile(resolve(opts.contextFile), "utf-8");
+  }
+  // Read from stdin if piped (non-TTY)
+  if (!process.stdin.isTTY) {
+    const chunks: Buffer[] = [];
+    for await (const chunk of process.stdin) {
+      chunks.push(chunk as Buffer);
+    }
+    const text = Buffer.concat(chunks).toString("utf-8").trim();
+    if (text.length > 0) return text;
+  }
+  return undefined;
+}
+
 export function findProjectRoot(): string {
   return process.cwd();
 }