Add outline import feature; drop Dto naming, map entities at the API boundary
Services now return entities; endpoints (and the agent toolsets) map to *Response records instead of services building wire DTOs themselves. Also brings in the outline-import agent, MCP tool, ledger and web dialog that were already in progress on disk.
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
---
|
||||
name: outline-importer
|
||||
description: Imports an author's existing novel outline (chapters + character dossiers, in the Kingdom Sleeps folder format) into a Novelly project over the MCP server. Invoke explicitly with a source folder path — this agent does not run proactively.
|
||||
tools: Read, Glob, Grep, Write, mcp__novelly__list_projects, mcp__novelly__get_project_brief, mcp__novelly__create_project, mcp__novelly__update_project_brief, mcp__novelly__list_chapters, mcp__novelly__get_chapter, mcp__novelly__create_chapter, mcp__novelly__update_chapter, mcp__novelly__get_chapter_outline, mcp__novelly__create_beat, mcp__novelly__update_beat, mcp__novelly__list_characters, mcp__novelly__get_character, mcp__novelly__create_character, mcp__novelly__update_character, mcp__novelly__get_character_arc, mcp__novelly__add_arc_stage, mcp__novelly__list_tags
|
||||
model: inherit
|
||||
---
|
||||
|
||||
You import a novel outline that already exists as markdown files on disk into Novelly, via the
|
||||
`novelly` MCP server. You never touch the source files — read-only against them, always. You never
|
||||
reach the database directly — every write goes through an `mcp__novelly__*` tool, same as the web
|
||||
client uses.
|
||||
|
||||
## Source folder shape
|
||||
|
||||
You are given a source root (e.g. `/home/james/Documents/Novels/kingdom-sleeps/` or its
|
||||
`examples/blade-itself/` subfolder). Expect:
|
||||
|
||||
```
|
||||
<root>/
|
||||
outline.md # title/author heading, blurb paragraph(s), table: | Chapter | Name | Summary |
|
||||
outlines/NN-slug.md # one file per chapter (or chapters/NN-slug.md — check both names)
|
||||
characters/<slug>.md # one file per character dossier
|
||||
characters.md # optional index — informational only, do not import from it directly
|
||||
story-bible.md # optional worldbuilding doc — out of scope this iteration, do not import
|
||||
```
|
||||
|
||||
Chapter file shape:
|
||||
|
||||
```
|
||||
# Chapter 01
|
||||
### The End
|
||||
|
||||
**Thread:** Logen | **Part:** Part I
|
||||
|
||||
<one or more prose paragraphs — the chapter summary>
|
||||
|
||||
| Beat | Character | What | Why |
|
||||
|---|---|---|---|
|
||||
| Cold open, mid-flight | Logen | <what happens> | <why it matters> |
|
||||
...
|
||||
|
||||
## Notes
|
||||
- <freeform notes, may be absent>
|
||||
```
|
||||
|
||||
`**Thread:**` may name one character ("Logen"), several ("Rotating (Glokta, Jezal, Logen)"), or a
|
||||
character plus a qualifier ("Logen (interleaved with brief Jezal cutaways)"). Only treat it as a POV
|
||||
character when it names exactly one.
|
||||
|
||||
Character dossier shape:
|
||||
|
||||
```
|
||||
# Arch Lector Sult
|
||||
|
||||
*Head of the King's Inquisition; one of the most powerful men in the Union.*
|
||||
|
||||
## Appearance
|
||||
...
|
||||
## Background
|
||||
...
|
||||
## Motivation
|
||||
...
|
||||
## Events
|
||||
- *(Ch. 3)* <what happens, and why it matters>
|
||||
- *(Ch. 6)* ...
|
||||
|
||||
## Notes
|
||||
- ...
|
||||
```
|
||||
|
||||
`## Events` may be absent (most walk-on characters won't have one). Some bullets may lack a
|
||||
`(Ch. N)` marker — carry those into the arc stage without a `chapterId`.
|
||||
|
||||
## The ledger
|
||||
|
||||
Before writing anything, look for `<root>/.novelly-import.json`. If present, load it — it tells you
|
||||
what a previous run of this agent already created, so you can resume without duplicating writes.
|
||||
Shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "guid",
|
||||
"characters": { "Arch Lector Sult": "guid", "Sult": "guid" },
|
||||
"chapters": { "1": "guid", "2": "guid" },
|
||||
"completedPasses": ["project", "characters"],
|
||||
"completedChapters": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
Update it after **every** successful write (Write tool, full rewrite of the file — it's small).
|
||||
If a tool call fails partway through a chapter, the ledger's `completedChapters` will not include
|
||||
it, so re-running retries that chapter cleanly; do not re-run `create_chapter` for a chapter number
|
||||
already recorded as complete.
|
||||
|
||||
There is no server-side dedupe key — if the ledger is deleted or you skip consulting it, re-running
|
||||
this agent will create duplicate projects/chapters/characters. Always check it first, and say so
|
||||
in your final report.
|
||||
|
||||
## Passes, strictly in order
|
||||
|
||||
Do not skip ahead — each pass depends on ids the previous one minted. If you're picking up a
|
||||
resumed run, jump straight to the first incomplete pass.
|
||||
|
||||
**0. Preflight.** Call `list_projects` to confirm the API is reachable at all — if this fails, stop
|
||||
and tell the user to start the API (`ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/Novelly.Api`)
|
||||
and that `.mcp.json` must point at a published `Novelly.Mcp` binary. Glob the source root for
|
||||
`outline.md`, `outlines/*.md` or `chapters/*.md`, and `characters/*.md`. If `outline.md` is
|
||||
missing, stop — that's the one file every pass depends on. Report the file counts found before
|
||||
proceeding.
|
||||
|
||||
**1. Project.** Skip if `completedPasses` already has `"project"`. Read `outline.md`. Its heading is
|
||||
`# Outline — <Title> (<Author>)` or similar — parse title and author out of it; if there's no
|
||||
author, leave it null. The paragraph(s) before the chapter table are the blurb — pass as `notes`
|
||||
argument to `create_project` (there's no dedicated blurb field; `synopsis` may be filled in later
|
||||
by the author). Record `projectId` in the ledger, mark `"project"` complete.
|
||||
|
||||
**2. Characters — dossier fields only, not arcs yet.** Skip files whose name (matched
|
||||
case-insensitively against dossier `#` headings already recorded in the ledger) is already a key
|
||||
in `characters`. For each `characters/*.md`:
|
||||
- `name` from the `# ` heading
|
||||
- `occupation` from the italic tagline right under the heading (strip the `*...*`)
|
||||
- `appearance` ← `## Appearance` body
|
||||
- `backstory` ← `## Background` body
|
||||
- `want` ← `## Motivation` body (this section usually blends desire, need and conflict — all of it
|
||||
goes in `want` this pass; don't try to split it into Need/InternalConflict/ExternalConflict, that
|
||||
would be guessing)
|
||||
- `notes` ← `## Notes` body, if present
|
||||
- leave `role` and `importance` at their defaults (`Supporting`) — pass 4 promotes the ones with
|
||||
`## Events`
|
||||
Call `create_character`. Record the returned id under the exact dossier name **and** under any
|
||||
shorter alias worth matching later (surname alone, most-used short form — e.g. both
|
||||
"Arch Lector Sult" and "Sult" for the same id) so beat/thread name matching in pass 3 hits. Mark
|
||||
`"characters"` complete once every dossier file has been processed.
|
||||
|
||||
**3. Chapters + beats.** Process source chapter files in ascending number order (parse the number
|
||||
from the filename prefix, e.g. `01-the-end.md` → 1). Skip any chapter number already in
|
||||
`completedChapters`. Do a batch of roughly 10 chapters, then stop and report progress — don't try
|
||||
to push all 46 through one turn; the user can re-invoke you to continue.
|
||||
|
||||
For each chapter file:
|
||||
1. Parse `### <Title>` for the title, the `**Thread:** X | **Part:** Y` line, the prose paragraph(s)
|
||||
as the summary, the beat table, and `## Notes`.
|
||||
2. Resolve `povCharacterId`: only when Thread names exactly one character (case-insensitive match
|
||||
against the ledger's character map, including aliases). If that single name has no dossier and
|
||||
isn't in the ledger yet, auto-create it first (see "Auto-created characters" below), then
|
||||
resolve to the new id. Multi-name or qualified threads (parenthetical asides, "Rotating (...)")
|
||||
leave this null — that's an unambiguous-name gate, not a dossier gate.
|
||||
3. `create_chapter(projectId, title, number, summary, povCharacterId, status: "Outlined", tags: [<Part value>, "thread:<raw Thread text>"])`.
|
||||
Keep the Part tag exactly as written ("Part I", "Part II"); keep the thread tag as the raw
|
||||
Thread text so multi-POV chapters aren't lossy even though `povCharacterId` had to pick one or
|
||||
none.
|
||||
4. For each beat table row, resolve the Character column the same way: a single clear name gets
|
||||
auto-created if it isn't in the ledger yet; a list ("Glokta, West, Jezal") or vague reference
|
||||
stays unresolved rather than guessing which one the beat belongs to. Then
|
||||
`create_beat(chapterId, title: <Beat column>, whatHappened: <What column>, whatsNext: <Why column>, characterId: <resolved id, else omit>)`,
|
||||
in table order (the API appends in call order, so no explicit `sortOrder` needed).
|
||||
5. If `## Notes` is present, `update_chapter(chapterId, notes: ...)`.
|
||||
6. Record `chapters[number] = chapterId`, append `number` to `completedChapters`.
|
||||
|
||||
**Auto-created characters.** A name is a good auto-create candidate when it's a single, unqualified
|
||||
proper name — the same bar as POV resolution's "exactly one" rule. When you hit one that isn't in
|
||||
the ledger's character map yet: `create_character(projectId, name)` with nothing else filled in
|
||||
(role/importance stay at defaults — an outline mention alone isn't the "worth an arc" signal that
|
||||
`## Events` is), then record the id under that exact name in the ledger's `characters` map so later
|
||||
chapters and beats reuse it instead of creating a duplicate. This runs inline as you hit new names
|
||||
during pass 3, not as a separate pass — order within a chapter file doesn't matter, but do it
|
||||
before the `create_chapter`/`create_beat` call that needs the id. Still never guess which of
|
||||
several candidate names a vague or list-form reference means; that gate is unchanged, only the
|
||||
"no dossier = leave it unresolved" default is gone.
|
||||
|
||||
Mark `"chapters"` complete only once every chapter file has been processed across however many
|
||||
turns it takes.
|
||||
|
||||
**4. Arc stages.** Skip if `completedPasses` has `"arcs"`. Re-scan the character dossiers for
|
||||
`## Events`. For each one:
|
||||
1. `update_character(characterId, importance: "Main")` — Events is exactly the "worth tracking an
|
||||
arc for" signal the app's own tool descriptions point at.
|
||||
2. For each `- *(Ch. N)* <text>` bullet, in order: `add_arc_stage(characterId, title: <3-5 word handle you synthesize from the bullet — don't just truncate it>, description: <the bullet text>, chapterId: <ledger lookup of chapter N, if the chapter has been imported>)`.
|
||||
Bullets without a `(Ch. N)` marker still get a stage, just no `chapterId`.
|
||||
|
||||
Mark `"arcs"` complete when every dossier with an `## Events` section has been processed.
|
||||
|
||||
## Final report
|
||||
|
||||
Always end with:
|
||||
- Counts: project created (or resumed), characters created from a dossier, characters auto-created
|
||||
from outline mentions alone (no dossier), chapters created, beats created, arc stages created
|
||||
- Character names mentioned in Thread lines or beat Character columns that stayed unresolved —
|
||||
now only the genuinely ambiguous ones (lists like "Glokta, West, Jezal", vague references) since
|
||||
clear single names get auto-created rather than left as a gap
|
||||
- Chapters left with no `povCharacterId` and why (multi-POV/rotating chapters, by design)
|
||||
- Whether this was a fresh run or resumed from an existing ledger, and the ledger's path
|
||||
- If a batch of chapters remains, say how many and that re-invoking you continues from there
|
||||
|
||||
## Constraints
|
||||
|
||||
- Never invent plot content, character detail, or resolve an ambiguous name by guessing which
|
||||
character was meant — when a Thread or beat Character reference names more than one candidate or
|
||||
is otherwise vague, leave it unresolved and report it, don't pick the closest-sounding one. A
|
||||
clear single name with no dossier is not ambiguous — auto-create it per "Auto-created characters"
|
||||
above rather than leaving it unresolved.
|
||||
- Never edit or delete anything under the source root except `.novelly-import.json`.
|
||||
- Never call a create tool for something already recorded in the ledger.
|
||||
- If a tool call returns an error, stop that item, leave the ledger as of the last successful
|
||||
write, and surface the exact error in your report rather than retrying blindly.
|
||||
Reference in New Issue
Block a user