neo
AGENTS.md1.7k starsChanged 7 days agoMITView on GitHub
NEO is a local word processor for books. It is an Electron app made of plain JavaScript, HTML, and CSS. There is no bundler, no framework, and no compile step. npm start runs electron .. Read CONTRIBUTING.md before adding a feature. The product is opinionated on purpose. A change that helps someone finish a book belongs here. A change that adds a panel, a prompt, or a dependency usually does not. app.js section banners look like /* SAVING */. Start…
- Installs packages
Trending
More talk lately.thin dataabout the whole repository
Who's talking about hughhowey/neo
- @michelroltv.eurosky.social“Para quienes escribáis relatos y novelas: en el último noticiario rolero hablamos de Neo, una aplicación …”Bluesky · Oct 7, 2026 · 42 likes
- @fraying.bsky.social“Neat! Hugh Howey, the author of the books that the Silo series is based on, wrote his own novel writing …”Bluesky · Sep 11, 2026 · 18 likes
- @electrostani.bsky.social“Hugh Howey, the author who wrote the Silo novels, created his own word processor (Neo) for people writing …”Bluesky · Aug 16, 2026 · 9 likes
- A Hacker News story“Neo: A distraction-free word processor for authors, by a wannabe author”Hacker News · Aug 15, 2026 · 9 points
- @dwightsilverman.com“So @hughhowey.com, the guy who wrote the books on which the excellent Apple TV series Silo is based, has …”Bluesky · Aug 15, 2026 · 2 likes
The biggest 5 of 13 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 13
Talked about on GitHub, Bluesky and Hacker News (13 times).
On GitHub · 5
- Nov 5, 2026Feature suggestion : simple keyboard commands for paragraph / section / chapter — Sadsnake1/word-smith0 reactions · 1 comment
- Oct 27, 2026M0: Fork builds use the NeoCraft identity, library and update feed (#8) — iannitram/neo0 reactions · 2 comments
- Oct 25, 2026Add NEO 1.6.1 (new cask) — Homebrew/homebrew-cask0 reactions · 1 comment
- Oct 18, 2026check out NEO — Sadsnake1/word-smith0 reactions · 1 comment
- Sep 27, 2026Add NEO — AppImage/appimage.github.io0 reactions · 1 comment
On Bluesky · 5
- Oct 7, 2026Para quienes escribáis relatos y novelas: en el último noticiario rolero hablamos de Neo, una aplicación gratuita (un .exe que se ejecuta… — @michelroltv.eurosky.social42 likes and reposts · 3 replies
- Sep 11, 2026Neat! Hugh Howey, the author of the books that the Silo series is based on, wrote his own novel writing software, Neo, and released it for… — @fraying.bsky.social18 likes and reposts
- Aug 16, 2026Hugh Howey, the author who wrote the Silo novels, created his own word processor (Neo) for people writing books: github.com/hughhowey/neo — @electrostani.bsky.social9 likes and reposts · 1 reply
- Aug 15, 2026So @hughhowey.com, the guy who wrote the books on which the excellent Apple TV series Silo is based, has written a app for writing novels.… — @dwightsilverman.com2 likes and reposts
- Aug 23, 2026. @hughhowey.com author of the Silo trilogy has written a writing tool for authors, he's also fully supportive of his fan fiction writers… — @ianhopkinson.bsky.social0 likes and reposts · 1 reply
On Hacker News · 3
- Aug 15, 2026Neo: A distraction-free word processor for authors, by a wannabe author9 points · 2 comments
- Aug 29, 2026Hugh Howey's distraction-free word processor1 point
- Aug 20, 2026Neo: A novel-writing tool created by a novelist (Silo's author)1 point · 1 comment
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
The file
We split this AGENTS.md into the rules it gives an agent
7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 5
Read CONTRIBUTING.md before adding a feature.
- Distinctive: line 14
Do not save UI decoration into chapter HTML.
- Distinctive: line 15
Do not rewrite a chapter that has not changed.
- Very specific: line 141
Default library: ~/Documents/NEO Library (app.getPath('documents')).
- Very specific: line 164
Every book id, chapter id, and sidecar name passes through libName() in main.js.
- Very specific: line 187
Do not replace this with last-write-wins.
- Distinctive: line 193
After adding or changing strings:
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
NEO — notes for agents
NEO is a local word processor for books. It is an Electron app made of plain JavaScript, HTML, and CSS. There is no bundler, no framework, and no compile step. npm start runs electron ..
Read CONTRIBUTING.md before adding a feature. The product is opinionated on purpose. A change that helps someone finish a book belongs here. A change that adds a panel, a prompt, or a dependency usually does not.
Rules that override convenience
- Nothing interrupts a writer mid-sentence. No popups, no squiggles, no notifications while typing. Spellcheck is off until the writer asks for a pass.
- Controls stay hidden until hover or keyboard focus.
- Words are never discarded. Deleting text, unbinding a shelf, or losing a trash operation must leave the words recoverable (Darlings, a sibling chapter, or the system trash).
book:deleteusesshell.trashItem. If trash fails, leave the folder and show it. - Books are plain files. No database, no proprietary format.
- The renderer never touches the filesystem. Disk access goes through
window.neo(preload.js) to handlers inmain.js. - Do not save UI decoration into chapter HTML. Search highlights, spellcheck underlines, and focus dimming use the CSS Highlight API so they stay out of the file.
- Do not rewrite a chapter that has not changed. Libraries are synced with iCloud and Syncthing. A timer that writes every chapter on an interval will fight the other device.
Where the code is
| File | Role |
main.js | Window, menus, every filesystem operation, import parsing, PDF, backups |
preload.js | The entire renderer API, window.neo |
index.html | Two views: #bookshelf-view and #editor-view. CSP is script-src 'self' |
app.js | The whole UI, in banner-marked sections. Search for the banner before reading the file |
styles.css | All styling. Tokens are CSS variables at the top |
covers.js | Shelf covers in the window: seeded canvas art plus real title type. window.NeoCovers |
art.js | Painted covers in the main process. OpenAI only. Title and author are never sent to the image model |
i18n.js | t() / tk(), shared by main and the window. English source text is the key |
spell-worker.js | Hunspell WASM, forked with utilityProcess. Messages: load, check, suggest, add |
backup-worker.js | The day's backup zip, forked with utilityProcess by dailyBackup so a file that won't come off iCloud never holds the main process |
spell-ro.js | Romanian diacritics, used by the worker. Does not alter the manuscript |
manuscript.js | Standard manuscript format: the word count's rounding, the header's surname, the .docx's parts and the PDF's page, from a model the window hands it. Plain JavaScript that also runs in Node (window.NeoManuscript, loaded by index.html) |
locales/<code>.json | One language. Regional files (fr-CA.json) hold only the strings that differ |
pocket/ | Capacitor shell. It does not contain its own editor |
print/ | Vendored Paged.js and hyphenation patterns for paperback PDFs |
app.js section banners look like /* SAVING */. Start there: bookshelf, bound shelves, editor open, typing, poetry, screenplays, placeholders, nav, tabs, outline, outline cards, darlings, counters, saving, refresh, structural undo, find, import, spellcheck, focus, goals, export, manuscript format.
Menus are built in buildMenu() in main.js. A menu click sends { type, ... } to the window; app.js handles it on window.neo.onMenu.
Simple NEO and Show More Features
NEO opens simple. File → Show More Features (library.moreFeatures) brings back the rest: SIMPLE NEO near the end of app.js, and moreState / simpleTemplate in main.js.
- A menu item behind the switch carries
more: trueinbuildMenu();simpleTemplateleaves it (and the separators it would strand) out. A key, right-click choice or pane behind it asksmoreOn()inapp.js. - Behind the switch now: alignment, poetry paragraphs, footnotes (⌘⇧N), Markdown emphasis (italic typed; off in simple NEO), Other Font…, non-breaking marks, pages views, page width, interface size, focus mode, vim keys, read aloud, Look Up, the tab keys, chapter notes pages, outline cards (loose cards, cards beside the page), New Books Open To, painted covers and Cover Art…, pen names, binding shelves, duplicate, Reshelve, scripts and Fountain/Final Draft import, Import Chapters, txt/md/html exports, Chapter Titles Only, the shelf anthology, Manuscript Format, and the two language menus.
- Simple NEO treats those settings as NEO's defaults (
applyLook) and never erases them: the writer's choices wait in library.json for the switch. - Simple NEO's own, because the writer's eyes decide them: the page (View → Page: Night, Paper, Light) and Brighter Interface. Both are in the menu without the switch and in Pocket's ⋯ sheet, and
brightNownever asksmoreOn(). A library with a brighter interface set is no sign of more (usesMoreFeatures). - Nothing a book already holds is hidden. A script, footnote, poetry paragraph, bound shelf, painted cover, loose card or chapter notes page with notes on it opens and exports as before, and other pen names stay reachable.
- A book's right-click in simple NEO is the larger cards (
optionModal), with no cover menu: art is dragged onto the book; New cover and Remove cover art stand with the rest. Word goals are set in Goals, inside the book, never from the shelf. - The shortcuts window (
bookShortcutSections) lists a key only when its feature is showing. - A library from before the switch is decided once (
decideMoreFeatures): on when it uses something behind it, off otherwise (with one toast saying where things went). - Pocket (Android and iOS) is always simple and has no switch; its ⋯ sheet in
pocket/www/index.htmloffers only simple NEO's choices. - A new feature goes behind the switch unless Hugh says it belongs in simple NEO.
scripts/simple-neo.test.js,npm run test:simple.
Outline cards
The Outline tab shows the book as index cards (OUTLINE CARDS in app.js) unless library.outlineView is 'list'. A script's Outline is always cards, one per scene (scriptScenes): heading, length in eighths, cast, and a note in book.sceneNotes, keyed by data-scene-id on the heading line. Dragging a scene card calls spMoveScene.
- Cards come from the manuscript, not a separate structure: each chapter is cut at its
p.scene-breaklines (chapterSegments). A section that holds ap[data-sec-id]belongs to that note inbook.sectionNotes; the first section to carry an id owns it, because paragraphs split from a written ghost inherit the id. Others show their first line. - Moving a card moves its paragraphs and *** between chapter bodies, then
syncChapterandorderSectionNotes. Every move takes asnapshotStructurefirst. syncGhostsleaves a ghost where it stands and places a note the page lacks before the next section that's there. It never reorders ghosts.- Loose cards are
book.looseCards, shown in the right-hand pane while the Outline is up. A section card dragged there (or Move to loose cards) goes both ways: a section with writing takes its words along ascard.html, out of the manuscript and the counts until it's placed again. Chapter cards and a script's scene cards go there too: the card carries the chapter (words, title, notes, kind;held: 'chapter') or the scene's lines (held: 'scene'), written to book.json before they leave the page, and comes back whole (a chapter only between chapters). A card's Delete sends held words to Darlings first. A section card's Delete section sends its writing to Darlings (deleteSectionToDarlings). - Several cards at once: ⌘-click (Ctrl-click) toggles a card, Shift-click takes a run, and a drag across empty outline space sweeps (
cardLasso). The picked cards arecardSel(keys fromcardKey), kept across a redraw and dropped after a move. Delete/Backspace or the right-click menu runsdeleteSelectedCards: onesnapshotStructure, the single deletes run quiet, sections and scenes from the last up, and a book keeps at least one chapter. joinChaptermakes a chapter a section of another (List Tab on a chapter line, or a chapter card dropped on the middle of another). It moves the lines, persists the receiving chapter, and only then deletes the emptied one.- In the List, Enter always makes a chapter and Tab always makes a section.
- On a card made new (
cardEditor.fresh), Enter with words on it opens the next new card; Enter on an empty one, or Esc, stops. A pasted list (pastedOutline: the clipboard's HTML levels from<ul>/<ol>or Word'smso-list, elseoutlineLineson the text) becomes one card per item, and in the List one line per item: on a chapter, top items are chapters and deeper ones their sections.scripts/outline-paste.test.jscovers the text's markers. - While writing, the top of the right-hand pane shows the cards for where the caret is (
sideCardsUpdate,#side-cards): the chapter's and its sections' (the caret's marked red), or the script scene's. Typed on there, they're the same notes; a scene gets its id with its first note. - A script scene's card note shows in gray on the scene's empty first line (
spSceneNoteGhosts, adata-scene-notescreen mark thatcaptureBodystrips).
Brighter Interface is two settings: library.uiBright while writing (and on the shelf), library.uiBrightAside on the other tabs, bright unless turned off. applyBright picks one on every tab switch.
- The walking note (
walkNoteUpdate) is an overlay inside.chapter, plus adata-walkmark on the caret's paragraph thatcaptureBodystrips.note.dismissedhides it for good.
Screenplays
A book whose book.json says "format": "screenplay" is a script. Right-click (long-press) a shelf's + for New Script. The SCREENPLAYS section of app.js holds the feature; scripts/screenplay.test.js tests its rules.
- The whole script is one chapter, so selection and the arrow keys run through every scene. Scenes are found by their headings.
- Each line is a
<p>whose class is its element:sp-heading,sp-character,sp-paren,sp-dialogue,sp-transition,sp-shot. Action has no class. - Page breaks, page numbers, (CONT'D) and the gray suggestions come from
data-pg,data-fill,data-contdanddata-ghostmarks.captureBodystrips them. Never save them. - Lengths in
styles.cssare in em of the script's type (51em = 8.5in, 1em = one 12pt line), so a line wraps the same on screen, in the off-screen measuring room and in the PDF.spPaginateplaces the pages from the line counts. data-newpageon a line is the writer's own page break (right-click → Page Break Here; Backspace at the line's start removes it). Unlike the screen marks it is saved, and it travels as===in Fountain andStartsNewPage="Yes"in Final Draft.- A script's style lives in its
book.json:underlineHeadings: true(Format → Underline Scene Headings) andcontd: false((CONT'D) turned off). - A script exports as a PDF (letter, printed with
print: 'screenplay'), Fountain or Final Draft (.fdx). The book formats don't apply. - A
.fountainor.fdxfile dropped on a shelf or picked with Import becomes a new script.importFileinmain.jsonly reads the file;spFromFountainandspFromFdxinapp.jssort it into elements. Both readers are plain string functions, so the tests cover them (scripts/fixtures/holds a Final Draft file written by screenplain, an outside tool).
Manuscript format
File → Export → Manuscript Format… (MANUSCRIPT FORMAT in app.js, the pages in manuscript.js) writes the open book the way agents and editors ask for it (William Shunn's novel format): a title page with the writer's contact block, "about 90,000 words" (roundWords: to the thousand from 10,000 up, else the hundred) and the title and byline halfway down, no header; then the story numbered from 1 under "Surname / TITLE / page", double-spaced 12 point Times New Roman (or Courier), one-inch margins, every paragraph indented half an inch, each chapter on a new page a third of the way down, # for scene breaks, parts on their own page, an epigraph kept, END at the close. The copyright, dedication, contents, acknowledgments and about pages stay out. One story with no chapters gets the short-story layout (title halfway down page 1, the text under it, the header from page 2).
- .docx (
docxEntries): the title page is its own section with no header, the story the next one withpgNumType start=1and a PAGE field inheader1.xml; a short story is one section withtitlePgand an empty first-page header. Asettings.xmlkeeps Word out of Compatibility Mode. Saved throughexport:save. - PDF (
html): one page for Paged.js, the header in@page's@top-rightmargin box,@page titlepagewithout it;print:manuscriptlays it out withrenderPaged(the story's first section is.pg1, so it's page 1) and saves it. It shares the print window with the paperback, so each refuses while the other runs. Courier Prime (bundled) goes into the PDF; Word gets Courier New. - The dialog remembers the contact block for every book (
library.manuscript: name, address, phone, email, font, end) and the byline and header for this book (book.manuscript, only where they differ from NEO's own choice). "Anonymous" is never taken for a byline. Values go into the fields from code, so a quotation mark can't cut one short. It never opens over another dialog or twice. Scripts keep their own PDF; Pocket has no manuscript export (its page doesn't loadmanuscript.js). scripts/manuscript.test.js;npm run test:manuscript(scripts/manuscript.e2e.js: the dialog, both files, the .docx read back by NEO's import, the PDF's pages read back withpdftotextwhere it's installed).
Paperbacks for KDP
Export → Paperback for KDP… (also on the shelf's right-click Export) writes a print interior PDF and a cover template PDF beside it. printPaperback and buildPrintHtml are the PRINT BOOK section of app.js; makePaperback, renderPaged and kdpCoverHtml are in main.js, behind print:paperback.
- Trims are KDP's four most-used: 5×8, 5.25×8, 5.5×8.5, 6×9 (
PRINT_TRIMSinapp.js,KDP_TRIMSinmain.js). Paper thickness, page limits and the inside-margin bands (kdpGutterMin) come from KDP's help pages; NEO adds a quarter inch to the minimum. If the page count crosses a band, the book is laid out again with the wider margin. - Pages are set by Paged.js (
print/paged.polyfill.js, vendored, MIT) in one offscreen window per export. A hidden window stalls its animation frames; a second offscreen window opened right after one closes fails. The window closes when the export ends. One patch in it is markedNEO:(a word hyphenated across a page turn broke a letter late). - Hyphenation is soft hyphens put in by
hyphenateHtmlinmain.jswith TeX patterns (print/hyphen/, ISC), because Chromium only hyphenates on macOS. Onlyp.hyprose is touched; names (capitalised words, except in German) and a paragraph's last word stay whole. - Chromium rounds page sizes to 0.01 in;
exactPageBoxrewrites the MediaBox to the exact size in the same number of bytes. - The page count is kept even. Chapters open on a right-hand page; blank pages carry no head or number. Page numbers sit at the outside corners (left pages at the left, right pages at the right); the regular PDF keeps them centered.
- Choices are kept: trim, paper, ISBN and fiction notice in
book.print; on the pen name,author.printholds the back-matter links,alsoByText(the writer's own list, one title per line) andreviewText(own wording, with{title}for the book's title; empty means NEO's translated wording). A dedication typed in the dialog becomes the book's own Dedication page. - Page 1 is the story's first page (the first
chaptersection, so a prologue). In the paperback that section is.pg1and each page's number is set on it after layout. The regular PDF stops counting on@page frontpages and numbers its contents from thedata-p1link; pages after page 1 that show no number (a part's title) are.page.counted. scripts/print.test.jscovers the margin bands, hyphenation, the page box and the cover's size. Pocket has no paperback export.
Snapshots
⌘S (Ctrl+S; File → Take Snapshot) snapshots the whole book (SNAPSHOTS in app.js). NEO also takes one before the first change of each writing day (maybeDailySnapshot, from scheduleChapterSave and snapshotStructure, built from savedHTML so it holds the day's start). No timer, and nothing is taken when nothing changed.
- One folder per snapshot,
book-…/snapshots/<UTC stamp>Z-<kind>-<chapters changed>-<words>-<rand>/, holding only the chapters that changed since the last snapshot and, written last,snapshot.json(order, titles, kinds, atextHashof every chapter). Files are written once (snapshot:list/read/write/removeinmain.js, the same four in Pocket's bridge). A chapter's text at a snapshot is in the newest snapshot at or before it holding the file (snapHolder). - Kinds:
k⌘S, kept until deleted;dstart of a day andrjust before a restore, which go after 30 days (snapsToThin). A snapshot that goes first copies its chapter files into the next one that lacks them, so later snapshots still read whole. - The Darlings tab's right-hand pane lists them (and stays open there); picking one shows the book as it was in
#snap-view, read-only, words gone since marked with the CSS Highlight API (wordsGone). Restore This Chapter and Restore Whole Book take anrsnapshot and asnapshotStructurefirst, so ⌘Z undoes them. A passage can be set aside as a darling from the right-click menu. Pocket takes snapshots but has no list yet. - The daily backup zip leaves
snapshots/out.scripts/snapshots.test.jscovers the names, the fingerprint, thinning, where a chapter's text is and the marks. - The snapshot on the page and the list belong to the open book:
openBookandbackToShelfclear both (forgetSnapshots), another tab only the page (hideSnapshot). A snapshot's chapters are.chapter-bodytoo, inside#snap-viewrather than a.chapter, so code that walks every.chapter-bodymust pass over them.
Per device
How NEO looks belongs to each device: DEVICE_LOOK in app.js (page theme, brightness, zoom, type size, page width, typewriter, non-breaking marks, pages view, focus, counters, outline view, vim keys). Every library write also keeps them in this device's localStorage; every library read takes them back from there (applyDeviceLook). The library's copy is the last device's, which is what a device new to the library starts with. Zoom is per device and per view: the page per mode, the cards, and the shelf (neo.shelfZoom, ⌘+/− and pinch on the shelf), each in localStorage; the page moves in tenths. The desktop also keeps its page theme in settings.json for the window's color at launch, and exportFolder there, so save dialogs open where the last export went.
Format → Page Width (Regular, Wide, Wider; PAGE_WIDTHS in app.js) sets --page-wf and --page-pad-x, which --page-w and the sheet's side padding in styles.css follow. The page also grows a little by itself in a wide window (--page-w clamps 680 to 760 px by the window); Pocket's page fills the screen up to 680 px (pocket.css).
A book's right-click on the shelf is a small menu (popMenu) on the desktop, with the cover's choices one level in; touch keeps the larger cards. Duplicate (book:duplicate, and duplicateBook in Pocket's bridge) copies the folder under a new id and title.
Pocket makes PDFs through NeoPdf, a native plugin in each project: Android opens its print screen (Save as PDF), iOS draws the pages into a file for the share sheet. A script's title can be set bold, underlined or italic as a whole (book.titleStyle, ⌘B ⌘U ⌘I on the title page), and keeps it in the PDF, Fountain and Final Draft.
Processes
index.html + app.js → preload.js (window.neo) → main.js → NEO Library
↓
spell-worker.jsThe window is created with contextIsolation: true and nodeIntegration: false. New renderer capabilities are added in three places: an ipcMain.handle in main.js, a method on window.neo in preload.js, and the call site in app.js.
Files on disk
Default library: ~/Documents/NEO Library (app.getPath('documents')). File → Library Folder… stores another path in userData/settings.json and restarts. NEO does not move existing books.
NEO Library/
library.json shelves, author, pen names, customWords, spellLanguage
_catalog.txt regenerated map of folder → title; edits are ignored
neo-errors.log
Backups/neo-backup-YYYY-MM-DD.zip one per day, 14 kept
Exports/ emailed PDF snapshots
book-<slug>-<id>/
book.json metadata and chapterOrder
chapters/<id>.html
notes.html the Notes tab's page for the whole book
notes-<chapter id>.html a chapter's notes page (NOTES PAGES in app.js)
outline.html
darlings.json
stickies.json
cover-<ts>.<ext> writer-chosen image
art-<ts>.<ext> painted image, plus art.jsonApp settings and the cover-art API key live in Electron userData (settings.json, secrets.json), not in the library. The key is encrypted with safeStorage when the OS allows it. Do not write secrets into the library.
Every book id, chapter id, and sidecar name passes through libName() in main.js. It allows one path segment and rejects ., .., slashes, and null bytes. Keep new files inside that helper.
Every library write goes through writeFileDurable: file.tmp, fsync, then rename into place, so a power cut can't leave an empty file. writeJSON also keeps the last version that read whole as file.bak, and readJSON falls back on .tmp, then .bak. A book.json lost with no copy is rebuilt from the chapter files (rebuildBookMeta). There is no append and no partial chapter update.
json:write and aux:write will create any single-segment <name>.json or <name>.html in the book folder. Prefer the existing names unless a new sidecar is actually required.
Saving and sync
The window holds the open book in memory (chapterHTML, book, stickies, darlings) and remembers what it last wrote (savedHTML, savedMetaSig).
- Chapter and notes edits debounce 800 ms, then write only if the HTML changed and the chapter is still in
chapterOrder. flushAllSavesruns every 20 s and on blur, hide, and close. It also storeslastPositioninbook.json. A scroll-only change is not a new position.refreshFromDiskruns on focus, on visibility, and every 30 s while visible. It comparesmtimeMs:sizestamps and re-reads only changed chapters.- Unchanged local chapter plus a changed file: adopt the file. If the file only has fewer words, adopt it and keep the displaced text in Darlings.
- Both sides changed: keep the local text on the page and insert the disk text as the next chapter, titled as from the other device.
- Skip a chapter whose write is still in flight. Drop a read that overlapped a local save. An empty read must not wipe a chapter that already has text.
- The same generation check applies to
library.jsonon the shelf (libraryGeneration,libraryWritesPending). - Today's word count (
book.dailyCounts,{ start, end }per writing day) counts only what is typed or cut on this page. Text read from disk (opening, a chapter adopted or kept as the other device's twin) goes throughwordsFromDisk, which moves the day's start with it. Words moved between chapters (a split, a card dropped on another chapter or made a chapter) are neither written nor cut:holdDayputs the day and a running sprint back once they have settled. A chapter made a part and back, and the undo of that, go throughwordsFromDisktoo. Another device's larger tally for a day is taken in (mergeDayCounts).scripts/daily-words.test.js, and the #345 cases innpm run test:words.
Structural undo (snapshotStructure, structuralUndo, banner STRUCTURAL UNDO) holds UNDO_DEPTH (50) actions. An undo puts back only what its action changed: writing made since stays, or, in a chapter the action also touched, goes to Darlings with a toast that says so. What an undo took back goes on redoStack (⇧⌘Z, Ctrl+Z's pair; Ctrl+Y off the Mac); any input event anywhere, or a new action, empties it. A redo is an undo of the undo, so it keeps the same protections.
On a Mac, iCloud ("Optimize Mac Storage") can turn library files into placeholders: the size is there, the words aren't, and a read waits on iCloud, forever if iCloud is stuck. A synchronous read of one froze the main process at launch, with no window. So cloudOnlyAmong spots them (no blocks on disk, then the dataless flag), requestDownload asks brctl for them, and the read handlers wait for them with afterLocal without blocking, answering at once when nothing is in iCloud. readJSON throws ECLOUD for one rather than falling back on .bak, and no write goes over a placeholder. The daily backup runs in its own process (backup-worker.js), two minutes after launch: it leaves placeholders out and names them, gives each other file 20 seconds, and is ended after 15 minutes. 1.5.1 read every file in the main process at launch, and one stuck file kept NEO from opening, and from updating, which needs NEO open. scripts/icloud.test.js plays iCloud with sparse files, and a stuck file with a named pipe. Don't add a direct readFileSync of a library file, and never walk the whole library in the main process.
Do not replace this with last-write-wins. The comments in persistChapter and refreshFromDisk explain cases that look redundant and are not.
Interface language
Wrap writer-visible strings in t('English text', { placeholder }). Use tk() for strings translated later, at the point of display. In index.html, use data-i18n, data-i18n-title, data-i18n-placeholder, or data-i18n-ph.
After adding or changing strings:
node scripts/i18n.js template
node scripts/i18n.js check frscripts/i18n.js only scans app.js, main.js, covers.js, index.html, and Pocket's own pocket/www/index.html and pocket/www/pocket-bridge.js. A new string in another file will not enter the template until that list includes it. Pocket's page marks its words with the same data-i18n* attributes as the desktop's, and its ⋯ sheet and bridge use t().
Details, plural forms, and regional fallback (fr-CA → fr → English) are in TRANSLATING.md. Quotation marks follow the spellcheck language (QUOTE_STYLES in app.js). Import chapter detection is CHAPTER_WORDS in main.js. Cover small-words are CONNECTORS in covers.js.
Italian has no spellcheck dictionary: the only Hunspell package on npm is GPL-3.0-only, and NEO is MIT. Do not add it.
pocket/ is a Capacitor app that runs the desktop editor. Its bridge (pocket/www/pocket-bridge.js) implements window.neo against the phone's library folder. Android shares Documents/NEO Library via sync. iOS uses the app folder, optionally iCloud, with LibraryHome.swift locating that folder.
scripts/pocket-www.js (run by CI, and by hand before a local build) copies app.js, covers.js, styles.css, i18n.js, fonts/, locales/, Hunspell's browser build, and the SPELL_LANGUAGES dictionaries from main.js into pocket/www/. Pocket's checker is pocket/www/pocket-spell.js, a module worker with the same messages as spell-worker.js. A change to those files changes Pocket. Pocket-only behavior belongs in pocket-bridge.js or the native projects, not behind a desktop-only branch scattered through app.js.
Commands
npm install
npm start
npm test # node --test scripts/*.test.js
npm run test:coverage # node --test --experimental-test-coverage scripts/*.test.js
npm run lint # oxlint, Electron's standard-style JavaScript rules
npm run test:spellcheck # node --test scripts/spellcheck.test.js
npm run test:dashes # node --test scripts/dashes.test.js
npm run bundle # Hugh: brings in the newest .bundle from ~/Downloads and pushes main
npm run try # Hugh: opens the newest .bundle's NEO on a copy of the library, pushes nothing (-- fresh: an empty library)
npm run release # Hugh: next version (x.y.9 → x.(y+1).0), commit, push, tag (npm run release -- 2.0.0 for another)
npm run package:mac # macOS build; npm run package calls this
npm run package:linux # AppImage via electron-builder; also package, package:mac, package:win, package:allTests use node:test and load app.js or spell-worker.js inside vm. They are not run by CI. The only CI check is a Windows smoke test that the packaged exe boots and creates a library (.github/workflows/build.yml, on v* tags). Pocket builds from .github/workflows/pocket.yml.
node scripts/check-romanian-package.js <Resources dir> compares a packaged app's dictionaries to the source tree. node scripts/benchmark-spellcheck.js times the checker. Neither is an npm script.
Handing changes to Hugh
Hugh pushes and releases himself and isn't a git user. Hand him work as a git bundle of main..your-branch, built on the latest origin/main, then tell him: save it to Downloads, npm run bundle. To ship a desktop release: npm run release. Both commands check their footing and stop with a plain sentence instead of half-finishing. Don't give him raw git or npm version steps when these cover it.
When you change something
- A new filesystem operation needs a handler, a
libName()boundary, and apreload.jsmethod. Match the existing IPC names (library:,book:,chapter:,aux:,json:,cover:). - A new writer-visible string needs
t()and a template refresh. - A new way to remove text needs a recovery path and a sentence in the UI that says where the words went.
- Export formats are assembled in
app.jsand written byexport:saveinmain.js. EPUB is a zip built in memory. PDF is printed from temporary HTML. - Errors in the main process are appended to
neo-errors.logvialogError. Renderer failures go throughwindow.neo.logError. Do not swallow a save failure;persistChapterrollssavedHTMLback so the next flush retries.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

