Prechádzať zdrojové kódy

feat: initial InkOS multi-agent novel production system

TypeScript monorepo with pnpm workspaces:
- packages/core: LLM provider (streaming), 5 agents (architect, writer,
  continuity-auditor, reviser, radar), pipeline runner with auto-revise,
  daemon scheduler, state manager, notification dispatch (Telegram,
  Feishu, WeCom)
- packages/cli: Commander.js CLI with init, config, book, write, review,
  status, radar, daemon (up/down), doctor commands

Pipeline: radar → architect → writer → audit → [auto-revise] → review
State: current_state.md + particle_ledger.md + pending_hooks.md per book
majx_mac 8 mesiacov pred
commit
f4a5a772a9
40 zmenil súbory, kde vykonal 4391 pridanie a 0 odobranie
  1. 8 0
      .gitignore
  2. 21 0
      LICENSE
  3. 22 0
      package.json
  4. 28 0
      packages/cli/package.json
  5. 85 0
      packages/cli/src/commands/book.ts
  6. 61 0
      packages/cli/src/commands/config.ts
  7. 99 0
      packages/cli/src/commands/daemon.ts
  8. 78 0
      packages/cli/src/commands/doctor.ts
  9. 71 0
      packages/cli/src/commands/init.ts
  10. 56 0
      packages/cli/src/commands/radar.ts
  11. 109 0
      packages/cli/src/commands/review.ts
  12. 42 0
      packages/cli/src/commands/status.ts
  13. 56 0
      packages/cli/src/commands/write.ts
  14. 32 0
      packages/cli/src/index.ts
  15. 37 0
      packages/cli/src/utils.ts
  16. 8 0
      packages/cli/tsconfig.json
  17. 28 0
      packages/core/package.json
  18. 123 0
      packages/core/src/agents/architect.ts
  19. 27 0
      packages/core/src/agents/base.ts
  20. 140 0
      packages/core/src/agents/continuity.ts
  21. 83 0
      packages/core/src/agents/radar.ts
  22. 129 0
      packages/core/src/agents/reviser.ts
  23. 289 0
      packages/core/src/agents/writer.ts
  24. 29 0
      packages/core/src/index.ts
  25. 69 0
      packages/core/src/llm/provider.ts
  26. 40 0
      packages/core/src/models/book.ts
  27. 29 0
      packages/core/src/models/chapter.ts
  28. 52 0
      packages/core/src/models/project.ts
  29. 52 0
      packages/core/src/models/state.ts
  30. 49 0
      packages/core/src/notify/dispatcher.ts
  31. 34 0
      packages/core/src/notify/feishu.ts
  32. 25 0
      packages/core/src/notify/telegram.ts
  33. 22 0
      packages/core/src/notify/wechat-work.ts
  34. 194 0
      packages/core/src/pipeline/runner.ts
  35. 132 0
      packages/core/src/pipeline/scheduler.ts
  36. 99 0
      packages/core/src/state/manager.ts
  37. 8 0
      packages/core/tsconfig.json
  38. 1904 0
      pnpm-lock.yaml
  39. 2 0
      pnpm-workspace.yaml
  40. 19 0
      tsconfig.json

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+node_modules/
+dist/
+.env
+.env.local
+*.tgz
+.DS_Store
+coverage/
+.turbo/

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 InkOS Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 22 - 0
package.json

@@ -0,0 +1,22 @@
+{
+  "name": "inkos",
+  "version": "0.1.0",
+  "private": true,
+  "description": "Multi-agent novel production system",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/user/inkos"
+  },
+  "scripts": {
+    "build": "pnpm -r build",
+    "dev": "pnpm -r dev",
+    "test": "pnpm -r test",
+    "lint": "pnpm -r lint",
+    "typecheck": "pnpm -r typecheck"
+  },
+  "engines": {
+    "node": ">=20.0.0",
+    "pnpm": ">=9.0.0"
+  }
+}

+ 28 - 0
packages/cli/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "inkos",
+  "version": "0.1.0",
+  "description": "InkOS CLI — multi-agent novel production system",
+  "type": "module",
+  "bin": {
+    "inkos": "dist/index.js"
+  },
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@inkos/core": "workspace:*",
+    "commander": "^13.0.0",
+    "chalk": "^5.4.0",
+    "ora": "^8.2.0",
+    "inquirer": "^12.3.0",
+    "dotenv": "^16.4.0"
+  },
+  "devDependencies": {
+    "typescript": "^5.8.0",
+    "vitest": "^3.0.0",
+    "@types/node": "^22.0.0"
+  }
+}

+ 85 - 0
packages/cli/src/commands/book.ts

@@ -0,0 +1,85 @@
+import { Command } from "commander";
+import { PipelineRunner, StateManager, type BookConfig } from "@inkos/core";
+import { loadConfig, createClient, findProjectRoot, log, logError } from "../utils.js";
+
+export const bookCommand = new Command("book")
+  .description("Manage books");
+
+bookCommand
+  .command("create")
+  .description("Create a new book with AI-generated foundation")
+  .requiredOption("--title <title>", "Book title")
+  .option("--genre <genre>", "Genre", "xuanhuan")
+  .option("--platform <platform>", "Target platform", "tomato")
+  .option("--target-chapters <n>", "Target chapter count", "200")
+  .option("--chapter-words <n>", "Words per chapter", "3000")
+  .action(async (opts) => {
+    try {
+      const config = await loadConfig();
+      const client = createClient(config);
+      const root = findProjectRoot();
+
+      const bookId = opts.title
+        .toLowerCase()
+        .replace(/[^a-z0-9\u4e00-\u9fff]/g, "-")
+        .replace(/-+/g, "-")
+        .slice(0, 30);
+
+      const now = new Date().toISOString();
+      const book: BookConfig = {
+        id: bookId,
+        title: opts.title,
+        platform: opts.platform,
+        genre: opts.genre,
+        status: "outlining",
+        targetChapters: parseInt(opts.targetChapters, 10),
+        chapterWordCount: parseInt(opts.chapterWords, 10),
+        createdAt: now,
+        updatedAt: now,
+      };
+
+      log(`Creating book "${book.title}" (${book.genre} / ${book.platform})...`);
+
+      const pipeline = new PipelineRunner({
+        client,
+        model: config.llm.model,
+        projectRoot: root,
+      });
+
+      await pipeline.initBook(book);
+
+      log(`Book created: ${bookId}`);
+      log(`  Location: books/${bookId}/`);
+      log(`  Story bible, outline, style guide generated.`);
+      log("");
+      log(`Next: inkos write next ${bookId}`);
+    } catch (e) {
+      logError(`Failed to create book: ${e}`);
+      process.exit(1);
+    }
+  });
+
+bookCommand
+  .command("list")
+  .description("List all books")
+  .action(async () => {
+    try {
+      const root = findProjectRoot();
+      const state = new StateManager(root);
+      const bookIds = await state.listBooks();
+
+      if (bookIds.length === 0) {
+        log("No books found. Create one with: inkos book create --title '...'");
+        return;
+      }
+
+      for (const id of bookIds) {
+        const book = await state.loadBookConfig(id);
+        const nextChapter = await state.getNextChapterNumber(id);
+        log(`  ${id} | ${book.title} | ${book.genre}/${book.platform} | ${book.status} | chapters: ${nextChapter - 1}`);
+      }
+    } catch (e) {
+      logError(`Failed to list books: ${e}`);
+      process.exit(1);
+    }
+  });

+ 61 - 0
packages/cli/src/commands/config.ts

@@ -0,0 +1,61 @@
+import { Command } from "commander";
+import { readFile, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { findProjectRoot, log, logError } from "../utils.js";
+
+export const configCommand = new Command("config")
+  .description("Manage project configuration");
+
+configCommand
+  .command("set")
+  .description("Set a configuration value")
+  .argument("<key>", "Config key (e.g., llm.apiKey)")
+  .argument("<value>", "Config value")
+  .action(async (key: string, value: string) => {
+    const root = findProjectRoot();
+    const configPath = join(root, "inkos.json");
+
+    try {
+      const raw = await readFile(configPath, "utf-8");
+      const config = JSON.parse(raw);
+
+      const keys = key.split(".");
+      let target = config;
+      for (let i = 0; i < keys.length - 1; i++) {
+        const k = keys[i]!;
+        if (!(k in target)) {
+          target[k] = {};
+        }
+        target = target[k];
+      }
+      target[keys[keys.length - 1]!] = value;
+
+      await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
+      log(`Set ${key} = ${value}`);
+    } catch (e) {
+      logError(`Failed to update config: ${e}`);
+      process.exit(1);
+    }
+  });
+
+configCommand
+  .command("show")
+  .description("Show current configuration")
+  .action(async () => {
+    const root = findProjectRoot();
+    const configPath = join(root, "inkos.json");
+
+    try {
+      const raw = await readFile(configPath, "utf-8");
+      const config = JSON.parse(raw);
+      // Mask API key
+      if (config.llm?.apiKey) {
+        const key = config.llm.apiKey;
+        config.llm.apiKey = key.slice(0, 8) + "..." + key.slice(-4);
+      }
+      log(JSON.stringify(config, null, 2));
+    } catch (e) {
+      logError(`Failed to read config: ${e}`);
+      process.exit(1);
+    }
+  });

+ 99 - 0
packages/cli/src/commands/daemon.ts

@@ -0,0 +1,99 @@
+import { Command } from "commander";
+import { Scheduler, createLLMClient } from "@inkos/core";
+import { loadConfig, findProjectRoot, log, logError } from "../utils.js";
+import { writeFile, readFile, unlink } from "node:fs/promises";
+import { join } from "node:path";
+
+const PID_FILE = "inkos.pid";
+
+export const upCommand = new Command("up")
+  .description("Start the InkOS daemon (autonomous mode)")
+  .option("--foreground", "Run in foreground instead of background")
+  .action(async (opts) => {
+    try {
+      const config = await loadConfig();
+      const client = createLLMClient(config.llm);
+      const root = findProjectRoot();
+
+      // Check if already running
+      const pidPath = join(root, PID_FILE);
+      try {
+        const existingPid = await readFile(pidPath, "utf-8");
+        logError(`Daemon already running (PID: ${existingPid.trim()}). Run 'inkos down' first.`);
+        process.exit(1);
+      } catch {
+        // No PID file, good
+      }
+
+      log("Starting InkOS daemon...");
+      log(`  Write cycle: ${config.daemon.schedule.writeCron}`);
+      log(`  Radar scan: ${config.daemon.schedule.radarCron}`);
+      log(`  Max concurrent books: ${config.daemon.maxConcurrentBooks}`);
+      log("");
+
+      // Write PID file
+      await writeFile(pidPath, String(process.pid), "utf-8");
+
+      const scheduler = new Scheduler({
+        client,
+        model: config.llm.model,
+        projectRoot: root,
+        notifyChannels: config.notify,
+        radarCron: config.daemon.schedule.radarCron,
+        writeCron: config.daemon.schedule.writeCron,
+        auditCron: config.daemon.schedule.auditCron,
+        maxConcurrentBooks: config.daemon.maxConcurrentBooks,
+        onChapterComplete: (bookId, chapter, status) => {
+          const icon = status === "approved" ? "+" : "!";
+          log(`  [${icon}] ${bookId} Ch.${chapter} — ${status}`);
+        },
+        onError: (bookId, error) => {
+          logError(`${bookId}: ${error.message}`);
+        },
+      });
+
+      // Handle shutdown
+      const shutdown = async () => {
+        log("\nShutting down daemon...");
+        scheduler.stop();
+        try {
+          await unlink(pidPath);
+        } catch {
+          // ignore
+        }
+        process.exit(0);
+      };
+
+      process.on("SIGINT", shutdown);
+      process.on("SIGTERM", shutdown);
+
+      await scheduler.start();
+      log("Daemon running. Press Ctrl+C to stop.");
+
+      // Keep process alive
+      await new Promise(() => {});
+    } catch (e) {
+      logError(`Failed to start daemon: ${e}`);
+      process.exit(1);
+    }
+  });
+
+export const downCommand = new Command("down")
+  .description("Stop the InkOS daemon")
+  .action(async () => {
+    const root = findProjectRoot();
+    const pidPath = join(root, PID_FILE);
+
+    try {
+      const pid = (await readFile(pidPath, "utf-8")).trim();
+      try {
+        process.kill(parseInt(pid, 10), "SIGTERM");
+        log(`Daemon (PID: ${pid}) stopped.`);
+      } catch {
+        log(`Daemon (PID: ${pid}) not found. Cleaning up.`);
+      }
+      await unlink(pidPath);
+    } catch {
+      log("No daemon running.");
+    }
+  });

+ 78 - 0
packages/cli/src/commands/doctor.ts

@@ -0,0 +1,78 @@
+import { Command } from "commander";
+import { readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { findProjectRoot, log, logError } from "../utils.js";
+
+export const doctorCommand = new Command("doctor")
+  .description("Check environment and project health")
+  .action(async () => {
+    const checks: Array<{ name: string; ok: boolean; detail: string }> = [];
+    const root = findProjectRoot();
+
+    // 1. Check Node.js version
+    const nodeVersion = process.version;
+    const major = parseInt(nodeVersion.slice(1).split(".")[0]!, 10);
+    checks.push({
+      name: "Node.js >= 20",
+      ok: major >= 20,
+      detail: nodeVersion,
+    });
+
+    // 2. Check inkos.json exists
+    try {
+      await readFile(join(root, "inkos.json"), "utf-8");
+      checks.push({ name: "inkos.json", ok: true, detail: "Found" });
+    } catch {
+      checks.push({ name: "inkos.json", ok: false, detail: "Not found. Run 'inkos init'" });
+    }
+
+    // 3. Check .env exists
+    try {
+      await readFile(join(root, ".env"), "utf-8");
+      checks.push({ name: ".env", ok: true, detail: "Found" });
+    } catch {
+      checks.push({ name: ".env", ok: false, detail: "Not found" });
+    }
+
+    // 4. Check LLM API key
+    try {
+      const raw = await readFile(join(root, "inkos.json"), "utf-8");
+      const config = JSON.parse(raw);
+      const hasKey = config.llm?.apiKey && config.llm.apiKey.length > 10;
+      checks.push({
+        name: "LLM API Key",
+        ok: hasKey,
+        detail: hasKey ? "Configured" : "Missing or too short",
+      });
+    } catch {
+      checks.push({ name: "LLM API Key", ok: false, detail: "Cannot read config" });
+    }
+
+    // 5. Check books directory
+    try {
+      const { StateManager } = await import("@inkos/core");
+      const state = new StateManager(root);
+      const books = await state.listBooks();
+      checks.push({
+        name: "Books",
+        ok: true,
+        detail: `${books.length} book(s) found`,
+      });
+    } catch {
+      checks.push({ name: "Books", ok: true, detail: "0 books" });
+    }
+
+    // Output
+    log("InkOS Doctor\n");
+    for (const check of checks) {
+      const icon = check.ok ? "[OK]" : "[!!]";
+      log(`  ${icon} ${check.name}: ${check.detail}`);
+    }
+
+    const failed = checks.filter((c) => !c.ok);
+    if (failed.length > 0) {
+      log(`\n${failed.length} issue(s) found.`);
+    } else {
+      log("\nAll checks passed.");
+    }
+  });

+ 71 - 0
packages/cli/src/commands/init.ts

@@ -0,0 +1,71 @@
+import { Command } from "commander";
+import { writeFile, mkdir } from "node:fs/promises";
+import { join } from "node:path";
+import { log, logError } from "../utils.js";
+
+export const initCommand = new Command("init")
+  .description("Initialize a new InkOS project")
+  .argument("[name]", "Project name", "my-novel-project")
+  .action(async (name: string) => {
+    const projectDir = join(process.cwd(), name);
+
+    try {
+      await mkdir(projectDir, { recursive: true });
+      await mkdir(join(projectDir, "books"), { recursive: true });
+      await mkdir(join(projectDir, "radar"), { recursive: true });
+
+      const config = {
+        name,
+        version: "0.1.0",
+        llm: {
+          provider: "anthropic",
+          baseUrl: process.env.INKOS_LLM_BASE_URL ?? "https://api.anthropic.com/v1",
+          apiKey: process.env.INKOS_LLM_API_KEY ?? "",
+          model: process.env.INKOS_LLM_MODEL ?? "claude-sonnet-4-5-20250514",
+        },
+        notify: [],
+        daemon: {
+          schedule: {
+            radarCron: "0 9 * * *",
+            writeCron: "0 14 * * *",
+            auditCron: "0 17 * * *",
+          },
+          maxConcurrentBooks: 3,
+        },
+      };
+
+      await writeFile(
+        join(projectDir, "inkos.json"),
+        JSON.stringify(config, null, 2),
+        "utf-8",
+      );
+
+      await writeFile(
+        join(projectDir, ".env"),
+        [
+          "INKOS_LLM_PROVIDER=anthropic",
+          "INKOS_LLM_BASE_URL=https://api.anthropic.com/v1",
+          "INKOS_LLM_API_KEY=your-api-key-here",
+          "INKOS_LLM_MODEL=claude-sonnet-4-5-20250514",
+        ].join("\n"),
+        "utf-8",
+      );
+
+      await writeFile(
+        join(projectDir, ".gitignore"),
+        [".env", "node_modules/", ".DS_Store"].join("\n"),
+        "utf-8",
+      );
+
+      log(`Project initialized at ${projectDir}`);
+      log("");
+      log("Next steps:");
+      log(`  cd ${name}`);
+      log("  # Edit .env with your LLM API credentials");
+      log("  inkos book create --title '我的小说' --genre xuanhuan --platform tomato");
+      log("  inkos write next <book-id>");
+    } catch (e) {
+      logError(`Failed to initialize project: ${e}`);
+      process.exit(1);
+    }
+  });

+ 56 - 0
packages/cli/src/commands/radar.ts

@@ -0,0 +1,56 @@
+import { Command } from "commander";
+import { PipelineRunner } from "@inkos/core";
+import { loadConfig, createClient, findProjectRoot, log, logError } from "../utils.js";
+import { writeFile, mkdir } from "node:fs/promises";
+import { join } from "node:path";
+
+export const radarCommand = new Command("radar")
+  .description("Market intelligence");
+
+radarCommand
+  .command("scan")
+  .description("Scan market for opportunities")
+  .option("--platforms <platforms>", "Platforms to scan (comma-separated)", "tomato,feilu")
+  .action(async (opts) => {
+    try {
+      const config = await loadConfig();
+      const client = createClient(config);
+      const root = findProjectRoot();
+
+      const pipeline = new PipelineRunner({
+        client,
+        model: config.llm.model,
+        projectRoot: root,
+      });
+
+      log("Scanning market...");
+
+      const result = await pipeline.runRadar();
+
+      log(`\nMarket Summary:\n${result.marketSummary}\n`);
+      log("Recommendations:");
+
+      for (const rec of result.recommendations) {
+        log(`  [${(rec.confidence * 100).toFixed(0)}%] ${rec.platform}/${rec.genre}`);
+        log(`    Concept: ${rec.concept}`);
+        log(`    Reasoning: ${rec.reasoning}`);
+        log(`    Benchmarks: ${rec.benchmarkTitles.join(", ")}`);
+        log("");
+      }
+
+      // Save radar result
+      const radarDir = join(root, "radar");
+      await mkdir(radarDir, { recursive: true });
+      const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
+      await writeFile(
+        join(radarDir, `scan-${timestamp}.json`),
+        JSON.stringify(result, null, 2),
+        "utf-8",
+      );
+
+      log(`Radar result saved to radar/scan-${timestamp}.json`);
+    } catch (e) {
+      logError(`Radar scan failed: ${e}`);
+      process.exit(1);
+    }
+  });

+ 109 - 0
packages/cli/src/commands/review.ts

@@ -0,0 +1,109 @@
+import { Command } from "commander";
+import { StateManager } from "@inkos/core";
+import { findProjectRoot, log, logError } from "../utils.js";
+
+export const reviewCommand = new Command("review")
+  .description("Review and approve chapters");
+
+reviewCommand
+  .command("list")
+  .description("List chapters pending review")
+  .argument("[book-id]", "Book ID (optional, lists all books if omitted)")
+  .action(async (bookId?: string) => {
+    try {
+      const root = findProjectRoot();
+      const state = new StateManager(root);
+
+      const bookIds = bookId ? [bookId] : await state.listBooks();
+
+      for (const id of bookIds) {
+        const index = await state.loadChapterIndex(id);
+        const pending = index.filter(
+          (ch) =>
+            ch.status === "ready-for-review" || ch.status === "audit-failed",
+        );
+
+        if (pending.length === 0) continue;
+
+        const book = await state.loadBookConfig(id);
+        log(`\n${book.title} (${id}):`);
+        for (const ch of pending) {
+          log(
+            `  Ch.${ch.number} "${ch.title}" | ${ch.wordCount}字 | ${ch.status}`,
+          );
+          if (ch.auditIssues.length > 0) {
+            for (const issue of ch.auditIssues) {
+              log(`    - ${issue}`);
+            }
+          }
+        }
+      }
+    } catch (e) {
+      logError(`Failed to list reviews: ${e}`);
+      process.exit(1);
+    }
+  });
+
+reviewCommand
+  .command("approve")
+  .description("Approve a chapter")
+  .argument("<book-id>", "Book ID")
+  .argument("<chapter>", "Chapter number")
+  .action(async (bookId: string, chapterStr: string) => {
+    try {
+      const root = findProjectRoot();
+      const state = new StateManager(root);
+      const chapterNum = parseInt(chapterStr, 10);
+
+      const index = [...(await state.loadChapterIndex(bookId))];
+      const idx = index.findIndex((ch) => ch.number === chapterNum);
+      if (idx === -1) {
+        logError(`Chapter ${chapterNum} not found`);
+        process.exit(1);
+      }
+
+      index[idx] = {
+        ...index[idx]!,
+        status: "approved",
+        updatedAt: new Date().toISOString(),
+      };
+      await state.saveChapterIndex(bookId, index);
+      log(`Chapter ${chapterNum} approved.`);
+    } catch (e) {
+      logError(`Failed to approve: ${e}`);
+      process.exit(1);
+    }
+  });
+
+reviewCommand
+  .command("reject")
+  .description("Reject a chapter")
+  .argument("<book-id>", "Book ID")
+  .argument("<chapter>", "Chapter number")
+  .option("--reason <reason>", "Rejection reason")
+  .action(async (bookId: string, chapterStr: string, opts) => {
+    try {
+      const root = findProjectRoot();
+      const state = new StateManager(root);
+      const chapterNum = parseInt(chapterStr, 10);
+
+      const index = [...(await state.loadChapterIndex(bookId))];
+      const idx = index.findIndex((ch) => ch.number === chapterNum);
+      if (idx === -1) {
+        logError(`Chapter ${chapterNum} not found`);
+        process.exit(1);
+      }
+
+      index[idx] = {
+        ...index[idx]!,
+        status: "rejected",
+        reviewNote: opts.reason ?? "Rejected without reason",
+        updatedAt: new Date().toISOString(),
+      };
+      await state.saveChapterIndex(bookId, index);
+      log(`Chapter ${chapterNum} rejected.`);
+    } catch (e) {
+      logError(`Failed to reject: ${e}`);
+      process.exit(1);
+    }
+  });

+ 42 - 0
packages/cli/src/commands/status.ts

@@ -0,0 +1,42 @@
+import { Command } from "commander";
+import { StateManager } from "@inkos/core";
+import { findProjectRoot, log, logError } from "../utils.js";
+
+export const statusCommand = new Command("status")
+  .description("Show project status")
+  .action(async () => {
+    try {
+      const root = findProjectRoot();
+      const state = new StateManager(root);
+
+      const bookIds = await state.listBooks();
+
+      log(`InkOS Project: ${root}`);
+      log(`Books: ${bookIds.length}`);
+      log("");
+
+      for (const id of bookIds) {
+        const book = await state.loadBookConfig(id);
+        const index = await state.loadChapterIndex(id);
+        const nextChapter = await state.getNextChapterNumber(id);
+
+        const approved = index.filter((ch) => ch.status === "approved").length;
+        const pending = index.filter(
+          (ch) => ch.status === "ready-for-review",
+        ).length;
+        const failed = index.filter(
+          (ch) => ch.status === "audit-failed",
+        ).length;
+
+        log(`  ${book.title} (${id})`);
+        log(`    Status: ${book.status}`);
+        log(`    Platform: ${book.platform} | Genre: ${book.genre}`);
+        log(`    Chapters: ${nextChapter - 1} / ${book.targetChapters}`);
+        log(`    Approved: ${approved} | Pending: ${pending} | Failed: ${failed}`);
+        log("");
+      }
+    } catch (e) {
+      logError(`Failed to get status: ${e}`);
+      process.exit(1);
+    }
+  });

+ 56 - 0
packages/cli/src/commands/write.ts

@@ -0,0 +1,56 @@
+import { Command } from "commander";
+import { PipelineRunner } from "@inkos/core";
+import { loadConfig, createClient, findProjectRoot, log, logError } from "../utils.js";
+
+export const writeCommand = new Command("write")
+  .description("Write chapters");
+
+writeCommand
+  .command("next")
+  .description("Write the next chapter for a book")
+  .argument("<book-id>", "Book ID")
+  .option("--count <n>", "Number of chapters to write", "1")
+  .action(async (bookId: string, opts) => {
+    try {
+      const config = await loadConfig();
+      const client = createClient(config);
+      const root = findProjectRoot();
+
+      const pipeline = new PipelineRunner({
+        client,
+        model: config.llm.model,
+        projectRoot: root,
+        notifyChannels: config.notify,
+      });
+
+      const count = parseInt(opts.count, 10);
+
+      for (let i = 0; i < count; i++) {
+        log(`Writing chapter for "${bookId}"...`);
+
+        const result = await pipeline.writeNextChapter(bookId);
+
+        log(`  Chapter ${result.chapterNumber}: ${result.title}`);
+        log(`  Words: ${result.wordCount}`);
+        log(`  Audit: ${result.auditResult.passed ? "PASSED" : "NEEDS REVIEW"}`);
+        if (result.revised) {
+          log("  Auto-revised: YES (critical issues were fixed)");
+        }
+        log(`  Status: ${result.status}`);
+
+        if (result.auditResult.issues.length > 0) {
+          log("  Issues:");
+          for (const issue of result.auditResult.issues) {
+            log(`    [${issue.severity}] ${issue.category}: ${issue.description}`);
+          }
+        }
+
+        log("");
+      }
+
+      log("Done.");
+    } catch (e) {
+      logError(`Failed to write chapter: ${e}`);
+      process.exit(1);
+    }
+  });

+ 32 - 0
packages/cli/src/index.ts

@@ -0,0 +1,32 @@
+#!/usr/bin/env node
+
+import { Command } from "commander";
+import { initCommand } from "./commands/init.js";
+import { configCommand } from "./commands/config.js";
+import { bookCommand } from "./commands/book.js";
+import { writeCommand } from "./commands/write.js";
+import { reviewCommand } from "./commands/review.js";
+import { statusCommand } from "./commands/status.js";
+import { radarCommand } from "./commands/radar.js";
+import { upCommand, downCommand } from "./commands/daemon.js";
+import { doctorCommand } from "./commands/doctor.js";
+
+const program = new Command();
+
+program
+  .name("inkos")
+  .description("InkOS — Multi-agent novel production system")
+  .version("0.1.0");
+
+program.addCommand(initCommand);
+program.addCommand(configCommand);
+program.addCommand(bookCommand);
+program.addCommand(writeCommand);
+program.addCommand(reviewCommand);
+program.addCommand(statusCommand);
+program.addCommand(radarCommand);
+program.addCommand(upCommand);
+program.addCommand(downCommand);
+program.addCommand(doctorCommand);
+
+program.parse();

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

@@ -0,0 +1,37 @@
+import { readFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { config as loadEnv } from "dotenv";
+import { createLLMClient, type ProjectConfig, ProjectConfigSchema } from "@inkos/core";
+
+export function findProjectRoot(): string {
+  return process.cwd();
+}
+
+export async function loadConfig(): Promise<ProjectConfig> {
+  const root = findProjectRoot();
+
+  // Load .env from project root
+  loadEnv({ path: join(root, ".env") });
+
+  const configPath = join(root, "inkos.json");
+  try {
+    const raw = await readFile(configPath, "utf-8");
+    return ProjectConfigSchema.parse(JSON.parse(raw));
+  } catch (e) {
+    throw new Error(
+      `Failed to load inkos.json from ${root}. Run 'inkos init' first.`,
+    );
+  }
+}
+
+export function createClient(config: ProjectConfig) {
+  return createLLMClient(config.llm);
+}
+
+export function log(message: string): void {
+  process.stdout.write(`${message}\n`);
+}
+
+export function logError(message: string): void {
+  process.stderr.write(`[ERROR] ${message}\n`);
+}

+ 8 - 0
packages/cli/tsconfig.json

@@ -0,0 +1,8 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "dist",
+    "rootDir": "src"
+  },
+  "include": ["src"]
+}

+ 28 - 0
packages/core/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "@inkos/core",
+  "version": "0.1.0",
+  "description": "InkOS core: models, agents, pipeline, LLM integration",
+  "type": "module",
+  "main": "dist/index.js",
+  "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.js"
+    }
+  },
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "openai": "^4.80.0",
+    "zod": "^3.24.0"
+  },
+  "devDependencies": {
+    "typescript": "^5.8.0",
+    "vitest": "^3.0.0"
+  }
+}

+ 123 - 0
packages/core/src/agents/architect.ts

@@ -0,0 +1,123 @@
+import { BaseAgent } from "./base.js";
+import type { AgentContext } from "./base.js";
+import type { BookConfig } from "../models/book.js";
+import { readFile, writeFile, mkdir } from "node:fs/promises";
+import { join } from "node:path";
+
+export interface ArchitectOutput {
+  readonly storyBible: string;
+  readonly volumeOutline: string;
+  readonly styleGuide: string;
+  readonly currentState: string;
+  readonly pendingHooks: string;
+}
+
+export class ArchitectAgent extends BaseAgent {
+  get name(): string {
+    return "architect";
+  }
+
+  async generateFoundation(book: BookConfig): Promise<ArchitectOutput> {
+    const systemPrompt = `你是一个专业的网络小说架构师。你的任务是为一本新小说生成完整的基础设定。
+
+要求:
+- 平台:${book.platform}
+- 题材:${book.genre}
+- 目标章数:${book.targetChapters}章
+- 每章字数:${book.chapterWordCount}字
+
+你需要生成以下内容,每个部分用 === SECTION: <name> === 分隔:
+
+=== SECTION: story_bible ===
+世界观设定、势力分布、核心规则体系、主角设定(身份/金手指/性格底色)、重要配角
+
+=== SECTION: volume_outline ===
+卷纲规划,每卷包含:卷名、章节范围、核心冲突、关键转折、收益目标
+
+=== SECTION: style_guide ===
+文风锁定:叙事视角、语言风格、禁忌清单、爽点回路设计、节奏规则
+
+=== SECTION: current_state ===
+初始状态卡(第0章),包含:
+| 字段 | 值 |
+|------|-----|
+| 当前章节 | 0 |
+| 当前位置 | (起始地点) |
+| 主角状态 | (初始状态) |
+| 当前目标 | (第一个目标) |
+| 当前限制 | (初始限制) |
+| 当前敌我 | (初始关系) |
+| 当前冲突 | (第一个冲突) |
+
+=== SECTION: pending_hooks ===
+初始伏笔池(Markdown表格):
+| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |
+
+生成内容必须:
+1. 符合${book.platform}平台口味
+2. 主角杀伐果断,不圣母
+3. 有明确的数值/资源体系可追踪
+4. 伏笔前后呼应,不留悬空线`;
+
+    const response = await this.chat([
+      { role: "system", content: systemPrompt },
+      {
+        role: "user",
+        content: `请为标题为"${book.title}"的${book.genre}小说生成完整基础设定。`,
+      },
+    ], { maxTokens: 16384, temperature: 0.8 });
+
+    return this.parseSections(response.content);
+  }
+
+  async writeFoundationFiles(
+    bookDir: string,
+    output: ArchitectOutput,
+  ): Promise<void> {
+    const storyDir = join(bookDir, "story");
+    await mkdir(storyDir, { recursive: true });
+
+    await Promise.all([
+      writeFile(join(storyDir, "story_bible.md"), output.storyBible, "utf-8"),
+      writeFile(
+        join(storyDir, "volume_outline.md"),
+        output.volumeOutline,
+        "utf-8",
+      ),
+      writeFile(join(storyDir, "style_guide.md"), output.styleGuide, "utf-8"),
+      writeFile(
+        join(storyDir, "current_state.md"),
+        output.currentState,
+        "utf-8",
+      ),
+      writeFile(
+        join(storyDir, "pending_hooks.md"),
+        output.pendingHooks,
+        "utf-8",
+      ),
+      writeFile(
+        join(storyDir, "particle_ledger.md"),
+        "# 资源账本\n\n| 章节 | 期初值 | 来源 | 完整度 | 增量 | 期末值 | 依据 |\n|------|--------|------|--------|------|--------|------|\n| 0 | 0 | 初始化 | - | 0 | 0 | 开书初始 |\n",
+        "utf-8",
+      ),
+    ]);
+  }
+
+  private parseSections(content: string): ArchitectOutput {
+    const extract = (name: string): string => {
+      const regex = new RegExp(
+        `=== SECTION: ${name} ===\\s*([\\s\\S]*?)(?==== SECTION:|$)`,
+      );
+      const match = content.match(regex);
+      return match?.[1]?.trim() ?? `[${name} 生成失败,需要重新生成]`;
+    };
+
+    return {
+      storyBible: extract("story_bible"),
+      volumeOutline: extract("volume_outline"),
+      styleGuide: extract("style_guide"),
+      currentState: extract("current_state"),
+      pendingHooks: extract("pending_hooks"),
+    };
+  }
+}

+ 27 - 0
packages/core/src/agents/base.ts

@@ -0,0 +1,27 @@
+import type OpenAI from "openai";
+import type { LLMMessage, LLMResponse } from "../llm/provider.js";
+import { chatCompletion } from "../llm/provider.js";
+
+export interface AgentContext {
+  readonly client: OpenAI;
+  readonly model: string;
+  readonly projectRoot: string;
+  readonly bookId?: string;
+}
+
+export abstract class BaseAgent {
+  protected readonly ctx: AgentContext;
+
+  constructor(ctx: AgentContext) {
+    this.ctx = ctx;
+  }
+
+  protected async chat(
+    messages: ReadonlyArray<LLMMessage>,
+    options?: { readonly temperature?: number; readonly maxTokens?: number },
+  ): Promise<LLMResponse> {
+    return chatCompletion(this.ctx.client, this.ctx.model, messages, options);
+  }
+
+  abstract get name(): string;
+}

+ 140 - 0
packages/core/src/agents/continuity.ts

@@ -0,0 +1,140 @@
+import { BaseAgent } from "./base.js";
+import { readFile } from "node:fs/promises";
+import { join } from "node:path";
+
+export interface AuditResult {
+  readonly passed: boolean;
+  readonly issues: ReadonlyArray<AuditIssue>;
+  readonly summary: string;
+}
+
+export interface AuditIssue {
+  readonly severity: "critical" | "warning" | "info";
+  readonly category: string;
+  readonly description: string;
+  readonly suggestion: string;
+}
+
+export class ContinuityAuditor extends BaseAgent {
+  get name(): string {
+    return "continuity-auditor";
+  }
+
+  async auditChapter(
+    bookDir: string,
+    chapterContent: string,
+    chapterNumber: number,
+  ): Promise<AuditResult> {
+    const [currentState, ledger, hooks, styleGuide] = await Promise.all([
+      this.readFileSafe(join(bookDir, "story/current_state.md")),
+      this.readFileSafe(join(bookDir, "story/particle_ledger.md")),
+      this.readFileSafe(join(bookDir, "story/pending_hooks.md")),
+      this.readFileSafe(join(bookDir, "story/style_guide.md")),
+    ]);
+
+    const systemPrompt = `你是一位严格的网络小说审稿编辑。你的任务是对章节进行连续性、一致性和质量审查。
+
+审查维度:
+1. OOC检查:角色行为是否符合已确立人设
+2. 时间线检查:时间/地点是否连贯
+3. 设定冲突:是否违反已确立的世界观规则
+4. 战力崩坏:是否出现不合理的实力变化
+5. 数值检查:资源/数值变动是否与账本一致
+6. 伏笔检查:是否有遗漏或矛盾的伏笔
+7. 节奏检查:是否拖沓或跳跃
+8. 文风检查:是否偏离风格指南
+9. 信息越界:角色是否知道了不该知道的信息
+10. 词汇疲劳:是否有过度重复的表达
+
+输出格式必须为 JSON:
+{
+  "passed": true/false,
+  "issues": [
+    {
+      "severity": "critical|warning|info",
+      "category": "审查维度名称",
+      "description": "具体问题描述",
+      "suggestion": "修改建议"
+    }
+  ],
+  "summary": "一句话总结审查结论"
+}
+
+只有当存在 critical 级别问题时,passed 才为 false。`;
+
+    const userPrompt = `请审查第${chapterNumber}章。
+
+## 当前状态卡
+${currentState}
+
+## 资源账本
+${ledger}
+
+## 伏笔池
+${hooks}
+
+## 文风指南
+${styleGuide}
+
+## 待审章节内容
+${chapterContent}`;
+
+    const response = await this.chat(
+      [
+        { role: "system", content: systemPrompt },
+        { role: "user", content: userPrompt },
+      ],
+      { temperature: 0.3, maxTokens: 4096 },
+    );
+
+    return this.parseAuditResult(response.content);
+  }
+
+  private parseAuditResult(content: string): AuditResult {
+    const jsonMatch = content.match(/\{[\s\S]*\}/);
+    if (!jsonMatch) {
+      return {
+        passed: false,
+        issues: [
+          {
+            severity: "critical",
+            category: "系统错误",
+            description: "审稿输出格式异常,无法解析",
+            suggestion: "重新运行审稿",
+          },
+        ],
+        summary: "审稿输出解析失败",
+      };
+    }
+
+    try {
+      const parsed = JSON.parse(jsonMatch[0]);
+      return {
+        passed: Boolean(parsed.passed),
+        issues: Array.isArray(parsed.issues) ? parsed.issues : [],
+        summary: String(parsed.summary ?? ""),
+      };
+    } catch {
+      return {
+        passed: false,
+        issues: [
+          {
+            severity: "critical",
+            category: "系统错误",
+            description: "审稿 JSON 解析失败",
+            suggestion: "重新运行审稿",
+          },
+        ],
+        summary: "审稿 JSON 解析失败",
+      };
+    }
+  }
+
+  private async readFileSafe(path: string): Promise<string> {
+    try {
+      return await readFile(path, "utf-8");
+    } catch {
+      return "(文件不存在)";
+    }
+  }
+}

+ 83 - 0
packages/core/src/agents/radar.ts

@@ -0,0 +1,83 @@
+import { BaseAgent } from "./base.js";
+import type { Platform, Genre } from "../models/book.js";
+
+export interface RadarResult {
+  readonly recommendations: ReadonlyArray<RadarRecommendation>;
+  readonly marketSummary: string;
+  readonly timestamp: string;
+}
+
+export interface RadarRecommendation {
+  readonly platform: Platform;
+  readonly genre: Genre;
+  readonly concept: string;
+  readonly confidence: number;
+  readonly reasoning: string;
+  readonly benchmarkTitles: ReadonlyArray<string>;
+}
+
+export class RadarAgent extends BaseAgent {
+  get name(): string {
+    return "radar";
+  }
+
+  async scan(platforms: ReadonlyArray<Platform>): Promise<RadarResult> {
+    const systemPrompt = `你是一个专业的网络小说市场分析师。你的任务是分析当前网文市场热度,推荐有潜力的题材和概念。
+
+你需要基于以下平台的当前趋势进行分析:${platforms.join("、")}
+
+分析维度:
+1. 当前热门题材和标签
+2. 高追读/收藏的作品类型
+3. 市场空白和机会点
+4. 风险提示(过度饱和的题材)
+
+输出格式必须为 JSON:
+{
+  "recommendations": [
+    {
+      "platform": "平台名",
+      "genre": "题材类型",
+      "concept": "一句话概念描述",
+      "confidence": 0.0-1.0,
+      "reasoning": "推荐理由",
+      "benchmarkTitles": ["对标书1", "对标书2"]
+    }
+  ],
+  "marketSummary": "整体市场概述"
+}
+
+推荐数量:3-5个,按 confidence 降序排列。`;
+
+    const response = await this.chat(
+      [
+        { role: "system", content: systemPrompt },
+        {
+          role: "user",
+          content: `请分析当前${platforms.join("、")}平台的网文市场热度,给出开书建议。`,
+        },
+      ],
+      { temperature: 0.6, maxTokens: 4096 },
+    );
+
+    return this.parseResult(response.content);
+  }
+
+  private parseResult(content: string): RadarResult {
+    const jsonMatch = content.match(/\{[\s\S]*\}/);
+    if (!jsonMatch) {
+      throw new Error("Radar output format error: no JSON found");
+    }
+
+    try {
+      const parsed = JSON.parse(jsonMatch[0]);
+      return {
+        recommendations: parsed.recommendations ?? [],
+        marketSummary: parsed.marketSummary ?? "",
+        timestamp: new Date().toISOString(),
+      };
+    } catch (e) {
+      throw new Error(`Radar JSON parse error: ${e}`);
+    }
+  }
+}

+ 129 - 0
packages/core/src/agents/reviser.ts

@@ -0,0 +1,129 @@
+import { BaseAgent } from "./base.js";
+import type { BookConfig } from "../models/book.js";
+import type { AuditIssue } from "./continuity.js";
+import { readFile, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+
+export interface ReviseOutput {
+  readonly revisedContent: string;
+  readonly wordCount: number;
+  readonly fixedIssues: ReadonlyArray<string>;
+  readonly updatedState: string;
+  readonly updatedLedger: string;
+  readonly updatedHooks: string;
+}
+
+export class ReviserAgent extends BaseAgent {
+  get name(): string {
+    return "reviser";
+  }
+
+  async reviseChapter(
+    bookDir: string,
+    chapterContent: string,
+    chapterNumber: number,
+    issues: ReadonlyArray<AuditIssue>,
+  ): Promise<ReviseOutput> {
+    const [currentState, ledger, hooks, styleGuide] = await Promise.all([
+      this.readFileSafe(join(bookDir, "story/current_state.md")),
+      this.readFileSafe(join(bookDir, "story/particle_ledger.md")),
+      this.readFileSafe(join(bookDir, "story/pending_hooks.md")),
+      this.readFileSafe(join(bookDir, "story/style_guide.md")),
+    ]);
+
+    const issueList = issues
+      .map((i) => `- [${i.severity}] ${i.category}: ${i.description}\n  建议: ${i.suggestion}`)
+      .join("\n");
+
+    const systemPrompt = `你是一位专业的网络小说修稿编辑。你的任务是根据审稿意见对章节进行最小幅度修正。
+
+修稿原则:
+1. 只修问题,不改风格
+2. 修根因,不做表面润色
+3. 数值错误必须精确修正,前后对账
+4. 伏笔状态必须与伏笔池同步
+5. 不改变剧情走向和核心冲突
+6. 保持原文的语言风格和节奏
+7. 修改后同步更新状态卡、账本、伏笔池
+
+输出格式:
+
+=== FIXED_ISSUES ===
+(逐条说明修正了什么,一行一条)
+
+=== REVISED_CONTENT ===
+(修正后的完整正文)
+
+=== UPDATED_STATE ===
+(更新后的完整状态卡)
+
+=== UPDATED_LEDGER ===
+(更新后的完整资源账本)
+
+=== UPDATED_HOOKS ===
+(更新后的完整伏笔池)`;
+
+    const userPrompt = `请修正第${chapterNumber}章。
+
+## 审稿问题
+${issueList}
+
+## 当前状态卡
+${currentState}
+
+## 资源账本
+${ledger}
+
+## 伏笔池
+${hooks}
+
+## 文风指南
+${styleGuide}
+
+## 待修正章节
+${chapterContent}`;
+
+    const response = await this.chat(
+      [
+        { role: "system", content: systemPrompt },
+        { role: "user", content: userPrompt },
+      ],
+      { temperature: 0.3, maxTokens: 16384 },
+    );
+
+    return this.parseOutput(response.content);
+  }
+
+  private parseOutput(content: string): ReviseOutput {
+    const extract = (tag: string): string => {
+      const regex = new RegExp(
+        `=== ${tag} ===\\s*([\\s\\S]*?)(?==== [A-Z_]+ ===|$)`,
+      );
+      const match = content.match(regex);
+      return match?.[1]?.trim() ?? "";
+    };
+
+    const revisedContent = extract("REVISED_CONTENT");
+    const fixedRaw = extract("FIXED_ISSUES");
+
+    return {
+      revisedContent,
+      wordCount: revisedContent.length,
+      fixedIssues: fixedRaw
+        .split("\n")
+        .map((l) => l.trim())
+        .filter((l) => l.length > 0),
+      updatedState: extract("UPDATED_STATE") || "(状态卡未更新)",
+      updatedLedger: extract("UPDATED_LEDGER") || "(账本未更新)",
+      updatedHooks: extract("UPDATED_HOOKS") || "(伏笔池未更新)",
+    };
+  }
+
+  private async readFileSafe(path: string): Promise<string> {
+    try {
+      return await readFile(path, "utf-8");
+    } catch {
+      return "(文件不存在)";
+    }
+  }
+}

+ 289 - 0
packages/core/src/agents/writer.ts

@@ -0,0 +1,289 @@
+import { BaseAgent } from "./base.js";
+import type { AgentContext } from "./base.js";
+import type { BookConfig } from "../models/book.js";
+import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
+import { join } from "node:path";
+
+export interface WriteChapterInput {
+  readonly book: BookConfig;
+  readonly bookDir: string;
+  readonly chapterNumber: number;
+}
+
+export interface WriteChapterOutput {
+  readonly chapterNumber: number;
+  readonly title: string;
+  readonly content: string;
+  readonly wordCount: number;
+  readonly preWriteCheck: string;
+  readonly postSettlement: string;
+  readonly updatedState: string;
+  readonly updatedLedger: string;
+  readonly updatedHooks: string;
+}
+
+export class WriterAgent extends BaseAgent {
+  get name(): string {
+    return "writer";
+  }
+
+  async writeChapter(input: WriteChapterInput): Promise<WriteChapterOutput> {
+    const { book, bookDir, chapterNumber } = input;
+
+    const [storyBible, volumeOutline, styleGuide, currentState, ledger, hooks] =
+      await Promise.all([
+        this.readFileOrDefault(join(bookDir, "story/story_bible.md")),
+        this.readFileOrDefault(join(bookDir, "story/volume_outline.md")),
+        this.readFileOrDefault(join(bookDir, "story/style_guide.md")),
+        this.readFileOrDefault(join(bookDir, "story/current_state.md")),
+        this.readFileOrDefault(join(bookDir, "story/particle_ledger.md")),
+        this.readFileOrDefault(join(bookDir, "story/pending_hooks.md")),
+      ]);
+
+    const recentChapters = await this.loadRecentChapters(bookDir, chapterNumber);
+
+    const systemPrompt = this.buildSystemPrompt(book, styleGuide);
+    const userPrompt = this.buildUserPrompt({
+      chapterNumber,
+      storyBible,
+      volumeOutline,
+      currentState,
+      ledger,
+      hooks,
+      recentChapters,
+      wordCount: book.chapterWordCount,
+    });
+
+    const response = await this.chat(
+      [
+        { role: "system", content: systemPrompt },
+        { role: "user", content: userPrompt },
+      ],
+      { maxTokens: 16384, temperature: 0.7 },
+    );
+
+    return this.parseOutput(chapterNumber, response.content);
+  }
+
+  async saveChapter(
+    bookDir: string,
+    output: WriteChapterOutput,
+  ): Promise<void> {
+    const chaptersDir = join(bookDir, "chapters");
+    const storyDir = join(bookDir, "story");
+    await mkdir(chaptersDir, { recursive: true });
+
+    const paddedNum = String(output.chapterNumber).padStart(4, "0");
+    const filename = `${paddedNum}_${this.sanitizeFilename(output.title)}.md`;
+
+    const chapterContent = [
+      `# 第${output.chapterNumber}章 ${output.title}`,
+      "",
+      output.content,
+    ].join("\n");
+
+    await Promise.all([
+      writeFile(join(chaptersDir, filename), chapterContent, "utf-8"),
+      writeFile(join(storyDir, "current_state.md"), output.updatedState, "utf-8"),
+      writeFile(join(storyDir, "particle_ledger.md"), output.updatedLedger, "utf-8"),
+      writeFile(join(storyDir, "pending_hooks.md"), output.updatedHooks, "utf-8"),
+    ]);
+  }
+
+  private buildSystemPrompt(book: BookConfig, styleGuide: string): string {
+    return `你是一位专业的${book.genre}网络小说作家。你为${book.platform}平台写作。
+
+## 核心规则
+
+1. 以简体中文工作,句子长短交替,段落适合手机阅读
+2. 每章${book.chapterWordCount}字左右
+3. 主角杀伐果断、极致利己、有底线但不圣母
+4. 严格维护数值体系,资源变动必须精确到具体数字
+5. 伏笔前后呼应,不留悬空线
+6. 只读必要上下文,不机械重复已有内容
+7. 配角有血有肉,反派不降智
+
+## 创作铁律
+
+- Show, don't tell:用细节堆砌真实,用行动证明强大
+- 反派的每一步动作必须追溯到其已知信息、资源约束和性格习惯
+- 反派不能基于不可能知道的信息行动(信息越界检查)
+- 三章内必有明确反馈:打脸、收益兑现、信息反转、地位变化
+- 每段至少带来一项新信息、态度变化或利益变化,避免空转
+- 同质资源重复吞噬必须写明衰减,不得默认全额结算
+- 不要用"暴涨""海量""难以估量"跳过数值结算
+- 不要让主角为推剧情突然仁慈、犯蠢、讲武德
+- 不要无铺垫强行让退场角色回归
+
+## 语言约束
+
+- 避免连续重复同一句式、同一主语开头
+- 高疲劳词(冷笑、蝼蚁、倒吸凉气、瞳孔骤缩)单章最多出现1次
+- 群像反应不要一律"全场震惊",改写成1-2个具体角色的身体反应
+- 台词贴身份、阶层和处境,不堆通用狠话
+
+## 章节类型识别
+
+动笔前先判断本章类型:
+- 战斗章:重画面、受力、收益兑现
+- 布局章:重试探、交易、威慑、利益交换
+- 过渡章:重状态变化、战后余波、下一步钩子
+- 回收章:优先回应旧伏笔,再打开新问题
+
+## 文风指南
+
+${styleGuide}
+
+## 动笔前必须自问
+
+1. 主角此刻利益最大化的选择是什么?
+2. 这场冲突是谁先动手,为什么非做不可?
+3. 配角/反派是否有明确诉求、恐惧和反制?
+4. 反派当前掌握了哪些已知信息?哪些信息只有读者知道?
+5. 本章收益能否落到具体资源、数值增量、地位变化或已回收伏笔?
+6. 如果任何问题答不上来,先补逻辑链,再写正文
+
+## 输出格式(严格遵守)
+
+=== PRE_WRITE_CHECK ===
+(必须输出Markdown表格)
+| 检查项 | 本章记录 | 备注 |
+|--------|----------|------|
+| 上下文范围 | 第X章至第Y章 / 状态卡 / 设定文件 | |
+| 当前锚点 | 地点 / 对手 / 收益目标 | 锚点必须具体 |
+| 当前资源总量 | X | 与账本一致 |
+| 本章预计增量 | +X(来源) | 无增量写+0 |
+| 待回收伏笔 | Hook-A / Hook-B | 与伏笔池一致 |
+| 本章冲突 | 一句话概括 | |
+| 章节类型 | 战斗/布局/过渡/回收 | |
+| 风险扫描 | OOC/信息越界/设定冲突/战力崩坏/节奏/词汇疲劳 | |
+
+=== CHAPTER_TITLE ===
+(章节标题,不含"第X章")
+
+=== CHAPTER_CONTENT ===
+(正文内容,${book.chapterWordCount}字左右)
+
+=== POST_SETTLEMENT ===
+(如有数值变动,必须输出Markdown表格)
+| 结算项 | 本章记录 | 备注 |
+|--------|----------|------|
+| 资源账本 | 期初X / 增量+Y / 期末Z | 无增量写+0 |
+| 重要资源 | 资源名 -> 贡献+Y(依据) | 无写"无" |
+| 伏笔变动 | 新增/回收/延后 Hook | 同步更新伏笔池 |
+
+=== UPDATED_STATE ===
+(更新后的完整状态卡,Markdown表格格式)
+
+=== UPDATED_LEDGER ===
+(更新后的完整资源账本,Markdown表格格式)
+
+=== UPDATED_HOOKS ===
+(更新后的完整伏笔池,Markdown表格格式)`;
+  }
+
+  private buildUserPrompt(params: {
+    readonly chapterNumber: number;
+    readonly storyBible: string;
+    readonly volumeOutline: string;
+    readonly currentState: string;
+    readonly ledger: string;
+    readonly hooks: string;
+    readonly recentChapters: string;
+    readonly wordCount: number;
+  }): string {
+    return `请续写第${params.chapterNumber}章。
+
+## 当前状态卡
+${params.currentState}
+
+## 资源账本
+${params.ledger}
+
+## 伏笔池
+${params.hooks}
+
+## 最近章节
+${params.recentChapters || "(这是第一章,无前文)"}
+
+## 世界观设定
+${params.storyBible}
+
+## 卷纲
+${params.volumeOutline}
+
+要求:
+- 正文不少于${params.wordCount}字
+- 写完后更新状态卡、资源账本、伏笔池
+- 先输出写作自检表,再写正文`;
+  }
+
+  private async loadRecentChapters(
+    bookDir: string,
+    currentChapter: number,
+  ): Promise<string> {
+    const chaptersDir = join(bookDir, "chapters");
+    try {
+      const files = await readdir(chaptersDir);
+      const mdFiles = files
+        .filter((f) => f.endsWith(".md"))
+        .sort()
+        .slice(-5);
+
+      if (mdFiles.length === 0) return "";
+
+      const contents = await Promise.all(
+        mdFiles.map(async (f) => {
+          const content = await readFile(join(chaptersDir, f), "utf-8");
+          return content;
+        }),
+      );
+
+      return contents.join("\n\n---\n\n");
+    } catch {
+      return "";
+    }
+  }
+
+  private async readFileOrDefault(path: string): Promise<string> {
+    try {
+      return await readFile(path, "utf-8");
+    } catch {
+      return "(文件尚未创建)";
+    }
+  }
+
+  private parseOutput(
+    chapterNumber: number,
+    content: string,
+  ): WriteChapterOutput {
+    const extract = (tag: string): string => {
+      const regex = new RegExp(
+        `=== ${tag} ===\\s*([\\s\\S]*?)(?==== [A-Z_]+ ===|$)`,
+      );
+      const match = content.match(regex);
+      return match?.[1]?.trim() ?? "";
+    };
+
+    const chapterContent = extract("CHAPTER_CONTENT");
+
+    return {
+      chapterNumber,
+      title: extract("CHAPTER_TITLE") || `第${chapterNumber}章`,
+      content: chapterContent,
+      wordCount: chapterContent.length,
+      preWriteCheck: extract("PRE_WRITE_CHECK"),
+      postSettlement: extract("POST_SETTLEMENT"),
+      updatedState: extract("UPDATED_STATE") || "(状态卡未更新)",
+      updatedLedger: extract("UPDATED_LEDGER") || "(账本未更新)",
+      updatedHooks: extract("UPDATED_HOOKS") || "(伏笔池未更新)",
+    };
+  }
+
+  private sanitizeFilename(title: string): string {
+    return title
+      .replace(/[/\\?%*:|"<>]/g, "")
+      .replace(/\s+/g, "_")
+      .slice(0, 50);
+  }
+}

+ 29 - 0
packages/core/src/index.ts

@@ -0,0 +1,29 @@
+// Models
+export { type BookConfig, type Platform, type Genre, type BookStatus, BookConfigSchema, PlatformSchema, GenreSchema, BookStatusSchema } from "./models/book.js";
+export { type ChapterMeta, type ChapterStatus, ChapterMetaSchema, ChapterStatusSchema } from "./models/chapter.js";
+export { type ProjectConfig, type LLMConfig, type NotifyChannel, ProjectConfigSchema, LLMConfigSchema } from "./models/project.js";
+export { type CurrentState, type ParticleLedger, type PendingHooks, type PendingHook, type LedgerEntry } from "./models/state.js";
+
+// LLM
+export { createLLMClient, chatCompletion, type LLMResponse, type LLMMessage } from "./llm/provider.js";
+
+// Agents
+export { BaseAgent, type AgentContext } from "./agents/base.js";
+export { ArchitectAgent, type ArchitectOutput } from "./agents/architect.js";
+export { WriterAgent, type WriteChapterInput, type WriteChapterOutput } from "./agents/writer.js";
+export { ContinuityAuditor, type AuditResult, type AuditIssue } from "./agents/continuity.js";
+export { ReviserAgent, type ReviseOutput } from "./agents/reviser.js";
+export { RadarAgent, type RadarResult, type RadarRecommendation } from "./agents/radar.js";
+
+// Pipeline
+export { PipelineRunner, type PipelineConfig, type ChapterPipelineResult } from "./pipeline/runner.js";
+export { Scheduler, type SchedulerConfig } from "./pipeline/scheduler.js";
+
+// State
+export { StateManager } from "./state/manager.js";
+
+// Notify
+export { dispatchNotification, type NotifyMessage } from "./notify/dispatcher.js";
+export { sendTelegram, type TelegramConfig } from "./notify/telegram.js";
+export { sendFeishu, type FeishuConfig } from "./notify/feishu.js";
+export { sendWechatWork, type WechatWorkConfig } from "./notify/wechat-work.js";

+ 69 - 0
packages/core/src/llm/provider.ts

@@ -0,0 +1,69 @@
+import OpenAI from "openai";
+import type { LLMConfig } from "../models/project.js";
+
+export interface LLMResponse {
+  readonly content: string;
+  readonly usage: {
+    readonly promptTokens: number;
+    readonly completionTokens: number;
+    readonly totalTokens: number;
+  };
+}
+
+export interface LLMMessage {
+  readonly role: "system" | "user" | "assistant";
+  readonly content: string;
+}
+
+export function createLLMClient(config: LLMConfig): OpenAI {
+  return new OpenAI({
+    apiKey: config.apiKey,
+    baseURL: config.baseUrl,
+  });
+}
+
+export async function chatCompletion(
+  client: OpenAI,
+  model: string,
+  messages: ReadonlyArray<LLMMessage>,
+  options?: {
+    readonly temperature?: number;
+    readonly maxTokens?: number;
+  },
+): Promise<LLMResponse> {
+  // Use streaming mode — some providers (codex-for.me) require it
+  const stream = await client.chat.completions.create({
+    model,
+    messages: messages.map((m) => ({ role: m.role, content: m.content })),
+    temperature: options?.temperature ?? 0.7,
+    max_tokens: options?.maxTokens ?? 8192,
+    stream: true,
+  });
+
+  const chunks: string[] = [];
+  let promptTokens = 0;
+  let completionTokens = 0;
+  let totalTokens = 0;
+
+  for await (const chunk of stream) {
+    const delta = chunk.choices[0]?.delta?.content;
+    if (delta) {
+      chunks.push(delta);
+    }
+    if (chunk.usage) {
+      promptTokens = chunk.usage.prompt_tokens ?? 0;
+      completionTokens = chunk.usage.completion_tokens ?? 0;
+      totalTokens = chunk.usage.total_tokens ?? 0;
+    }
+  }
+
+  const content = chunks.join("");
+  if (!content) {
+    throw new Error("LLM returned empty response");
+  }
+
+  return {
+    content,
+    usage: { promptTokens, completionTokens, totalTokens },
+  };
+}

+ 40 - 0
packages/core/src/models/book.ts

@@ -0,0 +1,40 @@
+import { z } from "zod";
+
+export const PlatformSchema = z.enum(["tomato", "feilu", "qidian", "other"]);
+export type Platform = z.infer<typeof PlatformSchema>;
+
+export const GenreSchema = z.enum([
+  "xuanhuan",
+  "xianxia",
+  "urban",
+  "game",
+  "fanfic",
+  "horror",
+  "short",
+  "other",
+]);
+export type Genre = z.infer<typeof GenreSchema>;
+
+export const BookStatusSchema = z.enum([
+  "incubating",
+  "outlining",
+  "active",
+  "paused",
+  "completed",
+  "dropped",
+]);
+export type BookStatus = z.infer<typeof BookStatusSchema>;
+
+export const BookConfigSchema = z.object({
+  id: z.string().min(1),
+  title: z.string().min(1),
+  platform: PlatformSchema,
+  genre: GenreSchema,
+  status: BookStatusSchema,
+  targetChapters: z.number().int().min(1).default(200),
+  chapterWordCount: z.number().int().min(1000).default(3000),
+  createdAt: z.string().datetime(),
+  updatedAt: z.string().datetime(),
+});
+
+export type BookConfig = z.infer<typeof BookConfigSchema>;

+ 29 - 0
packages/core/src/models/chapter.ts

@@ -0,0 +1,29 @@
+import { z } from "zod";
+
+export const ChapterStatusSchema = z.enum([
+  "card-generated",
+  "drafting",
+  "drafted",
+  "auditing",
+  "audit-passed",
+  "audit-failed",
+  "revising",
+  "ready-for-review",
+  "approved",
+  "rejected",
+  "published",
+]);
+export type ChapterStatus = z.infer<typeof ChapterStatusSchema>;
+
+export const ChapterMetaSchema = z.object({
+  number: z.number().int().min(1),
+  title: z.string(),
+  status: ChapterStatusSchema,
+  wordCount: z.number().int().default(0),
+  createdAt: z.string().datetime(),
+  updatedAt: z.string().datetime(),
+  auditIssues: z.array(z.string()).default([]),
+  reviewNote: z.string().optional(),
+});
+
+export type ChapterMeta = z.infer<typeof ChapterMetaSchema>;

+ 52 - 0
packages/core/src/models/project.ts

@@ -0,0 +1,52 @@
+import { z } from "zod";
+
+export const LLMConfigSchema = z.object({
+  provider: z.enum(["anthropic", "openai", "custom"]),
+  baseUrl: z.string().url(),
+  apiKey: z.string().min(1),
+  model: z.string().min(1),
+});
+
+export type LLMConfig = z.infer<typeof LLMConfigSchema>;
+
+export const NotifyChannelSchema = z.discriminatedUnion("type", [
+  z.object({
+    type: z.literal("telegram"),
+    botToken: z.string().min(1),
+    chatId: z.string().min(1),
+  }),
+  z.object({
+    type: z.literal("wechat-work"),
+    webhookUrl: z.string().url(),
+  }),
+  z.object({
+    type: z.literal("feishu"),
+    webhookUrl: z.string().url(),
+  }),
+]);
+
+export type NotifyChannel = z.infer<typeof NotifyChannelSchema>;
+
+export const ProjectConfigSchema = z.object({
+  name: z.string().min(1),
+  version: z.literal("0.1.0"),
+  llm: LLMConfigSchema,
+  notify: z.array(NotifyChannelSchema).default([]),
+  daemon: z.object({
+    schedule: z.object({
+      radarCron: z.string().default("0 9 * * *"),
+      writeCron: z.string().default("0 14 * * *"),
+      auditCron: z.string().default("0 17 * * *"),
+    }),
+    maxConcurrentBooks: z.number().int().min(1).default(3),
+  }).default({
+    schedule: {
+      radarCron: "0 9 * * *",
+      writeCron: "0 14 * * *",
+      auditCron: "0 17 * * *",
+    },
+    maxConcurrentBooks: 3,
+  }),
+});
+
+export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;

+ 52 - 0
packages/core/src/models/state.ts

@@ -0,0 +1,52 @@
+/**
+ * Canonical state files — the three sources of truth per book.
+ * These are persisted as markdown files but parsed/validated here.
+ */
+
+export interface CurrentState {
+  readonly chapter: number;
+  readonly location: string;
+  readonly protagonist: {
+    readonly status: string;
+    readonly currentGoal: string;
+    readonly constraints: string;
+  };
+  readonly enemies: ReadonlyArray<{
+    readonly name: string;
+    readonly relationship: string;
+    readonly threat: string;
+  }>;
+  readonly knownTruths: ReadonlyArray<string>;
+  readonly currentConflict: string;
+  readonly anchor: string;
+}
+
+export interface LedgerEntry {
+  readonly chapter: number;
+  readonly openingValue: number;
+  readonly source: string;
+  readonly resourceCompleteness: string;
+  readonly delta: number;
+  readonly closingValue: number;
+  readonly basis: string;
+}
+
+export interface ParticleLedger {
+  readonly hardCap: number;
+  readonly currentTotal: number;
+  readonly entries: ReadonlyArray<LedgerEntry>;
+}
+
+export interface PendingHook {
+  readonly id: string;
+  readonly originChapter: number;
+  readonly type: string;
+  readonly status: "open" | "progressing" | "resolved";
+  readonly lastProgress: string;
+  readonly expectedResolution: string;
+  readonly note: string;
+}
+
+export interface PendingHooks {
+  readonly hooks: ReadonlyArray<PendingHook>;
+}

+ 49 - 0
packages/core/src/notify/dispatcher.ts

@@ -0,0 +1,49 @@
+import type { NotifyChannel } from "../models/project.js";
+import { sendTelegram } from "./telegram.js";
+import { sendFeishu } from "./feishu.js";
+import { sendWechatWork } from "./wechat-work.js";
+
+export interface NotifyMessage {
+  readonly title: string;
+  readonly body: string;
+}
+
+export async function dispatchNotification(
+  channels: ReadonlyArray<NotifyChannel>,
+  message: NotifyMessage,
+): Promise<void> {
+  const fullText = `**${message.title}**\n\n${message.body}`;
+
+  const tasks = channels.map(async (channel) => {
+    try {
+      switch (channel.type) {
+        case "telegram":
+          await sendTelegram(
+            { botToken: channel.botToken, chatId: channel.chatId },
+            fullText,
+          );
+          break;
+        case "feishu":
+          await sendFeishu(
+            { webhookUrl: channel.webhookUrl },
+            message.title,
+            message.body,
+          );
+          break;
+        case "wechat-work":
+          await sendWechatWork(
+            { webhookUrl: channel.webhookUrl },
+            fullText,
+          );
+          break;
+      }
+    } catch (e) {
+      // Log but don't throw — notification failure shouldn't block pipeline
+      process.stderr.write(
+        `[notify] ${channel.type} failed: ${e}\n`,
+      );
+    }
+  });
+
+  await Promise.all(tasks);
+}

+ 34 - 0
packages/core/src/notify/feishu.ts

@@ -0,0 +1,34 @@
+export interface FeishuConfig {
+  readonly webhookUrl: string;
+}
+
+export async function sendFeishu(
+  config: FeishuConfig,
+  title: string,
+  content: string,
+): Promise<void> {
+  const response = await fetch(config.webhookUrl, {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+      msg_type: "interactive",
+      card: {
+        header: {
+          title: { tag: "plain_text", content: title },
+          template: "blue",
+        },
+        elements: [
+          {
+            tag: "markdown",
+            content,
+          },
+        ],
+      },
+    }),
+  });
+
+  if (!response.ok) {
+    const body = await response.text();
+    throw new Error(`Feishu send failed: ${response.status} ${body}`);
+  }
+}

+ 25 - 0
packages/core/src/notify/telegram.ts

@@ -0,0 +1,25 @@
+export interface TelegramConfig {
+  readonly botToken: string;
+  readonly chatId: string;
+}
+
+export async function sendTelegram(
+  config: TelegramConfig,
+  message: string,
+): Promise<void> {
+  const url = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
+  const response = await fetch(url, {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+      chat_id: config.chatId,
+      text: message,
+      parse_mode: "Markdown",
+    }),
+  });
+
+  if (!response.ok) {
+    const body = await response.text();
+    throw new Error(`Telegram send failed: ${response.status} ${body}`);
+  }
+}

+ 22 - 0
packages/core/src/notify/wechat-work.ts

@@ -0,0 +1,22 @@
+export interface WechatWorkConfig {
+  readonly webhookUrl: string;
+}
+
+export async function sendWechatWork(
+  config: WechatWorkConfig,
+  content: string,
+): Promise<void> {
+  const response = await fetch(config.webhookUrl, {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+      msgtype: "markdown",
+      markdown: { content },
+    }),
+  });
+
+  if (!response.ok) {
+    const body = await response.text();
+    throw new Error(`WeCom send failed: ${response.status} ${body}`);
+  }
+}

+ 194 - 0
packages/core/src/pipeline/runner.ts

@@ -0,0 +1,194 @@
+import type OpenAI from "openai";
+import type { BookConfig } from "../models/book.js";
+import type { ChapterMeta } from "../models/chapter.js";
+import type { NotifyChannel } from "../models/project.js";
+import { ArchitectAgent } from "../agents/architect.js";
+import { WriterAgent } from "../agents/writer.js";
+import { ContinuityAuditor } from "../agents/continuity.js";
+import { ReviserAgent } from "../agents/reviser.js";
+import { RadarAgent } from "../agents/radar.js";
+import { StateManager } from "../state/manager.js";
+import { dispatchNotification } from "../notify/dispatcher.js";
+import type { AgentContext } from "../agents/base.js";
+import type { AuditResult } from "../agents/continuity.js";
+import type { RadarResult } from "../agents/radar.js";
+import { readFile, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+
+export interface PipelineConfig {
+  readonly client: OpenAI;
+  readonly model: string;
+  readonly projectRoot: string;
+  readonly notifyChannels?: ReadonlyArray<NotifyChannel>;
+}
+
+export interface ChapterPipelineResult {
+  readonly chapterNumber: number;
+  readonly title: string;
+  readonly wordCount: number;
+  readonly auditResult: AuditResult;
+  readonly revised: boolean;
+  readonly status: "approved" | "needs-review";
+}
+
+export class PipelineRunner {
+  private readonly state: StateManager;
+  private readonly config: PipelineConfig;
+
+  constructor(config: PipelineConfig) {
+    this.config = config;
+    this.state = new StateManager(config.projectRoot);
+  }
+
+  private agentCtx(bookId?: string): AgentContext {
+    return {
+      client: this.config.client,
+      model: this.config.model,
+      projectRoot: this.config.projectRoot,
+      bookId,
+    };
+  }
+
+  async runRadar(): Promise<RadarResult> {
+    const radar = new RadarAgent(this.agentCtx());
+    return radar.scan(["tomato", "feilu"]);
+  }
+
+  async initBook(book: BookConfig): Promise<void> {
+    const architect = new ArchitectAgent(this.agentCtx(book.id));
+    const bookDir = this.state.bookDir(book.id);
+
+    await this.state.saveBookConfig(book.id, book);
+
+    const foundation = await architect.generateFoundation(book);
+    await architect.writeFoundationFiles(bookDir, foundation);
+    await this.state.saveChapterIndex(book.id, []);
+  }
+
+  async writeNextChapter(bookId: string): Promise<ChapterPipelineResult> {
+    const book = await this.state.loadBookConfig(bookId);
+    const bookDir = this.state.bookDir(bookId);
+    const chapterNumber = await this.state.getNextChapterNumber(bookId);
+
+    // 1. Write chapter
+    const writer = new WriterAgent(this.agentCtx(bookId));
+    const output = await writer.writeChapter({
+      book,
+      bookDir,
+      chapterNumber,
+    });
+
+    // 2. Audit chapter
+    const auditor = new ContinuityAuditor(this.agentCtx(bookId));
+    let auditResult = await auditor.auditChapter(
+      bookDir,
+      output.content,
+      chapterNumber,
+    );
+
+    let finalContent = output.content;
+    let finalWordCount = output.wordCount;
+    let revised = false;
+
+    // 3. If audit fails, try auto-revise once
+    if (!auditResult.passed) {
+      const criticalIssues = auditResult.issues.filter(
+        (i) => i.severity === "critical",
+      );
+      if (criticalIssues.length > 0) {
+        const reviser = new ReviserAgent(this.agentCtx(bookId));
+        const reviseOutput = await reviser.reviseChapter(
+          bookDir,
+          output.content,
+          chapterNumber,
+          auditResult.issues,
+        );
+
+        if (reviseOutput.revisedContent.length > 0) {
+          finalContent = reviseOutput.revisedContent;
+          finalWordCount = reviseOutput.wordCount;
+          revised = true;
+
+          // Re-audit the revised content
+          auditResult = await auditor.auditChapter(
+            bookDir,
+            finalContent,
+            chapterNumber,
+          );
+
+          // Update state files from revision
+          const storyDir = join(bookDir, "story");
+          if (reviseOutput.updatedState !== "(状态卡未更新)") {
+            await writeFile(join(storyDir, "current_state.md"), reviseOutput.updatedState, "utf-8");
+          }
+          if (reviseOutput.updatedLedger !== "(账本未更新)") {
+            await writeFile(join(storyDir, "particle_ledger.md"), reviseOutput.updatedLedger, "utf-8");
+          }
+          if (reviseOutput.updatedHooks !== "(伏笔池未更新)") {
+            await writeFile(join(storyDir, "pending_hooks.md"), reviseOutput.updatedHooks, "utf-8");
+          }
+        }
+      }
+    }
+
+    // 4. Save chapter (original or revised)
+    const chaptersDir = join(bookDir, "chapters");
+    const paddedNum = String(chapterNumber).padStart(4, "0");
+    const title = revised ? output.title : output.title;
+    const filename = `${paddedNum}_${title.replace(/[/\\?%*:|"<>]/g, "").replace(/\s+/g, "_").slice(0, 50)}.md`;
+
+    await writeFile(
+      join(chaptersDir, filename),
+      `# 第${chapterNumber}章 ${title}\n\n${finalContent}`,
+      "utf-8",
+    );
+
+    // Save original state files if not revised
+    if (!revised) {
+      await writer.saveChapter(bookDir, output);
+    }
+
+    // 5. Update chapter index
+    const existingIndex = await this.state.loadChapterIndex(bookId);
+    const now = new Date().toISOString();
+    const newEntry: ChapterMeta = {
+      number: chapterNumber,
+      title: output.title,
+      status: auditResult.passed ? "ready-for-review" : "audit-failed",
+      wordCount: finalWordCount,
+      createdAt: now,
+      updatedAt: now,
+      auditIssues: auditResult.issues.map(
+        (i) => `[${i.severity}] ${i.description}`,
+      ),
+    };
+    await this.state.saveChapterIndex(bookId, [...existingIndex, newEntry]);
+
+    // 6. Send notification
+    if (this.config.notifyChannels && this.config.notifyChannels.length > 0) {
+      const statusEmoji = auditResult.passed ? "✅" : "⚠️";
+      await dispatchNotification(this.config.notifyChannels, {
+        title: `${statusEmoji} ${book.title} 第${chapterNumber}章`,
+        body: [
+          `**${output.title}** | ${finalWordCount}字`,
+          revised ? "📝 已自动修正" : "",
+          `审稿: ${auditResult.passed ? "通过" : "需人工审核"}`,
+          ...auditResult.issues
+            .filter((i) => i.severity !== "info")
+            .map((i) => `- [${i.severity}] ${i.description}`),
+        ]
+          .filter(Boolean)
+          .join("\n"),
+      });
+    }
+
+    return {
+      chapterNumber,
+      title: output.title,
+      wordCount: finalWordCount,
+      auditResult,
+      revised,
+      status: auditResult.passed ? "approved" : "needs-review",
+    };
+  }
+}

+ 132 - 0
packages/core/src/pipeline/scheduler.ts

@@ -0,0 +1,132 @@
+import { PipelineRunner } from "./runner.js";
+import type { PipelineConfig } from "./runner.js";
+import { StateManager } from "../state/manager.js";
+import type { BookConfig } from "../models/book.js";
+
+export interface SchedulerConfig extends PipelineConfig {
+  readonly radarCron: string;
+  readonly writeCron: string;
+  readonly auditCron: string;
+  readonly maxConcurrentBooks: number;
+  readonly onChapterComplete?: (bookId: string, chapter: number, status: string) => void;
+  readonly onError?: (bookId: string, error: Error) => void;
+}
+
+interface ScheduledTask {
+  readonly name: string;
+  readonly intervalMs: number;
+  timer?: ReturnType<typeof setInterval>;
+}
+
+export class Scheduler {
+  private readonly pipeline: PipelineRunner;
+  private readonly state: StateManager;
+  private readonly config: SchedulerConfig;
+  private tasks: ScheduledTask[] = [];
+  private running = false;
+
+  constructor(config: SchedulerConfig) {
+    this.config = config;
+    this.pipeline = new PipelineRunner(config);
+    this.state = new StateManager(config.projectRoot);
+  }
+
+  async start(): Promise<void> {
+    if (this.running) return;
+    this.running = true;
+
+    // Run write cycle immediately on start, then schedule
+    await this.runWriteCycle();
+
+    // Schedule recurring write cycle (default: every 2 hours)
+    const writeCycleMs = this.cronToMs(this.config.writeCron);
+    const writeTask: ScheduledTask = {
+      name: "write-cycle",
+      intervalMs: writeCycleMs,
+    };
+    writeTask.timer = setInterval(() => {
+      this.runWriteCycle().catch((e) => {
+        this.config.onError?.("scheduler", e as Error);
+      });
+    }, writeCycleMs);
+    this.tasks.push(writeTask);
+
+    // Schedule radar scan (default: daily)
+    const radarMs = this.cronToMs(this.config.radarCron);
+    const radarTask: ScheduledTask = {
+      name: "radar-scan",
+      intervalMs: radarMs,
+    };
+    radarTask.timer = setInterval(() => {
+      this.runRadarScan().catch((e) => {
+        this.config.onError?.("radar", e as Error);
+      });
+    }, radarMs);
+    this.tasks.push(radarTask);
+  }
+
+  stop(): void {
+    this.running = false;
+    for (const task of this.tasks) {
+      if (task.timer) clearInterval(task.timer);
+    }
+    this.tasks = [];
+  }
+
+  get isRunning(): boolean {
+    return this.running;
+  }
+
+  private async runWriteCycle(): Promise<void> {
+    const bookIds = await this.state.listBooks();
+
+    const activeBooks: Array<{ id: string; config: BookConfig }> = [];
+    for (const id of bookIds) {
+      const config = await this.state.loadBookConfig(id);
+      if (config.status === "active" || config.status === "outlining") {
+        activeBooks.push({ id, config });
+      }
+    }
+
+    const booksToWrite = activeBooks.slice(0, this.config.maxConcurrentBooks);
+
+    for (const book of booksToWrite) {
+      try {
+        const result = await this.pipeline.writeNextChapter(book.id);
+        this.config.onChapterComplete?.(
+          book.id,
+          result.chapterNumber,
+          result.status,
+        );
+      } catch (e) {
+        this.config.onError?.(book.id, e as Error);
+      }
+    }
+  }
+
+  private async runRadarScan(): Promise<void> {
+    try {
+      await this.pipeline.runRadar();
+    } catch (e) {
+      this.config.onError?.("radar", e as Error);
+    }
+  }
+
+  private cronToMs(cron: string): number {
+    // Simple cron-to-interval mapping for common patterns
+    // "0 9 * * *" = daily = 24h
+    // "0 14 * * *" = daily = 24h
+    // "0 */2 * * *" = every 2h
+    const parts = cron.split(" ");
+    if (parts.length >= 5) {
+      const hour = parts[1]!;
+      if (hour.startsWith("*/")) {
+        const interval = parseInt(hour.slice(2), 10);
+        return interval * 60 * 60 * 1000;
+      }
+      // Default: treat as daily
+      return 24 * 60 * 60 * 1000;
+    }
+    return 24 * 60 * 60 * 1000;
+  }
+}

+ 99 - 0
packages/core/src/state/manager.ts

@@ -0,0 +1,99 @@
+import { readFile, writeFile, mkdir, readdir, stat } from "node:fs/promises";
+import { join } from "node:path";
+import type { BookConfig } from "../models/book.js";
+import type { ChapterMeta } from "../models/chapter.js";
+
+export class StateManager {
+  constructor(private readonly projectRoot: string) {}
+
+  get booksDir(): string {
+    return join(this.projectRoot, "books");
+  }
+
+  bookDir(bookId: string): string {
+    return join(this.booksDir, bookId);
+  }
+
+  async loadProjectConfig(): Promise<Record<string, unknown>> {
+    const configPath = join(this.projectRoot, "inkos.json");
+    const raw = await readFile(configPath, "utf-8");
+    return JSON.parse(raw);
+  }
+
+  async saveProjectConfig(config: Record<string, unknown>): Promise<void> {
+    const configPath = join(this.projectRoot, "inkos.json");
+    await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
+  }
+
+  async loadBookConfig(bookId: string): Promise<BookConfig> {
+    const configPath = join(this.bookDir(bookId), "book.json");
+    const raw = await readFile(configPath, "utf-8");
+    return JSON.parse(raw);
+  }
+
+  async saveBookConfig(bookId: string, config: BookConfig): Promise<void> {
+    const dir = this.bookDir(bookId);
+    await mkdir(dir, { recursive: true });
+    await writeFile(
+      join(dir, "book.json"),
+      JSON.stringify(config, null, 2),
+      "utf-8",
+    );
+  }
+
+  async listBooks(): Promise<ReadonlyArray<string>> {
+    try {
+      const entries = await readdir(this.booksDir);
+      const bookIds: string[] = [];
+      for (const entry of entries) {
+        const bookJsonPath = join(this.booksDir, entry, "book.json");
+        try {
+          await stat(bookJsonPath);
+          bookIds.push(entry);
+        } catch {
+          // not a book directory
+        }
+      }
+      return bookIds;
+    } catch {
+      return [];
+    }
+  }
+
+  async getNextChapterNumber(bookId: string): Promise<number> {
+    const chaptersDir = join(this.bookDir(bookId), "chapters");
+    try {
+      const files = await readdir(chaptersDir);
+      const chapterFiles = files.filter((f) => f.endsWith(".md")).sort();
+      if (chapterFiles.length === 0) return 1;
+      const lastFile = chapterFiles[chapterFiles.length - 1]!;
+      const match = lastFile.match(/^(\d+)/);
+      return match ? parseInt(match[1], 10) + 1 : 1;
+    } catch {
+      return 1;
+    }
+  }
+
+  async loadChapterIndex(bookId: string): Promise<ReadonlyArray<ChapterMeta>> {
+    const indexPath = join(this.bookDir(bookId), "chapters", "index.json");
+    try {
+      const raw = await readFile(indexPath, "utf-8");
+      return JSON.parse(raw);
+    } catch {
+      return [];
+    }
+  }
+
+  async saveChapterIndex(
+    bookId: string,
+    index: ReadonlyArray<ChapterMeta>,
+  ): Promise<void> {
+    const chaptersDir = join(this.bookDir(bookId), "chapters");
+    await mkdir(chaptersDir, { recursive: true });
+    await writeFile(
+      join(chaptersDir, "index.json"),
+      JSON.stringify(index, null, 2),
+      "utf-8",
+    );
+  }
+}

+ 8 - 0
packages/core/tsconfig.json

@@ -0,0 +1,8 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "dist",
+    "rootDir": "src"
+  },
+  "include": ["src"]
+}

+ 1904 - 0
pnpm-lock.yaml

@@ -0,0 +1,1904 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .: {}
+
+  packages/cli:
+    dependencies:
+      '@inkos/core':
+        specifier: workspace:*
+        version: link:../core
+      chalk:
+        specifier: ^5.4.0
+        version: 5.6.2
+      commander:
+        specifier: ^13.0.0
+        version: 13.1.0
+      dotenv:
+        specifier: ^16.4.0
+        version: 16.6.1
+      inquirer:
+        specifier: ^12.3.0
+        version: 12.11.1(@types/node@22.19.15)
+      ora:
+        specifier: ^8.2.0
+        version: 8.2.0
+    devDependencies:
+      '@types/node':
+        specifier: ^22.0.0
+        version: 22.19.15
+      typescript:
+        specifier: ^5.8.0
+        version: 5.9.3
+      vitest:
+        specifier: ^3.0.0
+        version: 3.2.4(@types/node@22.19.15)
+
+  packages/core:
+    dependencies:
+      openai:
+        specifier: ^4.80.0
+        version: 4.104.0(zod@3.25.76)
+      zod:
+        specifier: ^3.24.0
+        version: 3.25.76
+    devDependencies:
+      typescript:
+        specifier: ^5.8.0
+        version: 5.9.3
+      vitest:
+        specifier: ^3.0.0
+        version: 3.2.4(@types/node@22.19.15)
+
+packages:
+
+  '@esbuild/aix-ppc64@0.27.3':
+    resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/android-arm64@0.27.3':
+    resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm@0.27.3':
+    resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-x64@0.27.3':
+    resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/darwin-arm64@0.27.3':
+    resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.27.3':
+    resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/freebsd-arm64@0.27.3':
+    resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.27.3':
+    resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.27.3':
+    resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.27.3':
+    resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.27.3':
+    resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.27.3':
+    resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.27.3':
+    resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.27.3':
+    resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.27.3':
+    resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.27.3':
+    resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.27.3':
+    resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-arm64@0.27.3':
+    resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.27.3':
+    resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-arm64@0.27.3':
+    resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.27.3':
+    resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openharmony-arm64@0.27.3':
+    resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@esbuild/sunos-x64@0.27.3':
+    resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/win32-arm64@0.27.3':
+    resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.27.3':
+    resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.27.3':
+    resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@inquirer/ansi@1.0.2':
+    resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==}
+    engines: {node: '>=18'}
+
+  '@inquirer/checkbox@4.3.2':
+    resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/confirm@5.1.21':
+    resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/core@10.3.2':
+    resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/editor@4.2.23':
+    resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/expand@4.0.23':
+    resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/external-editor@1.0.3':
+    resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/figures@1.0.15':
+    resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==}
+    engines: {node: '>=18'}
+
+  '@inquirer/input@4.3.1':
+    resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/number@3.0.23':
+    resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/password@4.0.23':
+    resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/prompts@7.10.1':
+    resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/rawlist@4.1.11':
+    resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/search@3.2.2':
+    resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/select@4.4.2':
+    resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@inquirer/type@3.0.10':
+    resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  '@jridgewell/sourcemap-codec@1.5.5':
+    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+  '@rollup/rollup-android-arm-eabi@4.59.0':
+    resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
+    cpu: [arm]
+    os: [android]
+
+  '@rollup/rollup-android-arm64@4.59.0':
+    resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
+    cpu: [arm64]
+    os: [android]
+
+  '@rollup/rollup-darwin-arm64@4.59.0':
+    resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@rollup/rollup-darwin-x64@4.59.0':
+    resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@rollup/rollup-freebsd-arm64@4.59.0':
+    resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@rollup/rollup-freebsd-x64@4.59.0':
+    resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.59.0':
+    resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm-musleabihf@4.59.0':
+    resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
+    cpu: [arm]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-arm64-gnu@4.59.0':
+    resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm64-musl@4.59.0':
+    resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-loong64-gnu@4.59.0':
+    resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
+    cpu: [loong64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-loong64-musl@4.59.0':
+    resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
+    cpu: [loong64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-ppc64-gnu@4.59.0':
+    resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-ppc64-musl@4.59.0':
+    resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-riscv64-gnu@4.59.0':
+    resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-riscv64-musl@4.59.0':
+    resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-s390x-gnu@4.59.0':
+    resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-gnu@4.59.0':
+    resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-musl@4.59.0':
+    resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-openbsd-x64@4.59.0':
+    resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@rollup/rollup-openharmony-arm64@4.59.0':
+    resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@rollup/rollup-win32-arm64-msvc@4.59.0':
+    resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
+    cpu: [arm64]
+    os: [win32]
+
+  '@rollup/rollup-win32-ia32-msvc@4.59.0':
+    resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
+    cpu: [ia32]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-gnu@4.59.0':
+    resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
+    cpu: [x64]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-msvc@4.59.0':
+    resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
+    cpu: [x64]
+    os: [win32]
+
+  '@types/chai@5.2.3':
+    resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+  '@types/deep-eql@4.0.2':
+    resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+  '@types/estree@1.0.8':
+    resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+  '@types/node-fetch@2.6.13':
+    resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
+
+  '@types/node@18.19.130':
+    resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
+
+  '@types/node@22.19.15':
+    resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
+
+  '@vitest/expect@3.2.4':
+    resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
+
+  '@vitest/mocker@3.2.4':
+    resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
+    peerDependencies:
+      msw: ^2.4.9
+      vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
+    peerDependenciesMeta:
+      msw:
+        optional: true
+      vite:
+        optional: true
+
+  '@vitest/pretty-format@3.2.4':
+    resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
+
+  '@vitest/runner@3.2.4':
+    resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
+
+  '@vitest/snapshot@3.2.4':
+    resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
+
+  '@vitest/spy@3.2.4':
+    resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
+
+  '@vitest/utils@3.2.4':
+    resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
+
+  abort-controller@3.0.0:
+    resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+    engines: {node: '>=6.5'}
+
+  agentkeepalive@4.6.0:
+    resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
+    engines: {node: '>= 8.0.0'}
+
+  ansi-regex@5.0.1:
+    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+    engines: {node: '>=8'}
+
+  ansi-regex@6.2.2:
+    resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+    engines: {node: '>=12'}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  assertion-error@2.0.1:
+    resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+    engines: {node: '>=12'}
+
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+  cac@6.7.14:
+    resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+    engines: {node: '>=8'}
+
+  call-bind-apply-helpers@1.0.2:
+    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+    engines: {node: '>= 0.4'}
+
+  chai@5.3.3:
+    resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
+    engines: {node: '>=18'}
+
+  chalk@5.6.2:
+    resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+  chardet@2.1.1:
+    resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
+
+  check-error@2.1.3:
+    resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
+    engines: {node: '>= 16'}
+
+  cli-cursor@5.0.0:
+    resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
+    engines: {node: '>=18'}
+
+  cli-spinners@2.9.2:
+    resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
+    engines: {node: '>=6'}
+
+  cli-width@4.1.0:
+    resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
+    engines: {node: '>= 12'}
+
+  color-convert@2.0.1:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  color-name@1.1.4:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
+  commander@13.1.0:
+    resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
+    engines: {node: '>=18'}
+
+  debug@4.4.3:
+    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  deep-eql@5.0.2:
+    resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+    engines: {node: '>=6'}
+
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
+  dotenv@16.6.1:
+    resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
+    engines: {node: '>=12'}
+
+  dunder-proto@1.0.1:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
+  emoji-regex@10.6.0:
+    resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
+  emoji-regex@8.0.0:
+    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+  es-define-property@1.0.1:
+    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+    engines: {node: '>= 0.4'}
+
+  es-errors@1.3.0:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
+  es-module-lexer@1.7.0:
+    resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
+  es-object-atoms@1.1.1:
+    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+    engines: {node: '>= 0.4'}
+
+  es-set-tostringtag@2.1.0:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
+  esbuild@0.27.3:
+    resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  estree-walker@3.0.3:
+    resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+  event-target-shim@5.0.1:
+    resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
+    engines: {node: '>=6'}
+
+  expect-type@1.3.0:
+    resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+    engines: {node: '>=12.0.0'}
+
+  fdir@6.5.0:
+    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+    engines: {node: '>=12.0.0'}
+    peerDependencies:
+      picomatch: ^3 || ^4
+    peerDependenciesMeta:
+      picomatch:
+        optional: true
+
+  form-data-encoder@1.7.2:
+    resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
+
+  form-data@4.0.5:
+    resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+    engines: {node: '>= 6'}
+
+  formdata-node@4.4.1:
+    resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
+    engines: {node: '>= 12.20'}
+
+  fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  function-bind@1.1.2:
+    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+  get-east-asian-width@1.5.0:
+    resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
+    engines: {node: '>=18'}
+
+  get-intrinsic@1.3.0:
+    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+    engines: {node: '>= 0.4'}
+
+  get-proto@1.0.1:
+    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+    engines: {node: '>= 0.4'}
+
+  gopd@1.2.0:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
+
+  has-symbols@1.1.0:
+    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+    engines: {node: '>= 0.4'}
+
+  has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
+  hasown@2.0.2:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    engines: {node: '>= 0.4'}
+
+  humanize-ms@1.2.1:
+    resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+
+  iconv-lite@0.7.2:
+    resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
+    engines: {node: '>=0.10.0'}
+
+  inquirer@12.11.1:
+    resolution: {integrity: sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@types/node': '>=18'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+
+  is-fullwidth-code-point@3.0.0:
+    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+    engines: {node: '>=8'}
+
+  is-interactive@2.0.0:
+    resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
+    engines: {node: '>=12'}
+
+  is-unicode-supported@1.3.0:
+    resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
+    engines: {node: '>=12'}
+
+  is-unicode-supported@2.1.0:
+    resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
+    engines: {node: '>=18'}
+
+  js-tokens@9.0.1:
+    resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
+  log-symbols@6.0.0:
+    resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
+    engines: {node: '>=18'}
+
+  loupe@3.2.1:
+    resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+
+  magic-string@0.30.21:
+    resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+  math-intrinsics@1.1.0:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
+  mime-db@1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+
+  mime-types@2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+
+  mimic-function@5.0.1:
+    resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
+    engines: {node: '>=18'}
+
+  ms@2.1.3:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  mute-stream@2.0.0:
+    resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
+    engines: {node: ^18.17.0 || >=20.5.0}
+
+  nanoid@3.3.11:
+    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  node-domexception@1.0.0:
+    resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+    engines: {node: '>=10.5.0'}
+    deprecated: Use your platform's native DOMException instead
+
+  node-fetch@2.7.0:
+    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+    engines: {node: 4.x || >=6.0.0}
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+
+  onetime@7.0.0:
+    resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
+    engines: {node: '>=18'}
+
+  openai@4.104.0:
+    resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==}
+    hasBin: true
+    peerDependencies:
+      ws: ^8.18.0
+      zod: ^3.23.8
+    peerDependenciesMeta:
+      ws:
+        optional: true
+      zod:
+        optional: true
+
+  ora@8.2.0:
+    resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
+    engines: {node: '>=18'}
+
+  pathe@2.0.3:
+    resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+  pathval@2.0.1:
+    resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
+    engines: {node: '>= 14.16'}
+
+  picocolors@1.1.1:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+  picomatch@4.0.3:
+    resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+    engines: {node: '>=12'}
+
+  postcss@8.5.8:
+    resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  restore-cursor@5.1.0:
+    resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
+    engines: {node: '>=18'}
+
+  rollup@4.59.0:
+    resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+    hasBin: true
+
+  run-async@4.0.6:
+    resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==}
+    engines: {node: '>=0.12.0'}
+
+  rxjs@7.8.2:
+    resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
+
+  safer-buffer@2.1.2:
+    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+  siginfo@2.0.0:
+    resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+  signal-exit@4.1.0:
+    resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+    engines: {node: '>=14'}
+
+  source-map-js@1.2.1:
+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+    engines: {node: '>=0.10.0'}
+
+  stackback@0.0.2:
+    resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+  std-env@3.10.0:
+    resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
+  stdin-discarder@0.2.2:
+    resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
+    engines: {node: '>=18'}
+
+  string-width@4.2.3:
+    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+    engines: {node: '>=8'}
+
+  string-width@7.2.0:
+    resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+    engines: {node: '>=18'}
+
+  strip-ansi@6.0.1:
+    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+    engines: {node: '>=8'}
+
+  strip-ansi@7.2.0:
+    resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+    engines: {node: '>=12'}
+
+  strip-literal@3.1.0:
+    resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
+
+  tinybench@2.9.0:
+    resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+  tinyexec@0.3.2:
+    resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
+  tinyglobby@0.2.15:
+    resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+    engines: {node: '>=12.0.0'}
+
+  tinypool@1.1.1:
+    resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+
+  tinyrainbow@2.0.0:
+    resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
+    engines: {node: '>=14.0.0'}
+
+  tinyspy@4.0.4:
+    resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
+    engines: {node: '>=14.0.0'}
+
+  tr46@0.0.3:
+    resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+  tslib@2.8.1:
+    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+  typescript@5.9.3:
+    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  undici-types@5.26.5:
+    resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
+  undici-types@6.21.0:
+    resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+  vite-node@3.2.4:
+    resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+    hasBin: true
+
+  vite@7.3.1:
+    resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^20.19.0 || >=22.12.0
+      jiti: '>=1.21.0'
+      less: ^4.0.0
+      lightningcss: ^1.21.0
+      sass: ^1.70.0
+      sass-embedded: ^1.70.0
+      stylus: '>=0.54.8'
+      sugarss: ^5.0.0
+      terser: ^5.16.0
+      tsx: ^4.8.1
+      yaml: ^2.4.2
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      jiti:
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+      tsx:
+        optional: true
+      yaml:
+        optional: true
+
+  vitest@3.2.4:
+    resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+    hasBin: true
+    peerDependencies:
+      '@edge-runtime/vm': '*'
+      '@types/debug': ^4.1.12
+      '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+      '@vitest/browser': 3.2.4
+      '@vitest/ui': 3.2.4
+      happy-dom: '*'
+      jsdom: '*'
+    peerDependenciesMeta:
+      '@edge-runtime/vm':
+        optional: true
+      '@types/debug':
+        optional: true
+      '@types/node':
+        optional: true
+      '@vitest/browser':
+        optional: true
+      '@vitest/ui':
+        optional: true
+      happy-dom:
+        optional: true
+      jsdom:
+        optional: true
+
+  web-streams-polyfill@4.0.0-beta.3:
+    resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
+    engines: {node: '>= 14'}
+
+  webidl-conversions@3.0.1:
+    resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+  whatwg-url@5.0.0:
+    resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+  why-is-node-running@2.3.0:
+    resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+    engines: {node: '>=8'}
+    hasBin: true
+
+  wrap-ansi@6.2.0:
+    resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+    engines: {node: '>=8'}
+
+  yoctocolors-cjs@2.1.3:
+    resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
+    engines: {node: '>=18'}
+
+  zod@3.25.76:
+    resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+
+snapshots:
+
+  '@esbuild/aix-ppc64@0.27.3':
+    optional: true
+
+  '@esbuild/android-arm64@0.27.3':
+    optional: true
+
+  '@esbuild/android-arm@0.27.3':
+    optional: true
+
+  '@esbuild/android-x64@0.27.3':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.27.3':
+    optional: true
+
+  '@esbuild/darwin-x64@0.27.3':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.27.3':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.27.3':
+    optional: true
+
+  '@esbuild/linux-arm64@0.27.3':
+    optional: true
+
+  '@esbuild/linux-arm@0.27.3':
+    optional: true
+
+  '@esbuild/linux-ia32@0.27.3':
+    optional: true
+
+  '@esbuild/linux-loong64@0.27.3':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.27.3':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.27.3':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.27.3':
+    optional: true
+
+  '@esbuild/linux-s390x@0.27.3':
+    optional: true
+
+  '@esbuild/linux-x64@0.27.3':
+    optional: true
+
+  '@esbuild/netbsd-arm64@0.27.3':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.27.3':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.27.3':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.27.3':
+    optional: true
+
+  '@esbuild/openharmony-arm64@0.27.3':
+    optional: true
+
+  '@esbuild/sunos-x64@0.27.3':
+    optional: true
+
+  '@esbuild/win32-arm64@0.27.3':
+    optional: true
+
+  '@esbuild/win32-ia32@0.27.3':
+    optional: true
+
+  '@esbuild/win32-x64@0.27.3':
+    optional: true
+
+  '@inquirer/ansi@1.0.2': {}
+
+  '@inquirer/checkbox@4.3.2(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/ansi': 1.0.2
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/figures': 1.0.15
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+      yoctocolors-cjs: 2.1.3
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/confirm@5.1.21(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/core@10.3.2(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/ansi': 1.0.2
+      '@inquirer/figures': 1.0.15
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+      cli-width: 4.1.0
+      mute-stream: 2.0.0
+      signal-exit: 4.1.0
+      wrap-ansi: 6.2.0
+      yoctocolors-cjs: 2.1.3
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/editor@4.2.23(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/external-editor': 1.0.3(@types/node@22.19.15)
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/expand@4.0.23(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+      yoctocolors-cjs: 2.1.3
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/external-editor@1.0.3(@types/node@22.19.15)':
+    dependencies:
+      chardet: 2.1.1
+      iconv-lite: 0.7.2
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/figures@1.0.15': {}
+
+  '@inquirer/input@4.3.1(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/number@3.0.23(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/password@4.0.23(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/ansi': 1.0.2
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/prompts@7.10.1(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/checkbox': 4.3.2(@types/node@22.19.15)
+      '@inquirer/confirm': 5.1.21(@types/node@22.19.15)
+      '@inquirer/editor': 4.2.23(@types/node@22.19.15)
+      '@inquirer/expand': 4.0.23(@types/node@22.19.15)
+      '@inquirer/input': 4.3.1(@types/node@22.19.15)
+      '@inquirer/number': 3.0.23(@types/node@22.19.15)
+      '@inquirer/password': 4.0.23(@types/node@22.19.15)
+      '@inquirer/rawlist': 4.1.11(@types/node@22.19.15)
+      '@inquirer/search': 3.2.2(@types/node@22.19.15)
+      '@inquirer/select': 4.4.2(@types/node@22.19.15)
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/rawlist@4.1.11(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+      yoctocolors-cjs: 2.1.3
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/search@3.2.2(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/figures': 1.0.15
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+      yoctocolors-cjs: 2.1.3
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/select@4.4.2(@types/node@22.19.15)':
+    dependencies:
+      '@inquirer/ansi': 1.0.2
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/figures': 1.0.15
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+      yoctocolors-cjs: 2.1.3
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@inquirer/type@3.0.10(@types/node@22.19.15)':
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  '@jridgewell/sourcemap-codec@1.5.5': {}
+
+  '@rollup/rollup-android-arm-eabi@4.59.0':
+    optional: true
+
+  '@rollup/rollup-android-arm64@4.59.0':
+    optional: true
+
+  '@rollup/rollup-darwin-arm64@4.59.0':
+    optional: true
+
+  '@rollup/rollup-darwin-x64@4.59.0':
+    optional: true
+
+  '@rollup/rollup-freebsd-arm64@4.59.0':
+    optional: true
+
+  '@rollup/rollup-freebsd-x64@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-arm-musleabihf@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-gnu@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-musl@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-loong64-gnu@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-loong64-musl@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-ppc64-gnu@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-ppc64-musl@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-gnu@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-musl@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-s390x-gnu@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-x64-gnu@4.59.0':
+    optional: true
+
+  '@rollup/rollup-linux-x64-musl@4.59.0':
+    optional: true
+
+  '@rollup/rollup-openbsd-x64@4.59.0':
+    optional: true
+
+  '@rollup/rollup-openharmony-arm64@4.59.0':
+    optional: true
+
+  '@rollup/rollup-win32-arm64-msvc@4.59.0':
+    optional: true
+
+  '@rollup/rollup-win32-ia32-msvc@4.59.0':
+    optional: true
+
+  '@rollup/rollup-win32-x64-gnu@4.59.0':
+    optional: true
+
+  '@rollup/rollup-win32-x64-msvc@4.59.0':
+    optional: true
+
+  '@types/chai@5.2.3':
+    dependencies:
+      '@types/deep-eql': 4.0.2
+      assertion-error: 2.0.1
+
+  '@types/deep-eql@4.0.2': {}
+
+  '@types/estree@1.0.8': {}
+
+  '@types/node-fetch@2.6.13':
+    dependencies:
+      '@types/node': 22.19.15
+      form-data: 4.0.5
+
+  '@types/node@18.19.130':
+    dependencies:
+      undici-types: 5.26.5
+
+  '@types/node@22.19.15':
+    dependencies:
+      undici-types: 6.21.0
+
+  '@vitest/expect@3.2.4':
+    dependencies:
+      '@types/chai': 5.2.3
+      '@vitest/spy': 3.2.4
+      '@vitest/utils': 3.2.4
+      chai: 5.3.3
+      tinyrainbow: 2.0.0
+
+  '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15))':
+    dependencies:
+      '@vitest/spy': 3.2.4
+      estree-walker: 3.0.3
+      magic-string: 0.30.21
+    optionalDependencies:
+      vite: 7.3.1(@types/node@22.19.15)
+
+  '@vitest/pretty-format@3.2.4':
+    dependencies:
+      tinyrainbow: 2.0.0
+
+  '@vitest/runner@3.2.4':
+    dependencies:
+      '@vitest/utils': 3.2.4
+      pathe: 2.0.3
+      strip-literal: 3.1.0
+
+  '@vitest/snapshot@3.2.4':
+    dependencies:
+      '@vitest/pretty-format': 3.2.4
+      magic-string: 0.30.21
+      pathe: 2.0.3
+
+  '@vitest/spy@3.2.4':
+    dependencies:
+      tinyspy: 4.0.4
+
+  '@vitest/utils@3.2.4':
+    dependencies:
+      '@vitest/pretty-format': 3.2.4
+      loupe: 3.2.1
+      tinyrainbow: 2.0.0
+
+  abort-controller@3.0.0:
+    dependencies:
+      event-target-shim: 5.0.1
+
+  agentkeepalive@4.6.0:
+    dependencies:
+      humanize-ms: 1.2.1
+
+  ansi-regex@5.0.1: {}
+
+  ansi-regex@6.2.2: {}
+
+  ansi-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  assertion-error@2.0.1: {}
+
+  asynckit@0.4.0: {}
+
+  cac@6.7.14: {}
+
+  call-bind-apply-helpers@1.0.2:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+
+  chai@5.3.3:
+    dependencies:
+      assertion-error: 2.0.1
+      check-error: 2.1.3
+      deep-eql: 5.0.2
+      loupe: 3.2.1
+      pathval: 2.0.1
+
+  chalk@5.6.2: {}
+
+  chardet@2.1.1: {}
+
+  check-error@2.1.3: {}
+
+  cli-cursor@5.0.0:
+    dependencies:
+      restore-cursor: 5.1.0
+
+  cli-spinners@2.9.2: {}
+
+  cli-width@4.1.0: {}
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.4: {}
+
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
+  commander@13.1.0: {}
+
+  debug@4.4.3:
+    dependencies:
+      ms: 2.1.3
+
+  deep-eql@5.0.2: {}
+
+  delayed-stream@1.0.0: {}
+
+  dotenv@16.6.1: {}
+
+  dunder-proto@1.0.1:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
+  emoji-regex@10.6.0: {}
+
+  emoji-regex@8.0.0: {}
+
+  es-define-property@1.0.1: {}
+
+  es-errors@1.3.0: {}
+
+  es-module-lexer@1.7.0: {}
+
+  es-object-atoms@1.1.1:
+    dependencies:
+      es-errors: 1.3.0
+
+  es-set-tostringtag@2.1.0:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
+  esbuild@0.27.3:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.27.3
+      '@esbuild/android-arm': 0.27.3
+      '@esbuild/android-arm64': 0.27.3
+      '@esbuild/android-x64': 0.27.3
+      '@esbuild/darwin-arm64': 0.27.3
+      '@esbuild/darwin-x64': 0.27.3
+      '@esbuild/freebsd-arm64': 0.27.3
+      '@esbuild/freebsd-x64': 0.27.3
+      '@esbuild/linux-arm': 0.27.3
+      '@esbuild/linux-arm64': 0.27.3
+      '@esbuild/linux-ia32': 0.27.3
+      '@esbuild/linux-loong64': 0.27.3
+      '@esbuild/linux-mips64el': 0.27.3
+      '@esbuild/linux-ppc64': 0.27.3
+      '@esbuild/linux-riscv64': 0.27.3
+      '@esbuild/linux-s390x': 0.27.3
+      '@esbuild/linux-x64': 0.27.3
+      '@esbuild/netbsd-arm64': 0.27.3
+      '@esbuild/netbsd-x64': 0.27.3
+      '@esbuild/openbsd-arm64': 0.27.3
+      '@esbuild/openbsd-x64': 0.27.3
+      '@esbuild/openharmony-arm64': 0.27.3
+      '@esbuild/sunos-x64': 0.27.3
+      '@esbuild/win32-arm64': 0.27.3
+      '@esbuild/win32-ia32': 0.27.3
+      '@esbuild/win32-x64': 0.27.3
+
+  estree-walker@3.0.3:
+    dependencies:
+      '@types/estree': 1.0.8
+
+  event-target-shim@5.0.1: {}
+
+  expect-type@1.3.0: {}
+
+  fdir@6.5.0(picomatch@4.0.3):
+    optionalDependencies:
+      picomatch: 4.0.3
+
+  form-data-encoder@1.7.2: {}
+
+  form-data@4.0.5:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      hasown: 2.0.2
+      mime-types: 2.1.35
+
+  formdata-node@4.4.1:
+    dependencies:
+      node-domexception: 1.0.0
+      web-streams-polyfill: 4.0.0-beta.3
+
+  fsevents@2.3.3:
+    optional: true
+
+  function-bind@1.1.2: {}
+
+  get-east-asian-width@1.5.0: {}
+
+  get-intrinsic@1.3.0:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-define-property: 1.0.1
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      function-bind: 1.1.2
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-symbols: 1.1.0
+      hasown: 2.0.2
+      math-intrinsics: 1.1.0
+
+  get-proto@1.0.1:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.1
+
+  gopd@1.2.0: {}
+
+  has-symbols@1.1.0: {}
+
+  has-tostringtag@1.0.2:
+    dependencies:
+      has-symbols: 1.1.0
+
+  hasown@2.0.2:
+    dependencies:
+      function-bind: 1.1.2
+
+  humanize-ms@1.2.1:
+    dependencies:
+      ms: 2.1.3
+
+  iconv-lite@0.7.2:
+    dependencies:
+      safer-buffer: 2.1.2
+
+  inquirer@12.11.1(@types/node@22.19.15):
+    dependencies:
+      '@inquirer/ansi': 1.0.2
+      '@inquirer/core': 10.3.2(@types/node@22.19.15)
+      '@inquirer/prompts': 7.10.1(@types/node@22.19.15)
+      '@inquirer/type': 3.0.10(@types/node@22.19.15)
+      mute-stream: 2.0.0
+      run-async: 4.0.6
+      rxjs: 7.8.2
+    optionalDependencies:
+      '@types/node': 22.19.15
+
+  is-fullwidth-code-point@3.0.0: {}
+
+  is-interactive@2.0.0: {}
+
+  is-unicode-supported@1.3.0: {}
+
+  is-unicode-supported@2.1.0: {}
+
+  js-tokens@9.0.1: {}
+
+  log-symbols@6.0.0:
+    dependencies:
+      chalk: 5.6.2
+      is-unicode-supported: 1.3.0
+
+  loupe@3.2.1: {}
+
+  magic-string@0.30.21:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.5
+
+  math-intrinsics@1.1.0: {}
+
+  mime-db@1.52.0: {}
+
+  mime-types@2.1.35:
+    dependencies:
+      mime-db: 1.52.0
+
+  mimic-function@5.0.1: {}
+
+  ms@2.1.3: {}
+
+  mute-stream@2.0.0: {}
+
+  nanoid@3.3.11: {}
+
+  node-domexception@1.0.0: {}
+
+  node-fetch@2.7.0:
+    dependencies:
+      whatwg-url: 5.0.0
+
+  onetime@7.0.0:
+    dependencies:
+      mimic-function: 5.0.1
+
+  openai@4.104.0(zod@3.25.76):
+    dependencies:
+      '@types/node': 18.19.130
+      '@types/node-fetch': 2.6.13
+      abort-controller: 3.0.0
+      agentkeepalive: 4.6.0
+      form-data-encoder: 1.7.2
+      formdata-node: 4.4.1
+      node-fetch: 2.7.0
+    optionalDependencies:
+      zod: 3.25.76
+    transitivePeerDependencies:
+      - encoding
+
+  ora@8.2.0:
+    dependencies:
+      chalk: 5.6.2
+      cli-cursor: 5.0.0
+      cli-spinners: 2.9.2
+      is-interactive: 2.0.0
+      is-unicode-supported: 2.1.0
+      log-symbols: 6.0.0
+      stdin-discarder: 0.2.2
+      string-width: 7.2.0
+      strip-ansi: 7.2.0
+
+  pathe@2.0.3: {}
+
+  pathval@2.0.1: {}
+
+  picocolors@1.1.1: {}
+
+  picomatch@4.0.3: {}
+
+  postcss@8.5.8:
+    dependencies:
+      nanoid: 3.3.11
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  restore-cursor@5.1.0:
+    dependencies:
+      onetime: 7.0.0
+      signal-exit: 4.1.0
+
+  rollup@4.59.0:
+    dependencies:
+      '@types/estree': 1.0.8
+    optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.59.0
+      '@rollup/rollup-android-arm64': 4.59.0
+      '@rollup/rollup-darwin-arm64': 4.59.0
+      '@rollup/rollup-darwin-x64': 4.59.0
+      '@rollup/rollup-freebsd-arm64': 4.59.0
+      '@rollup/rollup-freebsd-x64': 4.59.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.59.0
+      '@rollup/rollup-linux-arm-musleabihf': 4.59.0
+      '@rollup/rollup-linux-arm64-gnu': 4.59.0
+      '@rollup/rollup-linux-arm64-musl': 4.59.0
+      '@rollup/rollup-linux-loong64-gnu': 4.59.0
+      '@rollup/rollup-linux-loong64-musl': 4.59.0
+      '@rollup/rollup-linux-ppc64-gnu': 4.59.0
+      '@rollup/rollup-linux-ppc64-musl': 4.59.0
+      '@rollup/rollup-linux-riscv64-gnu': 4.59.0
+      '@rollup/rollup-linux-riscv64-musl': 4.59.0
+      '@rollup/rollup-linux-s390x-gnu': 4.59.0
+      '@rollup/rollup-linux-x64-gnu': 4.59.0
+      '@rollup/rollup-linux-x64-musl': 4.59.0
+      '@rollup/rollup-openbsd-x64': 4.59.0
+      '@rollup/rollup-openharmony-arm64': 4.59.0
+      '@rollup/rollup-win32-arm64-msvc': 4.59.0
+      '@rollup/rollup-win32-ia32-msvc': 4.59.0
+      '@rollup/rollup-win32-x64-gnu': 4.59.0
+      '@rollup/rollup-win32-x64-msvc': 4.59.0
+      fsevents: 2.3.3
+
+  run-async@4.0.6: {}
+
+  rxjs@7.8.2:
+    dependencies:
+      tslib: 2.8.1
+
+  safer-buffer@2.1.2: {}
+
+  siginfo@2.0.0: {}
+
+  signal-exit@4.1.0: {}
+
+  source-map-js@1.2.1: {}
+
+  stackback@0.0.2: {}
+
+  std-env@3.10.0: {}
+
+  stdin-discarder@0.2.2: {}
+
+  string-width@4.2.3:
+    dependencies:
+      emoji-regex: 8.0.0
+      is-fullwidth-code-point: 3.0.0
+      strip-ansi: 6.0.1
+
+  string-width@7.2.0:
+    dependencies:
+      emoji-regex: 10.6.0
+      get-east-asian-width: 1.5.0
+      strip-ansi: 7.2.0
+
+  strip-ansi@6.0.1:
+    dependencies:
+      ansi-regex: 5.0.1
+
+  strip-ansi@7.2.0:
+    dependencies:
+      ansi-regex: 6.2.2
+
+  strip-literal@3.1.0:
+    dependencies:
+      js-tokens: 9.0.1
+
+  tinybench@2.9.0: {}
+
+  tinyexec@0.3.2: {}
+
+  tinyglobby@0.2.15:
+    dependencies:
+      fdir: 6.5.0(picomatch@4.0.3)
+      picomatch: 4.0.3
+
+  tinypool@1.1.1: {}
+
+  tinyrainbow@2.0.0: {}
+
+  tinyspy@4.0.4: {}
+
+  tr46@0.0.3: {}
+
+  tslib@2.8.1: {}
+
+  typescript@5.9.3: {}
+
+  undici-types@5.26.5: {}
+
+  undici-types@6.21.0: {}
+
+  vite-node@3.2.4(@types/node@22.19.15):
+    dependencies:
+      cac: 6.7.14
+      debug: 4.4.3
+      es-module-lexer: 1.7.0
+      pathe: 2.0.3
+      vite: 7.3.1(@types/node@22.19.15)
+    transitivePeerDependencies:
+      - '@types/node'
+      - jiti
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+      - tsx
+      - yaml
+
+  vite@7.3.1(@types/node@22.19.15):
+    dependencies:
+      esbuild: 0.27.3
+      fdir: 6.5.0(picomatch@4.0.3)
+      picomatch: 4.0.3
+      postcss: 8.5.8
+      rollup: 4.59.0
+      tinyglobby: 0.2.15
+    optionalDependencies:
+      '@types/node': 22.19.15
+      fsevents: 2.3.3
+
+  vitest@3.2.4(@types/node@22.19.15):
+    dependencies:
+      '@types/chai': 5.2.3
+      '@vitest/expect': 3.2.4
+      '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15))
+      '@vitest/pretty-format': 3.2.4
+      '@vitest/runner': 3.2.4
+      '@vitest/snapshot': 3.2.4
+      '@vitest/spy': 3.2.4
+      '@vitest/utils': 3.2.4
+      chai: 5.3.3
+      debug: 4.4.3
+      expect-type: 1.3.0
+      magic-string: 0.30.21
+      pathe: 2.0.3
+      picomatch: 4.0.3
+      std-env: 3.10.0
+      tinybench: 2.9.0
+      tinyexec: 0.3.2
+      tinyglobby: 0.2.15
+      tinypool: 1.1.1
+      tinyrainbow: 2.0.0
+      vite: 7.3.1(@types/node@22.19.15)
+      vite-node: 3.2.4(@types/node@22.19.15)
+      why-is-node-running: 2.3.0
+    optionalDependencies:
+      '@types/node': 22.19.15
+    transitivePeerDependencies:
+      - jiti
+      - less
+      - lightningcss
+      - msw
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+      - tsx
+      - yaml
+
+  web-streams-polyfill@4.0.0-beta.3: {}
+
+  webidl-conversions@3.0.1: {}
+
+  whatwg-url@5.0.0:
+    dependencies:
+      tr46: 0.0.3
+      webidl-conversions: 3.0.1
+
+  why-is-node-running@2.3.0:
+    dependencies:
+      siginfo: 2.0.0
+      stackback: 0.0.2
+
+  wrap-ansi@6.2.0:
+    dependencies:
+      ansi-styles: 4.3.0
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+
+  yoctocolors-cjs@2.1.3: {}
+
+  zod@3.25.76: {}

+ 2 - 0
pnpm-workspace.yaml

@@ -0,0 +1,2 @@
+packages:
+  - "packages/*"

+ 19 - 0
tsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "Node16",
+    "moduleResolution": "Node16",
+    "lib": ["ES2022"],
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "outDir": "dist",
+    "rootDir": "src"
+  },
+  "exclude": ["node_modules", "dist"]
+}