strom-research 1.0.0 → 1.1.0

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 (73) hide show
  1. package/README.md +13 -5
  2. package/assets/icon/NOTICE.md +26 -0
  3. package/assets/icon/favicon.svg +1 -0
  4. package/assets/icon/icon-256.png +0 -0
  5. package/assets/icon/icon-512.png +0 -0
  6. package/assets/icon/icon.ico +0 -0
  7. package/assets/icon/icon.svg +1 -0
  8. package/assets/lang/cs.json +184 -8
  9. package/assets/lang/de.json +184 -8
  10. package/assets/method/reading.md +4 -2
  11. package/assets/method/recording.md +10 -1
  12. package/assets/plugins/connectors/DISCOVERY.md +6 -2
  13. package/assets/plugins/connectors/README.md +27 -10
  14. package/dist/agents/files.js +27 -18
  15. package/dist/agents/global.js +15 -4
  16. package/dist/brief/brief.js +4 -0
  17. package/dist/cli/context.js +10 -4
  18. package/dist/cli/human.js +116 -0
  19. package/dist/cli/main.js +9 -0
  20. package/dist/cli/menu.js +254 -38
  21. package/dist/cli/ui.js +156 -7
  22. package/dist/cli/wizard.js +36 -9
  23. package/dist/commands/browse.js +243 -0
  24. package/dist/commands/clips.js +190 -0
  25. package/dist/commands/connectors.js +93 -28
  26. package/dist/commands/guide.js +28 -7
  27. package/dist/commands/index.js +3 -0
  28. package/dist/commands/media.js +42 -8
  29. package/dist/commands/meta.js +33 -9
  30. package/dist/commands/output.js +103 -13
  31. package/dist/commands/read.js +1 -1
  32. package/dist/commands/readers.js +62 -0
  33. package/dist/commands/research.js +62 -0
  34. package/dist/commands/session.js +76 -47
  35. package/dist/commands/setup.js +17 -10
  36. package/dist/commands/sources.js +88 -16
  37. package/dist/commands/start.js +57 -37
  38. package/dist/commands/tasks.js +43 -6
  39. package/dist/commands/transcripts.js +177 -0
  40. package/dist/core/chromium.js +60 -19
  41. package/dist/core/clipfinder.js +118 -0
  42. package/dist/core/config.js +42 -2
  43. package/dist/core/connector.js +56 -8
  44. package/dist/core/excerpt.js +219 -0
  45. package/dist/core/frontier.js +30 -5
  46. package/dist/core/git.js +1 -1
  47. package/dist/core/kin.js +90 -0
  48. package/dist/core/live.js +22 -13
  49. package/dist/core/media.js +37 -3
  50. package/dist/core/migrate.js +33 -0
  51. package/dist/core/model.js +7 -0
  52. package/dist/core/net.js +93 -19
  53. package/dist/core/overview.js +259 -0
  54. package/dist/core/phrases.js +27 -0
  55. package/dist/core/places.js +43 -0
  56. package/dist/core/queue.js +16 -3
  57. package/dist/core/review.js +369 -0
  58. package/dist/core/schema.js +4 -0
  59. package/dist/core/session.js +5 -1
  60. package/dist/core/shortcut.js +71 -11
  61. package/dist/core/stories.js +3 -4
  62. package/dist/core/stromapp.js +86 -25
  63. package/dist/core/transcriber.js +99 -0
  64. package/dist/core/tree.js +3 -1
  65. package/dist/core/uninstall.js +4 -2
  66. package/dist/core/update.js +48 -22
  67. package/dist/core/validate.js +3 -1
  68. package/dist/core/workers.js +13 -0
  69. package/dist/gedcom/export.js +58 -30
  70. package/dist/gedcom/lines.js +8 -0
  71. package/dist/gedcom/validate.js +2 -2
  72. package/dist/runners/claude.js +2 -0
  73. package/package.json +1 -1
@@ -31,8 +31,10 @@ Look at as few pixels as the question needs, and write down what you saw at once
31
31
  occupations, godparents, witnesses, midwife, remarks in the margin.
32
32
  - **Report image by image, as you go**: the image and page, what was found (or
33
33
  nothing), what was illegible and where, the hand, how sure each name is.
34
- - **Cite the image**: `strom source add … --media B0001:57 --locator "pag. 112,
35
- 2nd entry"`; a searched range goes in `strom search add … --pages 40-69`.
34
+ - **Cite the image, and where the entry is on it**: `strom source add …
35
+ --clip B0001:57@0.05,0.40,0.45,0.18 --locator "pag. 112, 2nd entry"` — the
36
+ crop you read it in, as its view prints it; a searched range goes in
37
+ `strom search add … --pages 40-69`.
36
38
  - Page ↔ image: `strom recordset calibrate B0001 --point 57=112` (measured on
37
39
  the image, never guessed); then `strom media view B0001 --page 112` works.
38
40
  - **No images here yet:** a connector for that archive
@@ -4,7 +4,7 @@ Record a found entry in one batch: write the lines to a file in notes/, run
4
4
  `strom batch --file notes/<file> --dry-run`, fix what it reports, then run it
5
5
  without --dry-run. `#name` labels what a line creates, `@name` uses it later.
6
6
 
7
- source add "Baptism of Jan Novák 1885" --kind baptism --recordset B0001 --media B0001:57 --locator "pag. 112, entry 2" --language la --information primary --transcript @notes/entry.txt #s
7
+ 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
8
  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
9
9
  cite E0001 @s --quote "natus 24. Junii" --status proven
10
10
  name add P0002 "Marie /Svobodová/" --kind birth --cite @s --quote "Maria filia Josephi Svoboda"
@@ -12,6 +12,15 @@ without --dry-run. `#name` labels what a line creates, `@name` uses it later.
12
12
  family add --partner @josef --child P0002 --cite @s --information secondary
13
13
  search add "Baptism of Jan Novák" --recordset B0001 --years 1884-1886 --pages 55-60 --method page-by-page --result found --found @s
14
14
 
15
+ - `--clip` is where the entry is on its image: the crop you read it in — the
16
+ view of it prints the option to copy (`the source of an entry read here:
17
+ --clip …`); an entry over a page break gets two. It cites the image too, and
18
+ the Strom app shows the entry cut out of the scan next to its words. Every
19
+ entry read from a scan gets it when it is recorded — you have the entry in
20
+ front of you; strom reminds you of one without it (source add, task done).
21
+ - `--transcript` is the entry's words as they stand in it — its own language,
22
+ spelling and abbreviations, line by line, unread letters `[?]`; the Strom app
23
+ shows them next to the entry's image. Every entry read gets them.
15
24
  - A fact already in the tree gets the citation (`cite E…`), not a second fact —
16
25
  and what the record adds to it: `event edit E… --age husband:27 --age wife:17
17
26
  --house 21 --with "witness:…" --with "officiant:…"` (filling in needs no reason).
@@ -24,8 +24,12 @@ strom runs the connector; you never run it yourself, you test it with
24
24
  only finds books and gives their links (`"can": ["find", "list"]`), and
25
25
  the user downloads by hand into the inbox.
26
26
  - `"unknown"`: you could not find out. Say so; the user decides.
27
- - `pace`: slower than strom's default when the terms or robots.txt ask for
28
- it, e.g. `{"minIntervalMs": 5000}` for a crawl-delay of 5.
27
+ - `pace`: the service's own, when it states one — slower when the terms
28
+ or robots.txt ask for it (`{"minIntervalMs": 5000}` for a crawl-delay of
29
+ 5), faster when its documentation allows it (an API, an image server:
30
+ `{"minIntervalMs": 500, "source": "<where it says so>"}`), its hourly cap
31
+ when it has one (`"perHour"`). Never make one up: without it strom keeps
32
+ its own pause and follows what the server answers.
29
33
  - **Never get round a technical measure**: logins you do not have, captchas,
30
34
  or tokens meant to stop scripts. Tiles are how many viewers show big images;
31
35
  they are a measure against downloading only when the terms or the portal
@@ -7,8 +7,13 @@ work folder by itself: it asks strom, which paces every request, keeps to the
7
7
  connector's hosts, writes the files and checks that an image is an image.
8
8
 
9
9
  This page is the whole contract. It is **version 1 and it does not change**:
10
- a connector written for it keeps working. (A different contract would get a
11
- new number, and strom would still run version 1.)
10
+ a connector written for it keeps working. It only grows — optional fields,
11
+ new capabilities, new routes:
12
+ - an older strom leaves out what it does not know yet (`strom connector show`
13
+ says so) and runs the rest;
14
+ - a different contract would get a new number, and strom would still run
15
+ version 1. A connector written for a newer version than strom runs asks for
16
+ `strom update`.
12
17
 
13
18
  ## The folder
14
19
 
@@ -78,11 +83,18 @@ starts with `.` or `_` are ignored (`_old-version/`).
78
83
  - `unknown`.
79
84
  - `terms` (a URL), `termsSummary`, `robots` and `officialExport`: shown to
80
85
  the user when they decide.
81
- - `pace` (optional): slower than strom's default, never faster.
82
- - `minIntervalMs`: time between two requests to a host. The default and
83
- the minimum is 2000.
84
- - `perHour`: requests to a host in an hour. The default and the maximum is
85
- 400.
86
+ - `pace` (optional): the service's own pace, where it states one — its
87
+ terms, robots.txt (a crawl-delay), the documentation of its API or
88
+ image server. Nothing made up: without it strom keeps its default.
89
+ - `minIntervalMs`: time between two requests to a host. The default is
90
+ 2000; less only with `source`, and never below 250.
91
+ - `perHour`: requests to a host in an hour, where the service has such a
92
+ cap. The default is none.
93
+ - `source`: where the service says so (a URL, or a sentence).
94
+
95
+ The user may set their own pace for a host
96
+ (`strom allow host <host> --pace <seconds> --per-hour <n>`); it comes
97
+ first.
86
98
  - `login` (optional): the portal gives more to users who log in, and the
87
99
  connector can use the user's own account.
88
100
  - `about`: what an account gives, in a sentence the user reads.
@@ -217,16 +229,21 @@ The answer is one line on stdin:
217
229
  - `refused`: the archive said no (401 or 403), or asked twice to slow down
218
230
  (429);
219
231
  - `blocked`: the archive refused earlier and is left alone for now;
220
- - `cap`: the hourly cap is reached;
232
+ - `cap`: the hourly cap of the service (or the user's) is reached, or the
233
+ server says its limit is used up for longer than strom waits;
221
234
  - `silent`: no answer. The server is down, or it blocks this IP;
222
235
  - `http`: the server keeps failing, or the request is not valid;
223
236
  - `too-big`: text over 5 MB (ask with `save`);
224
237
  - `login`: a request with a login value (section 4) that strom will not send.
225
238
 
226
239
  strom answers requests one at a time, in order, and paces them:
227
- - at least 2 s apart for each host, and at most 400 an hour;
240
+ - at the service's pace for each host (the connector's `pace`), else at
241
+ least 2 s apart; an hourly cap only where the service or the user sets one;
242
+ - longer apart while a host answers slowly (the pause is at least as long as
243
+ its answers take, counted from the answer);
228
244
  - shared by everything on this computer;
229
- - it waits by itself when an archive asks it to (Retry-After).
245
+ - it waits by itself when an archive asks it to (Retry-After), or says its
246
+ limit is used up (RateLimit headers).
230
247
 
231
248
  ### 4. The user's login
232
249
 
@@ -11,7 +11,7 @@ import { writeFileAtomic } from "../core/json.js";
11
11
  import { PROFILES, SELF_READING } from "./profiles.js";
12
12
  import { Settings } from "../core/config.js";
13
13
  import { configDir } from "../core/paths.js";
14
- import { browserConnectors } from "../core/connector.js";
14
+ import { treeBrowserConnectors } from "../core/connector.js";
15
15
  import { CHROME_ALLOW, CHROME_DENY, chromeDomain } from "../core/browser.js";
16
16
  export const MARKER = "<!-- strom: generated above this line (strom agents sync); your own notes below are kept -->";
17
17
  function agentsMd(tree) {
@@ -75,30 +75,37 @@ they ask.
75
75
  archive — a few minutes; it then fetches only the images we need, slowly").
76
76
  - **Stories of their ancestors**: on by default — once records tell a life,
77
77
  strom proposes a task to write it for the family book in the Strom app
78
- (the method: a story rests on recorded facts only). When the research
79
- starts, tell them in a sentence and that they may say no (\`strom\` shows
80
- whether they chose; \`strom config set stories no\`). A story stays a draft
81
- until they approve it.
78
+ (the method: a story rests on recorded facts only). When \`strom\` says to
79
+ tell them (it does once, not in every conversation), say it in a sentence
80
+ and that they may say no (\`strom config set stories no\`). A story stays a
81
+ draft until they approve it.
82
+ - **One person looked at again** ("check grandpa František", "find more
83
+ about her", after a new model): \`strom review <whom>\` — strom proposes
84
+ the work as tasks (what the tree already says of them elsewhere, entries to
85
+ read whole, facts to check); tell them how many and what a session costs,
86
+ and they choose: here with you, or the agent alone.
82
87
  - **What waits for them** (\`strom\` shows it): tell them plainly what to do
83
88
  and where, one thing at a time.
84
89
  - **Results and the Strom app**: \`output/tree-strom.ged\` is the family tree
85
90
  for the Strom app (https://stromapp.info) — strom's companion: a free family
86
91
  tree app, no account, their data stay on their computer; it shows the tree,
87
- the sources, a map, a family book. When they want to see the results (or
88
- once, when the first ones are there), suggest it gently, in a sentence or
89
- two: best installed as an app from the browser, from
92
+ the sources, a map, a family book. When they want to see the results, or
93
+ when \`strom\` says to offer it (it does once, not in every conversation),
94
+ suggest it gently, in a sentence or two: best installed as an app from the browser, from
90
95
  https://stromapp.info/run/ — \`strom app install\` opens it there and says
91
96
  where to click; it works offline then; \`strom app\` opens it, with this
92
97
  research when the app can take it — run by you, the app then follows the
93
98
  research live, what you record shows there by itself (else in the app:
94
- Import, and this file). \`strom\` says which, in its results line. A program they already use is fine too:
99
+ Import, and this file) — each register entry with its image, cut out of its
100
+ scan. \`strom\` says which, in its results line. A program they already use is fine too:
95
101
  \`output/tree.ged\`. If they do not want it, do not bring it up again.
96
102
  - **The tree in this conversation, app or not**: they can simply ask you —
97
103
  about anyone in the tree, a family, a line, what is proven and by which
98
104
  record, what is still missing. Answer from strom (\`strom person show\`,
99
105
  \`family show\`, \`research show\`, \`find\`, \`source show\`, \`story show\`,
100
106
  \`gaps\`, \`frontier\`) in plain words: names, dates, places and the record
101
- behind each fact, no IDs. Tell them once that they can ask like this.
107
+ behind each fact, no IDs. When \`strom\` says to (once), tell them they can
108
+ ask like this.
102
109
 
103
110
  ${SELF_READING}
104
111
  ${MARKER}
@@ -131,15 +138,17 @@ export function claudeSettings(tree) {
131
138
  const settings = new Settings(tree.env, {});
132
139
  const shared = settings.shared()?.value;
133
140
  const keys = permissionPath(configDir(tree.env));
134
- // Connectors whose images come through the user's browser: browser tools, for their sites only.
135
- const browser = browserConnectors(tree.env, shared);
141
+ // Connectors of this tree whose images come through the user's browser: browser tools, for their sites only.
142
+ const browser = treeBrowserConnectors(tree, shared);
136
143
  const sites = [...new Set(browser.flatMap((c) => c.manifest.hosts.map(chromeDomain)))];
137
144
  const downloads = permissionPath(settings.downloads());
138
145
  const lead = settings.models("claude", tree.config).lead;
146
+ // A rule for each of Claude Code's shells: Bash, and PowerShell (on Windows) — a Bash rule does not cover it.
147
+ const shell = (cmd) => [`Bash(${cmd})`, `PowerShell(${cmd})`];
139
148
  return {
140
149
  permissions: {
141
150
  allow: [
142
- "Bash(strom:*)",
151
+ ...shell("strom:*"),
143
152
  "Read(inputs/**)",
144
153
  "Read(output/**)",
145
154
  "Read(notes/**)",
@@ -160,13 +169,13 @@ export function claudeSettings(tree) {
160
169
  "Edit(data/**)",
161
170
  "Edit(strom.json)",
162
171
  "Edit(.git/**)",
163
- "Bash(git:*)",
172
+ ...shell("git:*"),
164
173
  // A password is typed by the user in their own terminal, and so is installing a plugin; the seal is strom's.
165
174
  // (Consents the agent may ask for — strom allow …: strom asks the person in a window.)
166
- "Bash(strom login:*)",
167
- "Bash(strom connector add:*)",
168
- "Bash(strom connector remove:*)",
169
- "Bash(strom seal:*)",
175
+ ...shell("strom login:*"),
176
+ ...shell("strom connector add:*"),
177
+ ...shell("strom connector remove:*"),
178
+ ...shell("strom seal:*"),
170
179
  // The limiter's memory (pace, refusals).
171
180
  ...(shared ? [`Edit(${permissionPath(path.join(shared, "net"))}/**)`] : []),
172
181
  // A session ends with its turn: nothing wakes it up later (a live run waited for a wake-up that never came).
@@ -49,7 +49,7 @@ mentions strom, a family tree or the Strom app.
49
49
  8. The Strom app (https://stromapp.info) is strom's companion: a free family
50
50
  tree app, no account, the data stay on the user's computer. The research's
51
51
  result (\`output/tree-strom.ged\`) opens in it. When the user wants to see
52
- the tree, suggest it gently — best installed as an app from the browser,
52
+ the tree (or \`strom\` says to offer it — once), suggest it gently — best installed as an app from the browser,
53
53
  from https://stromapp.info/run/ (\`strom app install\` opens it there; it
54
54
  then works offline); \`strom app\` opens it — with the research, followed
55
55
  live while you work, when the app can take it. Without it, the user can ask
@@ -58,18 +58,21 @@ mentions strom, a family tree or the Strom app.
58
58
  /**
59
59
  * The rules that let the agent run strom without asking, in its own settings' words.
60
60
  * Claude Code: `strom` on PATH, and the installer's command by its path — right after
61
- * the installer an agent calls it so, until a new terminal has it on PATH.
61
+ * the installer an agent calls it so, until a new terminal has it on PATH — for each of
62
+ * its shells: Bash, and PowerShell on Windows (a Bash rule does not cover it). Narrow
63
+ * rules like these hold in its auto mode too, where broad ones are dropped.
62
64
  */
63
65
  function allowRules(agent) {
64
66
  if (agent === "antigravity")
65
67
  return ["command(strom)"];
66
- return ["Bash(strom:*)", ...(installation().launchers ?? []).map((p) => `Bash(${p}:*)`)];
68
+ const commands = ["strom", ...(installation().launchers ?? [])];
69
+ return commands.flatMap((c) => [`Bash(${c}:*)`, `PowerShell(${c}:*)`]);
67
70
  }
68
71
  /** A rule of strom's in Claude Code's or Antigravity's settings, whichever installation wrote it. */
69
72
  function isStromRule(agent, rule) {
70
73
  if (agent === "antigravity")
71
74
  return rule === "command(strom)";
72
- return /^Bash\((?:.*[\\/])?strom(?:\.exe|\.cmd)?:\*\)$/.test(rule);
75
+ return /^(?:Bash|PowerShell)\((?:.*[\\/])?strom(?:\.exe|\.cmd)?:\*\)$/.test(rule);
73
76
  }
74
77
  /** OpenCode: the rule that lets it run strom without asking. */
75
78
  const OPENCODE_STROM = "strom *";
@@ -247,6 +250,14 @@ export function uninstallGlobal(t) {
247
250
  fs.rmSync(t.file);
248
251
  return true;
249
252
  }
253
+ /** What strom taught the agents (where it did), brought to this version's text; the agents whose files changed. */
254
+ export function refreshGlobal(env) {
255
+ const changed = new Set();
256
+ for (const t of globalTargets(env))
257
+ if (isInstalled(t) && installGlobal(t))
258
+ changed.add(t.agent);
259
+ return [...changed];
260
+ }
250
261
  export function isInstalled(t) {
251
262
  const cur = read(t.file);
252
263
  if (cur === undefined)
@@ -12,6 +12,7 @@ import { methodFor } from "../core/assets.js";
12
12
  import { recentSessions } from "../core/session.js";
13
13
  import { calibrationLine } from "../core/calibration.js";
14
14
  import { taskRecordsets } from "../core/frontier.js";
15
+ import { reviewItems } from "../core/review.js";
15
16
  import { readyConnectors } from "../core/connector.js";
16
17
  import { runs, shellArg } from "../cli/format.js";
17
18
  import { foldText } from "../core/text.js";
@@ -143,6 +144,9 @@ export function buildBrief(tree, opts) {
143
144
  ...task.notes.slice(-3).map((n) => `note: ${n.text}`),
144
145
  ].filter(Boolean).join("\n"),
145
146
  });
147
+ // 2. a review: what each item of the task is, as the tree has it
148
+ if (task.origin.startsWith("review:") && task.where.length)
149
+ sections.push({ name: "review", required: true, pointer: `strom task show ${task.id}`, text: ["## To review", ...reviewItems(tree, task)].join("\n") });
146
150
  // 2a. an imported tree: who in it is probably already in the tree
147
151
  const treeInputs = [...new Set([...(task?.subject ?? []), ...(task?.where ?? [])])]
148
152
  .map((id) => (/^I\d{4,}$/.test(id) ? tree.get(id) : undefined))
@@ -7,6 +7,7 @@ import { Writable } from "node:stream";
7
7
  import { Settings } from "../core/config.js";
8
8
  import { displayPath, expandHome } from "../core/paths.js";
9
9
  import { NeedsConsentError, NeedsInputError, StromError, UsageError } from "../core/errors.js";
10
+ import { migrate } from "../core/migrate.js";
10
11
  import { Tree, findTreeUpwards, isTreeDir } from "../core/tree.js";
11
12
  import { readJsonIfExists } from "../core/json.js";
12
13
  import { foldText } from "../core/text.js";
@@ -158,10 +159,12 @@ export class Context {
158
159
  }
159
160
  /**
160
161
  * One of numbered options (1…n, or keys of their own like "0"); Enter takes
161
- * the suggested one. Asks again on anything else. Undefined when a test ran
162
- * out of answers.
162
+ * the suggested one. Asks again on anything else. `back` adds "0" with that
163
+ * label: the person changes their mind, nothing changes — undefined, as when
164
+ * a test ran out of answers.
163
165
  */
164
- async choose(question, options, suggested) {
166
+ async choose(question, choices, suggested, opts = {}) {
167
+ const options = opts.back ? [...choices, { key: "0", label: opts.back }] : choices;
165
168
  const keys = options.map((o, i) => o.key ?? String(i + 1));
166
169
  for (;;) {
167
170
  this.io.stdout(`\n${question ? `${question}\n` : ""}${options.map((o, i) => ` ${keys[i].padStart(2)} ${o.label}`).join("\n")}\n`);
@@ -170,7 +173,7 @@ export class Context {
170
173
  const a = await this.ask(`${ui(this.uiLang(), "ui.choose")}`, keys[suggested]);
171
174
  const i = keys.indexOf(a.trim());
172
175
  if (i >= 0)
173
- return i;
176
+ return i < choices.length ? i : undefined;
174
177
  this.io.stdout(ui(this.uiLang(), "ui.bad.choice") + "\n");
175
178
  }
176
179
  }
@@ -276,6 +279,9 @@ export class Context {
276
279
  });
277
280
  }
278
281
  this.opened = Tree.open(root, this.env);
282
+ // Data an older strom wrote under an older schema: brought forward first (a dry run too — it is not the command's change).
283
+ for (const step of migrate(this.opened))
284
+ this.io.stderr(`${ui(this.uiLang(), "ui.migrated", { step })}\n`);
279
285
  this.opened.dryRun = this.dryRun;
280
286
  // A language passed from outside (--lang, STROM_LANG — e.g. by the Strom
281
287
  // app) applies to this invocation; the tree keeps its own.
@@ -0,0 +1,116 @@
1
+ // What a person reads about their family: dates, facts and how sure they are,
2
+ // in the research language — for the views of the tree (strom stats, pedigree,
3
+ // person card, recent). The GEDCOM forms stay in the data; these are only how
4
+ // they are shown.
5
+ import { MONTHS } from "../core/gdate.js";
6
+ import { ui } from "./ui.js";
7
+ /**
8
+ * English reads "31 Aug 1830"; the others their own numeric form ("31. 8. 1830", "31.8.1830"). A month and
9
+ * year alone are named ("srpen 1830") — after a word ("před 3/1853") in numbers, which no grammatical case bends.
10
+ */
11
+ function format(lang, date, parts) {
12
+ const en = lang === "en";
13
+ const opts = parts === "month"
14
+ ? { month: "long", year: "numeric" }
15
+ : parts === "monthafter"
16
+ ? { month: en ? "short" : "numeric", year: "numeric" }
17
+ : parts === "daymonth"
18
+ ? { day: "numeric", month: en ? "short" : "numeric" }
19
+ : { day: "numeric", month: en ? "short" : "numeric", year: "numeric" };
20
+ try {
21
+ return new Intl.DateTimeFormat(en ? "en-GB" : lang, { ...opts, timeZone: "UTC" }).format(date);
22
+ }
23
+ catch {
24
+ return new Intl.DateTimeFormat("en-GB", { ...opts, timeZone: "UTC" }).format(date);
25
+ }
26
+ }
27
+ /** One date without a qualifier: "31 AUG 1830", "AUG 1830", "1830" — `after` a word ("before", "between"). */
28
+ function simple(s, lang, after = true) {
29
+ const m = /^(?:(\d{1,2}) )?(?:([A-Z]{3}) )?(\d{3,4})$/.exec(s.trim());
30
+ if (!m)
31
+ return s;
32
+ const month = m[2] ? MONTHS.indexOf(m[2]) : -1;
33
+ if (month < 0)
34
+ return m[3];
35
+ const date = new Date(Date.UTC(2000, month, m[1] ? Number(m[1]) : 1));
36
+ date.setUTCFullYear(Number(m[3]));
37
+ return format(lang, date, m[1] ? "day" : after ? "monthafter" : "month");
38
+ }
39
+ /** A GEDCOM date as a person says it: "31. 8. 1830", "asi 1830", "před 1853", "mezi 1811 a 1812". */
40
+ export function humanDate(date, lang) {
41
+ if (!date)
42
+ return "";
43
+ const d = date.trim().toUpperCase();
44
+ let m = /^BET (.+) AND (.+)$/.exec(d);
45
+ if (m)
46
+ return ui(lang, "ui.date.between", { a: simple(m[1], lang), b: simple(m[2], lang) });
47
+ m = /^FROM (.+) TO (.+)$/.exec(d);
48
+ if (m)
49
+ return ui(lang, "ui.date.fromto", { a: simple(m[1], lang), b: simple(m[2], lang) });
50
+ m = /^(FROM|TO|ABT|CAL|EST|BEF|AFT) (.+)$/.exec(d);
51
+ if (m) {
52
+ const key = { FROM: "ui.date.from", TO: "ui.date.to", ABT: "ui.date.about", CAL: "ui.date.about", EST: "ui.date.about", BEF: "ui.date.before", AFT: "ui.date.after" };
53
+ return ui(lang, key[m[1]], { date: simple(m[2], lang) });
54
+ }
55
+ return simple(d, lang, false);
56
+ }
57
+ /** A day of the research (an ISO time): "25. 9.", with the year when it is not this one. */
58
+ export function humanDay(iso, lang) {
59
+ const d = new Date(iso);
60
+ return format(lang, d, d.getUTCFullYear() === new Date().getUTCFullYear() ? "daymonth" : "day");
61
+ }
62
+ /** An amount in US dollars (what the agents' providers bill). */
63
+ export function humanCost(usd, lang) {
64
+ try {
65
+ return new Intl.NumberFormat(lang === "en" ? "en-GB" : lang, { style: "currency", currency: "USD" }).format(usd);
66
+ }
67
+ catch {
68
+ return `$${usd.toFixed(2)}`;
69
+ }
70
+ }
71
+ const EVENTS = {
72
+ BIRT: "ui.ev.BIRT",
73
+ CHR: "ui.ev.CHR",
74
+ BAPM: "ui.ev.CHR",
75
+ DEAT: "ui.ev.DEAT",
76
+ BURI: "ui.ev.BURI",
77
+ CREM: "ui.ev.CREM",
78
+ MARR: "ui.ev.MARR",
79
+ MARB: "ui.ev.MARB",
80
+ ENGA: "ui.ev.ENGA",
81
+ DIV: "ui.ev.DIV",
82
+ OCCU: "ui.ev.OCCU",
83
+ RESI: "ui.ev.RESI",
84
+ EDUC: "ui.ev.EDUC",
85
+ RELI: "ui.ev.RELI",
86
+ TITL: "ui.ev.TITL",
87
+ CENS: "ui.ev.CENS",
88
+ CONF: "ui.ev.CONF",
89
+ EMIG: "ui.ev.EMIG",
90
+ IMMI: "ui.ev.IMMI",
91
+ NATU: "ui.ev.NATU",
92
+ PROB: "ui.ev.PROB",
93
+ WILL: "ui.ev.WILL",
94
+ };
95
+ /** The name of a kind of fact: "narození", "křest"; an EVEN by its own label. */
96
+ export function eventName(kind, lang, label) {
97
+ const key = EVENTS[kind];
98
+ return key ? ui(lang, key) : (label ?? kind);
99
+ }
100
+ const STATUS = { proven: "ui.st.proven", probable: "ui.st.probable", possible: "ui.st.possible", lead: "ui.st.lead" };
101
+ /** How sure a fact is: "doloženo", "jen stopa". */
102
+ export function statusName(status, lang) {
103
+ const key = STATUS[status];
104
+ return key ? ui(lang, key) : status;
105
+ }
106
+ /** A generation of ancestors: 2 the parents, 3 the grandparents, …; farther by its number. */
107
+ export function generationName(g, lang) {
108
+ const key = { 2: "ui.gen.2", 3: "ui.gen.3", 4: "ui.gen.4", 5: "ui.gen.5" };
109
+ return key[g] ? ui(lang, key[g]) : ui(lang, "ui.gen.n", { n: g });
110
+ }
111
+ /** Where a fact happened: the place, with the house when the record gives it. */
112
+ export function humanPlace(place, house, lang) {
113
+ if (!house)
114
+ return place ?? "";
115
+ return ui(lang, "ui.card.house", { place: place ?? "", house }).replace(/^[,\s]+/, "");
116
+ }
package/dist/cli/main.js CHANGED
@@ -9,6 +9,8 @@ import { resetCache } from "../core/git.js";
9
9
  import { VERSION } from "../core/tree.js";
10
10
  import { isAgent } from "../core/which.js";
11
11
  import { noticeStromApp } from "../core/stromapp.js";
12
+ import { isNewer } from "../core/update.js";
13
+ import { refreshGlobal } from "../agents/global.js";
12
14
  import { checkArgs, GroupOnly, parseOptions, resolveCommand, splitPassthrough } from "./execute.js";
13
15
  import "../commands/index.js";
14
16
  export { splitCommand } from "./execute.js";
@@ -110,6 +112,13 @@ export async function main(argv, io, env, cwd) {
110
112
  // Started by the Strom app: remembered quietly (it is where the results go).
111
113
  if (env.STROM_APP)
112
114
  noticeStromApp(ctx.settings, env);
115
+ // The first run of a newer strom: what it taught the agents outside the trees gets this version's text.
116
+ const last = ctx.settings.config.lastVersion;
117
+ if (ctx.settings.home() && (!last || isNewer(VERSION, last))) {
118
+ refreshGlobal(env);
119
+ ctx.settings.config.lastVersion = VERSION;
120
+ ctx.settings.save();
121
+ }
113
122
  const args = parsed.positionals;
114
123
  checkArgs(def, args);
115
124
  if (def.writes && v["dry-run"])