Commit Graph
25 Commits
Author SHA1 Message Date
James Wampler c879d9bfce Add publish-mcp.sh so the standalone MCP binary stops going stale
Aspire doesn't run or manage src/Novelly.Mcp — it's a separate stdio
process the MCP client spawns from a published binary that nothing
rebuilds automatically. It had drifted 12 days out of date and was
silently missing the service-API-key auth header, causing confusing
401s. Script wraps the existing dotnet publish command via the shared
ensure_dotnet helper; README points at it instead of the raw command.

Also registers a UserSecretsId on Novelly.Api so Auth:ServiceApiKey
can be set locally without landing in appsettings.
2026-08-17 18:38:48 -07:00
James Wampler cb66ef7343 Add dashboard tag cloud and spectrum tag color picker
- Dashboard's Characters column gains a tag cloud below it, sized by
  usage count, linking into the tags page.
- Tags page color editor replaced with 16 preset swatches across the
  spectrum plus a "Custom..." link to the native hex picker.
- Fix: tag color/name updates weren't invalidating the tag-references
  query, so the reference panel showed a stale color after editing.
- Tag cloud/tag list selection now round-trips through a ?tag= query
  param so clicking a cloud tag selects it on the tags page.
2026-08-17 17:52:43 -07:00
James Wampler 92fe4317e2 Split character table and detail into separate pages
Characters list (/characters) is now just the filterable/sortable
table; clicking a row navigates to /characters/:characterId instead
of expanding an inline sheet below the table.
2026-08-17 17:37:52 -07:00
James Wampler f124b9b4bb Add character aliases/identity links, move-beats, keyboard help overlay
- Characters can carry aliases and be linked as the same underlying
  person (canonical SameCharacterAsId, optional reveal chapter/note),
  surfaced through the API, MCP tools, agent toolset, and web UI.
- Characters page redesigned as a filterable/sortable table (name+
  aliases, role, importance, occupation, tags) instead of a sidebar
  list, to stay usable as the cast grows.
- Beats can be moved between chapters (BeatService.MoveAsync + MCP/
  agent tool + endpoint).
- Add a keyboard-shortcuts help overlay (HelpButton/HelpOverlayContext)
  wired into the project layout.
- CLAUDE.md: require every frontend component to carry a unique id
  attribute; apply it to CharacterMultiSelect and MarkdownEditor.
2026-08-17 17:26:50 -07:00
Claude b4f4b35e3c Gate the Add chapter button behind CreateContent, matching characters/beats
Editors and Reviewers could see and click Add chapter even though the
API rejects the create with 403 for anyone without CreateContent — the
gate existed everywhere else (characters, beats) but was missed here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JC8Q7Gkv8K7LnJrwkx1xrh
2026-08-17 00:13:53 +00:00
Claude e2a2e69631 Surface project ownership/role on the wire and gate the web UI by it
ProjectResponse now carries OwnerId and a server-resolved MyRole so
Editors/Reviewers see read-only fields and no delete/grant-management
affordances instead of only finding out via a 403 after the fact.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuBH9QSv66DPXSSBERmPs6
2026-08-16 15:37:50 +00:00
James Wampler e598c18d67 Add users, roles, and per-novel permissions
Introduces accounts (ASP.NET Identity + cookie auth), four global
roles (Admin/Writer/Editor/Reviewer), per-novel ownership and grants
via ProjectMember, and a service-API-key principal for the MCP server
and background import jobs. Enforcement lives in the application
services (not endpoint filters) so the embedded agent and MCP tools,
which call the same services directly, can't bypass it. Web client
gets a login page, session-aware routing, and a People section for
managing per-novel access.

Also includes prior in-flight changes from this branch (CLAUDE.md
compliance pass, dev-deploy docker-compose setup) that were
uncommitted when this feature work started.
2026-08-15 22:29:33 -07:00
James Wampler 7d8dd0c4fd Remove chapter-level POV character
Character assignment now lives on beats only, per batch-assign. Drops
Chapter.PovCharacterId (entity, contracts, service, agent/import/MCP
tools, web UI) and the backing column via migration.
2026-08-15 20:44:10 -07:00
James Wampler df10b1f99b Add batch character assignment for chapter outline beats
Lets a writer select several beats and add one character to all of them
at once, without disturbing each beat's existing characters. Surfaced
through the REST API, the embedded agent, and the MCP server per the
project's rule that all three share the same service methods.
2026-08-15 20:36:25 -07:00
James Wampler 6774a38684 Add Novelly.Web.esproj and wire into solution 2026-08-15 20:24:43 -07:00
James Wampler 27c287b9c8 Add genre feature; extract ConfirmModal for reusable confirm dialogs
Genres seed on boot, are editable per project, and gate agent config
errors more gracefully. ConfirmModal replaces ad-hoc confirm prompts
across chapters, tags, and characters pages.
2026-08-15 11:25:13 -07:00
James Wampler ffb476a81a Add dashboard back-link to chapters list; add character via text selection
Chapters page now links back to the project dashboard. Right-clicking
highlighted text in the chapter summary or a beat's What happened /
What's next fields offers "Add Character", creating a character from
the selection and attaching it to the beat when applicable.
2026-08-12 17:20:56 -07:00
James Wampler 23348327a9 Remove Scenes, group beats by multiple characters; strip comments repo-wide
Drop the Scene entity/grouping in favor of chapters carrying prose directly
and beats belonging to many characters. Add markdown editor + character
multi-select components to the web client. Remove all XML doc and inline
comments across the touched C#/TS/CSS files in favor of self-documenting
names, and record that convention in CLAUDE.md. Add .mcp.json (local MCP
server config, no secrets) and ignore .idea/.
2026-08-11 21:05:13 -07:00
James Wampler 1ce526019f Add keyboard shortcuts and a help sidebar
Global hotkey registry (src/keyboard) with chord support (g d, g c,
...) and a "?" help sidebar that lists whatever's registered on the
current screen. Wired up nav chords plus the primary create action on
each page (new novel/character/chapter/beat/scene, new conversation),
and moved the agent's mod+Enter send through the same registry.
2026-08-11 20:35:23 -07:00
James Wampler e7d89a6d41 Add novel dashboard with project phases; fix beat/chapter overflow
Dashboard replaces the tabbed overview: Outline + Characters cards
(recent-first), gated by a new Project.Phase (Brainstorming ->
Outlining -> Writing -> Editing -> Complete). Brainstorming shows just
a notes editor; existing projects backfill to Outlining. Old brief
editor moves to a Settings route.

Beat table now renders as plain readable text, turning into editable
fields only for the clicked row. Fixed horizontal overflow on the
chapters list and beat table caused by unconstrained CSS grid/table
tracks sizing to their widest unwrapped child. Widened the project
layout's max width.
2026-08-10 22:46:09 -07:00
James Wampler 2ccebb31eb Remove NotFoundException; services return null on lookup miss
A missing record isn't exceptional — services now return null (logged
at Info) instead of throwing, and endpoints map null to 404. Agent and
import toolsets route not-found through their existing OrNotFound
result pattern rather than a caught exception.
2026-08-06 21:22:40 -07:00
James Wampler 189ebf3237 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.
2026-08-06 18:36:40 -07:00
James Wampler 40f93e40a8 Stop throwing for not-found; add Guard and request validation
Not-found lookups return null/false instead of throwing NotFoundException
across all services — a missing row is expected control flow, not an
exceptional condition. NotFoundException stays for embedded precondition
checks inside mutations (missing parent, invalid foreign reference).

Guard (copied from mic-check) enforces required arguments at the top of
every service method. A ported IModelValidator<T> framework validates
every request DTO at the API layer via a new ValidationEndpointFilter,
returning a 400 with field-level messages; services re-run the same
validator and throw for direct callers that bypass the API.

Endpoints translate null/false into 404 via a new ToApiResult() helper.
The agent toolset boundary translates the same nullable/bool results
into the tool-error text the model already expected.
2026-08-06 15:13:36 -07:00
James Wampler 04917fa09e Add Serilog console logging across the API
Information at endpoint and service-method boundaries, Debug in deeper
helpers, Warning before expected/recoverable failures (not-found,
validation, agent tool errors), Error on caught exceptions. Serilog
wraps the exception handler so request-completion logs report the
resolved status code rather than the raw exception. Never logs prose
bodies or the Anthropic API key.
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 4f396bb5f9 Surface arcs, character beats and open questions in the web client
The character page gains three sections under the dossier: the arc as an
editable ordered table with each stage pinnable to a chapter, every beat the
character appears in across the book (each row linking into that chapter's
outline), and the character's open questions. The sidebar groups main
characters above supporting ones, and both the sheet and the add dialog let you
set importance.

The arc section shows for main characters, and also for supporting ones that
already have stages — demoting someone should not hide work they thought they
had lost.

The outline page gains a notes section and an open-questions section at the
bottom. Beat rows are now anchored so the character page can link straight to a
row. Raising a question from either page attaches it to what that page is
about, and the section hides the association it is already scoped to rather
than repeating "Landfall" on every row.

Also fixes an ordering wart the browser run exposed: both CharacterRole and
CharacterImportance are stored as text, so ordering them in SQL ordered the
spelling — "Deuteragonist" beat "Protagonist" and the sidebar put the second
lead above the character the book is about. Listing now sorts after
materialising, which uses the enums' declaration order. The test for it was
checked both ways: it fails on the SQL ordering and passes on the fix.

73 tests pass, the web client builds and lints clean. Driven in a browser
end to end: resolving a question with "also add to notes" drops it off the open
list and appends the decision under the chapter's existing note, "show
resolved" brings it back with a Reopen button, and a beat link on the character
page lands on the right chapter outline at that beat's anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 0358667679 Add main/supporting characters, character arcs and open questions
Three things the outline could not express before:

Main vs supporting. A new CharacterImportance sits alongside CharacterRole
rather than inside it — role is the part a character plays (protagonist,
mentor, foil), importance is how much of the book they carry, and a mentor can
be either. Characters start Supporting and get promoted. Listings put main
characters first.

Character arcs. A main character's arc is a flat ordered list of stages, the
same shape as a chapter's beats and for the same reason: an arc is a sequence
of changes, not a tree. A stage can be pinned to the chapter where it lands.
Nothing refuses an arc on a supporting character — demoting someone should not
delete their work.

Open questions. What the writer has not decided yet, hanging off a chapter
outline, a character, both, or neither. They can be resolved, reopened or
deleted, and resolving can append the decision to the notes of whatever the
question was attached to, so it lands where the writer will re-read it.
Resolved questions drop off the list unless asked for.

Also adds GET /api/characters/{id}/beats — every beat a character appears in,
in manuscript order, carrying each beat's chapter so the character page can
link straight into that chapter's outline.

Deletes are deliberately asymmetric: deleting a chapter unpins arc stages and
detaches questions rather than taking them, because a plan outlives a decision
about where the chapter break falls. Deleting a character or project does take
their arcs and questions.

All three capabilities are surfaced in the REST API, the agent toolset and the
MCP server, per the one-source-of-truth rule.

Two things worth flagging in the migration: EF's generated default for the new
Importance column was an empty string, which does not parse back to a
CharacterImportance and would have faulted every read of an existing dossier —
it now defaults to Supporting, verified by migrating a database seeded on the
old schema and reading the row back through the API. And the earlier migrations
were renamed to the namespace EF derives from the output folder, so future
`migrations add` runs stop drifting.

72 tests pass (28 new). The endpoints were also exercised over curl end to end:
arc stages resolving their chapter, a character's beats across chapters, and a
question attached to both a chapter and a character resolving into both sets of
notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 725758ccd9 Reorganise by feature, rename to Novelly, add Aspire and a pre-push hook
The layered split into Domain/Application/Infrastructure/Api was forcing
organisation by layer: adding one capability meant touching four projects and
four folders that each held a slice of it. Those four projects are now one
feature-organised Novelly.Api, where each folder — Projects, Characters,
Chapters, Beats, Scenes, Tags, Agent — holds its entity, DTOs, service and
endpoints together. Common/ holds what genuinely crosses features (the patch
semantics, the two exception types, DraftStatus) and Data/ holds the DbContext
and migrations.

Six .NET projects become five: the three layer projects are gone, and
Novelly.AppHost and Novelly.ServiceDefaults are new.

- Namespaces move from NovelSoftware.* to Novelly.*, including the entity type
  names recorded in the EF model snapshots. The migration ids are untouched, so
  an existing novel.db still migrates cleanly — verified against a fresh file.
- Aspire orchestration mirrors the mic-check setup: the AppHost starts the API
  on :5080 and the Vite dev server on :5173, and the API picks up OpenTelemetry,
  health checks and service discovery from ServiceDefaults. /health and /alive
  now answer in development.
- A Husky pre-push hook runs scripts/ci/prepush.sh: build, test, then a web
  build. The scripts are plain bash so CI can run the same steps.
- The MCP server's env var is now NOVELLY_API_URL.

Verified beyond the build: 44 tests pass, the web client builds, the API was
exercised over curl (project/chapter/beat/tag round trip, tag cross-reference,
503 on the agent without a key while conversation listing still returns 200),
the MCP server was driven over stdio JSON-RPC (26 tools, errors still surface
the API's own message rather than being flattened), and the AppHost was run to
confirm both resources come up and Vite proxies /api through to the API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 1852ceb2d1 Adopt the mic-check CLAUDE.md and .editorconfig house standards
Ported both files from wamplerj/mic-check and retargeted them to this project's
stack, then brought the code into line with the rules rather than watering the
rules down to fit the code.

.editorconfig — C# rules carried over verbatim, with four changes:

- Added root = true and a [*] section (utf-8, space indent, final newline,
  trim trailing whitespace). Without root the file inherits from any parent
  .editorconfig above the checkout.
- end_of_line lf rather than crlf. Every file here is LF and there is no
  .gitattributes to normalise on checkout, so crlf would rewrite the tree on
  first save.
- csharp_style_namespace_declarations file_scoped, was block_scoped. The source
  file sets file_scoped under [*.{cs,vb}] and block_scoped under [*.cs]; the
  C#-specific key wins, so the two disagreeing meant C# silently got
  block_scoped. Every .cs file here is file-scoped.
- Added sections for the React client (ts/tsx/js 2-space, 100 cols), json/yaml,
  css/html, markdown (trailing whitespace preserved — it is a line break there)
  and MSBuild files.

Also dropped a duplicated dotnet_naming_style.pascal_case block that appeared
twice verbatim in the source.

CLAUDE.md — same structure and voice, retargeted: React not Vue, xUnit and
FluentAssertions not NUnit and jest, this repo's six projects, and the real
testing approach (in-memory SQLite via TestDatabase, model calls faked at the
IAgentModelClient seam). Added sections the standards did not cover: the
three-front-ends-one-API rule, PATCH semantics, and a note that build-and-tests
green is not the same as working, with the commands to actually run each piece.

Code brought into compliance:

- Removed sealed from five types (the standard says no sealed)
- NovelAgentToolset.ExecuteAsync returned a named tuple; it now returns an
  AgentToolResult record (the standard says no tuples for return types)
- Added LangVersion latest to all six csproj files

None of the style rules produce build warnings — the IDE analyzers behind them
are off unless EnforceCodeStyleInBuild is set, and verified they stay silent
with it on too. 44 tests still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 7678cc7275 Replace the outline tree with chapter beat tables and tags
The self-nesting outline tree was more structure than chapter outlining needs.
A chapter outline is now a paragraph plus a flat, ordered table of beats, and
tags do the cross-referencing that nesting was doing badly.

A beat is one row: a three-to-five word title, an optional character, what
happened, and what's next. Ordering is a SortOrder column within the chapter —
no parent pointers, no cycle guards, no recursive tree building. Reordering is
one call taking beat ids in the order wanted; ids left out keep their relative
position at the end rather than jumping to the front.

Beats plan, scenes carry prose. The two layers stay separate and a beat's
SceneId is the optional link between them, nullable in both directions —
deleting a scene ungroups its beats rather than deleting the plan, since that
is a decision about prose and not about the outline.

Tags are project-scoped, unique by name case-insensitively, and attach to
characters, chapters and beats through three join tables so cascade deletes are
the database's job rather than ours. Applying an unknown tag by name creates it,
which keeps tagging a single action; GET /api/tags/{id}/references returns
everything carrying a tag across all three kinds at once.

Removed: OutlineNode, OutlineService, its endpoints, agent and MCP tools, and
the Outline tab. Added: Beat and Tag with their services, endpoints, 5 agent
tools and 10 MCP tools, a beat table on the chapter page, a tag editor used in
three places, and a Tags tab for cross-referencing.

Migration drops OutlineNodes — the scaffolder's data-loss warning is the
intended removal, not an accident.

44 tests, up from 31.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 0d7b7a6f30 Add novel-writing app: .NET 10 API, React front end, agent and MCP server
Builds out the vertical slice for planning and writing a novel. Three front
ends — the React UI, an embedded Claude agent, and an MCP stdio server — all go
through one REST API, so an edit made from Claude Code and one made in the
browser are the same edit.

Layout:
  Domain          entities and enums, no dependencies
  Application     services, DTOs, the agent tool-use loop and its 15 tools
  Infrastructure  EF Core 10 + SQLite, Anthropic SDK client
  Api             ASP.NET Core 10 minimal APIs, OpenAPI, ProblemDetails
  Mcp             MCP stdio server, 21 tools over the same REST API
  Web             React 19 + Vite + TanStack Query + Tailwind v4

Data model is Project > Characters / OutlineNodes / Chapters > Scenes, plus
agent conversations. The outline is a self-nesting tree so acts, sequences and
beats can be arranged however the book wants; scenes carry goal/conflict/outcome
because that is what the agent drafts prose from.

Notes on a few choices:

- Conversation history replays to the model as text only. The agent re-reads
  current state through its tools rather than trusting a record of edits that
  may since have changed in the UI.
- The user's turn is persisted before the tool loop runs, so a question is
  recorded even when the model call fails. Turn order uses an explicit sequence
  column; timestamps tie when a turn completes inside one tick.
- Tool failures return is_error results rather than throwing, so the model can
  read the message and correct itself. MCP tools do the same via CallToolResult,
  which keeps the API's own message instead of a generic SDK error.
- The Anthropic client is constructed lazily. It is injected into the agent
  service, which also serves read-only endpoints, and those should keep working
  on an install with no key. Sending without one returns 503, not 400.
- DateTimeOffset is stored as UTC ticks. SQLite refuses to ORDER BY the default
  text form, which every "recently updated first" listing depends on.

Tests run against real in-memory SQLite rather than the EF in-memory provider so
they exercise the cascade deletes and query translation that actually ship.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00