@actalk/inkos-studio 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,5 +7,4 @@ export declare class ApiError extends Error {
7
7
  readonly code: string;
8
8
  constructor(status: number, code: string, message: string);
9
9
  }
10
- export declare function isMissingFileError(error: unknown): boolean;
11
10
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM1D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM1D"}
@@ -12,7 +12,4 @@ export class ApiError extends Error {
12
12
  this.code = code;
13
13
  }
14
14
  }
15
- export function isMissingFileError(error) {
16
- return error?.code === "ENOENT";
17
- }
18
15
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,IAAY,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAQ,KAA2C,EAAE,IAAI,KAAK,QAAQ,CAAC;AACzE,CAAC"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,IAAY,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF"}
@@ -1,31 +1,3 @@
1
- /**
2
- * Security validation utilities for Studio API.
3
- * Ported from PR #97 (PapainTea) — ReDoS prevention, path traversal, port parsing.
4
- */
5
- /** Validates fileId against whitelist to prevent path traversal. */
6
- export declare function isSafeUploadFileId(fileId: string): boolean;
7
1
  /** Validates bookId — blocks traversal sequences and null bytes. */
8
2
  export declare function isSafeBookId(bookId: string): boolean;
9
- /**
10
- * Builds a regex from user-provided pattern with ReDoS prevention.
11
- * Blocks grouping, alternation, backreferences, and lookahead/behind.
12
- */
13
- export declare function buildImportRegex(pattern: string): RegExp;
14
- /** Strips filesystem paths from upload responses — only returns relative/safe fields. */
15
- export declare function createUploadResponse(input: {
16
- readonly fileId: string;
17
- readonly size: number;
18
- readonly chapterCount: number;
19
- readonly firstTitle: string;
20
- readonly totalChars: number;
21
- }): {
22
- readonly ok: true;
23
- readonly fileId: string;
24
- readonly size: number;
25
- readonly chapterCount: number;
26
- readonly firstTitle: string;
27
- readonly totalChars: number;
28
- };
29
- /** Parses port from environment with safe fallback. */
30
- export declare function resolveServerPort(env?: Record<string, string | undefined>): number;
31
3
  //# sourceMappingURL=safety.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/api/safety.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,oEAAoE;AACpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAUpD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,yFAAyF;AACzF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B,GAAG;IACF,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B,CASA;AAED,uDAAuD;AACvD,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAAG,MAAM,CAI/F"}
1
+ {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/api/safety.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAUpD"}
@@ -1,14 +1,4 @@
1
- /**
2
- * Security validation utilities for Studio API.
3
- * Ported from PR #97 (PapainTea) — ReDoS prevention, path traversal, port parsing.
4
- */
5
- const SAFE_UPLOAD_FILE_ID = /^[A-Za-z0-9-]{1,64}$/;
6
- const MAX_IMPORT_PATTERN_LENGTH = 160;
7
- const SAFE_IMPORT_PATTERN = /^[\p{L}\p{N}\s[\]\\.^$*+?{}\-,。::、_]+$/u;
8
- /** Validates fileId against whitelist to prevent path traversal. */
9
- export function isSafeUploadFileId(fileId) {
10
- return typeof fileId === "string" && SAFE_UPLOAD_FILE_ID.test(fileId);
11
- }
1
+ // Validates book IDs to prevent path traversal in API requests.
12
2
  /** Validates bookId — blocks traversal sequences and null bytes. */
13
3
  export function isSafeBookId(bookId) {
14
4
  return (typeof bookId === "string"
@@ -19,46 +9,4 @@ export function isSafeBookId(bookId) {
19
9
  && !bookId.includes("..")
20
10
  && !/[\\/\0]/.test(bookId));
21
11
  }
22
- /**
23
- * Builds a regex from user-provided pattern with ReDoS prevention.
24
- * Blocks grouping, alternation, backreferences, and lookahead/behind.
25
- */
26
- export function buildImportRegex(pattern) {
27
- const normalized = String(pattern ?? "").trim();
28
- if (!normalized) {
29
- throw new Error("Import pattern is required");
30
- }
31
- if (normalized.length > MAX_IMPORT_PATTERN_LENGTH) {
32
- throw new Error(`Import pattern too long (max ${MAX_IMPORT_PATTERN_LENGTH} chars)`);
33
- }
34
- if (/[()|]/.test(normalized) || /\\[1-9]/.test(normalized) || normalized.includes("(?")) {
35
- throw new Error("Import pattern uses unsafe regex features");
36
- }
37
- if (!SAFE_IMPORT_PATTERN.test(normalized)) {
38
- throw new Error("Import pattern is invalid");
39
- }
40
- try {
41
- return new RegExp(normalized, "g");
42
- }
43
- catch {
44
- throw new Error("Import pattern is invalid");
45
- }
46
- }
47
- /** Strips filesystem paths from upload responses — only returns relative/safe fields. */
48
- export function createUploadResponse(input) {
49
- return {
50
- ok: true,
51
- fileId: input.fileId,
52
- size: input.size,
53
- chapterCount: input.chapterCount,
54
- firstTitle: input.firstTitle,
55
- totalChars: input.totalChars,
56
- };
57
- }
58
- /** Parses port from environment with safe fallback. */
59
- export function resolveServerPort(env = process.env) {
60
- const raw = env.INKOS_STUDIO_PORT ?? env.PORT ?? "4567";
61
- const parsed = Number.parseInt(raw, 10);
62
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 4567;
63
- }
64
12
  //# sourceMappingURL=safety.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/api/safety.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,mBAAmB,GAAG,sBAAsB,CAAC;AACnD,MAAM,yBAAyB,GAAG,GAAG,CAAC;AACtC,MAAM,mBAAmB,GAAG,yCAAyC,CAAC;AAEtE,oEAAoE;AACpE,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,OAAO,CACL,OAAO,MAAM,KAAK,QAAQ;WACvB,MAAM,CAAC,MAAM,GAAG,CAAC;WACjB,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM;WACxB,MAAM,KAAK,GAAG;WACd,MAAM,KAAK,IAAI;WACf,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;WACtB,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,GAAG,yBAAyB,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,gCAAgC,yBAAyB,SAAS,CAAC,CAAC;IACtF,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACxF,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,CAAC;QACH,OAAO,IAAI,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,oBAAoB,CAAC,KAMpC;IAQC,OAAO;QACL,EAAE,EAAE,IAAI;QACR,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;KAC7B,CAAC;AACJ,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,iBAAiB,CAAC,MAA0C,OAAO,CAAC,GAAG;IACrF,MAAM,GAAG,GAAG,GAAG,CAAC,iBAAiB,IAAI,GAAG,CAAC,IAAI,IAAI,MAAM,CAAC;IACxD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACxC,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/D,CAAC"}
1
+ {"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/api/safety.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAEhE,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,OAAO,CACL,OAAO,MAAM,KAAK,QAAQ;WACvB,MAAM,CAAC,MAAM,GAAG,CAAC;WACjB,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM;WACxB,MAAM,KAAK,GAAG;WACd,MAAM,KAAK,IAAI;WACf,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;WACtB,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAC3B,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAQL,KAAK,aAAa,EAGnB,MAAM,oBAAoB,CAAC;AAqB5B,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,8EAkuC5E;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,SAAO,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAaL,KAAK,aAAa,EAGnB,MAAM,oBAAoB,CAAC;AAqB5B,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,8EAswC5E;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,SAAO,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
@@ -2,7 +2,7 @@ import { Hono } from "hono";
2
2
  import { cors } from "hono/cors";
3
3
  import { streamSSE } from "hono/streaming";
4
4
  import { serve } from "@hono/node-server";
5
- import { StateManager, PipelineRunner, createLLMClient, createLogger, computeAnalytics, loadProjectConfig, } from "@actalk/inkos-core";
5
+ import { StateManager, PipelineRunner, createLLMClient, createLogger, createInteractionToolsFromDeps, computeAnalytics, loadProjectConfig, loadProjectSession, processProjectInteractionInput, processProjectInteractionRequest, resolveSessionActiveBook, } from "@actalk/inkos-core";
6
6
  import { access, readFile, readdir } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
  import { isSafeBookId } from "./safety.js";
@@ -54,7 +54,7 @@ export function createStudioServer(initialConfig, root) {
54
54
  cachedConfig = freshConfig;
55
55
  return freshConfig;
56
56
  }
57
- async function buildPipelineConfig() {
57
+ async function buildPipelineConfig(overrides) {
58
58
  const currentConfig = await loadCurrentProjectConfig();
59
59
  const logger = createLogger({ tag: "studio", sinks: [sseSink] });
60
60
  return {
@@ -74,6 +74,7 @@ export function createStudioServer(initialConfig, root) {
74
74
  });
75
75
  }
76
76
  },
77
+ externalContext: overrides?.externalContext,
77
78
  };
78
79
  }
79
80
  // --- Books ---
@@ -131,9 +132,23 @@ export function createStudioServer(initialConfig, root) {
131
132
  broadcast("book:creating", { bookId, title: body.title });
132
133
  bookCreateStatus.set(bookId, { status: "creating" });
133
134
  const pipeline = new PipelineRunner(await buildPipelineConfig());
134
- pipeline.initBook(bookConfig).then(() => {
135
- bookCreateStatus.delete(bookId);
136
- broadcast("book:created", { bookId });
135
+ const tools = createInteractionToolsFromDeps(pipeline, state);
136
+ processProjectInteractionRequest({
137
+ projectRoot: root,
138
+ request: {
139
+ intent: "create_book",
140
+ title: body.title,
141
+ genre: body.genre,
142
+ language: body.language === "en" ? "en" : body.language === "zh" ? "zh" : undefined,
143
+ platform: body.platform,
144
+ chapterWordCount: body.chapterWordCount,
145
+ targetChapters: body.targetChapters,
146
+ },
147
+ tools,
148
+ }).then((result) => {
149
+ const createdBookId = result.details?.bookId ?? result.session.activeBookId ?? bookId;
150
+ bookCreateStatus.delete(createdBookId);
151
+ broadcast("book:created", { bookId: createdBookId });
137
152
  }, (e) => {
138
153
  const error = e instanceof Error ? e.message : String(e);
139
154
  bookCreateStatus.set(bookId, { status: "error", error });
@@ -266,9 +281,19 @@ export function createStudioServer(initialConfig, root) {
266
281
  const num = parseInt(c.req.param("num"), 10);
267
282
  try {
268
283
  const index = await state.loadChapterIndex(id);
269
- const updated = index.map((ch) => ch.number === num ? { ...ch, status: "rejected" } : ch);
270
- await state.saveChapterIndex(id, updated);
271
- return c.json({ ok: true, chapterNumber: num, status: "rejected" });
284
+ const target = index.find((ch) => ch.number === num);
285
+ if (!target) {
286
+ return c.json({ error: `Chapter ${num} not found` }, 404);
287
+ }
288
+ const rollbackTarget = num - 1;
289
+ const discarded = await state.rollbackToChapter(id, rollbackTarget);
290
+ return c.json({
291
+ ok: true,
292
+ chapterNumber: num,
293
+ status: "rejected",
294
+ rolledBackTo: rollbackTarget,
295
+ discarded,
296
+ });
272
297
  }
273
298
  catch (e) {
274
299
  return c.json({ error: String(e) }, 500);
@@ -434,22 +459,44 @@ export function createStudioServer(initialConfig, root) {
434
459
  }
435
460
  });
436
461
  // --- Agent chat ---
462
+ app.get("/api/interaction/session", async (c) => {
463
+ const session = await loadProjectSession(root);
464
+ const activeBookId = await resolveSessionActiveBook(root, session);
465
+ return c.json({
466
+ session: activeBookId && session.activeBookId !== activeBookId
467
+ ? { ...session, activeBookId }
468
+ : session,
469
+ activeBookId,
470
+ });
471
+ });
437
472
  app.post("/api/agent", async (c) => {
438
- const { instruction } = await c.req.json();
473
+ const { instruction, activeBookId } = await c.req.json();
439
474
  if (!instruction?.trim()) {
440
475
  return c.json({ error: "No instruction provided" }, 400);
441
476
  }
442
- broadcast("agent:start", { instruction });
477
+ broadcast("agent:start", { instruction, activeBookId });
443
478
  try {
444
- const { runAgentLoop } = await import("@actalk/inkos-core");
445
- const result = await runAgentLoop(await buildPipelineConfig(), instruction);
446
- broadcast("agent:complete", { instruction, response: result });
447
- return c.json({ response: result });
479
+ const pipeline = new PipelineRunner(await buildPipelineConfig());
480
+ const tools = createInteractionToolsFromDeps(pipeline, state);
481
+ const result = await processProjectInteractionInput({
482
+ projectRoot: root,
483
+ input: instruction,
484
+ tools,
485
+ activeBookId,
486
+ });
487
+ const response = result.responseText ?? "Acknowledged.";
488
+ broadcast("agent:complete", { instruction, activeBookId, response });
489
+ return c.json({ response, session: result.session, request: result.request });
448
490
  }
449
491
  catch (e) {
450
492
  const msg = e instanceof Error ? e.message : String(e);
451
- broadcast("agent:error", { instruction, error: msg });
452
- return c.json({ response: msg });
493
+ broadcast("agent:error", { instruction, activeBookId, error: msg });
494
+ return c.json({
495
+ error: {
496
+ code: "INTERACTION_ERROR",
497
+ message: msg,
498
+ },
499
+ }, 500);
453
500
  }
454
501
  });
455
502
  // --- Language setup ---
@@ -505,7 +552,9 @@ export function createStudioServer(initialConfig, root) {
505
552
  const id = c.req.param("id");
506
553
  const chapterNum = parseInt(c.req.param("chapter"), 10);
507
554
  const bookDir = state.bookDir(id);
508
- const body = await c.req.json().catch(() => ({ mode: "spot-fix" }));
555
+ const body = await c.req
556
+ .json()
557
+ .catch(() => ({ mode: "spot-fix", brief: undefined }));
509
558
  broadcast("revise:start", { bookId: id, chapter: chapterNum });
510
559
  try {
511
560
  const book = await state.loadBookConfig(id);
@@ -515,25 +564,11 @@ export function createStudioServer(initialConfig, root) {
515
564
  const match = files.find((f) => f.startsWith(paddedNum) && f.endsWith(".md"));
516
565
  if (!match)
517
566
  return c.json({ error: "Chapter not found" }, 404);
518
- const content = await readFile(join(chaptersDir, match), "utf-8");
519
- // Get audit issues first
520
- const index = await state.loadChapterIndex(id);
521
- const chapterMeta = index.find((ch) => ch.number === chapterNum);
522
- const issues = (chapterMeta?.auditIssues ?? []).map((desc) => ({
523
- severity: "warning",
524
- category: "general",
525
- description: desc,
526
- suggestion: "",
567
+ const pipeline = new PipelineRunner(await buildPipelineConfig({
568
+ externalContext: body.brief,
527
569
  }));
528
- const currentConfig = await loadCurrentProjectConfig();
529
- const { ReviserAgent } = await import("@actalk/inkos-core");
530
- const reviser = new ReviserAgent({
531
- client: createLLMClient(currentConfig.llm),
532
- model: currentConfig.llm.model,
533
- projectRoot: root,
534
- bookId: id,
535
- });
536
- const result = await reviser.reviseChapter(bookDir, content, chapterNum, issues, (body.mode ?? "spot-fix"), book.genre);
570
+ const normalizedMode = body.mode ?? "spot-fix";
571
+ const result = await pipeline.reviseDraft(id, chapterNum, normalizedMode);
537
572
  broadcast("revise:complete", { bookId: id, chapter: chapterNum });
538
573
  return c.json(result);
539
574
  }
@@ -602,43 +637,30 @@ export function createStudioServer(initialConfig, root) {
602
637
  app.post("/api/books/:id/export-save", async (c) => {
603
638
  const id = c.req.param("id");
604
639
  const { format, approvedOnly } = await c.req.json().catch(() => ({ format: "txt", approvedOnly: false }));
605
- const bookDir = state.bookDir(id);
606
- const chaptersDir = join(bookDir, "chapters");
607
640
  const fmt = format ?? "txt";
608
641
  try {
609
- const book = await state.loadBookConfig(id);
610
- const index = await state.loadChapterIndex(id);
611
- const approvedNums = new Set(approvedOnly ? index.filter((ch) => ch.status === "approved").map((ch) => ch.number) : []);
612
- const files = await readdir(chaptersDir);
613
- const mdFiles = files.filter((f) => f.endsWith(".md") && /^\d{4}/.test(f)).sort();
614
- const filteredFiles = approvedOnly
615
- ? mdFiles.filter((f) => approvedNums.has(parseInt(f.slice(0, 4), 10)))
616
- : mdFiles;
617
- const contents = await Promise.all(filteredFiles.map((f) => readFile(join(chaptersDir, f), "utf-8")));
618
- const { writeFile: writeFileFs } = await import("node:fs/promises");
619
- let outputPath;
620
- let body;
621
- if (fmt === "md") {
622
- body = contents.join("\n\n---\n\n");
623
- outputPath = join(bookDir, `${id}.md`);
624
- }
625
- else if (fmt === "epub") {
626
- const chapters = contents.map((content, i) => {
627
- const title = content.match(/^#\s+(.+)$/m)?.[1] ?? `Chapter ${i + 1}`;
628
- const html = content.split("\n").filter((l) => !l.startsWith("#")).map((l) => l.trim() ? `<p>${l}</p>` : "").join("\n");
629
- return { title, html };
630
- });
631
- const toc = chapters.map((ch, i) => `<li><a href="#ch${i}">${ch.title}</a></li>`).join("\n");
632
- const chapterHtml = chapters.map((ch, i) => `<h2 id="ch${i}">${ch.title}</h2>\n${ch.html}`).join("\n<hr/>\n");
633
- body = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${book.title}</title><style>body{font-family:serif;max-width:40em;margin:auto;padding:2em;line-height:1.8}h2{margin-top:3em}</style></head><body><h1>${book.title}</h1><nav><ol>${toc}</ol></nav><hr/>${chapterHtml}</body></html>`;
634
- outputPath = join(bookDir, `${id}.html`);
635
- }
636
- else {
637
- body = contents.join("\n\n");
638
- outputPath = join(bookDir, `${id}.txt`);
639
- }
640
- await writeFileFs(outputPath, body, "utf-8");
641
- return c.json({ ok: true, path: outputPath, format: fmt, chapters: filteredFiles.length });
642
+ const pipeline = new PipelineRunner(await buildPipelineConfig());
643
+ const tools = createInteractionToolsFromDeps(pipeline, state);
644
+ const bookDir = state.bookDir(id);
645
+ const outputPath = join(bookDir, `${id}.${fmt === "epub" ? "epub" : fmt}`);
646
+ const result = await processProjectInteractionRequest({
647
+ projectRoot: root,
648
+ request: {
649
+ intent: "export_book",
650
+ bookId: id,
651
+ format: fmt,
652
+ approvedOnly,
653
+ outputPath,
654
+ },
655
+ tools,
656
+ activeBookId: id,
657
+ });
658
+ return c.json({
659
+ ok: true,
660
+ path: result.details?.outputPath ?? outputPath,
661
+ format: fmt,
662
+ chapters: result.details?.chaptersExported ?? 0,
663
+ });
642
664
  }
643
665
  catch (e) {
644
666
  return c.json({ error: String(e) }, 500);
@@ -779,21 +801,41 @@ export function createStudioServer(initialConfig, root) {
779
801
  app.post("/api/books/:id/rewrite/:chapter", async (c) => {
780
802
  const id = c.req.param("id");
781
803
  const chapterNum = parseInt(c.req.param("chapter"), 10);
804
+ const body = await c.req
805
+ .json()
806
+ .catch(() => ({}));
782
807
  broadcast("rewrite:start", { bookId: id, chapter: chapterNum });
783
808
  try {
784
- const restored = await state.restoreState(id, chapterNum);
785
- if (!restored) {
786
- return c.json({ error: `Cannot restore state to chapter ${chapterNum}` }, 400);
787
- }
788
- const pipeline = new PipelineRunner(await buildPipelineConfig());
809
+ const rollbackTarget = chapterNum - 1;
810
+ const discarded = await state.rollbackToChapter(id, rollbackTarget);
811
+ const pipeline = new PipelineRunner(await buildPipelineConfig({
812
+ externalContext: body.brief,
813
+ }));
789
814
  pipeline.writeNextChapter(id).then((result) => broadcast("rewrite:complete", { bookId: id, chapterNumber: result.chapterNumber, title: result.title, wordCount: result.wordCount }), (e) => broadcast("rewrite:error", { bookId: id, error: e instanceof Error ? e.message : String(e) }));
790
- return c.json({ status: "rewriting", bookId: id, chapter: chapterNum });
815
+ return c.json({ status: "rewriting", bookId: id, chapter: chapterNum, rolledBackTo: rollbackTarget, discarded });
791
816
  }
792
817
  catch (e) {
793
818
  broadcast("rewrite:error", { bookId: id, error: String(e) });
794
819
  return c.json({ error: String(e) }, 500);
795
820
  }
796
821
  });
822
+ app.post("/api/books/:id/resync/:chapter", async (c) => {
823
+ const id = c.req.param("id");
824
+ const chapterNum = parseInt(c.req.param("chapter"), 10);
825
+ const body = await c.req
826
+ .json()
827
+ .catch(() => ({}));
828
+ try {
829
+ const pipeline = new PipelineRunner(await buildPipelineConfig({
830
+ externalContext: body.brief,
831
+ }));
832
+ const result = await pipeline.resyncChapterArtifacts(id, chapterNum);
833
+ return c.json(result);
834
+ }
835
+ catch (e) {
836
+ return c.json({ error: String(e) }, 500);
837
+ }
838
+ });
797
839
  // --- Detect All chapters ---
798
840
  app.post("/api/books/:id/detect-all", async (c) => {
799
841
  const id = c.req.param("id");