Symbolon packs

kausalflow/lightkone

Lightkone core: search, BibTeX, keys, filing, merge, tidy and the library file

Version 1 · Proprietary (kausalflow) · by kausalflow · 64 obligations (64 decided, 0 default, 0 open) · 4 assumptions · 8 components · 340 checks

Use it · Free
symbolon pull kausalflow/lightkone
latest.tar.gz sha256 pack.md (for agents: brief and obligations, links to each component) llms.txt
Digest sha256:075c30cc624fda7b44c574318d20224fc97184ed3505597bf0fe7616defdfd10
Published by kausalflow

Evidence

8 of 8 components pass a build · 3 builds · 2 runners. Every signature and digest below was checked when this site was built; re-check one with symbolon verify on the linked file.

ComponentBuildResultContractRunner
bibtex-export-v1 go-1 rebuild passed 25 json this version runner-a
kausalflow/lightkone passed 25 json this version runner-a
AI builder passed 25 json this version runner-a
bibtex-import-v1 go-1 rebuild passed 52 json this version runner-a
kausalflow/lightkone passed 52 json this version runner-a
AI builder passed 41 json an unpublished draft runner-a
citation-key-v1 go-1 rebuild passed 25 json this version runner-a
kausalflow/lightkone passed 25 json this version runner-a
library-file-v1 go-1 rebuild passed 61 json this version runner-a
kausalflow/lightkone passed 61 json this version runner-a
library-merge-v1 go-1 rebuild passed 29 json this version runner-a
kausalflow/lightkone passed 29 json this version runner-a
pdf-filing-v1 go-1 rebuild passed 25 json this version runner-a
kausalflow/lightkone passed 25 json this version runner-a
search-query-v1 kausalflow/lightkone passed 95 json this version runner-c
go-1 rebuild passed 92 json an unpublished draft runner-a
AI builder passed 82 json an unpublished draft runner-a
tidy-v1 go-1 rebuild passed 28 json this version runner-a
kausalflow/lightkone passed 28 json this version runner-a

Brief

Lightkone

Lightkone is a research-paper library for people who read a lot of papers and want to own their data. It is a static web app with no server: the library is plain files (lightkone/library.json and a generated library.bib) in storage the owner chooses, such as the browser, an S3 or R2 bucket, or a GitHub repository.

People use it to collect papers (by DOI, arXiv id, BibTeX import or PDF), find them again with a small query language, file them in folders, tag them, read and highlight PDFs, and keep .bib files in sync with their manuscripts on Overleaf.

What this version of the pack covers

Version 1 covers the pure components everything else leans on, and the library file they all share:

Component What it does
lightkone/search-query@1 The search box: filters (tag:, author:, year:, in:, has:, no:, is:), words with accent folding, ranking, sort orders, and the trash rule
lightkone/bibtex-import@1 Reading BibTeX from Paperpile, Zotero, JabRef and Google Scholar into paper drafts, including LaTeX accents, macros, author names and arXiv ids
lightkone/bibtex-export@1 Writing a paper as the exact BibTeX entry lightkone writes, byte for byte
lightkone/citation-key@1 Paperpile-style citation keys for new papers, unique within the library
lightkone/pdf-filing@1 Where a paper's PDF goes, named the way Paperpile names it
lightkone/library-merge@1 Merging two devices' edits to library.json without losing either
lightkone/tidy@1 Finding duplicate papers and how to merge each group
lightkone/library-file@1 Reading library.json and writing it back, with library.bib, byte for byte

Later versions add the MCP server's tools and UI acceptance scenarios.

Who it is for

Builders who want lightkone's behavior in their own stack: a different language, a server instead of a static app, or a fork with their own features. A build that passes these contracts reads the same queries and BibTeX the same way lightkone does, so its users' libraries and habits carry over.

Non-goals

  • Full-text search inside PDFs. Search covers metadata and notes only.
  • Collaboration. A library belongs to one person; there are no shared libraries, roles or permissions.
  • Being a general BibTeX validator. Import reads what the common managers export and skips the rest.
  • Pixel-level UI. Later versions specify user flows, not layout or styling.

Priorities

When goals conflict, earlier ones win:

  1. The owner's data stays theirs and stays readable. Never lose or corrupt a library, and keep it in plain files.
  2. Compatibility with lightkone's files. A build reads and writes the same library.json and library.bib, byte for byte where the contracts say so.
  3. Same answers to the same queries, so habits carry over between builds.
  4. Speed and features beyond the pack.

Who resolves gaps

kausalflow, as the namespace holder. If the pack doesn't settle something, record the question and the reading you chose in NOTES.md next to your build; obligations marked open in obligations.toml are yours to choose. Unsettled questions are answered in the next version of the pack.

Assumptions

IdStatementMonitor
K-bibtex-sourcesImported BibTeX comes from Paperpile, Zotero, JabRef or Google Scholar exports, or from a library.bib lightkone wrote.Count entries dropped per import; report when more than 5% of an import is dropped.
K-iso-timestampsEvery added and opened timestamp was written by JavaScript's toISOString (UTC, milliseconds), so string order is time order.On load, report papers whose timestamps do not match the schema pattern.
K-personal-libraryA library belongs to one person, holds at most tens of thousands of papers, and is searched in memory on their device.Report the library size when it is loaded.
K-single-writerTwo clients (devices, or Claude through MCP) do not write the same library within the moment between lightkone re-reading library.json and writing it.After each save, re-read library.json and report when it differs from what was written.

Obligations

IdStatusStatement
search.results-from-library decided Results are distinct ids of papers in the input library.
search.trash decided Trashed papers appear only with is:trash, and then only trashed papers appear; an empty trashed field is not trashed.
search.query-tokens decided Terms split on whitespace; quotes make one word; an unknown field:value or unmatched quote is an ordinary word.
search.field-edge-cases decided A field with nothing after the colon is an ordinary word; an unknown field with a quoted value is one word, quotes included.
search.terms-conjunctive decided Every filter and every word must hold (except year:, see search.year-last-wins).
search.folding decided Text and query compare after NFKD, removing combining marks, and lower-casing.
search.word-scoring decided A word scores 5 in the title, 4 in authors joined by ' ; ', 1 in the rest text; a paper with any zero-scoring word is left out.
search.filter-tag decided tag:T matches a whole tag after folding, never a substring.
search.filter-author decided author:A is a substring of the folded authors joined with ' ; '.
search.filter-year decided year:Y is an exact year and year:A-B an inclusive range; a missing year never matches.
search.year-last-wins decided Only the last year: term that counts applies; a term whose value is not a number does not count.
search.year-malformed decided year:A-B parts: A not a number or 0 means the term does not count; B missing, empty, 0 or not a number means A; later parts are ignored. A typo such as year:20l7 filters nothing, by kausalflow's decision (2026-10-24): forgiving, low stakes.
search.filter-folder decided in:F matches folder F and folders inside it, after folding, ignoring trailing slashes, never by name prefix.
search.filter-has-no decided has:/no: test presence of pdf, abstract, arxiv and notes; empty counts as absent; has:pdf also accepts file; unknown values are ignored.
search.filter-is decided is:starred and is:recent select starred and opened papers; unknown is: values are ignored.
search.default-order decided The default sort is newest added first, relevance when there are words, opened with is:recent; ties keep input order, relevance ties fall back to newest added.
search.explicit-sorts decided year: later first, missing as 0; opened: latest first, never-opened last; an explicit sort overrides the default.
search.title-sort-ascii decided title: sort is ascending and stable for titles of lowercase ASCII letters, digits and spaces.
search.title-sort-other decided title: sort uses Unicode collation (CLDR root, tertiary), as JavaScript's localeCompare does: accents and case only break ties. Kept by kausalflow's decision (2026-10-24); builds need an ICU-grade collator.
import.never-fails decided Import never fails: malformed parts are skipped and the rest is read.
import.entries decided @type{key, fields} and @type(key, fields) become drafts; @comment, @preamble and untitled entries do not.
import.malformed-field-skip decided A field without a name or '=' is skipped to the next comma or the end of the entry, skipping its braced and quoted values whole; the fields after it are read.
import.values decided Values are braced, quoted, numeric or macro parts joined by #; @string and month macros expand; later fields replace earlier ones; whitespace collapses.
import.non-ascii-whitespace decided Whitespace is JavaScript's \s set (incl. no-break space, U+2028, U+3000, BOM), everywhere in import. Kept by kausalflow's decision (2026-10-24).
import.latex-to-plain decided Title, venue, abstract, names and tags go through plain(): LaTeX accents and specials become Unicode, formatting commands unwrap, braces go.
import.authors decided Authors split on 'and'; First Last flips to Last, First; comma names, 'others' and {braced} names are kept; editor stands in only without an author field.
import.identifiers decided doi loses its resolver prefix; arxiv ids come from arxivid, arXiv eprints, arxiv.org URLs or new-style bare eprints, without version.
import.draft-fields decided year is the first four digits; venue is journal, else booktitle, else publisher; notes is annote, else note; empty abstract, doi and venue are absent.
import.venue-empty-journal decided The venue is the first of journal, booktitle, publisher that is not empty, so an empty journal does not hide a booktitle. Fixed in lightkone#53; until then an empty journal left the paper without a venue.
import.tags decided keywords split on ; or , into tags, dropping identifiers and URLs.
export.byte-exact decided Export writes the exact entry lightkone writes: frame, aligned names, field order.
export.values decided Values are written as given, except that & % # are escaped once in title, venue and abstract.
export.venue decided The venue is written as journal, or booktitle for inproceedings and incollection, unless a non-empty journal field is already there.
export.authors-and-keywords decided Authors join with ' and ', whole names braced; tags join with ';' as keywords.
export.empty-values decided Empty values are left out, except title and year 0; an empty type becomes misc.
export.no-private-fields decided Fields whose names start with lightkone- are private and never written to BibTeX.
keys.format decided A new key is surname (NFKD, ASCII letters, Anon if none) + year + '-' + first letters of the first two title words longer than 3 letters.
keys.unique decided Keys are unique in the library: a taken key counts up from 2, and keys given earlier in a batch are taken.
keys.stable decided A key a paper arrives with is kept when it is free, so keys from an imported .bib never change.
filing.paperpile-layout decided PDFs are filed as papers/All Papers/<SURNAME>/<Who Year - Title>.pdf, Paperpile's own layout, so a Paperpile folder can be copied in unchanged.
filing.clean-names decided File names hold no characters a file system or Drive rejects; they become ' - ', and -- / --- become dashes.
filing.length decided Long author lists and names are shortened the way Paperpile shortens them.
filing.no-overwrite decided A new PDF never takes a name another paper uses; it gets (2), (3), ...
merge.no-lost-edits decided A save never silently drops a change made elsewhere: one-sided changes are taken, different fields of one paper both survive.
merge.conflict-ours decided When both sides change the same field differently, this device's edit wins.
merge.sets decided tags and folders merge as sets: additions from either side are kept, removals from either side stick.
merge.deletes decided A paper deleted on one side and unchanged on the other is gone; changed on the other, it survives.
merge.order-and-count decided Their order first, then papers only here; fromThem counts papers that took a change made elsewhere.
merge.bibsync decided bibSync takes theirs unless changed here, and is written only when non-empty.
tidy.match decided Duplicates share a DOI, an arXiv id, a long title, or a short title and first author, after normalising; chains join.
tidy.trash-ignored decided Papers in the trash are never grouped.
tidy.keeper decided The kept paper is the one with a PDF, then notes, then the oldest.
tidy.nothing-lost decided Merging keeps every tag, folder, note and star, and fills the keeper's missing fields from the others.
tidy.blank-ids-ignored decided A DOI or arXiv id that is empty once trimmed and stripped of its prefix gives no match key, so blank ones never make papers duplicates. Fixed in lightkone#52; until then whitespace-only DOIs matched each other.
file.refuse-foreign decided A file that is not a lightkone library (not JSON, wrong format, no papers list, a paper that is not an object) is refused with an error and nothing is written.
file.keeps-papers decided Opening and saving keeps every paper and every key of every paper, in order, plus bibSync when it has entries.
file.keeps-unknown-keys decided Top-level keys other than format, version, papers and bibSync are kept on save, after lightkone's own, in the file's order, so another tool's data at the top level survives. Fixed in lightkone#51; until then they were dropped.
file.newer-version-refused decided A library.json whose version is a number above 1 was saved by a newer lightkone and is refused, writing nothing; any other version, or none, is read and saved as 1. Fixed in lightkone#53; until then a newer file was read as version 1 and could lose data on save.
file.folders decided Papers without folders take them once from Paperpile's Drive paths, skipping All Papers, author folders and Other.
file.folders-author-only decided A single directory is skipped as an author folder only when it is Other or one of the paper's authors' surnames in capitals, so a user's own ML or 2019 folder is kept. With no authors to compare, a directory in capitals with a letter is still skipped. Fixed in lightkone#52; until then every single directory in capitals was skipped.
file.exact-text decided library.json is written byte for byte as lightkone writes it: JSON with a one-space indent, JavaScript's key order, escaping and number printing.
file.no-needless-write decided Nothing is written when the file would not change, so opening a library on several devices does not cause sync churn.
file.bib-alongside decided library.bib is written with every save: the BibTeX entry of each paper not in the trash, in library order.
file.bib-leaves-out-trash decided Papers in the trash are left out of library.bib, so Overleaf does not see deleted papers. A paper is in the trash when its trashed value is true in JavaScript. Fixed in lightkone#51; until then they were written.

Files

/ (4)
assumptions (4)
components/bibtex-export/versions/1 (4)
components/bibtex-export/versions/1/schema (2)
components/bibtex-import/versions/1 (4)
components/bibtex-import/versions/1/schema (2)
components/citation-key/versions/1 (4)
components/citation-key/versions/1/schema (2)
components/library-file/versions/1 (4)
components/library-file/versions/1/fixtures (3)
components/library-file/versions/1/schema (2)
components/library-merge/versions/1 (4)
components/library-merge/versions/1/fixtures (1)
components/library-merge/versions/1/schema (2)
components/pdf-filing/versions/1 (4)
components/pdf-filing/versions/1/schema (2)
components/search-query/versions/1 (4)
components/search-query/versions/1/fixtures (2)
components/search-query/versions/1/schema (2)
components/tidy/versions/1 (4)
components/tidy/versions/1/schema (2)