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.
- package/README.md +13 -5
- package/assets/icon/NOTICE.md +26 -0
- package/assets/icon/favicon.svg +1 -0
- package/assets/icon/icon-256.png +0 -0
- package/assets/icon/icon-512.png +0 -0
- package/assets/icon/icon.ico +0 -0
- package/assets/icon/icon.svg +1 -0
- package/assets/lang/cs.json +184 -8
- package/assets/lang/de.json +184 -8
- package/assets/method/reading.md +4 -2
- package/assets/method/recording.md +10 -1
- package/assets/plugins/connectors/DISCOVERY.md +6 -2
- package/assets/plugins/connectors/README.md +27 -10
- package/dist/agents/files.js +27 -18
- package/dist/agents/global.js +15 -4
- package/dist/brief/brief.js +4 -0
- package/dist/cli/context.js +10 -4
- package/dist/cli/human.js +116 -0
- package/dist/cli/main.js +9 -0
- package/dist/cli/menu.js +254 -38
- package/dist/cli/ui.js +156 -7
- package/dist/cli/wizard.js +36 -9
- package/dist/commands/browse.js +243 -0
- package/dist/commands/clips.js +190 -0
- package/dist/commands/connectors.js +93 -28
- package/dist/commands/guide.js +28 -7
- package/dist/commands/index.js +3 -0
- package/dist/commands/media.js +42 -8
- package/dist/commands/meta.js +33 -9
- package/dist/commands/output.js +103 -13
- package/dist/commands/read.js +1 -1
- package/dist/commands/readers.js +62 -0
- package/dist/commands/research.js +62 -0
- package/dist/commands/session.js +76 -47
- package/dist/commands/setup.js +17 -10
- package/dist/commands/sources.js +88 -16
- package/dist/commands/start.js +57 -37
- package/dist/commands/tasks.js +43 -6
- package/dist/commands/transcripts.js +177 -0
- package/dist/core/chromium.js +60 -19
- package/dist/core/clipfinder.js +118 -0
- package/dist/core/config.js +42 -2
- package/dist/core/connector.js +56 -8
- package/dist/core/excerpt.js +219 -0
- package/dist/core/frontier.js +30 -5
- package/dist/core/git.js +1 -1
- package/dist/core/kin.js +90 -0
- package/dist/core/live.js +22 -13
- package/dist/core/media.js +37 -3
- package/dist/core/migrate.js +33 -0
- package/dist/core/model.js +7 -0
- package/dist/core/net.js +93 -19
- package/dist/core/overview.js +259 -0
- package/dist/core/phrases.js +27 -0
- package/dist/core/places.js +43 -0
- package/dist/core/queue.js +16 -3
- package/dist/core/review.js +369 -0
- package/dist/core/schema.js +4 -0
- package/dist/core/session.js +5 -1
- package/dist/core/shortcut.js +71 -11
- package/dist/core/stories.js +3 -4
- package/dist/core/stromapp.js +86 -25
- package/dist/core/transcriber.js +99 -0
- package/dist/core/tree.js +3 -1
- package/dist/core/uninstall.js +4 -2
- package/dist/core/update.js +48 -22
- package/dist/core/validate.js +3 -1
- package/dist/core/workers.js +13 -0
- package/dist/gedcom/export.js +58 -30
- package/dist/gedcom/lines.js +8 -0
- package/dist/gedcom/validate.js +2 -2
- package/dist/runners/claude.js +2 -0
- package/package.json +1 -1
package/assets/method/reading.md
CHANGED
|
@@ -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 …
|
|
35
|
-
|
|
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 --
|
|
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`:
|
|
28
|
-
|
|
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.
|
|
11
|
-
new
|
|
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):
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- `
|
|
85
|
-
|
|
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
|
|
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
|
|
package/dist/agents/files.js
CHANGED
|
@@ -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 {
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
88
|
-
|
|
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)
|
|
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.
|
|
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 =
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
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).
|
package/dist/agents/global.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/dist/brief/brief.js
CHANGED
|
@@ -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))
|
package/dist/cli/context.js
CHANGED
|
@@ -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.
|
|
162
|
-
*
|
|
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,
|
|
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"])
|