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
package/dist/cli/menu.js CHANGED
@@ -12,9 +12,19 @@ import { ui } from "./ui.js";
12
12
  import { Tree, VERSION } from "../core/tree.js";
13
13
  import { liveHolder } from "../core/lock.js";
14
14
  import { newerVersion } from "../core/update.js";
15
- import { waitingForUser } from "../commands/tasks.js";
16
- import { noticeStromApp, stromAppState } from "../core/stromapp.js";
15
+ import { taskQueue, waitingForUser } from "../commands/tasks.js";
16
+ import { humanTask } from "../commands/browse.js";
17
+ import { lacksImages } from "../core/queue.js";
18
+ import { othersAtWork } from "../core/session.js";
19
+ import { DEFAULT_RUN_MINUTES } from "../core/config.js";
20
+ import { truncate } from "./format.js";
21
+ import { installedStromApp, noticeStromApp, stromAppState, stromAppUrl } from "../core/stromapp.js";
22
+ import { chromiumBrowser } from "../core/chromium.js";
23
+ import { liveWorkers, runsAtWork } from "../core/workers.js";
24
+ import { askStromApp } from "./wizard.js";
17
25
  import { openForUser } from "../core/open.js";
26
+ import { displayName, findPersons, label } from "../core/people.js";
27
+ import { mainPerson } from "../core/kin.js";
18
28
  import { AGENTS, findAgent } from "../core/which.js";
19
29
  import { agentsHere } from "../core/apps.js";
20
30
  import { PROFILES } from "../agents/profiles.js";
@@ -70,6 +80,8 @@ export async function runMenu(ctx, run) {
70
80
  continue;
71
81
  }
72
82
  const items = [];
83
+ let appItem;
84
+ let appTip;
73
85
  out();
74
86
  out(t("ui.menu.title", { version: VERSION }));
75
87
  const newer = await newerVersion(ctx.settings, ctx.env);
@@ -82,8 +94,9 @@ export async function runMenu(ctx, run) {
82
94
  out(t("ui.menu.tree", { name: tree.config.name, persons: tree.count("person") }));
83
95
  if (waiting)
84
96
  out(t("ui.menu.waiting", { count: waiting }));
85
- if (liveHolder(path.join(root, ".strom", "run.lock")))
86
- out(t("ui.menu.working"));
97
+ const runs = runsAtWork(root).length;
98
+ if (runs)
99
+ out(t(runs > 1 ? "ui.menu.working.more" : "ui.menu.working", { n: runs }));
87
100
  items.push({ key: "1", label: t(started ? "ui.menu.chat" : "ui.menu.start"), act: async () => void (await run(["chat"])) }, {
88
101
  key: "2",
89
102
  label: t("ui.menu.run"),
@@ -95,14 +108,68 @@ export async function runMenu(ctx, run) {
95
108
  await pause(lang);
96
109
  return;
97
110
  }
98
- let n = 0;
99
- while (!(Number.isInteger(n) && n > 0 && n <= 100)) {
100
- if (outOfAnswers())
111
+ // What comes next, as the agent working alone takes it (a story now and then), without what others hold.
112
+ const fresh = Tree.open(root, ctx.env);
113
+ const held = othersAtWork(fresh, fresh.env).tasks;
114
+ const queue = taskQueue(fresh, { strategy: ctx.settings.strategy(fresh.config), storyTurn: true }).filter((x) => !held.has(x.id));
115
+ const shown = queue.slice(0, RUN_LIST);
116
+ // Another run already at work: said first, and started beside it only on a yes.
117
+ const running = runsAtWork(root).length;
118
+ if (running) {
119
+ out(t("ui.run.beside", { n: running }));
120
+ if (!(await ctx.confirm(t("ui.run.beside.sure"), false)))
101
121
  return;
102
- n = Number(await ctx.ask(t("ui.run.how"), "3"));
122
+ }
123
+ let args;
124
+ if (shown.length) {
125
+ out(t("ui.run.next"));
126
+ for (const [k, x] of shown.entries())
127
+ out(` ${String(k + 1).padStart(2)}. ${truncate(humanTask(fresh, x.what, lang), 100)}${x.level === "narrate" ? t("ui.run.story") : ""}${lacksImages(fresh, x) ? t("ui.plan.images") : ""}`);
128
+ if (queue.length > shown.length)
129
+ out(` ${t("ui.plan.more", { n: queue.length - shown.length })}`);
130
+ }
131
+ out(t("ui.run.limit", { minutes: ctx.settings.number("run.minutes", fresh.config, DEFAULT_RUN_MINUTES) }));
132
+ const how = shown.length ? await ctx.choose(t("ui.run.what"), [{ label: t("ui.run.queue") }, { label: t("ui.run.pick") }], 0, { back: t("ui.browse.back") }) : 0;
133
+ if (how === undefined)
134
+ return;
135
+ if (how === 0) {
136
+ let n = -1;
137
+ while (!(Number.isInteger(n) && n >= 0 && n <= 100)) {
138
+ if (outOfAnswers())
139
+ return;
140
+ n = Number(await ctx.ask(t("ui.run.how"), String(Math.min(3, Math.max(1, queue.length)))));
141
+ }
142
+ if (n === 0)
143
+ return;
144
+ args = ["--max", String(n)];
145
+ }
146
+ else {
147
+ while (!args) {
148
+ if (outOfAnswers())
149
+ return;
150
+ const a = (await ctx.ask(t("ui.run.which"))).trim();
151
+ if (!a || a === "0")
152
+ return;
153
+ const picked = pickNumbers(a, shown.length);
154
+ if (picked)
155
+ args = ["--task", picked.map((k) => shown[k - 1].id).join(",")];
156
+ else
157
+ out(t("ui.run.which.bad", { n: shown.length }));
158
+ }
159
+ }
160
+ // With the Strom app, the work can be watched live meanwhile (a Chromium browser reaches the bridge); the last answer is suggested.
161
+ const app = stromAppState(ctx.settings);
162
+ if ((app === "yes" || app === "seen") && chromiumBrowser(ctx.env)) {
163
+ const follow = await ctx.confirm(t("ui.run.follow"), ctx.settings.config.stromAppFollow !== "no");
164
+ if (ctx.settings.config.stromAppFollow !== (follow ? "yes" : "no")) {
165
+ ctx.settings.config.stromAppFollow = follow ? "yes" : "no";
166
+ ctx.settings.save();
167
+ }
168
+ if (follow)
169
+ await run(["app", "--live"]);
103
170
  }
104
171
  out(t("ui.run.start"));
105
- await run(["run", "--max", String(n)]);
172
+ await run(["run", ...args]);
106
173
  await pause(lang);
107
174
  },
108
175
  }, {
@@ -121,28 +188,25 @@ export async function runMenu(ctx, run) {
121
188
  },
122
189
  }, {
123
190
  key: "4",
124
- label: t("ui.menu.results"),
125
- act: async () => {
126
- const outDir = path.join(root, "output");
127
- const geds = ["tree.ged", "tree-strom.ged"].map((f) => path.join(outDir, f)).filter((f) => fs.existsSync(f));
128
- if (!geds.length)
129
- out(t("ui.results.none"));
130
- else {
131
- out(t("ui.results.files"));
132
- for (const g of geds)
133
- out(` ${ctx.display(g)}`);
134
- const strom = geds.find((g) => g.endsWith("tree-strom.ged"));
135
- const app = stromAppState(ctx.settings);
136
- if (strom && (app === "yes" || app === "seen"))
137
- out(t("ui.results.app", { file: ctx.display(strom) }));
138
- else if (strom && app === "unknown")
139
- out(t("ui.results.app.maybe", { file: ctx.display(strom) }));
140
- if (await ctx.confirm(t("ui.results.open"), false))
141
- openForUser(outDir, ctx.env);
142
- }
143
- await pause(lang);
144
- },
145
- });
191
+ label: t("ui.menu.browse"),
192
+ act: async () => browse(ctx, run, lang, root),
193
+ }, ...(tree.count("person") === 0 ? [] : [{
194
+ key: "4",
195
+ label: t("ui.menu.review"),
196
+ act: async () => {
197
+ // one person, looked at again: what the tree says of them elsewhere, what to read whole, what to check
198
+ const person = await pickPerson(ctx, lang, root, t("ui.review.who"));
199
+ if (!person)
200
+ return;
201
+ const family = await ctx.confirm(t("ui.review.family"), false);
202
+ await run(["review", person.id, ...(family ? ["--scope", "family"] : [])]);
203
+ const research = Tree.open(root, ctx.env).list("research").find((r) => r.direction === "person" && r.focus === person.id && r.review);
204
+ const open = research ? Tree.open(root, ctx.env).list("task").filter((x) => x.research === research.id && x.state === "open").length : 0;
205
+ if (research && open && (await ctx.confirm(t("ui.review.run", { n: open }), false)))
206
+ await run(["run", "--research", research.id, "--max", String(open)]);
207
+ await pause(lang);
208
+ },
209
+ }]));
146
210
  // Several agents on this computer: this conversation with another one (the default stays).
147
211
  const current = ctx.settings.agent(tree.config).value;
148
212
  const others = agentsHere(ctx.env)
@@ -153,13 +217,31 @@ export async function runMenu(ctx, run) {
153
217
  key: "9",
154
218
  label: t("ui.menu.otheragent"),
155
219
  act: async () => {
156
- const i = others.length === 1 ? 0 : await ctx.choose(t("ui.setup.agent.pick"), others.map((a) => ({ label: a.name })), 0);
220
+ // Enter (and 0) stay with the agent of the research: choosing another is a step the person takes on purpose.
221
+ const i = await ctx.choose(t("ui.setup.agent.pick"), others.map((a) => ({ label: a.name })), others.length, { back: t("ui.back.stay", { name: PROFILES[current]?.name ?? current }) });
157
222
  if (i !== undefined)
158
223
  await run(["chat", "--agent", others[i].id]);
159
224
  },
160
225
  });
161
- if (stromAppState(ctx.settings) !== "no")
162
- items.push({ key: "5", label: t("ui.menu.app"), act: async () => void (await run(["app"])) });
226
+ // The Strom app: open the research in it (live while an agent is at work); not known yet: what it is, and whether they want it.
227
+ const app = stromAppState(ctx.settings);
228
+ if (app !== "no") {
229
+ const atWork = liveWorkers(root).length > 0 || runsAtWork(root).length > 0;
230
+ const hasApp = app !== "unknown" || Boolean(installedStromApp(ctx.env, process.platform, stromAppUrl(ctx.settings)));
231
+ appItem = {
232
+ key: "5",
233
+ label: t(!hasApp ? "ui.menu.app.new" : atWork ? "ui.menu.app.watch" : "ui.menu.app"),
234
+ act: async () => {
235
+ if (hasApp)
236
+ return void (await run(["app"]));
237
+ const answer = await askStromApp(ctx, lang);
238
+ if (answer === "install" && (await ctx.confirm(t("ui.app.open.now"), true)))
239
+ await run(["app"]);
240
+ },
241
+ };
242
+ appTip = !hasApp ? "ui.menu.app.tip" : atWork ? "ui.menu.app.live" : undefined;
243
+ items.push(appItem);
244
+ }
163
245
  }
164
246
  else {
165
247
  // Several trees and none chosen: pick one first.
@@ -180,6 +262,10 @@ export async function runMenu(ctx, run) {
180
262
  if (root)
181
263
  items.push({ key: "8", label: known.length > 1 ? t("ui.menu.trees") : t("ui.menu.newtree"), act: async () => pickTree(ctx, run, lang) });
182
264
  items.push({ key: "0", label: t("ui.menu.quit"), act: async () => true });
265
+ // One quiet line on the Strom app: what it can do while the person has not said, or that the agent can be watched now.
266
+ const numbered = items.filter((it) => it.key !== "0");
267
+ if (appItem && appTip)
268
+ out(t(appTip, { key: numbered.indexOf(appItem) + 1 }));
183
269
  // Numbered in the order shown; 0 always quits.
184
270
  let n = 0;
185
271
  const i = await ctx.choose("", items.map((it) => ({ key: it.key === "0" ? "0" : String(++n), label: it.label })), 0);
@@ -192,18 +278,148 @@ export async function runMenu(ctx, run) {
192
278
  }
193
279
  }
194
280
  }
195
- /** Choose another family tree, or create a new one (and start its research). */
281
+ /** How many tasks of the queue the menu lists before the agent works alone. */
282
+ const RUN_LIST = 15;
283
+ /** "2", "1,4", "2-5", "1 3 6-8" → those numbers (1…max), in the order given, each once; undefined when anything else. */
284
+ export function pickNumbers(answer, max) {
285
+ const out = [];
286
+ for (const part of answer.replace(/\s*[-–]\s*/g, "-").split(/[\s,;]+/).filter(Boolean)) {
287
+ const m = /^(\d+)(?:\s*[-–]\s*(\d+))?$/.exec(part);
288
+ if (!m)
289
+ return undefined;
290
+ const [a, b] = [Number(m[1]), Number(m[2] ?? m[1])];
291
+ if (a < 1 || b > max || a > b)
292
+ return undefined;
293
+ for (let k = a; k <= b; k++)
294
+ if (!out.includes(k))
295
+ out.push(k);
296
+ }
297
+ return out.length ? out : undefined;
298
+ }
299
+ /** Ask for a person by name or ID; several match: pick one. Nobody (Enter, or no answers left): undefined. */
300
+ async function pickPerson(ctx, lang, root, question, suggested) {
301
+ const tree = Tree.open(root, ctx.env);
302
+ for (;;) {
303
+ if (ctx.io.answers !== undefined && ctx.io.answers.length === 0)
304
+ return undefined;
305
+ const who = (await ctx.ask(question, suggested)).trim();
306
+ if (!who || who === "0")
307
+ return undefined;
308
+ const byId = /^P\d+$/i.test(who) ? tree.get(who.toUpperCase()) : undefined;
309
+ const hits = byId && !byId.retracted ? [byId] : findPersons(tree, who);
310
+ if (!hits.length)
311
+ ctx.io.stdout(ui(lang, "ui.review.none", { who }) + "\n");
312
+ else if (hits.length === 1)
313
+ return hits[0];
314
+ else {
315
+ const i = await ctx.choose(ui(lang, "ui.review.pick"), hits.slice(0, 9).map((p) => ({ label: label(p) })), 0, { back: ui(lang, "ui.browse.back") });
316
+ return i === undefined ? undefined : hits[i];
317
+ }
318
+ }
319
+ }
320
+ /**
321
+ * Look through the family tree: the research at a glance, what is new, what the agent does next, one person, the
322
+ * ancestors of one, and the result files — each an ordinary command (stats, recent, plan, person card, pedigree),
323
+ * shown in the person's language.
324
+ */
325
+ async function browse(ctx, run, lang, root) {
326
+ const t = (key, values = {}) => ui(lang, key, values);
327
+ const out = (line = "") => ctx.io.stdout(line + "\n");
328
+ const pause = async () => {
329
+ if (!(ctx.io.answers !== undefined && ctx.io.answers.length === 0))
330
+ await ctx.ask(t("ui.enter"));
331
+ };
332
+ for (;;) {
333
+ if (ctx.io.answers !== undefined && ctx.io.answers.length === 0)
334
+ return;
335
+ const tree = Tree.open(root, ctx.env);
336
+ const people = tree.count("person") > 0;
337
+ const items = [
338
+ ...(people
339
+ ? [
340
+ { key: "1", label: t("ui.browse.stats"), act: async () => void (await run(["stats"])) },
341
+ { key: "2", label: t("ui.browse.recent"), act: async () => void (await run(["recent"])) },
342
+ { key: "2", label: t("ui.browse.plan"), act: async () => void (await run(["plan"])) },
343
+ {
344
+ key: "3",
345
+ label: t("ui.browse.card"),
346
+ act: async () => {
347
+ const p = await pickPerson(ctx, lang, root, t("ui.review.who"));
348
+ if (p)
349
+ await run(["person", "card", p.id]);
350
+ },
351
+ },
352
+ {
353
+ key: "4",
354
+ label: t("ui.browse.pedigree"),
355
+ act: async () => {
356
+ const main = mainPerson(tree);
357
+ const p = await pickPerson(ctx, lang, root, t("ui.browse.whose"), main ? displayName(tree.get(main)) : undefined);
358
+ if (p)
359
+ await run(["pedigree", p.id]);
360
+ },
361
+ },
362
+ ]
363
+ : []),
364
+ {
365
+ key: "5",
366
+ label: t("ui.browse.files"),
367
+ act: async () => {
368
+ const outDir = path.join(root, "output");
369
+ const geds = ["tree.ged", "tree-strom.ged"].map((f) => path.join(outDir, f)).filter((f) => fs.existsSync(f));
370
+ if (!geds.length)
371
+ out(t("ui.results.none"));
372
+ else {
373
+ out(t("ui.results.files"));
374
+ for (const g of geds)
375
+ out(` ${ctx.display(g)}`);
376
+ const strom = geds.find((g) => g.endsWith("tree-strom.ged"));
377
+ const app = stromAppState(ctx.settings);
378
+ if (strom && (app === "yes" || app === "seen"))
379
+ out(t("ui.results.app", { file: ctx.display(strom) }));
380
+ else if (strom && app === "unknown")
381
+ out(t("ui.results.app.maybe", { file: ctx.display(strom) }));
382
+ if (await ctx.confirm(t("ui.results.open"), false))
383
+ openForUser(outDir, ctx.env);
384
+ }
385
+ },
386
+ },
387
+ ...(stromAppState(ctx.settings) === "no" ? [] : [{ key: "6", label: t("ui.menu.app"), act: async () => void (await run(["app"])) }]),
388
+ { key: "0", label: t("ui.browse.back"), act: async () => true },
389
+ ];
390
+ out();
391
+ let n = 0;
392
+ const i = await ctx.choose("", items.map((it) => ({ key: it.key === "0" ? "0" : String(++n), label: it.label })), 0);
393
+ if (i === undefined)
394
+ return;
395
+ if ((await items[i].act()) === true)
396
+ return;
397
+ await pause();
398
+ }
399
+ }
400
+ /** Choose another family tree, or create a new one (and start its research). The one worked on now is suggested; 0 stays with it. */
196
401
  async function pickTree(ctx, run, lang) {
197
402
  const known = ctx.knownTrees();
403
+ let here;
404
+ try {
405
+ here = ctx.locateTree();
406
+ }
407
+ catch {
408
+ here = undefined;
409
+ }
410
+ const at = known.findIndex((k) => k.root === here);
198
411
  const options = [...known.map((k) => ({ label: `${k.name} (${ctx.display(k.root)})` })), { label: ui(lang, "ui.trees.new") }];
199
- const i = await ctx.choose(ui(lang, "ui.trees.pick"), options, 0);
200
- if (i === undefined)
412
+ const back = at >= 0 ? { back: ui(lang, "ui.back.stay", { name: known[at].name }) } : {};
413
+ const i = await ctx.choose(ui(lang, "ui.trees.pick"), options, Math.max(0, at), back);
414
+ if (i === undefined || i === at)
201
415
  return;
202
416
  if (i < known.length) {
203
417
  await run(["trees", "use", known[i].root], true);
204
418
  return;
205
419
  }
206
- const name = await ctx.ask(ui(lang, "ui.tree.name"), ui(lang, "ui.tree.default"));
420
+ const name = (await ctx.ask(ui(lang, "ui.tree.name.new"), ui(lang, "ui.tree.default"))).trim();
421
+ if (name === "0")
422
+ return;
207
423
  const made = await createTree(ctx, run, lang, name);
208
424
  if (!made)
209
425
  return;
package/dist/cli/ui.js CHANGED
@@ -17,6 +17,9 @@ export const UI = {
17
17
  "ui.consent.level.full": "Let the agent do everything except what strom forbids, without asking you?\n\nOnly for a computer where nothing else of value is kept.",
18
18
  "ui.consent.connector": "Allow the downloader \"{name}\" to fetch images for the research?",
19
19
  "ui.consent.connector.revoke": "Stop the downloader \"{name}\"?",
20
+ "ui.consent.host.pace": "Your own pace for {host}: at least {seconds} s between two requests, {cap}. Going too fast slows the archive for everyone and can get your IP blocked — set it so?",
21
+ "ui.pace.cap": "at most {n} an hour",
22
+ "ui.pace.nocap": "no hourly cap",
20
23
  "ui.consent.host": "Allow automatic downloading from {host}?",
21
24
  "ui.consent.host.revoke": "Stop automatic downloading from {host}?",
22
25
  "ui.consent.connector.hosts": "It will reach only {hosts} — slowly, through strom's limiter.",
@@ -25,6 +28,7 @@ export const UI = {
25
28
  "ui.consent.connector.terms": "The archive's terms of use: {terms}",
26
29
  "ui.consent.host.unblock": "{host} refused us. Allow it again — only if the archive said it is fine?",
27
30
  "ui.consent.connectors.off": "Let downloaders run without asking you first?",
31
+ "ui.migrated": "The family tree's data were brought up to date for this version of strom ({step}).",
28
32
  "ui.menu.title": "Strom research {version}",
29
33
  "ui.menu.tree": "Family tree: {name} · {persons} people",
30
34
  "ui.menu.waiting": "Waiting for you: {count}",
@@ -34,14 +38,13 @@ export const UI = {
34
38
  "ui.menu.otheragent": "Talk with another agent this time",
35
39
  "ui.menu.run": "Let the agent work on its own",
36
40
  "ui.menu.waitlist": "What waits for you",
37
- "ui.menu.results": "Results",
38
41
  "ui.menu.settings": "Settings",
39
42
  "ui.menu.doctor": "Check the installation",
40
43
  "ui.menu.trees": "Another family tree",
41
44
  "ui.menu.newtree": "Create a family tree",
42
45
  "ui.menu.quit": "Quit",
43
46
  "ui.menu.bye": "See you next time. Start again with: strom",
44
- "ui.run.how": "How many tasks should the agent work on? (it stops earlier when there is nothing to do)",
47
+ "ui.run.how": "How many tasks? (it stops earlier when there is nothing to do; 0 goes back)",
45
48
  "ui.run.start": "The agent works on its own now. You can stop it with Ctrl-C — nothing is lost.",
46
49
  "ui.waiting.none": "Nothing waits for you.",
47
50
  "ui.waiting.open": "Open the folder for the images?",
@@ -56,7 +59,7 @@ export const UI = {
56
59
  "ui.tree.default": "My family",
57
60
  "ui.chat.open": "Opening the conversation with the agent ({agent}). When you are done, type {exit}.",
58
61
  "ui.chat.first": "Hello, I want to start researching my ancestors. Run `strom`, explain to me in a few sentences how we will work together, and ask me whom we are looking for and what we already know.",
59
- "ui.chat.continue": "Let's continue the research. Run `strom`, tell me briefly where the research stands and what waits for me, suggest what to do next, and tell me in a sentence what I can ask you about the tree.",
62
+ "ui.chat.continue": "Let's continue the research. Run `strom`, tell me briefly where the research stands and what waits for me, and suggest what to do next.",
60
63
  "ui.setup.welcome": "Welcome to Strom research. Let's set up a few things — press Enter to accept the suggested answer.",
61
64
  "ui.setup.again": "Settings — press Enter to keep the current value.",
62
65
  "ui.setup.lang": "Language (cs, en, de, pl, …)",
@@ -86,14 +89,24 @@ export const UI = {
86
89
  "ui.setup.shortcut": "Put a \"Strom research\" shortcut on the desktop?",
87
90
  "ui.setup.shortcut.done": "✓ Shortcut: {file}",
88
91
  "ui.setup.skill": "✓ {agent} now knows strom in any folder.",
89
- "ui.setup.app": "✓ The Strom app is on this computer — results will be ready to import into it.",
92
+ "ui.setup.app": "✓ The Strom app is on this computer — the menu opens the research in it, and while the agent works the app follows it live.",
90
93
  "ui.setup.done": "All set.",
94
+ "ui.setup.stromapp": "The Strom app (free, in the browser) shows the research as a family tree — each entry with its image cut out of the scan, the stories of the ancestors — and follows it live while the agent works. Do you want to use it?",
95
+ "ui.setup.stromapp.install": "Yes — install it now (it opens in the browser, strom says where to click)",
96
+ "ui.setup.stromapp.later": "Yes, but not now",
97
+ "ui.setup.stromapp.no": "No — do not mention it again",
98
+ "ui.menu.app.watch": "Watch the agent at work in the Strom app (live)",
99
+ "ui.menu.app.new": "The Strom app — the research as a family tree, followed live",
100
+ "ui.menu.app.tip": "Tip: the research can be seen as a family tree in the Strom app, and followed live while the agent works — choice {key}.",
101
+ "ui.menu.app.live": "The agent is at work — you can watch it live in the Strom app: choice {key}.",
102
+ "ui.app.open.now": "Once it is installed: open the research in it?",
103
+ "ui.run.follow": "Watch the work live in the Strom app meanwhile?",
91
104
  "ui.app.opened": "Opening the Strom app ({where}).",
92
105
  "ui.app.browser": "in your browser",
93
106
  "ui.app.url": "Open it here: {url}",
94
107
  "ui.app.installed": "The Strom app is installed ({kind}).",
95
108
  "ui.app.install": "The Strom app opened in your browser. To install it as an app, click the install icon at the right end of the address bar (Chrome, Edge); in Safari choose File → Add to Dock.",
96
- "ui.menu.app": "Open the Strom app",
109
+ "ui.menu.app": "Open the research in the Strom app",
97
110
  "ui.chat.agent.missing": "{agent} is not installed — run the settings (strom setup) to install it.",
98
111
  "ui.shortcut.done": "Shortcut on the desktop: {file}",
99
112
  "ui.o.title": "strom {version} — genealogical research toolkit for AI agents",
@@ -208,17 +221,29 @@ export const UI = {
208
221
  "ui.o.results": "results",
209
222
  "ui.o.results.installed": "{file} — for the Strom app, installed here: strom app opens it (in the app: Import; on the web: {url})",
210
223
  "ui.o.results.offer": "{file} — for the Strom app, not installed here yet: offer the user to install it from {url} (strom app install opens it there)",
224
+ "ui.o.results.offered": "{file} — for the Strom app, not installed here; the user has heard of it — bring it up again only when they want to see the results (strom app install, then strom app)",
225
+ "ui.o.ask": "asking",
226
+ "ui.o.ask.tell": "tell the user once, in a sentence, that they can ask you here about anyone in the tree — strom says this once",
211
227
  "ui.o.results.plain": "{file} — for any family tree program",
212
- "ui.o.results.installed.live": "{file} — for the Strom app, installed here: strom app opens this research in it, and while you work the app follows it live (what you record shows there by itself) — offer it once",
228
+ "ui.o.results.installed.live": "{file} — for the Strom app, installed here: strom app opens this research in it, and while you work the app follows it live (what you record shows there by itself) — offer it (strom says this once)",
213
229
  "ui.o.results.offer.live": "{file} — for the Strom app, not installed here yet: offer the user to watch the research grow in it live — installed from {url} (strom app install opens it there and says where to click), then strom app",
214
230
  "ui.chat.handover": "The research goes on in its own conversation: a new window opened with {agent} in your family tree's folder. Continue there — this conversation can end.",
215
231
  "ui.chat.handover.failed": "A new window could not be opened here. Start the research yourself: run strom in a terminal (or the Strom research shortcut on the desktop).",
232
+ "ui.chat.handover.recent": "A window with the research and {agent} opened a moment ago — continue there. This conversation can end.",
216
233
  "ui.chat.handover.end": "Continue there — this conversation can end.",
217
234
  "ui.why.handover": "the research belongs in its own conversation in the tree folder (its permissions, no asking at every step): this opens it — tell the user to continue there; go on here only if they want",
218
235
  "ui.app.research": "Opening the research {name} in the Strom app ({where}): the first time as a new tree, after that the same tree, updated.",
219
236
  "ui.app.following": "The Strom app follows the research {name} while it goes on ({where}): what is found shows there by itself.",
237
+ "ui.menu.review": "Review one person: check and complete what is known of them",
238
+ "ui.review.who": "Whom? (a name or an ID such as P0012; Enter or 0 goes back)",
239
+ "ui.review.none": "Nobody called \"{who}\" in this family tree.",
240
+ "ui.review.pick": "Which one?",
241
+ "ui.review.family": "With their spouse and children?",
242
+ "ui.review.run": "Let the agent work on it now, on its own? ({n} tasks)",
243
+ "ui.o.reread": "second reading",
244
+ "ui.o.reread.offer": "{n} facts rest only on readings by another model ({models}); the user reads with {model} now — offer it once, in a sentence: strom review <whom> --reread (a task per person; the user decides)",
220
245
  "ui.o.stories": "stories",
221
- "ui.o.stories.tell": "on (the default): when the research starts, tell the user in a sentence that you will also write stories of their ancestors for the family book in the Strom app, and that they may say no — strom config set stories no (yes: strom config set stories yes)",
246
+ "ui.o.stories.tell": "on (the default): tell the user (strom says this once) in a sentence that you will also write stories of their ancestors for the family book in the Strom app, and that they may say no — strom config set stories no (yes: strom config set stories yes)",
222
247
  "ui.o.stories.on": "on: strom proposes a story once records tell a life (narrate tasks)",
223
248
  "ui.o.stories.off": "off (the user said no) — do not bring it up",
224
249
  "ui.setup.model.strong": "The research needs a strong model — it reads old handwriting and weighs the records: in {agent}, choose its best model, not a fast or mini one.",
@@ -254,11 +279,135 @@ export const UI = {
254
279
  "ui.update.done": "✓ strom is updated: {previous} → {version}.",
255
280
  "ui.o.update": "update",
256
281
  "ui.o.update.new": "{version} is out (this is {current}) — tell the user; strom update installs it with their yes",
282
+ "ui.app.where.installed": "the installed app",
257
283
  "ui.app.drag": "The Strom app opened. Drag the file {file}{shown} into its window — the first time it becomes a new tree, after that the same tree is updated. With Chrome or Edge on this computer, strom hands it over by itself.",
258
284
  "ui.app.drag.shown": " (shown in the file window)",
259
285
  "ui.app.nolive": "Following the research live needs Chrome or Edge — Safari does not let a web page reach this computer.",
286
+ "ui.app.images": "With images: {n} entries cut out of their scans ({mb} MB).",
287
+ "ui.app.images.unclipped": "{n} more sources are on images, but nobody has marked where their entry is yet — the agent can do it.",
288
+ "ui.app.images.smaller": "To fit the limit of {limit} MB, the images are smaller than set: {level} instead of {asked}.",
289
+ "ui.app.images.left": "{n} entries farthest from the research are without their image, to fit the limit of {limit} MB.",
290
+ "ui.app.images.scope": "{n} entries of people farther from the research have no image (setting excerpts.for: {for}).",
260
291
  "ui.doc.installed": "strom {version} on its own Node {node} (official, from nodejs.org)",
261
292
  "ui.update.source": "A new version is out: {version}. This strom runs from its sources: git pull",
293
+ "ui.update.node.available": "Node {from} → {to}: security fixes of its line — strom update installs them.",
294
+ "ui.update.node.sure": "Update strom's Node from {from} to {to} (security fixes)?",
295
+ "ui.update.node.done": "✓ Node is updated: {from} → {to}.",
296
+ "ui.menu.browse": "Look through the family tree: overview, people, ancestors, what is new",
297
+ "ui.browse.stats": "Overview of the research",
298
+ "ui.browse.recent": "What is new (the last 7 days)",
299
+ "ui.browse.card": "One person: their life, family and records",
300
+ "ui.browse.pedigree": "The ancestors of one person",
301
+ "ui.browse.files": "The result files (GEDCOM)",
302
+ "ui.browse.back": "Back",
303
+ "ui.browse.whose": "Whose ancestors? (a name or an ID such as P0012; 0 goes back)",
304
+ "ui.date.about": "about {date}",
305
+ "ui.date.before": "before {date}",
306
+ "ui.date.after": "after {date}",
307
+ "ui.date.between": "between {a} and {b}",
308
+ "ui.date.fromto": "from {a} to {b}",
309
+ "ui.date.from": "from {date}",
310
+ "ui.date.to": "until {date}",
311
+ "ui.st.proven": "proven",
312
+ "ui.st.probable": "probable",
313
+ "ui.st.possible": "possible",
314
+ "ui.st.lead": "only a lead",
315
+ "ui.ev.BIRT": "birth",
316
+ "ui.ev.CHR": "baptism",
317
+ "ui.ev.DEAT": "death",
318
+ "ui.ev.BURI": "burial",
319
+ "ui.ev.CREM": "cremation",
320
+ "ui.ev.MARR": "marriage",
321
+ "ui.ev.MARB": "banns",
322
+ "ui.ev.ENGA": "engagement",
323
+ "ui.ev.DIV": "divorce",
324
+ "ui.ev.OCCU": "occupation",
325
+ "ui.ev.RESI": "residence",
326
+ "ui.ev.EDUC": "education",
327
+ "ui.ev.RELI": "religion",
328
+ "ui.ev.TITL": "title",
329
+ "ui.ev.CENS": "census",
330
+ "ui.ev.CONF": "confirmation",
331
+ "ui.ev.EMIG": "emigration",
332
+ "ui.ev.IMMI": "immigration",
333
+ "ui.ev.NATU": "naturalisation",
334
+ "ui.ev.PROB": "probate",
335
+ "ui.ev.WILL": "will",
336
+ "ui.gen.2": "parents",
337
+ "ui.gen.3": "grandparents",
338
+ "ui.gen.4": "great-grandparents",
339
+ "ui.gen.5": "great-great-grandparents",
340
+ "ui.gen.n": "generation {n}",
341
+ "ui.card.house": "{place}, house {house}",
342
+ "ui.card.also": "other forms of the name: {names}",
343
+ "ui.card.nofacts": "no facts recorded yet",
344
+ "ui.card.parents": "Parents: {names}",
345
+ "ui.card.noparents": "Parents: not known yet",
346
+ "ui.card.wife": "Wife: {name}",
347
+ "ui.card.husband": "Husband: {name}",
348
+ "ui.card.partner": "Partner: {name}",
349
+ "ui.card.unknown": "not known yet",
350
+ "ui.card.children": "Children: {names}",
351
+ "ui.card.story": "Story ({status}):",
352
+ "ui.card.story.draft": "a draft",
353
+ "ui.card.story.final": "approved",
354
+ "ui.stats.title": "{name} – the research at a glance",
355
+ "ui.stats.counts": "People: {persons} · families: {families} · records from sources: {sources} · scans: {images} · places: {places}",
356
+ "ui.stats.facts": "Facts: proven {proven} · probable {probable} · possible {possible} · only leads {lead}",
357
+ "ui.stats.from": "Ancestors of {name}:",
358
+ "ui.stats.gen": "{known} of {expected} known · birth proven: {proven}",
359
+ "ui.stats.oldest": "Earliest-born proven ancestor: {name}",
360
+ "ui.stats.noancestors": "No ancestors recorded yet.",
361
+ "ui.stats.tasks": "Tasks: in the queue {queued} · waiting for you {waiting} · done {done}",
362
+ "ui.stats.sessions": "Sessions of the agent: {n} · the last {last}",
363
+ "ui.stats.cost": " · cost so far {cost}",
364
+ "ui.stats.stories": "Stories: {n} (approved {final})",
365
+ "ui.ped.noparents": "parents not known yet",
366
+ "ui.ped.nofather": "father not known yet",
367
+ "ui.ped.nomother": "mother not known yet",
368
+ "ui.ped.seen": "(shown above)",
369
+ "ui.ped.legend": "Father first, then mother · ✓ birth proven by a record",
370
+ "ui.ped.more": "The known ancestors go on to generation {n}: strom pedigree {id} --generations {n}",
371
+ "ui.recent.title": "What came in since {since}",
372
+ "ui.recent.nothing": "Nothing new since {since}.",
373
+ "ui.recent.counts": "New people: {persons} · facts: new {facts}, refined {refined} · records from sources: {sources} · stories: {stories} · tasks done: {done}",
374
+ "ui.recent.persons": "New people:",
375
+ "ui.recent.facts": "New and refined facts:",
376
+ "ui.recent.refined": "refined",
377
+ "ui.recent.stories": "Stories written:",
378
+ "ui.recent.sessions": "What the agent did:",
379
+ "ui.recent.more": "… and {n} more",
380
+ "ui.browse.plan": "What the agent will do next",
381
+ "ui.plan.title": "What the agent will do next – {name}:",
382
+ "ui.plan.now": "now",
383
+ "ui.plan.images": " (needs the scans first)",
384
+ "ui.plan.more": "… and {n} more tasks",
385
+ "ui.plan.stories": "Then the stories: {n} ({names})",
386
+ "ui.plan.waiting": "Waiting for you: {n} (in the menu: What waits for you)",
387
+ "ui.plan.none": "Nothing in the queue — the agent proposes new tasks after its next session, or tell it in a conversation what to look for.",
388
+ "ui.plan.steer": "To change the order, tell the agent in a conversation what comes first.",
389
+ "ui.plan.session": "the session of {day}",
390
+ "ui.back.stay": "Back – stay with {name}",
391
+ "ui.keep": "Keep it as it is",
392
+ "ui.tree.name.new": "What should the new family tree be called? (e.g. the family surname; 0 goes back)",
393
+ "ui.run.skip": " · {task} left out: it is done or dropped already",
394
+ "ui.run.skip.held": " · {task} left out: another agent is working on it",
395
+ "ui.run.next": "Next in the queue, in the order the agent takes them:",
396
+ "ui.run.story": " (a story)",
397
+ "ui.run.what": "What should the agent do?",
398
+ "ui.run.queue": "Work through the queue in this order – you say how many",
399
+ "ui.run.pick": "Only the tasks you pick from the list",
400
+ "ui.run.which": "Which ones? Their numbers, e.g. 2 or 1,4 or 2-5 (0 goes back)",
401
+ "ui.run.which.bad": "Numbers from the list, 1 to {n}: e.g. 2 or 1,4 or 2-5.",
402
+ "ui.run.limit": "One task has at most {minutes} minutes; then the agent is stopped and the task goes back to the queue (setting run.minutes).",
403
+ "ui.run.empty": "Nothing in the queue — tell the agent in a conversation what to look for.",
404
+ "ui.run.browser": "· {name}: images through your browser — the agent gets browser tools for {hosts}; Chrome with the Claude extension must be running",
405
+ "ui.run.browser.model": " note: browser tools need a model that can review its actions — Sonnet or Opus, not {model}: strom config set model.lead sonnet",
406
+ "ui.run.full": "· permissions: full — the agent does everything but what the tree's permissions deny (back: strom config set agent.permissions auto)",
407
+ "ui.menu.working.more": "{n} agents are working on their own right now.",
408
+ "ui.run.beside": "⚠ An agent is already working on its own here (runs: {n}). Another one beside it takes other tasks — the costs add up.",
409
+ "ui.run.beside.sure": "Start another one beside it?",
410
+ "ui.run.others": "⚠ Another run works in this family tree already (since {since}): this one takes other tasks — the costs add up.",
262
411
  };
263
412
  /** A text for the person, in their language. */
264
413
  export function ui(lang, key, values = {}) {