strom-research 1.24.0-beta.2 → 1.25.0-beta.1

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.
package/CHANGELOG.cs.md CHANGED
@@ -5,14 +5,24 @@
5
5
  Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou na
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases) (anglicky).
7
7
 
8
+ ## 1.25.0
9
+
10
+ *Připravuje se: bety do 1.25.0-beta.1. Oddíl bude úplný s vydáním.*
11
+
12
+ - **Data pro widget Strom i na Windows.** Výzkum teď zapisuje to, co z něj ukazuje widget Strom (na čem se pracuje,
13
+ nálezy za posledních 24 hodin, co čeká), i na Windows, do `%USERPROFILE%\.config\strom\widget`, se značkou verze
14
+ strom, která je zapsala. Widget, který zrovna čte svůj soubor, most nikdy nezdrží. `strom uninstall` odebere i soubory
15
+ widgetu, také na Macu; výzkum a nastavení zůstávají.
16
+
8
17
  ## 1.24.0
9
18
 
10
- *Připravuje se: bety do 1.24.0-beta.2. Oddíl bude úplný s vydáním.*
19
+ *31. 10. 2026 · patří k aplikaci Strom 3.19.0.*
11
20
 
12
21
  - **Místa zmapovaná jednou pro všechny výzkumy na tomto počítači.** Místo, které jiný výzkum tady už přiřadil k regionům
13
22
  dějin, se převezme, když jde jistě o totéž místo (stejný název a stejné souřadnice, farnost nebo okres) a všechny
14
23
  výzkumy ho přiřazují stejně; přechází jen přiřazení místa, nic z rodiny. Místo stejného jména, které jistě totéž
15
- není, dostane agent jako návrh; sám přiřazuje jen zbytek. `strom history adopt --dry-run` řekne, co by se převzalo.
24
+ není, dostane agent jako návrh; sám přiřazuje jen zbytek. `strom history adopt --dry-run` řekne, co by se převzalo. Otázka na zapnutí nebo vypnutí událostí
25
+ dějin říká, že platí pro všechny výzkumy na tomto počítači, i jak to změnit jen pro jeden výzkum.
16
26
  - **Opravy výzkumu pro aplikaci Strom.** Most vypíše, co `strom fixes` ve výzkumu nabízí (název, co dělá, kolik
17
27
  položek, stav, jestli jde vrátit), zdarma a bez zápisu; odkaz `strom-research://fixes` otevře opravu v terminálu
18
28
  výzkumu, kde se oprava i její vrácení vždy nejdřív potvrzuje.
@@ -21,12 +31,14 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
21
31
  vynechává, a u pramenů bez výřezu. Jen veřejná webová stránka, nikdy adresa fondu; standardní soubor GEDCOM nechává
22
32
  adresu v poznámce. Datum verze výzkumu v souboru pro Strom nese i čas, takže aplikace ukáže den, kdy vznikla, i kolem
23
33
  půlnoci.
34
+ - **Vlastní rod osoby z aplikace Strom.** Když aplikace Strom u osoby označí, že zakládá vlastní rod nebo rod
35
+ rodiny nenese, `strom sync` to uloží do výzkumu a soubor pro Strom to vrací, takže další odeslání i jiné zařízení
36
+ ukážou stejné rody; sloučení dvou osob značku zachová. Standardní soubor GEDCOM o tom nic neříká. Potřebuje aplikaci
37
+ Strom 3.19.0.
24
38
  - **Číslo domu, věk a období tak, jak byly zapsány.** Slovo pro číslo domu („No.“, „čp.“) se dává jen před samotné číslo;
25
39
  ulice s číslem jde do souboru tak, jak je. Věk „05y“ se čte jako 5 let. Obojí se z aplikace Strom vrací stejné,
26
40
  není to úprava uživatele. Období, které vede aplikace Strom („1871/1882“, nebo otevřené na jednom konci), přijde do
27
41
  výzkumu jako od … do …, nikdy jako datum někde mezi nimi.
28
- - **Otázka na dějiny říká, kde platí.** Zapnutí nebo vypnutí událostí dějin platí pro všechny výzkumy na tomto
29
- počítači; otázka to říká, i jak to změnit jen pro jeden výzkum.
30
42
  - **Slabý nebo odmítnutý model agenta hlásí doctor.** Když výzkum nemá zvolený model a vlastní výchozí model agenta je
31
43
  ten, který výzkum shledal pro svou práci slabým nebo odmítnutým, `strom doctor` varuje a dá příkaz k volbě; sám
32
44
  nikdy nevolí.
package/CHANGELOG.de.md CHANGED
@@ -5,15 +5,26 @@
5
5
  Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Installationszeilen stehen bei
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases) (auf Englisch).
7
7
 
8
+ ## 1.25.0
9
+
10
+ *In Vorbereitung: die Betas bis 1.25.0-beta.1. Der Abschnitt ist mit dem Release vollständig.*
11
+
12
+ - **Daten für das Strom-Widget auch unter Windows.** Eine Forschung schreibt jetzt, was das Strom-Widget von ihr zeigt
13
+ (woran gearbeitet wird, die Funde der letzten 24 Stunden, was wartet), auch unter Windows, nach
14
+ `%USERPROFILE%\.config\strom\widget`, mit einer Marke des strom, der es geschrieben hat. Ein Widget, das gerade seine
15
+ Datei liest, hält die Brücke nie auf. `strom uninstall` entfernt auch die Dateien des Widgets, auch auf dem Mac; die
16
+ Forschung und die Einstellungen bleiben.
17
+
8
18
  ## 1.24.0
9
19
 
10
- *In Vorbereitung: die Betas bis 1.24.0-beta.2. Der Abschnitt ist mit dem Release vollständig.*
20
+ *31.10.2026 · gehört zur Strom-App 3.19.0.*
11
21
 
12
22
  - **Orte einmal zugeordnet für jede Forschung auf diesem Computer.** Ein Ort, den eine andere Forschung hier schon den
13
23
  Regionen der Geschichte zugeordnet hat, wird übernommen, wenn es sicher derselbe Ort ist (derselbe Name und dieselben
14
24
  Koordinaten, Pfarrei oder derselbe Bezirk) und jede Forschung ihn gleich zuordnet; nur die Zuordnung des Orts geht
15
25
  über, nichts von der Familie. Ein Ort gleichen Namens, der nicht sicher derselbe ist, geht als Vorschlag an den
16
- Agenten; er ordnet nur den Rest zu. `strom history adopt --dry-run` sagt, was übernommen würde.
26
+ Agenten; er ordnet nur den Rest zu. `strom history adopt --dry-run` sagt, was übernommen würde. Die Frage, die Ereignisse der Geschichte ein- oder
27
+ auszuschalten, sagt, dass es für jede Forschung auf diesem Computer gilt, und wie es sich für eine allein ändern lässt.
17
28
  - **Die Korrekturen einer Forschung für die Strom-App.** Die Brücke listet, was `strom fixes` in der Forschung anbietet
18
29
  (Titel, was sie tut, wie viele Einträge, ihr Stand, ob sie sich zurücknehmen lässt), kostenlos und ohne etwas zu
19
30
  schreiben; der Link `strom-research://fixes` öffnet eine Korrektur im Terminal der Forschung, wo sie und ihre
@@ -23,12 +34,14 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
23
34
  Ausschnitten, die die Datei auslässt, und bei Quellen ohne Ausschnitt. Nur eine öffentliche Webseite, nie die Adresse
24
35
  eines Bestands; die Standard-GEDCOM-Datei behält die Adresse in der Notiz. Das Datum der Version der
25
36
  Forschung in der Strom-Datei trägt auch die Uhrzeit, so zeigt die App den Tag ihrer Entstehung auch um Mitternacht.
37
+ - **Die eigene Linie einer Person aus der Strom-App.** Markiert die Strom-App, dass eine Person eine eigene Linie
38
+ gründet oder die der Familie nicht trägt, behält `strom sync` das in der Forschung, und die Strom-Datei gibt es
39
+ zurück, so zeigen die nächste Sendung und ein anderes Gerät dieselben Linien; das Zusammenführen zweier Personen
40
+ behält die Markierung. Die Standard-GEDCOM-Datei sagt nichts davon. Es braucht die Strom-App 3.19.0.
26
41
  - **Hausnummer, Alter und Zeitraum, wie sie geschrieben wurden.** Das Wort für eine Hausnummer („No.“, „čp.“) steht nur vor
27
42
  einer Zahl allein; eine Straße mit Nummer geht so in die Datei, wie sie ist. Ein Alter „05y“ gilt als 5 Jahre.
28
43
  Beides kommt aus der Strom-App gleich zurück, keine Änderung des Benutzers. Ein Zeitraum der Strom-App
29
44
  („1871/1882“ oder an einem Ende offen) kommt als von … bis … in die Forschung, nie als ein Datum irgendwo dazwischen.
30
- - **Die Frage nach der Geschichte sagt, wo sie gilt.** Die Ereignisse der Geschichte ein- oder auszuschalten gilt für
31
- jede Forschung auf diesem Computer; die Frage sagt es, und wie es sich für eine Forschung allein ändern lässt.
32
45
  - **Ein schwaches oder abgelehntes Modell des Agenten meldet der Doctor.** Ist für die Forschung kein Modell gewählt und
33
46
  das eigene Standardmodell des Agenten eines, das die Forschung für ihre Arbeit schwach oder abgelehnt fand, warnt
34
47
  `strom doctor` und nennt den Befehl zur Wahl; selbst wählt er nie.
package/CHANGELOG.md CHANGED
@@ -5,15 +5,25 @@ English · [Čeština](CHANGELOG.cs.md) · [Deutsch](CHANGELOG.de.md)
5
5
  The changes of each version in short. The full notes and the install lines are on
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases).
7
7
 
8
+ ## 1.25.0
9
+
10
+ *In preparation: the betas up to 1.25.0-beta.1. The section is complete with the release.*
11
+
12
+ - **Data for the Strom widget on Windows too.** A research now writes what the Strom widget shows of it (what it works
13
+ on, the finds of the last 24 hours, what waits) also on Windows, into `%USERPROFILE%\.config\strom\widget`, with a
14
+ mark of the strom that wrote it. A widget reading its file never holds up the bridge. `strom uninstall` takes the
15
+ widget's files too, on a Mac as well; the research and the settings stay.
16
+
8
17
  ## 1.24.0
9
18
 
10
- *In preparation: the betas up to 1.24.0-beta.2. The section is complete with the release.*
19
+ *2026-10-31 · goes with the Strom app 3.19.0.*
11
20
 
12
21
  - **Places mapped once for every research on this computer.** A place another research here has mapped to its
13
22
  regions of history is taken over when it is surely the same place (the same name and the same coordinates, parish
14
23
  or district) and every research maps it alike; only the place's map crosses, nothing of the family. A place of the
15
24
  same name that is not surely the same is offered to the agent as a suggestion; the agent maps only the rest.
16
- `strom history adopt --dry-run` says what would be taken.
25
+ `strom history adopt --dry-run` says what would be taken. The question about turning the events of history on or off
26
+ says it holds for every research on this computer, and how to change it for one research alone.
17
27
  - **The fixes of a research for the Strom app.** The bridge lists what `strom fixes` offers in the research (title,
18
28
  what it does, how many items, its state, whether it can be put back), free and without writing anything; the link
19
29
  `strom-research://fixes` opens a fix in the research's terminal, where setting it right or putting it back is
@@ -23,12 +33,14 @@ The changes of each version in short. The full notes and the install lines are o
23
33
  file leaves out and for the sources without an excerpt. Only a public web page, never a recordset's address; the
24
34
  standard GEDCOM file keeps the address in the note. The date of the research's version in the Strom file carries its
25
35
  time too, so the app shows the day it was made also around midnight.
36
+ - **A person's own lineage from the Strom app.** Where the Strom app marks that a person founds a lineage of their
37
+ own or carries none of the family's, `strom sync` keeps it in the research and the Strom file gives it back, so the
38
+ next send and another device show the same lineages; a merge of two people keeps the mark. The standard GEDCOM file
39
+ says nothing of it. It needs the Strom app 3.19.0.
26
40
  - **A house number, an age and a period as they were written.** The word for a house number ("No.", "čp.") is put only before
27
41
  a number standing alone; a street with its number goes into the file as it is. An age of "05y" is read as 5 years.
28
42
  Both come back from the Strom app the same, no edit of the user's. A period the Strom app keeps ("1871/1882", or
29
43
  open at one end) comes into the research as from … to …, never as a date somewhere between.
30
- - **The question about history says where it holds.** Turning the events of history on or off holds for every
31
- research on this computer; the question says so, and how to change it for one research alone.
32
44
  - **A weak or refused model of the agent said by the doctor.** When no model is chosen for the research and the
33
45
  agent's own default is one the research found weak or refused for its work, `strom doctor` warns and gives the
34
46
  command to choose one; it never chooses itself.
@@ -881,6 +881,7 @@
881
881
  "ui.uninstall.links": "odkazy z aplikace Strom",
882
882
  "ui.uninstall.git": "vlastní git stromu",
883
883
  "ui.uninstall.path": "záznam instalátoru v PATH",
884
+ "ui.uninstall.widget": "soubory widgetu Strom",
884
885
  "ui.uninstall.program": "program",
885
886
  "ui.uninstall.list": "strom z počítače odebere:",
886
887
  "ui.uninstall.keeps": "Výzkumy zůstávají ({home}), stejně jako nastavení s klíči, které je pečetí ({config}) – smazat je ručně, jen pokud už nikdy nebudou potřeba.",
@@ -881,6 +881,7 @@
881
881
  "ui.uninstall.links": "die Links aus der Strom-App",
882
882
  "ui.uninstall.git": "strom's eigenes git",
883
883
  "ui.uninstall.path": "der PATH-Eintrag des Installationsprogramms",
884
+ "ui.uninstall.widget": "die Dateien des Strom-Widgets",
884
885
  "ui.uninstall.program": "das Programm",
885
886
  "ui.uninstall.list": "strom entfernt von diesem Computer:",
886
887
  "ui.uninstall.keeps": "Die Forschungen bleiben ({home}), ebenso die Einstellungen mit den Schlüsseln, die sie versiegeln ({config}) – sie nur dann selbst löschen, wenn sie nie wieder gebraucht werden.",
package/dist/cli/main.js CHANGED
@@ -38,6 +38,7 @@ import { expandFromLine, refreshLinks } from "../core/links.js";
38
38
  import { clockLine, FINISH_LINE, finishLimitLine, finishOf, timeLine } from "../core/clock.js";
39
39
  import { currentSession } from "../core/session.js";
40
40
  import { backgroundOf } from "../core/fetching.js";
41
+ import { markWidget } from "../core/widget.js";
41
42
  import { callMistake, checkArgs, GroupOnly, parseOptions, resolveCommand, firstWord, severalRecords, splitPassthrough, usageOf } from "./execute.js";
42
43
  import { placeholders, UI, ui } from "./ui.js";
43
44
  import { catalog, localized, placeholdersOf, sayCommandAs } from "../core/phrases.js";
@@ -410,6 +411,8 @@ async function mainOf(argv, io, env, cwd) {
410
411
  ctx.settings.config.lastChannel = channel;
411
412
  ctx.settings.save();
412
413
  refreshGlobal(env);
414
+ // …and the widget on the desktop (macOS, Windows) learns this strom writes its files: index.json, even with no tree
415
+ markWidget(env);
413
416
  // …and the links from the Strom app lead to this strom again (where strom made them on this person's yes —
414
417
  // never another installation's: one with its own settings asks)
415
418
  try {
package/dist/cli/ui.js CHANGED
@@ -718,6 +718,7 @@ export const UI = {
718
718
  "ui.uninstall.links": "the links from the Strom app",
719
719
  "ui.uninstall.git": "strom's own git",
720
720
  "ui.uninstall.path": "the installer's PATH entry",
721
+ "ui.uninstall.widget": "the Strom widget's files",
721
722
  "ui.uninstall.program": "the program",
722
723
  "ui.uninstall.list": "strom takes this off the computer:",
723
724
  "ui.uninstall.keeps": "The research stays ({home}), and so do the settings with the keys that seal it ({config}) — delete them by hand only if they are never wanted again.",
@@ -68,6 +68,7 @@ import { ensureHooksDir } from "../core/hooks.js";
68
68
  import { claudeInChrome, CLAUDE_IN_CHROME_URL, downloadsDir } from "../core/browser.js";
69
69
  import { browserConnectors, fenceKeepsNetOff, listConnectors } from "../core/connector.js";
70
70
  import { appReadLimits } from "../core/live-read.js";
71
+ import { markWidget } from "../core/widget.js";
71
72
  export const SHARED_DIRS = ["media", "catalog", "tools", "cache", "inbox"];
72
73
  export function ensureShared(dir) {
73
74
  for (const d of SHARED_DIRS)
@@ -94,6 +95,8 @@ register({
94
95
  const s = ctx.settings;
95
96
  if (ctx.interactive && !isAgent(ctx.env)) {
96
97
  const r = await setupWizard(ctx, { research: Boolean(opts.research) });
98
+ // the widget on the desktop (macOS, Windows) learns this strom writes its files
99
+ markWidget(ctx.env);
97
100
  // run in an archive (the Strom app's link to the research's settings too): research offered beside it
98
101
  let root;
99
102
  try {
@@ -184,6 +187,7 @@ register({
184
187
  cfg.trees = trees;
185
188
  s.save();
186
189
  fs.mkdirSync(home, { recursive: true });
190
+ markWidget(ctx.env);
187
191
  const sharedDir = s.shared().value;
188
192
  const treesDir = s.trees().value;
189
193
  fs.mkdirSync(treesDir, { recursive: true });
@@ -497,7 +497,7 @@ register({
497
497
  },
498
498
  }, {
499
499
  path: ["uninstall"],
500
- summary: "Take strom off this computer: what the agents were taught, the shortcut, its own git, its PATH lines and the program — the research and the settings stay",
500
+ summary: "Take strom off this computer: what the agents were taught, the shortcut, its own git, its PATH lines, the widget's files and the program — the research and the settings stay",
501
501
  group: "setup",
502
502
  description: "The user's decision: a person at their terminal is asked there (--yes: no question); asked by an agent, strom asks\n" +
503
503
  "in a window of the system. Installed through npm, the program itself goes with npm uninstall -g strom-research.",
package/dist/core/json.js CHANGED
@@ -42,7 +42,7 @@ export function stringifyCanonical(value) {
42
42
  export const RENAME_PATIENCE = 1500;
43
43
  const RENAME_FIRST_WAIT = 25;
44
44
  /** What another program holding the target (an antivirus, the search indexer, a backup) makes a rename fail with. */
45
- const HELD = new Set(["EPERM", "EBUSY", "EACCES"]);
45
+ export const HELD = new Set(["EPERM", "EBUSY", "EACCES"]);
46
46
  /**
47
47
  * fn, again for a while on Windows when a file it uses is held: an antivirus, the indexer or a sync tool that has the
48
48
  * file open for a moment makes a rename, a read or a removal fail with EPERM, EBUSY or EACCES. The waits grow (25, 50,
@@ -88,14 +88,20 @@ export function readFileRetrying(file, platform = process.platform) {
88
88
  export function removeRetrying(file, platform = process.platform) {
89
89
  retryingHeld(file, platform, () => fs.rmSync(file, { force: true }));
90
90
  }
91
- /** Write via a temp file in the same directory and rename: never a half file. */
92
- export function writeFileAtomic(file, content) {
91
+ /**
92
+ * Write via a temp file in the same directory and rename: never a half file. `patient: false`: the rename tried once,
93
+ * never waited for a file another program holds (the widget's files: the bridge's tick is never held up by a reader).
94
+ */
95
+ export function writeFileAtomic(file, content, opts = {}) {
93
96
  const dir = path.dirname(file);
94
97
  fs.mkdirSync(dir, { recursive: true });
95
98
  const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`);
96
99
  fs.writeFileSync(tmp, content, "utf8");
97
100
  try {
98
- renameRetrying(tmp, file);
101
+ if (opts.patient === false)
102
+ fs.renameSync(tmp, file);
103
+ else
104
+ renameRetrying(tmp, file);
99
105
  }
100
106
  catch (err) {
101
107
  fs.rmSync(tmp, { force: true });
package/dist/core/live.js CHANGED
@@ -3727,8 +3727,8 @@ export function serveLive(root, env) {
3727
3727
  return atWorkNow;
3728
3728
  };
3729
3729
  let workers = safe("the start", () => JSON.stringify(atWork(seen))) ?? "";
3730
- // the research on the desktop of a Mac (the Strom Widget): the tree's overview written while the bridge runs
3731
- // (core/widget.ts; none off macOS) — the first a moment after the start, never in its way
3730
+ // the research on the desktop (the Strom Widget of a Mac, the widget of Windows 11): the tree's overview written
3731
+ // while the bridge runs (core/widget.ts; none but on macOS and Windows) — the first a moment after the start, never in its way
3732
3732
  const widget = widgetWriter(root, env);
3733
3733
  if (widget)
3734
3734
  setTimeout(() => safe("the widget", () => widget.tick(true)), 1000).unref?.();
@@ -562,11 +562,11 @@ export function appKnowsPrimaryName(settings, version) {
562
562
  * The first Strom app that keeps the person's own bounds of a lineage (spec 028; the app's ZADANI_VYZKUM_rody.md):
563
563
  * it reads 1 _STROM_LINE FOUNDER|NOCARRY on a person and 1 _STROM_LINES Y in the header of the Strom file, keeps them
564
564
  * as its lineageOverrides and sends them back. An older app skips the tags and sends none back (a send of it says
565
- * nothing of them: no header mark). Not released yet (the app's 3.19.0); an app of unknown version (strom app from a
565
+ * nothing of them: no header mark). Released with the app 3.19.0; an app of unknown version (strom app from a
566
566
  * terminal) stays off, as the groups of a surname's forms; another copy of the app (its beta, its development) gets
567
567
  * them from APP_READS_LINES_BETA.
568
568
  */
569
- export const APP_READS_LINES = undefined;
569
+ export const APP_READS_LINES = "3.19.0";
570
570
  /** The first beta of the app that reads and writes the bounds of a lineage (another copy of the app gets them from it). */
571
571
  export const APP_READS_LINES_BETA = "3.19.0-beta.4";
572
572
  /** Does the Strom app of this version keep the bounds of a lineage the person set (_STROM_LINE, _STROM_LINES)? */
package/dist/core/tree.js CHANGED
@@ -29,7 +29,7 @@ export const TREE_FILE = "strom.json";
29
29
  export const SEAL_FOREIGN = "seal.foreign";
30
30
  // between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
31
31
  // at a release the release's own (package.json)
32
- export const VERSION = "1.24.0-beta.2";
32
+ export const VERSION = "1.25.0-beta.1";
33
33
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
34
34
  .strom/
35
35
  /media/
@@ -1,8 +1,9 @@
1
1
  // What strom put on this computer outside the research, and taking it away
2
2
  // again (strom uninstall): what the agents were taught, the shortcut, strom's
3
- // own git, the PATH lines of the installer and the program itself. The
4
- // research stays, and so do the settings with the keys that seal it — they
5
- // are the user's; strom says where they are.
3
+ // own git, the PATH lines of the installer, the widget's files (what it
4
+ // showed of the research) and the program itself. The research stays, and so
5
+ // do the settings with the keys that seal it — they are the user's; strom
6
+ // says where they are.
6
7
  import fs from "node:fs";
7
8
  import path from "node:path";
8
9
  import { spawn, spawnSync } from "node:child_process";
@@ -13,6 +14,7 @@ import { globalTargets, isInstalled, uninstallGlobal } from "../agents/global.js
13
14
  import { PROFILES } from "../agents/profiles.js";
14
15
  import { holdsPath, installation, pathForms, PROGRAM_ENTRIES } from "./self.js";
15
16
  import { linkFiles, linkOwner, unregisterLinks } from "./links.js";
17
+ import { widgetDir } from "./widget.js";
16
18
  /** The mark the installer (install/install.sh) puts on the PATH line it adds. */
17
19
  const MARK = "# strom research";
18
20
  const sys = (cmd, args) => {
@@ -328,6 +330,11 @@ export function uninstallPlan(env, names, opts = {}) {
328
330
  // none
329
331
  }
330
332
  }
333
+ // the widget's files (macOS, Windows): only what strom wrote there, the folder when nothing else is left in it — the
334
+ // widget would show the research as not at work for ever
335
+ const widget = widgetDir(env, platform);
336
+ if (widget && widgetFiles(widget).length && !holdsKept(widget, keep, platform))
337
+ remove.push({ kind: "widget", path: widget, remove: () => removeWidgetFiles(widget) });
331
338
  const program = inst.kind === "installed" ? inst.root : undefined;
332
339
  // install.json's launchers: those of a launcher's name outside the research only
333
340
  const chosen = launchersToTake(inst.launchers ?? [], keep, platform);
@@ -346,3 +353,26 @@ export function uninstallPlan(env, names, opts = {}) {
346
353
  const skipped = chosen.skipped.filter((l) => !pathForms(l, platform).some((f) => taken.has(f)));
347
354
  return { remove, npm: inst.kind === "npm", ...(program ? { program, launchers: chosen.take, skipped } : {}) };
348
355
  }
356
+ /** What strom writes into the widget's folder: index.json, each tree's <ID>.json and a file being written beside one. */
357
+ const WIDGET_FILE = /^(?:index|[0-9A-Za-z-]{1,64})\.json$|^\..+\.json\.\d+\.[0-9a-f]+\.tmp$/u;
358
+ export function widgetFiles(dir) {
359
+ try {
360
+ return fs.readdirSync(dir).filter((n) => WIDGET_FILE.test(n) && fs.statSync(path.join(dir, n)).isFile());
361
+ }
362
+ catch {
363
+ return [];
364
+ }
365
+ }
366
+ /** The widget's files taken; the folder too when nothing else is in it (never what strom did not write there). */
367
+ export function removeWidgetFiles(dir) {
368
+ for (const n of widgetFiles(dir))
369
+ fs.rmSync(path.join(dir, n), { force: true });
370
+ try {
371
+ if (!fs.readdirSync(dir).length)
372
+ fs.rmdirSync(dir);
373
+ }
374
+ catch {
375
+ // gone already, or another program's file in it: it stays
376
+ }
377
+ return !widgetFiles(dir).length;
378
+ }
@@ -4,20 +4,22 @@
4
4
  // phone-finds.ts) and cleaned as for it (forPhone): never a path, a record's ID, a spend or a model. Beside each text
5
5
  // that names a person, the same without the names (the widget's "hide names").
6
6
  //
7
- // Written by the tree's bridge (strom live) while it runs, on macOS alone: ~/Library/Application Support/Strom/widget/
8
- // <tree id>.json and index.json (the trees there), each whole (a file beside it renamed over it). Looked at every
9
- // WIDGET_CHECK_MS, written when what it says changed, else every WIDGET_HEARTBEAT_MS; the bridge ending as it should
10
- // writes `running: false`, its research gone takes the file away. An archive has none. The widget only reads: nothing
11
- // of it comes back. An isolated installation (STROM_ISOLATED: a test's, a second strom) writes into its own settings
12
- // folder — a research of a test never shows in the person's widget.
7
+ // Written by the tree's bridge (strom live) while it runs, on macOS (~/Library/Application Support/Strom/widget/) and
8
+ // Windows (%USERPROFILE%\.config\strom\widget\, the Windows 11 widget of strom-win): <tree id>.json and index.json
9
+ // (the trees there), each whole (a file beside it renamed over it). Looked at every WIDGET_CHECK_MS, written when what
10
+ // it says changed, else every WIDGET_HEARTBEAT_MS; the bridge ending as it should writes `running: false`, its research
11
+ // gone takes the file away. An archive has none. The widget only reads: nothing of it comes back. An isolated
12
+ // installation (STROM_ISOLATED: a test's, a second strom) writes into its own settings folder — a research of a test
13
+ // never shows in the person's widget. index.json is there from this strom's first run and its setup on (`strom`: the
14
+ // version that wrote it), so the widget tells a strom too old to write any from one whose bridge has not run yet.
13
15
  import fs from "node:fs";
14
16
  import path from "node:path";
15
17
  import { configDir, isolated, userHome } from "./paths.js";
16
18
  import { Settings } from "./config.js";
17
- import { Tree } from "./tree.js";
19
+ import { Tree, VERSION } from "./tree.js";
18
20
  import { actsAsArchive } from "./state.js";
19
21
  import { foldText } from "./text.js";
20
- import { writeFileAtomic } from "./json.js";
22
+ import { writeFileAtomic, HELD } from "./json.js";
21
23
  import { appUrlSetting } from "./stromapp.js";
22
24
  import { forPhone, treeOverview } from "./phone-watch.js";
23
25
  import { treeFinds } from "./phone-finds.js";
@@ -37,17 +39,23 @@ export const WIDGET_PENDING = 3;
37
39
  /** What stands for a name taken out of a text. */
38
40
  const HIDDEN = "…";
39
41
  /**
40
- * Where the widget's files are written: none but on macOS; STROM_WIDGET_DIR when set; an isolated installation into its
41
- * own settings folder; else ~/Library/Application Support/Strom/widget of the HOME strom runs with.
42
+ * Where the widget's files are written: none but on macOS and Windows; STROM_WIDGET_DIR when set; an isolated
43
+ * installation into its own settings folder; else on macOS ~/Library/Application Support/Strom/widget of the HOME strom
44
+ * runs with, on Windows %USERPROFILE%\.config\strom\widget — the profile's own folder (HOME may be Git Bash's), also
45
+ * with settings elsewhere (STROM_CONFIG_DIR), as the Mac's ~/Library. Never under AppData: an app packaged as MSIX
46
+ * (Claude's desktop app, whose agent starts the bridge) gets every new file there as a private copy of its own, which
47
+ * the widget would never see (found 2026-10-30 with the seal keys; core/paths.ts configDir).
42
48
  */
43
49
  export function widgetDir(env, platform = process.platform) {
44
- if (platform !== "darwin")
50
+ if (platform !== "darwin" && platform !== "win32")
45
51
  return undefined;
46
52
  const said = env.STROM_WIDGET_DIR?.trim();
47
53
  if (said)
48
54
  return path.resolve(said);
49
55
  if (isolated(env))
50
56
  return path.join(configDir(env, platform), "widget");
57
+ if (platform === "win32")
58
+ return path.join(env.USERPROFILE || userHome(env), ".config", "strom", "widget");
51
59
  return path.join(userHome(env), "Library", "Application Support", "Strom", "widget");
52
60
  }
53
61
  /** A moment as the widget reads it: ISO 8601 to the second, in UTC (Swift's .iso8601 reads no fractions). */
@@ -318,15 +326,37 @@ export function widgetTree(root, env, opts) {
318
326
  const treeFile = (dir, key) => path.join(dir, `${key}.json`);
319
327
  /** A tree's key as a file's name: the tree's ID (a UUID) — never a path. */
320
328
  const KEY = /^[0-9A-Za-z-]{1,64}$/u;
321
- /** index.json as the files of the trees there say it, written whole when it says something else. */
329
+ /**
330
+ * A file of the widget written whole (a file beside it renamed over it) — at once or not now: the widget reading it this
331
+ * moment holds it on Windows (a rename over an open file fails with EPERM or EBUSY there), and the bridge never waits
332
+ * for it (writeFileAtomic's patience would hold the bridge's tick for 1.5 s). False: not written, nothing left beside
333
+ * it; the next look writes it.
334
+ */
335
+ function writeNow(file, content) {
336
+ try {
337
+ writeFileAtomic(file, content, { patient: false });
338
+ return true;
339
+ }
340
+ catch (err) {
341
+ if (HELD.has(err.code ?? ""))
342
+ return false;
343
+ throw err;
344
+ }
345
+ }
346
+ /**
347
+ * index.json as the files of the trees there say it, written whole when it says something else (or another strom wrote
348
+ * it); the folder made when there is none (the mark of this strom: strom's first run, its setup). False: held by the
349
+ * widget this moment, not written.
350
+ */
322
351
  export function writeWidgetIndex(dir, now = Date.now()) {
323
352
  const trees = [];
324
353
  let names = [];
325
354
  try {
326
355
  names = fs.readdirSync(dir).filter((n) => n.endsWith(".json") && n !== "index.json" && !n.startsWith("."));
327
356
  }
328
- catch {
329
- return;
357
+ catch (err) {
358
+ if (err.code !== "ENOENT")
359
+ return true;
330
360
  }
331
361
  for (const n of names.sort()) {
332
362
  try {
@@ -342,34 +372,64 @@ export function writeWidgetIndex(dir, now = Date.now()) {
342
372
  const file = path.join(dir, "index.json");
343
373
  try {
344
374
  const was = JSON.parse(fs.readFileSync(file, "utf8"));
345
- if (JSON.stringify(was.trees) === JSON.stringify(trees) && was.schemaVersion === WIDGET_SCHEMA)
346
- return;
375
+ if (JSON.stringify(was.trees) === JSON.stringify(trees) && was.schemaVersion === WIDGET_SCHEMA && was.strom === VERSION)
376
+ return true;
347
377
  }
348
378
  catch {
349
379
  // none yet
350
380
  }
351
- writeFileAtomic(file, `${JSON.stringify({ schemaVersion: WIDGET_SCHEMA, generatedAt: isoSecond(now), trees }, null, 2)}\n`);
381
+ return writeNow(file, `${JSON.stringify({ schemaVersion: WIDGET_SCHEMA, generatedAt: isoSecond(now), strom: VERSION, trees }, null, 2)}\n`);
382
+ }
383
+ /**
384
+ * The mark that this strom writes the widget's files (on macOS and Windows): index.json, with no tree in it when no
385
+ * bridge ran yet — written at strom's first run of a version and at its setup. A widget that finds none tells a strom too
386
+ * old to write any (strom update) from one whose bridge has not run yet. Never in the way: any failure said nowhere.
387
+ */
388
+ export function markWidget(env, platform = process.platform) {
389
+ const dir = widgetDir(env, platform);
390
+ if (!dir)
391
+ return;
392
+ try {
393
+ writeWidgetIndex(dir);
394
+ }
395
+ catch {
396
+ // written by the next bridge
397
+ }
352
398
  }
353
- /** A tree's file written whole (a file beside it renamed over it), then index.json. */
399
+ /** A tree's file written whole (a file beside it renamed over it), then index.json. False: held by the widget, not now. */
354
400
  export function writeWidgetTree(dir, data) {
355
401
  if (!KEY.test(data.treeKey))
356
- return;
357
- writeFileAtomic(treeFile(dir, data.treeKey), `${JSON.stringify(data, null, 2)}\n`);
358
- writeWidgetIndex(dir, Date.parse(data.generatedAt) || Date.now());
402
+ return true;
403
+ if (!writeNow(treeFile(dir, data.treeKey), `${JSON.stringify(data, null, 2)}\n`))
404
+ return false;
405
+ return writeWidgetIndex(dir, Date.parse(data.generatedAt) || Date.now());
359
406
  }
360
- /** A tree's file taken away (its research gone, an archive now), and index.json without it. */
361
- export function dropWidgetTree(dir, key) {
407
+ /**
408
+ * A tree's file taken away (its research gone, an archive now), and index.json without it. One the widget holds without
409
+ * letting it go (Windows) stays: it says at least `running: false` (as `last` was, or as it says), not a research at work
410
+ * for the quarter of an hour the widget takes to call it old.
411
+ */
412
+ export function dropWidgetTree(dir, key, last) {
362
413
  if (!KEY.test(key))
363
414
  return;
415
+ const file = treeFile(dir, key);
364
416
  try {
365
- fs.rmSync(treeFile(dir, key), { force: true });
417
+ fs.rmSync(file, { force: true });
366
418
  }
367
419
  catch {
420
+ try {
421
+ const was = last ?? JSON.parse(fs.readFileSync(file, "utf8"));
422
+ const at = isoSecond(Date.now());
423
+ writeWidgetTree(dir, { ...was, running: false, generatedAt: at, lastActiveAt: at });
424
+ }
425
+ catch {
426
+ // nothing more to do: the widget calls it old by itself
427
+ }
368
428
  return;
369
429
  }
370
430
  writeWidgetIndex(dir);
371
431
  }
372
- /** The writer of a tree's bridge; none off macOS. */
432
+ /** The writer of a tree's bridge; none but on macOS and Windows. */
373
433
  export function widgetWriter(root, env, platform = process.platform, clock = Date.now) {
374
434
  const dir = widgetDir(env, platform);
375
435
  if (!dir)
@@ -381,40 +441,54 @@ export function widgetWriter(root, env, platform = process.platform, clock = Dat
381
441
  let said = "";
382
442
  let last;
383
443
  let key;
444
+ // what the widget held at the last look (Windows: renamed over an open file): written at the next tick, not the next look
445
+ let held;
384
446
  // what moves with the clock alone says nothing new
385
447
  const content = (t) => JSON.stringify({ ...t, generatedAt: "", lastActiveAt: "", findings: { ...t.findings, windowStart: "" } });
448
+ const write = (t, c, now) => {
449
+ if (!writeWidgetTree(dir, t)) {
450
+ held = { t, c };
451
+ return;
452
+ }
453
+ held = undefined;
454
+ said = c;
455
+ written = now;
456
+ };
386
457
  return {
387
458
  tick(force = false) {
388
459
  const now = clock();
389
- if (!force && now - checked < checkMs)
460
+ if (!force && now - checked < checkMs) {
461
+ if (held)
462
+ write({ ...held.t, generatedAt: isoSecond(now), lastActiveAt: isoSecond(now) }, held.c, now);
390
463
  return;
464
+ }
391
465
  checked = now;
392
466
  const t = widgetTree(root, env, { running: true, now });
393
467
  if (!t) {
394
468
  // an archive now: nothing of it on the desktop
395
469
  if (key)
396
- dropWidgetTree(dir, key);
470
+ dropWidgetTree(dir, key, last);
397
471
  key = undefined;
398
472
  last = undefined;
473
+ held = undefined;
399
474
  said = "";
400
475
  return;
401
476
  }
402
477
  key = t.treeKey;
403
478
  last = t;
404
479
  const c = content(t);
405
- if (c === said && now - written < beatMs)
480
+ if (c === said && now - written < beatMs && !held)
406
481
  return;
407
- writeWidgetTree(dir, t);
408
- said = c;
409
- written = now;
482
+ write(t, c, now);
410
483
  },
411
484
  end(gone) {
412
485
  if (!key)
413
486
  return;
414
487
  // what the file says now is no longer what was written: the next look writes it again
415
488
  said = "";
489
+ held = undefined;
416
490
  if (gone)
417
- return dropWidgetTree(dir, key);
491
+ return dropWidgetTree(dir, key, last && { ...last, running: false });
418
492
  if (!last)
419
493
  return;
420
494
  const at = isoSecond(clock());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.24.0-beta.2",
3
+ "version": "1.25.0-beta.1",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {