strom-research 1.13.2-beta.2 → 1.13.2-beta.3

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.
Files changed (87) hide show
  1. package/assets/lang/cs.json +20 -0
  2. package/assets/lang/de.json +20 -0
  3. package/assets/method/core.md +3 -1
  4. package/assets/method/intake.md +11 -8
  5. package/assets/method/link.md +2 -1
  6. package/assets/method/reading.md +23 -5
  7. package/assets/method/recording.md +4 -3
  8. package/assets/method/verify.md +2 -1
  9. package/dist/agents/files.js +6 -4
  10. package/dist/agents/images.js +54 -0
  11. package/dist/agents/models.js +248 -0
  12. package/dist/agents/profiles.js +16 -6
  13. package/dist/brief/brief.js +265 -47
  14. package/dist/brief/sheet.js +65 -0
  15. package/dist/cli/context.js +3 -1
  16. package/dist/cli/execute.js +58 -6
  17. package/dist/cli/help.js +9 -1
  18. package/dist/cli/main.js +32 -2
  19. package/dist/cli/menu-links.js +3 -2
  20. package/dist/cli/menu-settings.js +40 -1
  21. package/dist/cli/model-choice.js +35 -0
  22. package/dist/cli/registry.js +20 -10
  23. package/dist/cli/ui.js +17 -0
  24. package/dist/cli/wizard.js +20 -10
  25. package/dist/commands/analysis.js +90 -193
  26. package/dist/commands/batch.js +95 -16
  27. package/dist/commands/browse.js +2 -1
  28. package/dist/commands/clips.js +6 -3
  29. package/dist/commands/connectors.js +38 -11
  30. package/dist/commands/guide.js +6 -3
  31. package/dist/commands/index.js +2 -0
  32. package/dist/commands/media.js +336 -53
  33. package/dist/commands/meta.js +1 -1
  34. package/dist/commands/people.js +5 -4
  35. package/dist/commands/read.js +86 -23
  36. package/dist/commands/readers.js +13 -3
  37. package/dist/commands/readings.js +265 -0
  38. package/dist/commands/research.js +9 -4
  39. package/dist/commands/session.js +83 -24
  40. package/dist/commands/setup.js +17 -2
  41. package/dist/commands/show.js +108 -0
  42. package/dist/commands/sources.js +40 -13
  43. package/dist/commands/start.js +5 -3
  44. package/dist/commands/tasks.js +85 -36
  45. package/dist/commands/transcripts.js +3 -0
  46. package/dist/core/actions.js +118 -11
  47. package/dist/core/age.js +4 -2
  48. package/dist/core/batches.js +3 -2
  49. package/dist/core/changelog.js +1 -1
  50. package/dist/core/check.js +10 -1
  51. package/dist/core/config.js +20 -1
  52. package/dist/core/connector.js +5 -0
  53. package/dist/core/edge.js +7 -8
  54. package/dist/core/errors.js +7 -0
  55. package/dist/core/frontier.js +3 -2
  56. package/dist/core/gdate.js +31 -0
  57. package/dist/core/git.js +10 -1
  58. package/dist/core/hypolinks.js +509 -0
  59. package/dist/core/integrity.js +15 -0
  60. package/dist/core/live.js +188 -47
  61. package/dist/core/mediaindex.js +107 -0
  62. package/dist/core/mode.js +11 -2
  63. package/dist/core/model.js +1 -0
  64. package/dist/core/net.js +62 -9
  65. package/dist/core/overview.js +13 -3
  66. package/dist/core/people.js +112 -1
  67. package/dist/core/phrases.js +3 -0
  68. package/dist/core/queue.js +3 -2
  69. package/dist/core/reader.js +96 -14
  70. package/dist/core/recent.js +95 -0
  71. package/dist/core/records.js +2 -2
  72. package/dist/core/review.js +2 -1
  73. package/dist/core/schema.js +2 -2
  74. package/dist/core/session.js +144 -28
  75. package/dist/core/text.js +31 -0
  76. package/dist/core/tidy.js +14 -0
  77. package/dist/core/tree.js +126 -15
  78. package/dist/core/views.js +53 -11
  79. package/dist/core/years.js +42 -0
  80. package/dist/gedcom/export.js +5 -4
  81. package/dist/runners/antigravity.js +119 -51
  82. package/dist/runners/claude.js +48 -5
  83. package/dist/runners/codex.js +9 -2
  84. package/dist/runners/jsonl.js +2 -1
  85. package/dist/runners/runner.js +8 -0
  86. package/dist/runners/script.js +8 -2
  87. package/package.json +1 -1
@@ -130,6 +130,13 @@
130
130
  "ui.setup.model.opus": "Opus – nejlépe čte staré rukopisy (doporučeno)",
131
131
  "ui.setup.model.sonnet": "Sonnet – rychlejší; tištěné dokumenty a běžná práce",
132
132
  "ui.setup.model.own": "Nechat na agentovi",
133
+ "ui.setup.model.agent": "Který model má v {agent} dělat výzkum? Nejsilnější nejlépe čtou staré rukopisy – rychleji spotřebují předplatné nebo stojí víc.",
134
+ "ui.setup.model.recommended": "{model} (doporučeno)",
135
+ "ui.setup.model.own.is": "Nechat na agentovi (teď {model})",
136
+ "ui.setup.model.now": "{model} (zvoleno teď)",
137
+ "ui.chat.app.model.chosen": "V aplikaci {app} zvolit pro výzkum model {model} (zvolený v nastavení).",
138
+ "ui.settings.model": "Model výzkumu ({agent}): {model}",
139
+ "ui.settings.model.own": "vlastní model agenta",
133
140
  "ui.setup.level": "Co smí agent dělat bez ptaní?",
134
141
  "ui.setup.level.ask": "Ptát se mě na všechno mimo výzkum",
135
142
  "ui.setup.level.auto": "Pracovat samostatně – ptát se jen u riskantních kroků (doporučeno)",
@@ -213,6 +220,7 @@
213
220
  "ui.run.stop.resets": " (obnoví se {at})",
214
221
  "ui.run.stop.auth": "agent není přihlášený – jednou spustit {command} a přihlásit se",
215
222
  "ui.run.stop.failed": "agent selhal: {detail}",
223
+ "ui.run.stop.model": "{agent} nepřijímá model {model} – zvolit jiný: strom config set model.lead <model> (nebo pro jeden běh: strom run --model <model>)",
216
224
  "ui.run.finishing": "■ Ctrl-C: sezení {session} dostalo žádost skončit – agent zapíše, co našel, a sezení uzavře, pak běh skončí (další sezení nezačne). Další Ctrl-C ho zastaví hned.",
217
225
  "ui.run.stopping": "■ zastavuji – sezení se zavře a jeho úkol se vrátí do fronty; další Ctrl-C skončí hned",
218
226
  "ui.run.gone": "· {session} zavřeno – jeho běh se zastavil{back}",
@@ -356,6 +364,9 @@
356
364
  "review.conflicts.what": "Rozhodnout, co zůstalo otevřené u osoby {name}: {list}{more}",
357
365
  "review.conflicts.why": "rozpory mezi prameny a soupeřící hypotézy čekají na rozhodnutí ({research})",
358
366
  "review.conflicts.done": "každý vyřešený nebo rozhodnutý se zdůvodněním (strom conflict resolve, strom hypothesis decide) – nebo úkol říká, co by rozhodlo",
367
+ "hypothesis.links.what": "Doplnit, co by spojovaly verze hypotéz {list}",
368
+ "hypothesis.links.why": "tyto hypotézy jmenují své osoby jen slovy svých verzí; aplikace Strom ukáže, co by verze spojila – i kde strom končí – jen z jejích vazeb",
369
+ "hypothesis.links.done": "každá verze přečtená (strom hypothesis show {first} …) a řečeno, co by spojila: strom hypothesis link H… <písmeno> --child P… --of F… (nebo --parents P… [P…], --same, --partners, --siblings); verze, která nic jasného nespojuje, zůstane bez vazeb",
359
370
  "review.marriage.what": "Sňatek: {name} a {partner}{at} (před prvním dítětem, {year})",
360
371
  "review.marriage.why": "jejich děti jsou zapsané, jejich sňatek ne; zápis dá rodiče obou, věk a domy ({research})",
361
372
  "review.marriage.done": "zápis nalezen a zapsán i se svědky – nebo roky {years} prohledány bez něj",
@@ -597,6 +608,7 @@
597
608
  "ui.stats.sessions": "Sezení agenta: {n} · poslední {last}",
598
609
  "ui.stats.cost": " · dosavadní cena {cost}",
599
610
  "ui.stats.cost.partial": " (bez přerušených sezení, která cenu neřekla: {n})",
611
+ "ui.stats.cost.readers": ", z toho čtenáři skenů {cost}",
600
612
  "ui.stats.stories": "Vyprávění: {n}",
601
613
  "ui.status.data": "Osoby: {persons} · rodiny: {families}",
602
614
  "ui.status.directions": " · směry výzkumu: {n}",
@@ -995,6 +1007,7 @@
995
1007
  "ui.archives.agentbrowser.on": "Dát agentovi prohlížeč v každém sezení?",
996
1008
  "ui.archives.agentbrowser.off": "Vrátit prohlížeč jen pro archivy přes prohlížeč?",
997
1009
  "ui.consent.remote": "Spouštět sezení Claude Code, která strom otevírá, se vzdáleným ovládáním (Remote Control), aby je šlo sledovat a řídit z claude.ai nebo z aplikace Claude v telefonu?",
1010
+ "ui.consent.addons": "Dovolit agentovi při samostatné práci načíst osobní doplňky z jeho nastavení (skilly, pluginy, MCP servery – třeba i poštu nebo dokumenty) jako v chatu? Pracuje přitom bez dozoru.",
998
1011
  "ui.consent.edits.user": "Mají mít úpravy v aplikaci Strom přednost před tím, co říká záznam? Údaj ze záznamu se pak stáhne s důvodem (synchronizace jde vrátit).",
999
1012
  "ui.consent.review.off": "Zapisovat, co pošle aplikace Strom, hned, bez ptaní u každého poslání? Přidané se zapíše, vodítka se opraví, změna údaje doloženého záznamem bude rozpor k rozhodnutí; strom sync undo poslání vrátí.",
1000
1013
  "ui.consent.mode.archive": "Udělat z výzkumu „{name}“ archiv? Data přicházejí z aplikace Strom a zapisují se, jak přijdou; žádný agent na něm nepracuje, jeho úkoly počkají odložené. Práci s agentem jde zase zapnout.",
@@ -1336,6 +1349,13 @@
1336
1349
  "ui.dirs.active.now": "▶ „{name}“ zase běží: jeho úkoly ({n}) jsou zpátky ve frontě.",
1337
1350
  "ui.research.resume": "Takový výzkum už je, {state}: „{name}“. Spustit ho znovu?",
1338
1351
  "ui.alone.gate.bad": "Čekám počet bodů od 0 do 100 (třeba 10).",
1352
+ "ui.alone.addons": "Bez osobních doplňků agenta (skilly, pluginy, MCP servery): {state}",
1353
+ "ui.alone.addons.about": "Agent při samostatné práci načte jen nástroje výzkumu. Spojení s aplikací Strom a prohlížeč fungují dál. Chat s agentem doplňky načítá vždy.",
1354
+ "ui.alone.addons.on": "Zapnout?",
1355
+ "ui.alone.addons.off": "Vypnout? (agent při samostatné práci pak načte i osobní doplňky)",
1356
+ "ui.setup.addons": "Bez osobních doplňků agenta (skilly, pluginy, MCP servery)?",
1357
+ "ui.setup.addons.yes": "Ano (doporučeno)",
1358
+ "ui.setup.addons.no": "Ne – i při samostatné práci s doplňky",
1339
1359
  "ui.waiting.long": "Na odpověď sem je to dlouhé (nejvýš {n} znaků) – stačí odkaz nebo věta, zbytek stačí říct agentovi v rozhovoru.",
1340
1360
  "ui.settings.remote.chats": "rozhovory ano, samostatná práce ne",
1341
1361
  "ui.settings.remote.own": "Rozhovory s agentem už z telefonu sledovat jde – zapíná to sám Claude Code (jeho vlastní nastavení). Samostatnou práci agenta (volba 2 v menu) až po zapnutí i tohohle.",
@@ -130,6 +130,13 @@
130
130
  "ui.setup.model.opus": "Opus – liest alte Handschriften am besten (empfohlen)",
131
131
  "ui.setup.model.sonnet": "Sonnet – schneller; gedruckte Dokumente und alltägliche Arbeit",
132
132
  "ui.setup.model.own": "Dem Agenten überlassen",
133
+ "ui.setup.model.agent": "Welches Modell soll in {agent} die Forschung machen? Die stärksten lesen alte Handschriften am besten – sie verbrauchen ein Abo schneller oder kosten mehr.",
134
+ "ui.setup.model.recommended": "{model} (empfohlen)",
135
+ "ui.setup.model.own.is": "Dem Agenten überlassen (jetzt {model})",
136
+ "ui.setup.model.now": "{model} (jetzt gewählt)",
137
+ "ui.chat.app.model.chosen": "In {app} für die Forschung das Modell {model} wählen (das in den Einstellungen gewählte).",
138
+ "ui.settings.model": "Modell der Forschung ({agent}): {model}",
139
+ "ui.settings.model.own": "das eigene des Agenten",
133
140
  "ui.setup.level": "Was darf der Agent tun, ohne nachzufragen?",
134
141
  "ui.setup.level.ask": "Mich bei allem außerhalb der Forschung fragen",
135
142
  "ui.setup.level.auto": "Selbstständig arbeiten – nur bei riskanten Schritten fragen (empfohlen)",
@@ -213,6 +220,7 @@
213
220
  "ui.run.stop.resets": " (erneuert sich {at})",
214
221
  "ui.run.stop.auth": "der Agent ist nicht angemeldet – einmal {command} starten und anmelden",
215
222
  "ui.run.stop.failed": "der Agent ist gescheitert: {detail}",
223
+ "ui.run.stop.model": "{agent} nimmt das Modell {model} nicht an – ein anderes wählen: strom config set model.lead <model> (oder für einen Lauf: strom run --model <model>)",
216
224
  "ui.run.finishing": "■ Strg-C: Sitzung {session} ist gebeten aufzuhören – der Agent schreibt auf, was er gefunden hat, und schließt sie, dann endet der Lauf (keine nächste Sitzung). Noch einmal Strg-C stoppt ihn sofort.",
217
225
  "ui.run.stopping": "■ wird angehalten – die Sitzung wird geschlossen und ihre Aufgabe kehrt in die Warteschlange zurück; nochmals Strg-C beendet sofort",
218
226
  "ui.run.gone": "· {session} geschlossen – ihr Lauf war abgebrochen{back}",
@@ -356,6 +364,9 @@
356
364
  "review.conflicts.what": "Entscheiden, was bei {name} offen geblieben ist: {list}{more}",
357
365
  "review.conflicts.why": "Widersprüche zwischen Quellen und konkurrierende Hypothesen warten auf eine Entscheidung ({research})",
358
366
  "review.conflicts.done": "jeder gelöst oder entschieden mit Begründung (strom conflict resolve, strom hypothesis decide) – oder eine Aufgabe sagt, was entscheiden würde",
367
+ "hypothesis.links.what": "Angeben, was die Varianten von {list} verbinden würden",
368
+ "hypothesis.links.why": "diese Hypothesen nennen ihre Personen nur in den Worten ihrer Varianten; die Strom-App zeigt, was eine Variante verbinden würde – und wo der Stammbaum endet – nur aus ihren Verknüpfungen",
369
+ "hypothesis.links.done": "jede Variante gelesen (strom hypothesis show {first} …) und angegeben, was sie verbinden würde: strom hypothesis link H… <Buchstabe> --child P… --of F… (oder --parents P… [P…], --same, --partners, --siblings); eine Variante, die nichts Klares verbindet, bleibt ohne Verknüpfungen",
359
370
  "review.marriage.what": "Heirat: {name} und {partner}{at} (vor dem ersten Kind, {year})",
360
371
  "review.marriage.why": "ihre Kinder sind erfasst, ihre Heirat nicht; der Eintrag nennt die Eltern beider, Alter und Häuser ({research})",
361
372
  "review.marriage.done": "der Eintrag gefunden und mit den Zeugen erfasst – oder {years} ohne ihn durchsucht",
@@ -597,6 +608,7 @@
597
608
  "ui.stats.sessions": "Sitzungen des Agenten: {n} · die letzte {last}",
598
609
  "ui.stats.cost": " · Kosten bisher {cost}",
599
610
  "ui.stats.cost.partial": " (ohne abgebrochene Sitzungen, die ihre Kosten nicht nannten: {n})",
611
+ "ui.stats.cost.readers": ", davon Leser der Scans {cost}",
600
612
  "ui.stats.stories": "Geschichten: {n}",
601
613
  "ui.status.data": "Personen: {persons} · Familien: {families}",
602
614
  "ui.status.directions": " · Richtungen der Forschung: {n}",
@@ -995,6 +1007,7 @@
995
1007
  "ui.archives.agentbrowser.on": "Dem Agenten den Browser in jeder Sitzung geben?",
996
1008
  "ui.archives.agentbrowser.off": "Zurück zum Browser nur für Archive über den Browser?",
997
1009
  "ui.consent.remote": "Die Claude-Code-Sitzungen, die strom öffnet, mit Remote Control starten, damit sie sich auf claude.ai oder in der Claude-App auf dem Telefon verfolgen und steuern lassen?",
1010
+ "ui.consent.addons": "Dem allein arbeitenden Agenten erlauben, die persönlichen Erweiterungen aus seinen Einstellungen zu laden (Skills, Plugins, MCP-Server – auch E-Mail oder Dokumente), wie im Chat? Er arbeitet dabei unbeaufsichtigt.",
998
1011
  "ui.consent.edits.user": "Sollen eigene Änderungen in der Strom-App vor dem gelten, was ein Eintrag sagt? Die Angabe aus dem Eintrag wird dann mit Begründung zurückgezogen (ein Abgleich lässt sich zurücknehmen).",
999
1012
  "ui.consent.review.off": "Was die Strom-App sendet, gleich übernehmen, ohne bei jeder Sendung zu fragen? Hinzugefügtes wird übernommen, Hinweise korrigiert, eine Änderung einer durch einen Eintrag belegten Angabe wird ein Widerspruch zur Entscheidung; strom sync undo nimmt eine Sendung zurück.",
1000
1013
  "ui.consent.mode.archive": "Die Forschung „{name}“ zu einem Archiv machen? Die Daten kommen aus der Strom-App und werden übernommen, wie sie gesendet werden; kein Agent arbeitet daran, ihre Aufgaben warten zurückgestellt. Die Arbeit mit einem Agenten lässt sich wieder einschalten.",
@@ -1336,6 +1349,13 @@
1336
1349
  "ui.dirs.active.now": "▶ „{name}“ läuft wieder: ihre Aufgaben ({n}) sind zurück in der Warteschlange.",
1337
1350
  "ui.research.resume": "Diese Forschung gibt es schon, {state}: „{name}“. Wieder aufnehmen?",
1338
1351
  "ui.alone.gate.bad": "Bitte eine Zahl von Punkten zwischen 0 und 100 (z. B. 10).",
1352
+ "ui.alone.addons": "Ohne die persönlichen Erweiterungen des Agenten (Skills, Plugins, MCP-Server): {state}",
1353
+ "ui.alone.addons.about": "Allein arbeitend lädt der Agent nur die Werkzeuge der Forschung. Die Verbindung zur Strom-App und der Browser funktionieren weiter. Im Chat mit dem Agenten werden die Erweiterungen immer geladen.",
1354
+ "ui.alone.addons.on": "Einschalten?",
1355
+ "ui.alone.addons.off": "Ausschalten? (der allein arbeitende Agent lädt dann auch die persönlichen Erweiterungen)",
1356
+ "ui.setup.addons": "Ohne die persönlichen Erweiterungen des Agenten (Skills, Plugins, MCP-Server)?",
1357
+ "ui.setup.addons.yes": "Ja (empfohlen)",
1358
+ "ui.setup.addons.no": "Nein – auch allein arbeitend mit den Erweiterungen",
1339
1359
  "ui.waiting.long": "Das ist für eine Antwort hier zu lang (höchstens {n} Zeichen) – ein Link oder ein Satz; das Übrige dem Agenten im Gespräch sagen.",
1340
1360
  "ui.settings.remote.chats": "Gespräche ja, allein arbeiten nein",
1341
1361
  "ui.settings.remote.own": "Gespräche mit dem Agenten lassen sich schon vom Telefon verfolgen – das macht Claude Code selbst (seine eigene Einstellung). Die Arbeit des Agenten allein (Punkt 2 im Menü) erst, wenn auch dies eingeschaltet ist.",
@@ -20,7 +20,9 @@ conclusion so the next researcher can follow it.
20
20
  - **Negative results are results.** Every search is recorded (`strom search add
21
21
  … --result negative`), with exactly what was covered. The most expensive
22
22
  mistake is to search the same book twice.
23
- - **Check the premise first.** Before opening anything: `strom searched <where>`.
23
+ - **Check the premise first.** Before opening anything, read the brief's
24
+ *Already known* — the searches of the task's books, people and surnames;
25
+ `strom searched <where>` only for a book, place or years it does not list.
24
26
  - **Extract everything the first time** you open a record: names, ages, house
25
27
  numbers, occupations, godparents, witnesses, midwife, remarks in the margin.
26
28
  Opening the same page again later costs more than writing it down now.
@@ -3,8 +3,9 @@
3
3
  An input is material the user gave you: scans, photos, documents, notes, a
4
4
  family tree. Your job is to turn it into evidence without inventing anything.
5
5
 
6
- 1. `strom input show I…` — read the file (images and PDFs directly, text is
7
- shown). Identify who it is about and what kind of document it is.
6
+ 1. The brief's *The material* says what each input is, where its file is, and
7
+ shows its text: read the file (images and PDFs directly). Identify who it
8
+ is about and what kind of document it is.
8
9
  2. **What is it?** An original civil or church document (birth, marriage or
9
10
  death certificate, extract from a register, military papers) is a **source**
10
11
  and can prove facts: `strom source add … --input I… --form original
@@ -14,7 +15,7 @@ family tree. Your job is to turn it into evidence without inventing anything.
14
15
  `strom source add "<what it is>" --kind family-memory --form authored
15
16
  --information secondary --input I…`; a family tree is `--kind family-tree`.
16
17
  Many facts from one input: one `strom batch --file notes/<input>.txt`
17
- (try it with `--dry-run` first).
18
+ (all or nothing: it says every failing line at once, nothing written).
18
19
  3. Record every person and every fact it states, with a citation to the source
19
20
  (`--cite S… --locator "…"`). Use the words of the record in `--quote`.
20
21
  4. Put names and places as they are written; normalise only dates.
@@ -26,8 +27,9 @@ family tree. Your job is to turn it into evidence without inventing anything.
26
27
  the tree says differently stays a lead or becomes a conflict, never a fact.
27
28
  6. Create the next tasks: where would the records that prove these leads be?
28
29
  (`locate` if the archive or book is unknown, `link` if it is known.)
29
- 7. Close the task with what came out of it (`strom task done T… --result "…"`;
30
- its input is marked processed with it).
30
+ 7. Close the task with what came out of it (`strom session close --done "…"
31
+ --next "…"`: the task and the session in one call; its input is marked
32
+ processed with it).
31
33
 
32
34
  ## A batch from the Strom app (many files at once)
33
35
 
@@ -36,9 +38,10 @@ lists about 25 of its files, with the path each had there
36
38
  (`Babička/Dopisy/1946.jpg` — the folders say much). They are kept unchanged
37
39
  outside the tree's history: what you do not record of them stays private.
38
40
 
39
- 1. **Look over all of them first, cheaply**: `strom input show I…` for each —
40
- the name, the path, the type; images in a small view (`strom media view
41
- I… --grid`), not read whole. Sort each at once: `strom input sort I… [I…]
41
+ 1. **Look over all of them first, cheaply**: the brief lists each — the
42
+ name, the path, the type; images in a small view (`strom media view
43
+ I… I… I… --grid`: several in one call, opened together), not read
44
+ whole. Sort each at once: `strom input sort I… [I…]
42
45
  --as source|document|photo|unrelated --person P…`. Read in depth only what
43
46
  belongs to the people of the research.
44
47
  2. **Not of the family** (a tax form, a holiday photo of strangers, the same
@@ -1,6 +1,7 @@
1
1
  # Method: proving a link (parents, marriage)
2
2
 
3
- 1. **Premise:** `strom searched B… --years …` and `strom task show T…` — if the
3
+ 1. **Premise:** the brief's *Already known* and the task's notes (`strom
4
+ searched B… --years …` only for a book or years it does not list) — if the
4
5
  range was already covered completely, do not repeat it; say so and close.
5
6
  2. **Index before book.** Use an index to find the page, then read the entry in
6
7
  the register itself. An index entry alone is a lead — and so is its absence:
@@ -7,12 +7,20 @@ search depends on** (a surname, a birthplace, a parish) is read again on a
7
7
  crop at full resolution before it is recorded; what stays unsure is recorded
8
8
  as unsure and said so.
9
9
 
10
- An image you open stays in your context and is paid for again on every turn.
11
- Look at as few pixels as the question needs, and write down what you saw at once.
10
+ Every image you open stays in your context to the end of the session: look at
11
+ as few pixels as the question needs, write down what you saw at once, and after
12
+ about 80 views write down what you found before you open more.
12
13
 
13
14
  - **Look only through views**: `strom media view B0001:57` (record set and
14
15
  image number) makes a file in `.strom/views/` — open that file. Whole images
15
16
  come reduced: good for "is our surname on this page?", not for reading.
17
+ - **The views of a scan, or of a batch, in one call**, then open all the files
18
+ it lists together — not one call and one look per view:
19
+ `strom media view B0001:57-60 --half both` (the two pages of each double
20
+ page, where they are sharper than one view of it), `--crop … --crop …`
21
+ (several entries of one image), `--split 2x2` (a page in overlapping parts),
22
+ `strom media view B0001 --page 112 113`. Each file comes with its image, page
23
+ and `--clip`. At most 12 images and 24 views in one call.
16
24
  - **Find, then crop.** `--grid` overlays tenths with labels; read off where the
17
25
  entry is and ask for exactly that part: `--crop 0.05,0.40,0.45,0.18` (x, y,
18
26
  width, height as fractions) or `--half left|right` for one page of a spread.
@@ -24,8 +32,15 @@ Look at as few pixels as the question needs, and write down what you saw at once
24
32
  …` (one request). Then view the same crop again: it comes from the sharper
25
33
  part by itself.
26
34
  - **Browsing a book is a reader's job**: `strom read B0001 --images 40-69
27
- --question "…"` — readers in batches of ten, each with the full question;
28
- their reports stay in notes/readings/ for the next session, you get the finds.
35
+ --question "…"` — readers in batches of ten, each with the full question.
36
+ Their reports stay in notes/readings/; `strom read` ends with what they
37
+ found, compact: the found and unclear entries whole, what was illegible
38
+ where nothing was found (a possible match often hides there — look at it),
39
+ and the gaps in the book. Later, or in the next session: `strom readings`
40
+ (`--match <surname>` across all readings, `--list`); one image's whole
41
+ block: `strom readings B0001 --image 57`. Do not open the reports whole —
42
+ they are many times longer. Images read before are said before they are
43
+ read again: another question or a blind check is fine, the same one is not.
29
44
  It waits for its readers (often ten minutes or more): run it in the
30
45
  foreground and let it finish — your session ends with your turn, and
31
46
  whatever is left running in the background ends with it.
@@ -42,7 +57,10 @@ Look at as few pixels as the question needs, and write down what you saw at once
42
57
  - **Cite the image, and where the entry is on it**: `strom source add …
43
58
  --clip B0001:57@0.05,0.40,0.45,0.18 --locator "pag. 112, 2nd entry"` — the
44
59
  crop you read it in, as its view prints it; a searched range goes in
45
- `strom search add … --pages 40-69`.
60
+ `strom search add … --pages 40-69`. Pages missing from the book (the
61
+ numbering jumps) were not searched: say so in `--note` (strom read puts the
62
+ gaps its readers saw there), or leave them out of the range
63
+ (`--pages 40-52,55-69`).
46
64
  - Page ↔ image: `strom recordset calibrate B0001 --point 57=112` (measured on
47
65
  the image, never guessed); then `strom media view B0001 --page 112` works.
48
66
  - **No images here yet:** a connector for that archive
@@ -1,8 +1,9 @@
1
1
  # Method: recording an entry
2
2
 
3
- Record a found entry in one batch: write the lines to a file in notes/, run
4
- `strom batch --file notes/<file> --dry-run`, fix what it reports, then run it
5
- without --dry-run. `#name` labels what a line creates, `@name` uses it later.
3
+ Record a found entry in one batch: write the lines to a file in notes/ and run
4
+ `strom batch --file notes/<file>` — all or nothing: when a line fails nothing is
5
+ written and every failing line is said at once; fix them, run it again.
6
+ `#name` labels what a line creates, `@name` uses it later.
6
7
 
7
8
  source add "Baptism of Jan Novák 1885" --kind baptism --recordset B0001 --clip B0001:57@0.05,0.40,0.45,0.18 --locator "pag. 112, entry 2" --language la --information primary --transcript @notes/entry.txt #s
8
9
  event add P0001 CHR --date "25 JUN 1885" --place "Týnec" --house 13 --cite @s --quote "baptizatus est" --with "godparent:Marie Dvořáková" --with "midwife:Anna Nová" --with "officiant:P. Josef Kříž" --status proven
@@ -6,7 +6,8 @@ first name) needs a second, independent reading.
6
6
  - The second reader must be **blind**: do not tell them the expected value, the
7
7
  name, the place or why it matters. Ask only what is written in that cell.
8
8
  - One cell, one second reader — not the whole page again.
9
- - Before asking, check whether two readings already exist (`strom find`).
9
+ - Before asking, check whether two readings already exist (`strom find`; what
10
+ readers wrote of an image: `strom readings B0001 --image 57`).
10
11
  - Certainty above "probable" only when two independent readings agree. When
11
12
  they disagree, record a conflict with both readings.
12
13
  - An independent reading of an entry you cited: `strom read M… --blind --crop
@@ -30,9 +30,10 @@ the researcher; \`strom\` is your only way to read and change the research.
30
30
  1. Start with \`strom\` — it says where things stand and what to do next.
31
31
  \`strom guide\` explains the work; \`strom help <command>\` any command.
32
32
  2. Work in sessions: \`strom session start\` gives you the brief for the next
33
- task; finish with \`strom session close --summary "…" --next "…"\`. The
34
- next step is strom's queue (\`strom\` names it, \`strom plan\` lists it):
35
- what the user wants instead becomes a task first — never search outside a
33
+ task; finish with \`strom session close --done "<result>" --next "…"\`
34
+ (the task and the session in one call). The next step is strom's queue
35
+ (\`strom\` names it, \`strom plan\` lists it): what the user wants
36
+ instead becomes a task first — never search outside a
36
37
  session and its task.
37
38
  3. Never create, edit or delete anything in \`data/\`, \`strom.json\` or \`.git\`,
38
39
  never read the files in \`data/\` (strom shows them better and shorter), and
@@ -45,7 +46,8 @@ the researcher; \`strom\` is your only way to read and change the research.
45
46
  terminal — never do it yourself.
46
47
  6. You may read \`inputs/\`, \`output/\`, \`notes/\` and \`.strom/views/\`, and write
47
48
  your own working notes in \`notes/\`. Scans are looked at through views:
48
- \`strom media view B0001:57 --half left\` writes one to \`.strom/views/\`.
49
+ \`strom media view B0001:57 --half left\` writes one to \`.strom/views/\`;
50
+ the views of a scan or a batch come in one call (\`B0001:57-60 --half both\`).
49
51
  7. Run strom commands on their own — no pipes (\`| head\`, \`| grep\`): output is
50
52
  already short, listings take \`--limit\` and \`--page\`, and piped commands may
51
53
  be refused by your permissions.
@@ -0,0 +1,54 @@
1
+ // How big an image each agent's model takes in whole. An agent shrinks a bigger image itself before its model sees
2
+ // it (and pays for the pixels it threw away in a slower read), so strom makes views no bigger than that:
3
+ // Claude Code ≤ 2000 px for Opus 4.7+, Sonnet 5+, Fable and Mythos (the image limit of their model catalogue;
4
+ // older models — Opus/Sonnet 4.6, Haiku — take 1568 on the API side)
5
+ // Codex ≤ 2048 px and about 2.5 megapixels at its default detail
6
+ // OpenCode ≤ 2000×2000 by default (attachment.image), the provider's model limit beyond it
7
+ // Grok Build shrinks to a size it does not say; Antigravity counts tokens per image, not pixels
8
+ // Unknown: 1568 px, what every vision model takes without shrinking it again.
9
+ /** The long side of a view every vision model takes in whole. */
10
+ export const IMAGE_MAX = 1568;
11
+ /** What the newer models take in whole, and Claude Code, Codex and OpenCode pass on unshrunk. */
12
+ export const IMAGE_MAX_LARGE = 2000;
13
+ /** A Claude model name or alias ("opus", "claude-opus-4-7", "claude-sonnet-5-5[1m]", "us.anthropic.claude-…"): its family and version. */
14
+ export function claudeModel(model) {
15
+ const m = /(opus|sonnet|haiku|fable|mythos)(?:[-.]?(\d{1,2})(?:[-.](\d{1,2})(?!\d))?(?!\d))?/.exec(model.toLowerCase());
16
+ if (!m)
17
+ return undefined;
18
+ // "claude-3-5-sonnet-…": the version before the family (an old model)
19
+ const before = /(\d{1,2})(?:[-.](\d{1,2}))?[-.](?:opus|sonnet|haiku)/.exec(model.toLowerCase());
20
+ if (!m[2] && before)
21
+ return { family: m[1], version: Number(before[1]) + Number(before[2] ?? 0) / 10 };
22
+ return { family: m[1], ...(m[2] ? { version: Number(m[2]) + Number(m[3] ?? 0) / 10 } : {}) };
23
+ }
24
+ /**
25
+ * The longest side of a view for this agent and model (the model strom starts it with; undefined = the agent's own
26
+ * default). Claude Code's own default is a model of the current line (Opus or Sonnet 5): 2000. A model given as a
27
+ * family alone ("opus", "sonnet", "fable") is that family's newest — haiku stays at 1568 (not among the larger ones).
28
+ */
29
+ export function imageMax(agent, model) {
30
+ const m = model?.trim() || undefined;
31
+ switch (agent) {
32
+ case "claude": {
33
+ if (!m || m === "default" || m === "best")
34
+ return IMAGE_MAX_LARGE;
35
+ const c = claudeModel(m);
36
+ if (!c)
37
+ return IMAGE_MAX;
38
+ if (c.family === "fable" || c.family === "mythos")
39
+ return IMAGE_MAX_LARGE;
40
+ if (c.version === undefined)
41
+ return c.family === "haiku" ? IMAGE_MAX : IMAGE_MAX_LARGE;
42
+ if (c.family === "opus")
43
+ return c.version >= 4.7 ? IMAGE_MAX_LARGE : IMAGE_MAX;
44
+ if (c.family === "sonnet")
45
+ return c.version >= 5 ? IMAGE_MAX_LARGE : IMAGE_MAX;
46
+ return IMAGE_MAX;
47
+ }
48
+ case "codex":
49
+ case "opencode":
50
+ return IMAGE_MAX_LARGE;
51
+ default:
52
+ return IMAGE_MAX;
53
+ }
54
+ }
@@ -0,0 +1,248 @@
1
+ // The strong models an agent can do the research with — offered in the setup wizard and the settings, the person
2
+ // picks one (or leaves it to the agent). Where the agent lists its models without a prompt (Antigravity's `agy
3
+ // models`, OpenCode's `opencode models`, Grok Build's `grok models`, Codex's `codex debug models`: its catalog for
4
+ // the account logged in), the strong ones are taken from that list; a list that cannot be had in a few seconds (offline,
5
+ // not logged in, another version) leaves a small built-in one. Nothing here runs a prompt or costs anything.
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { userHome } from "../core/paths.js";
9
+ import { findAgent, withoutAgentMarks } from "../core/which.js";
10
+ import { spawnAgent, stopTree } from "../runners/runner.js";
11
+ import { PROFILES } from "./profiles.js";
12
+ /** Claude Code: its aliases (always the newest of each line). */
13
+ const CLAUDE = [{ id: "opus" }, { id: "sonnet" }];
14
+ /**
15
+ * When the agent's own list cannot be had: strong models each agent took when this was written (Codex with a ChatGPT
16
+ * account: gpt-6-sol and gpt-6-astra are in its catalog, gpt-6.1-sol is refused; Antigravity: Gemini 3.1 Pro, Claude
17
+ * Opus 4.6). A model it no longer takes is said by strom run (the agent refuses it).
18
+ */
19
+ export const BUILTIN_MODELS = {
20
+ claude: CLAUDE,
21
+ codex: [{ id: "gpt-6-astra" }, { id: "gpt-6-sol" }],
22
+ antigravity: [
23
+ { id: "gemini-3.1-pro-high", label: "Gemini 3.1 Pro (High)" },
24
+ { id: "claude-opus-4-6-thinking", label: "Claude Opus 4.6 (Thinking)" },
25
+ ],
26
+ opencode: [{ id: "openai/gpt-6-astra" }, { id: "openai/gpt-6-sol" }],
27
+ grok: [{ id: "grok-4.7" }, { id: "grok-4.6" }],
28
+ };
29
+ /** How each agent lists its models (none: Claude Code — its aliases are the list). */
30
+ const LIST = {
31
+ codex: ["debug", "models"],
32
+ antigravity: ["models"],
33
+ opencode: ["models"],
34
+ grok: ["models"],
35
+ };
36
+ /** Words of a model's name (or its maker's description) that say it is the fast, cheap or small one. */
37
+ const WEAK = new Set([
38
+ "flash", "mini", "nano", "haiku", "lite", "light", "small", "tiny", "instant", "fast", "ultrafast", "spark", "free", "oss",
39
+ // a lower reasoning effort of a model (Antigravity: gemini-3.1-pro-low)
40
+ "low", "medium",
41
+ // OpenAI's lighter lines beside sol and astra
42
+ "luna", "terra",
43
+ "review", "auto",
44
+ ]);
45
+ /** Words of a maker's description of a lighter or older model (Codex's catalog). */
46
+ const WEAK_ABOUT = new Set(["affordable", "easier", "efficient", "legacy", "older", "cheap", "lightweight", "balanced", "fast"]);
47
+ /** The strongest line of a maker, then the one below it. */
48
+ const TOP = new Set(["opus", "pro", "ultra", "astra", "max"]);
49
+ const HIGH = new Set(["sonnet", "sol"]);
50
+ function words(s) {
51
+ return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
52
+ }
53
+ /** The version numbers in a model's name: gpt-6.1-sol → [6, 1], claude-opus-4-6 → [4, 6]. */
54
+ function version(id) {
55
+ const m = /\p{N}+(?:[.-]\p{N}+)*/u.exec(id.slice(id.lastIndexOf("/") + 1));
56
+ return m ? m[0].split(/[.-]/).map(Number) : [];
57
+ }
58
+ function newer(a, b) {
59
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
60
+ const d = (b[i] ?? -1) - (a[i] ?? -1);
61
+ if (d)
62
+ return d;
63
+ }
64
+ return 0;
65
+ }
66
+ /** The maker's line a model belongs to: openai/gpt-6-sol → openai/gpt, claude-opus-4-6 → claude. */
67
+ function family(id) {
68
+ const slash = id.lastIndexOf("/");
69
+ return id.slice(0, slash + 1) + (words(id.slice(slash + 1))[0] ?? "");
70
+ }
71
+ /**
72
+ * The strong models of a list, in the order to offer them: never a fast, small or cheap one; the strongest line of a
73
+ * maker first (Opus, Pro, Astra), then the one below (Sonnet, Sol), then the rest; makers in the order the agent lists
74
+ * them (its own first), the newest version first, at most two of one line, three in all.
75
+ */
76
+ export function strongModels(list, max = 3) {
77
+ const kept = list.filter((m) => !words(m.id).some((w) => WEAK.has(w)) && !words(m.label ?? "").some((w) => WEAK.has(w)) && !words(m.about ?? "").some((w) => WEAK_ABOUT.has(w)));
78
+ const tier = (m) => {
79
+ const w = [...words(m.id), ...words(m.label ?? "")];
80
+ return w.some((x) => TOP.has(x)) ? 2 : w.some((x) => HIGH.has(x)) ? 1 : 0;
81
+ };
82
+ const families = [];
83
+ for (const m of kept)
84
+ if (!families.includes(family(m.id)))
85
+ families.push(family(m.id));
86
+ const sorted = kept
87
+ .map((m, i) => ({ m, i, tier: tier(m), fam: families.indexOf(family(m.id)), v: version(m.id) }))
88
+ .sort((a, b) => b.tier - a.tier || a.fam - b.fam || newer(a.v, b.v) || a.i - b.i);
89
+ const out = [];
90
+ const perLine = new Map();
91
+ for (const { m, tier: t } of sorted) {
92
+ const line = `${family(m.id)}|${t}`;
93
+ if ((perLine.get(line) ?? 0) >= 2 || out.some((o) => o.id === m.id))
94
+ continue;
95
+ perLine.set(line, (perLine.get(line) ?? 0) + 1);
96
+ out.push(m.label ? { id: m.id, label: m.label } : { id: m.id });
97
+ if (out.length >= max)
98
+ break;
99
+ }
100
+ return out;
101
+ }
102
+ /** `agy models`: "<id>\t<name>" a line (after a line that says it fetches). */
103
+ export function parseAntigravity(text) {
104
+ return text
105
+ .split(/\r?\n/)
106
+ .map((l) => l.split("\t"))
107
+ .filter((p) => p.length >= 2 && p[0].trim() && !/\s/.test(p[0].trim()))
108
+ .map((p) => ({ id: p[0].trim(), label: p[1].trim() }));
109
+ }
110
+ /** `opencode models`: "<provider>/<model>" a line. */
111
+ export function parseOpencode(text) {
112
+ return text
113
+ .split(/\r?\n/)
114
+ .map((l) => l.trim())
115
+ .filter((l) => /^[^\s/]+\/\S+$/.test(l))
116
+ .map((id) => ({ id }));
117
+ }
118
+ /** `grok models`: " * <id> (default)" / " - <id>", and "Default model: <id>". */
119
+ export function parseGrok(text) {
120
+ const models = [];
121
+ let agentDefault;
122
+ for (const l of text.split(/\r?\n/)) {
123
+ const m = /^\s*[*-]\s+(\S+)(\s+\(default\))?\s*$/.exec(l);
124
+ if (m) {
125
+ models.push({ id: m[1] });
126
+ if (m[2])
127
+ agentDefault = m[1];
128
+ }
129
+ const d = /^\s*Default model:\s*(\S+)/i.exec(l);
130
+ if (d)
131
+ agentDefault = d[1];
132
+ }
133
+ return { models, ...(agentDefault ? { agentDefault } : {}) };
134
+ }
135
+ /** `codex debug models` (or its models_cache.json): the catalog for the account, hidden ones left out, in its order. */
136
+ export function parseCodex(text) {
137
+ let data;
138
+ try {
139
+ data = JSON.parse(text);
140
+ }
141
+ catch {
142
+ return [];
143
+ }
144
+ const list = data?.models;
145
+ if (!Array.isArray(list))
146
+ return [];
147
+ return list
148
+ .filter((m) => typeof m?.slug === "string")
149
+ .filter((m) => m.visibility !== "hide")
150
+ .sort((a, b) => (a.priority ?? 999) - (b.priority ?? 999))
151
+ .map((m) => ({ id: m.slug, ...(m.display_name ? { label: m.display_name } : {}), ...(m.description ? { about: m.description } : {}) }));
152
+ }
153
+ /** The model Codex takes when given none: `model = "…"` of its config.toml (before any [section]). */
154
+ export function codexConfigModel(env) {
155
+ const dir = env.CODEX_HOME || path.join(userHome(env), ".codex");
156
+ let text;
157
+ try {
158
+ text = fs.readFileSync(path.join(dir, "config.toml"), "utf8");
159
+ }
160
+ catch {
161
+ return undefined;
162
+ }
163
+ for (const l of text.split(/\r?\n/)) {
164
+ if (/^\s*\[/.test(l))
165
+ break;
166
+ const m = /^\s*model\s*=\s*["']([^"']+)["']/.exec(l);
167
+ if (m)
168
+ return m[1];
169
+ }
170
+ return undefined;
171
+ }
172
+ /** Run an agent's list command: its output, or undefined when it fails or takes longer than `ms`. */
173
+ function listOutput(program, args, env, ms) {
174
+ return new Promise((resolve) => {
175
+ let out = "";
176
+ let done = false;
177
+ const finish = (v) => {
178
+ if (done)
179
+ return;
180
+ done = true;
181
+ clearTimeout(timer);
182
+ resolve(v);
183
+ };
184
+ let child;
185
+ try {
186
+ child = spawnAgent(program, args, { env: { ...withoutAgentMarks(env), NO_COLOR: "1" }, stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
187
+ }
188
+ catch {
189
+ resolve(undefined);
190
+ return;
191
+ }
192
+ const timer = setTimeout(() => {
193
+ stopTree(child);
194
+ finish(undefined);
195
+ }, ms);
196
+ child.stdout?.setEncoding("utf8");
197
+ child.stdout?.on("data", (d) => {
198
+ if (out.length < 4_000_000)
199
+ out += d;
200
+ });
201
+ child.on("error", () => finish(undefined));
202
+ child.on("close", (code) => finish(code === 0 ? out : undefined));
203
+ });
204
+ }
205
+ /**
206
+ * The strong models to offer for an agent: from its own list when it gives one within `ms` (default 6 s; never
207
+ * hanging the wizard), else strom's built-in list.
208
+ */
209
+ export async function modelChoices(agent, env, ms = Number(env.STROM_MODELS_TIMEOUT_MS) || 6000) {
210
+ const builtin = BUILTIN_MODELS[agent] ?? [];
211
+ const configured = agent === "codex" ? codexConfigModel(env) : undefined;
212
+ const fallback = () => ({ models: builtin, source: "builtin", ...(configured ? { agentDefault: configured } : {}) });
213
+ const args = LIST[agent];
214
+ if (!args)
215
+ return fallback();
216
+ const program = findAgent(PROFILES[agent]?.command ?? agent, env);
217
+ if (!program)
218
+ return fallback();
219
+ const text = await listOutput(program, args, env, ms);
220
+ let list = [];
221
+ let agentDefault = configured;
222
+ if (text !== undefined) {
223
+ if (agent === "antigravity")
224
+ list = parseAntigravity(text);
225
+ else if (agent === "opencode")
226
+ list = parseOpencode(text);
227
+ else if (agent === "codex")
228
+ list = parseCodex(text);
229
+ else if (agent === "grok") {
230
+ const g = parseGrok(text);
231
+ list = g.models;
232
+ agentDefault = g.agentDefault;
233
+ }
234
+ }
235
+ // Codex keeps its catalog for the account beside its config: read when the command would not say it
236
+ if (!list.length && agent === "codex") {
237
+ try {
238
+ list = parseCodex(fs.readFileSync(path.join(env.CODEX_HOME || path.join(userHome(env), ".codex"), "models_cache.json"), "utf8"));
239
+ }
240
+ catch {
241
+ // none
242
+ }
243
+ }
244
+ const strong = strongModels(list);
245
+ if (!strong.length)
246
+ return fallback();
247
+ return { models: strong, source: "agent", ...(agentDefault ? { agentDefault } : {}) };
248
+ }