strom-research 1.1.1 → 1.3.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 (50) hide show
  1. package/README.md +6 -1
  2. package/assets/lang/cs.json +177 -6
  3. package/assets/lang/de.json +177 -6
  4. package/assets/method/core.md +6 -2
  5. package/assets/method/reading.md +2 -1
  6. package/assets/plugins/README.md +16 -2
  7. package/assets/plugins/gates/README.md +95 -0
  8. package/assets/plugins/gates/claude-usage/gate.json +10 -0
  9. package/assets/plugins/gates/claude-usage/gate.ts +125 -0
  10. package/assets/plugins/hooks/README.md +91 -0
  11. package/dist/agents/global.js +10 -0
  12. package/dist/agents/launch.js +1 -1
  13. package/dist/brief/brief.js +2 -0
  14. package/dist/cli/main.js +43 -2
  15. package/dist/cli/menu-parts.js +140 -0
  16. package/dist/cli/menu-research.js +194 -0
  17. package/dist/cli/menu-settings.js +273 -0
  18. package/dist/cli/menu-waiting.js +79 -0
  19. package/dist/cli/menu.js +113 -99
  20. package/dist/cli/ui.js +175 -5
  21. package/dist/cli/wizard.js +48 -2
  22. package/dist/commands/browse.js +4 -3
  23. package/dist/commands/connectors.js +25 -2
  24. package/dist/commands/gates.js +58 -0
  25. package/dist/commands/hooks.js +141 -0
  26. package/dist/commands/index.js +2 -0
  27. package/dist/commands/intake.js +2 -2
  28. package/dist/commands/research.js +60 -17
  29. package/dist/commands/session.js +161 -18
  30. package/dist/commands/setup.js +57 -1
  31. package/dist/commands/sources.js +24 -0
  32. package/dist/commands/start.js +5 -1
  33. package/dist/commands/tasks.js +41 -4
  34. package/dist/core/browser.js +50 -0
  35. package/dist/core/clock.js +55 -0
  36. package/dist/core/config.js +18 -0
  37. package/dist/core/gate.js +175 -0
  38. package/dist/core/hooks.js +210 -0
  39. package/dist/core/media.js +21 -7
  40. package/dist/core/overview.js +2 -0
  41. package/dist/core/phrases.js +1 -0
  42. package/dist/core/relocate.js +111 -0
  43. package/dist/core/tree.js +19 -2
  44. package/dist/runners/antigravity.js +2 -1
  45. package/dist/runners/claude.js +160 -114
  46. package/dist/runners/codex.js +14 -1
  47. package/dist/runners/jsonl.js +63 -37
  48. package/dist/runners/opencode.js +7 -2
  49. package/dist/runners/script.js +56 -47
  50. package/package.json +1 -1
package/dist/cli/menu.js CHANGED
@@ -14,6 +14,7 @@ import { liveHolder } from "../core/lock.js";
14
14
  import { newerVersion } from "../core/update.js";
15
15
  import { taskQueue, waitingForUser } from "../commands/tasks.js";
16
16
  import { humanTask } from "../commands/browse.js";
17
+ import { loadGate } from "../core/gate.js";
17
18
  import { lacksImages } from "../core/queue.js";
18
19
  import { othersAtWork } from "../core/session.js";
19
20
  import { DEFAULT_RUN_MINUTES } from "../core/config.js";
@@ -23,7 +24,12 @@ import { chromiumBrowser } from "../core/chromium.js";
23
24
  import { liveWorkers, runsAtWork } from "../core/workers.js";
24
25
  import { askStromApp } from "./wizard.js";
25
26
  import { openForUser } from "../core/open.js";
26
- import { displayName, findPersons, label } from "../core/people.js";
27
+ import { displayName } from "../core/people.js";
28
+ import { agentReady, guarded, pause as partsPause, pickPerson, subMenu } from "./menu-parts.js";
29
+ import { offerAgent } from "./fixes.js";
30
+ import { addToResearch } from "./menu-research.js";
31
+ import { waitingForYou } from "./menu-waiting.js";
32
+ import { settingsMenu } from "./menu-settings.js";
27
33
  import { mainPerson } from "../core/kin.js";
28
34
  import { AGENTS, findAgent } from "../core/which.js";
29
35
  import { agentsHere } from "../core/apps.js";
@@ -71,10 +77,13 @@ export async function runMenu(ctx, run) {
71
77
  if (outOfAnswers())
72
78
  return;
73
79
  const name = await ctx.ask(t("ui.tree.name"), t("ui.tree.default"));
74
- if (!(await createTree(ctx, run, lang, name)))
80
+ const made = await createTree(ctx, run, lang, name);
81
+ if (!made)
75
82
  return;
76
83
  reload();
77
- await run(["chat"]);
84
+ // Straight into the first conversation — with an agent here; without one, the menu says how to get it.
85
+ if (agentReady(ctx, Tree.open(made, ctx.env).config))
86
+ await run(["chat"]);
78
87
  if (outOfAnswers())
79
88
  return;
80
89
  continue;
@@ -82,26 +91,48 @@ export async function runMenu(ctx, run) {
82
91
  const items = [];
83
92
  let appItem;
84
93
  let appTip;
94
+ let other;
85
95
  out();
86
96
  out(t("ui.menu.title", { version: VERSION }));
87
97
  const newer = await newerVersion(ctx.settings, ctx.env);
88
- if (newer)
89
- out(t("ui.menu.update.line", { version: newer }));
90
98
  if (root) {
91
99
  const tree = Tree.open(root, ctx.env);
92
100
  const waiting = waitingForUser(tree).length;
93
101
  const started = tree.list("research").length > 0;
94
102
  out(t("ui.menu.tree", { name: tree.config.name, persons: tree.count("person") }));
103
+ // No agent yet (none installed, or not the one chosen): the research waits for it, and item 1 gets one.
104
+ const ready = agentReady(ctx, tree.config);
105
+ if (!ready)
106
+ out(t(agentsHere(ctx.env).length ? "ui.menu.noagent.chosen" : "ui.menu.noagent"));
95
107
  if (waiting)
96
108
  out(t("ui.menu.waiting", { count: waiting }));
97
109
  const runs = runsAtWork(root).length;
98
110
  if (runs)
99
111
  out(t(runs > 1 ? "ui.menu.working.more" : "ui.menu.working", { n: runs }));
100
- items.push({ key: "1", label: t(started ? "ui.menu.chat" : "ui.menu.start"), act: async () => void (await run(["chat"])) }, {
112
+ items.push(ready
113
+ ? { key: "1", label: t(started ? "ui.menu.chat" : "ui.menu.start"), act: async () => void (await run(["chat"])) }
114
+ : {
115
+ key: "1",
116
+ label: t("ui.menu.getagent"),
117
+ act: async () => {
118
+ // Nothing here at all: Claude Code offered (or the page about agents); another one here: the wizard lets them pick it.
119
+ if (agentsHere(ctx.env).length)
120
+ await run(["setup"]);
121
+ else
122
+ await offerAgent(ctx, out);
123
+ await pause(lang);
124
+ },
125
+ }, {
101
126
  key: "2",
102
127
  label: t("ui.menu.run"),
103
128
  act: async () => {
104
129
  // Working alone is the agent's CLI; with only its app on this computer, strom says so.
130
+ // No agent here yet: said, and item 1 gets one — the item stays, so the numbers never move.
131
+ if (!ready) {
132
+ out(t(agentsHere(ctx.env).length ? "ui.menu.noagent.chosen" : "ui.menu.noagent"));
133
+ await pause(lang);
134
+ return;
135
+ }
105
136
  const who = PROFILES[ctx.settings.agent(tree.config).value];
106
137
  if (who && !findAgent(who.command, ctx.env)) {
107
138
  out(t("ui.run.needscli", { agent: who.name }));
@@ -129,10 +160,15 @@ export async function runMenu(ctx, run) {
129
160
  out(` ${t("ui.plan.more", { n: queue.length - shown.length })}`);
130
161
  }
131
162
  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;
163
+ // With a gate of the user's (run.gate): on for as long as there is work and the gate lets it.
164
+ const gate = gateTitle(ctx);
165
+ const ways = [{ label: t("ui.run.queue") }, { label: t("ui.run.pick") }, ...(gate ? [{ label: t("ui.run.loop", { name: gate }) }] : [])];
166
+ const how = shown.length ? await ctx.choose(t("ui.run.what"), ways, 0, { back: t("ui.browse.back") }) : 0;
133
167
  if (how === undefined)
134
168
  return;
135
- if (how === 0) {
169
+ if (how === 2)
170
+ args = ["--loop"];
171
+ else if (how === 0) {
136
172
  let n = -1;
137
173
  while (!(Number.isInteger(n) && n >= 0 && n <= 100)) {
138
174
  if (outOfAnswers())
@@ -176,44 +212,28 @@ export async function runMenu(ctx, run) {
176
212
  key: "3",
177
213
  label: `${t("ui.menu.waitlist")}${waiting ? ` (${waiting})` : ""}`,
178
214
  act: async () => {
179
- if (!waiting)
180
- out(t("ui.waiting.none"));
181
- else {
182
- await run(["task", "list", "--state", "waiting"]);
183
- const inbox = ctx.settings.shared() ? path.join(ctx.settings.shared().value, "inbox") : undefined;
184
- if (inbox && (await ctx.confirm(t("ui.waiting.open"), false)))
185
- openForUser(inbox, ctx.env);
186
- }
215
+ if (waiting)
216
+ return waitingForYou(ctx, run, lang, root);
217
+ out(t("ui.waiting.none"));
187
218
  await pause(lang);
188
219
  },
220
+ }, {
221
+ key: "4",
222
+ label: t("ui.menu.more"),
223
+ act: async () => addToResearch(ctx, run, lang, root),
189
224
  }, {
190
225
  key: "4",
191
226
  label: t("ui.menu.browse"),
192
227
  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
- }]));
228
+ });
210
229
  // Several agents on this computer: this conversation with another one (the default stays).
211
230
  const current = ctx.settings.agent(tree.config).value;
212
231
  const others = agentsHere(ctx.env)
213
232
  .filter((a) => a.id !== current)
214
233
  .map((a) => AGENTS.find((x) => x.id === a.id));
215
- if (others.length)
216
- items.push({
234
+ other = !others.length
235
+ ? undefined
236
+ : {
217
237
  key: "9",
218
238
  label: t("ui.menu.otheragent"),
219
239
  act: async () => {
@@ -222,7 +242,7 @@ export async function runMenu(ctx, run) {
222
242
  if (i !== undefined)
223
243
  await run(["chat", "--agent", others[i].id]);
224
244
  },
225
- });
245
+ };
226
246
  // 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
247
  const app = stromAppState(ctx.settings);
228
248
  if (app !== "no") {
@@ -231,13 +251,7 @@ export async function runMenu(ctx, run) {
231
251
  appItem = {
232
252
  key: "5",
233
253
  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
- },
254
+ act: async () => openStromApp(ctx, run, lang, hasApp),
241
255
  };
242
256
  appTip = !hasApp ? "ui.menu.app.tip" : atWork ? "ui.menu.app.live" : undefined;
243
257
  items.push(appItem);
@@ -247,31 +261,28 @@ export async function runMenu(ctx, run) {
247
261
  // Several trees and none chosen: pick one first.
248
262
  items.push({ key: "1", label: t("ui.menu.trees"), act: async () => pickTree(ctx, run, lang) });
249
263
  }
250
- items.push({ key: "6", label: t("ui.menu.settings"), act: async () => void (await run(["setup"])) }, {
251
- key: "7",
252
- label: t("ui.menu.doctor"),
253
- act: async () => {
254
- const code = await run(["doctor"]);
255
- if (code !== 0 && (await ctx.confirm(t("ui.menu.fix"), true)))
256
- await run(["doctor", "--fix"]);
257
- await pause(lang);
258
- },
259
- });
260
- if (newer)
261
- items.push({ key: "9", label: t("ui.menu.update", { version: newer }), act: async () => void (await run(["update"])) });
264
+ // What changes nothing of the research: the newer strom, the settings of this computer (with the check), another tree.
265
+ // At most nine: the newer strom is taken in the settings, the line above says where.
266
+ const settings = { key: "6", label: t("ui.menu.settings"), act: async () => settingsMenu(ctx, run, lang, root, newer) };
267
+ items.push(settings);
262
268
  if (root)
263
269
  items.push({ key: "8", label: known.length > 1 ? t("ui.menu.trees") : t("ui.menu.newtree"), act: async () => pickTree(ctx, run, lang) });
270
+ // Only with several agents here: last, so that it moves no other number.
271
+ if (other)
272
+ items.push(other);
264
273
  items.push({ key: "0", label: t("ui.menu.quit"), act: async () => true });
265
274
  // 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
275
  const numbered = items.filter((it) => it.key !== "0");
267
276
  if (appItem && appTip)
268
277
  out(t(appTip, { key: numbered.indexOf(appItem) + 1 }));
278
+ if (newer)
279
+ out(t("ui.menu.update.where", { version: newer, key: numbered.indexOf(settings) + 1 }));
269
280
  // Numbered in the order shown; 0 always quits.
270
281
  let n = 0;
271
282
  const i = await ctx.choose("", items.map((it) => ({ key: it.key === "0" ? "0" : String(++n), label: it.label })), 0);
272
283
  if (i === undefined)
273
284
  return;
274
- const quit = await items[i].act();
285
+ const quit = await guarded(ctx, lang, items[i].act);
275
286
  if (quit === true) {
276
287
  out(t("ui.menu.bye"));
277
288
  return;
@@ -280,6 +291,20 @@ export async function runMenu(ctx, run) {
280
291
  }
281
292
  /** How many tasks of the queue the menu lists before the agent works alone. */
282
293
  const RUN_LIST = 15;
294
+ /** The title of the user's gate (run.gate), when there is one to ask. */
295
+ function gateTitle(ctx) {
296
+ const name = ctx.settings.runGate();
297
+ const shared = ctx.settings.shared()?.value;
298
+ if (!name || !shared)
299
+ return undefined;
300
+ try {
301
+ const g = loadGate(shared, name);
302
+ return g.manifest.title ?? g.name;
303
+ }
304
+ catch {
305
+ return name; // the run says what is wrong with it
306
+ }
307
+ }
283
308
  /** "2", "1,4", "2-5", "1 3 6-8" → those numbers (1…max), in the order given, each once; undefined when anything else. */
284
309
  export function pickNumbers(answer, max) {
285
310
  const out = [];
@@ -296,27 +321,6 @@ export function pickNumbers(answer, max) {
296
321
  }
297
322
  return out.length ? out : undefined;
298
323
  }
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
324
  /**
321
325
  * Look through the family tree: the research at a glance, what is new, what the agent does next, one person, the
322
326
  * ancestors of one, and the result files — each an ordinary command (stats, recent, plan, person card, pedigree),
@@ -325,28 +329,27 @@ async function pickPerson(ctx, lang, root, question, suggested) {
325
329
  async function browse(ctx, run, lang, root) {
326
330
  const t = (key, values = {}) => ui(lang, key, values);
327
331
  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"));
332
+ // A long answer waits for Enter before the menu comes again; 0 anywhere goes back without it.
333
+ const show = async (argv) => {
334
+ await run(argv);
335
+ await partsPause(ctx, lang);
331
336
  };
332
- for (;;) {
333
- if (ctx.io.answers !== undefined && ctx.io.answers.length === 0)
334
- return;
337
+ await subMenu(ctx, lang, () => {
335
338
  const tree = Tree.open(root, ctx.env);
336
339
  const people = tree.count("person") > 0;
337
340
  const items = [
338
341
  ...(people
339
342
  ? [
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
+ { key: "1", label: t("ui.browse.stats"), act: async () => show(["stats"]) },
344
+ { key: "2", label: t("ui.browse.recent"), act: async () => show(["recent"]) },
345
+ { key: "2", label: t("ui.browse.plan"), act: async () => show(["plan"]) },
343
346
  {
344
347
  key: "3",
345
348
  label: t("ui.browse.card"),
346
349
  act: async () => {
347
350
  const p = await pickPerson(ctx, lang, root, t("ui.review.who"));
348
351
  if (p)
349
- await run(["person", "card", p.id]);
352
+ await show(["person", "card", p.id]);
350
353
  },
351
354
  },
352
355
  {
@@ -356,7 +359,7 @@ async function browse(ctx, run, lang, root) {
356
359
  const main = mainPerson(tree);
357
360
  const p = await pickPerson(ctx, lang, root, t("ui.browse.whose"), main ? displayName(tree.get(main)) : undefined);
358
361
  if (p)
359
- await run(["pedigree", p.id]);
362
+ await show(["pedigree", p.id]);
360
363
  },
361
364
  },
362
365
  ]
@@ -384,18 +387,28 @@ async function browse(ctx, run, lang, root) {
384
387
  }
385
388
  },
386
389
  },
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 },
390
+ ...(stromAppState(ctx.settings) === "no" ? [] : [{ key: "6", label: t("ui.menu.app"), act: async () => openStromApp(ctx, run, lang, stromAppState(ctx.settings) !== "unknown") }]),
389
391
  ];
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();
392
+ return { items };
393
+ });
394
+ }
395
+ /**
396
+ * The Strom app: not asked about yet — what it is, and whether they want it; wanted but not installed here — installing
397
+ * it from the browser (an app of its own, PWA) is offered first, else it opens in the browser; installed — it opens.
398
+ */
399
+ async function openStromApp(ctx, run, lang, said) {
400
+ const t = (key) => ui(lang, key);
401
+ if (!said) {
402
+ const answer = await askStromApp(ctx, lang);
403
+ if (answer === "install" && (await ctx.confirm(t("ui.app.open.now"), true)))
404
+ await run(["app"]);
405
+ return;
406
+ }
407
+ if (!installedStromApp(ctx.env, process.platform, stromAppUrl(ctx.settings)) && (await ctx.confirm(t("ui.app.notinstalled"), true))) {
408
+ await run(["app", "install"]);
409
+ return;
398
410
  }
411
+ await run(["app"]);
399
412
  }
400
413
  /** Choose another family tree, or create a new one (and start its research). The one worked on now is suggested; 0 stays with it. */
401
414
  async function pickTree(ctx, run, lang) {
@@ -424,5 +437,6 @@ async function pickTree(ctx, run, lang) {
424
437
  if (!made)
425
438
  return;
426
439
  await run(["trees", "use", made], true);
427
- await run(["chat"]);
440
+ if (agentReady(ctx, Tree.open(made, ctx.env).config))
441
+ await run(["chat"]);
428
442
  }
package/dist/cli/ui.js CHANGED
@@ -38,7 +38,7 @@ export const UI = {
38
38
  "ui.menu.otheragent": "Talk with another agent this time",
39
39
  "ui.menu.run": "Let the agent work on its own",
40
40
  "ui.menu.waitlist": "What waits for you",
41
- "ui.menu.settings": "Settings",
41
+ "ui.menu.settings": "Settings: the agent, working alone, messages, archives, the check",
42
42
  "ui.menu.doctor": "Check the installation",
43
43
  "ui.menu.trees": "Another family tree",
44
44
  "ui.menu.newtree": "Create a family tree",
@@ -348,9 +348,7 @@ export const UI = {
348
348
  "ui.card.partner": "Partner: {name}",
349
349
  "ui.card.unknown": "not known yet",
350
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",
351
+ "ui.card.story": "Story:",
354
352
  "ui.stats.title": "{name} – the research at a glance",
355
353
  "ui.stats.counts": "People: {persons} · families: {families} · records from sources: {sources} · scans: {images} · places: {places}",
356
354
  "ui.stats.facts": "Facts: proven {proven} · probable {probable} · possible {possible} · only leads {lead}",
@@ -361,7 +359,8 @@ export const UI = {
361
359
  "ui.stats.tasks": "Tasks: in the queue {queued} · waiting for you {waiting} · done {done}",
362
360
  "ui.stats.sessions": "Sessions of the agent: {n} · the last {last}",
363
361
  "ui.stats.cost": " · cost so far {cost}",
364
- "ui.stats.stories": "Stories: {n} (approved {final})",
362
+ "ui.stats.cost.partial": " (without sessions stopped before they said what they cost: {n})",
363
+ "ui.stats.stories": "Stories: {n}",
365
364
  "ui.ped.noparents": "parents not known yet",
366
365
  "ui.ped.nofather": "father not known yet",
367
366
  "ui.ped.nomother": "mother not known yet",
@@ -378,6 +377,20 @@ export const UI = {
378
377
  "ui.recent.sessions": "What the agent did:",
379
378
  "ui.recent.more": "… and {n} more",
380
379
  "ui.browse.plan": "What the agent will do next",
380
+ "ui.review.new": "{name} — {n} new task(s):",
381
+ "ui.review.nothing": "{name} — nothing new to review",
382
+ "ui.review.would": " would add: {level} · {what}",
383
+ "ui.review.dry": "(dry run — nothing written)",
384
+ "ui.review.covered": " {name}: already covered by {task} ({state}) — {what}",
385
+ "ui.review.wake": " to go on with it: strom task wake {task}",
386
+ "ui.review.exhausted": " {name}: searched in vain ({tasks}) — what next is your decision",
387
+ "ui.review.open": "{n} task(s) open in {research}, about as many sessions",
388
+ "ui.review.cost": " — a session here costs ${cost} on average",
389
+ "ui.review.next": "next: strom run --research {research} (the agent alone) · or in a conversation: strom session start --research {research}",
390
+ "ui.review.state.open": "in the queue",
391
+ "ui.review.state.doing": "being worked on",
392
+ "ui.review.state.parked": "parked",
393
+ "ui.review.state.waiting": "waiting for you",
381
394
  "ui.plan.title": "What the agent will do next – {name}:",
382
395
  "ui.plan.now": "now",
383
396
  "ui.plan.images": " (needs the scans first)",
@@ -408,6 +421,163 @@ export const UI = {
408
421
  "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
422
  "ui.run.beside.sure": "Start another one beside it?",
410
423
  "ui.run.others": "⚠ Another run works in this family tree already (since {since}): this one takes other tasks — the costs add up.",
424
+ "ui.run.gate": "· condition: {name} — asked before each session; it may make the run wait",
425
+ "ui.run.gate.wait": "⏸ waiting until {at} — {reason} (Ctrl-C ends the run)",
426
+ "ui.run.stop.gate": "the condition “{name}” said stop — {reason}",
427
+ "ui.run.stop.gate.error": "the condition “{name}” gave no proper answer — {reason} (strom gate test)",
428
+ "ui.run.gate.once": "· condition: {name} — these tasks are yours to start: it is asked once, and if it would not start, you decide",
429
+ "ui.run.gate.anyway": "The condition “{name}” would not start now — {reason}. Start anyway?",
430
+ "ui.run.gate.later": "not yet",
431
+ "ui.run.gate.no": "no",
432
+ "ui.run.gate.noanswer": "no proper answer (strom gate test)",
433
+ "ui.run.stop.gate.declined": "not started — the condition “{name}”: {reason}",
434
+ "ui.run.loop": "Let it work on while there is work and the condition “{name}” allows it",
435
+ "ui.consent.gate.set": "Let the condition “{name}” decide when the agent working alone goes on?",
436
+ "ui.consent.gate.unset": "Remove the condition “{name}” — the agent working alone no longer asks it?",
437
+ "ui.consent.gate.skip": "Let the agent work on its own without the condition “{name}”?",
438
+ "ui.consent.remote": "Start the Claude Code sessions strom opens with Remote Control, so that you can follow and steer them from claude.ai or the Claude app on your phone?",
439
+ "ui.consent.hook.on": "Let the hook “{name}” be told of everything saved into your researches? It is a program on this computer and does with it what it was written to do.",
440
+ "ui.consent.hook.off": "Stop telling the hook “{name}” what is saved?",
441
+ "ui.menu.more": "Add to the research: material, a new direction, one person reviewed",
442
+ "ui.more.title": "Add to the research",
443
+ "ui.more.intake": "Material from the family: documents, photos, a family tree from another program, what you know",
444
+ "ui.more.research": "A new direction: the ancestors or descendants of someone else, or one question",
445
+ "ui.intake.what": "What do you bring?",
446
+ "ui.intake.files": "Files or a folder from this computer",
447
+ "ui.intake.text": "What you know — names, dates, places, family memories",
448
+ "ui.intake.write": "Write what you know (Enter or 0 goes back)",
449
+ "ui.intake.path": "The folder or file — you can drag it into this window (Enter or 0 goes back)",
450
+ "ui.intake.missing": "Not found: {path} — check the path, or drag the folder in.",
451
+ "ui.intake.scans": "The folder {folder} holds {n} images. Are they family documents and photos? (no: scans of a register or another book)",
452
+ "ui.intake.book": "{folder}: scans of a register or another book go with that book — tell the agent in a conversation where they come from, and it registers them.",
453
+ "ui.intake.known": "This material is in the research already — nothing added.",
454
+ "ui.intake.done": "✓ Added to the research: {n}. The agent goes through it in its next work.",
455
+ "ui.intake.chat": "Go through it with the agent now?",
456
+ "ui.intake.say": "I added new material to the research — let us go through it.",
457
+ "ui.research.kind": "What do you want to find out?",
458
+ "ui.research.ancestors": "The ancestors of someone",
459
+ "ui.research.descendants": "The descendants of someone",
460
+ "ui.research.question": "One question (e.g. who was Mary's father?)",
461
+ "ui.research.ask": "The question (Enter or 0 goes back)",
462
+ "ui.research.whom": "Who is it about?",
463
+ "ui.research.known": "Someone already in the family tree",
464
+ "ui.research.newperson": "Someone new",
465
+ "ui.research.name": "Their name (e.g. John Smith; 0 goes back)",
466
+ "ui.research.sex": "A man or a woman?",
467
+ "ui.research.male": "A man",
468
+ "ui.research.female": "A woman",
469
+ "ui.research.unknown": "I don't know",
470
+ "ui.research.title": "The research's name",
471
+ "ui.research.title.ancestors": "Ancestors: {name}",
472
+ "ui.research.title.descendants": "Descendants: {name}",
473
+ "ui.research.knows": "What do you know of it? Dates, places, stories of the family help the agent (Enter: nothing)",
474
+ "ui.research.done": "✓ The research “{name}” is started.",
475
+ "ui.research.chat": "Start on it with the agent now?",
476
+ "ui.research.say": "I started a new research, “{name}” — let us begin with it.",
477
+ "ui.settings.title": "Settings (on this computer)",
478
+ "ui.settings.wizard": "Language, agent, model, what the agent may do alone, stories… (the setup)",
479
+ "ui.settings.alone": "The agent working alone: {minutes} min a task · condition: {gate}",
480
+ "ui.settings.gate.none": "none",
481
+ "ui.settings.hooks": "Messages of what is saved (hooks): {state}",
482
+ "ui.settings.on": "on",
483
+ "ui.settings.off": "off",
484
+ "ui.settings.remote": "Watch the agent from your phone (Remote Control): {state}",
485
+ "ui.settings.remote.about": "With it on you can follow and steer the Claude Code sessions strom starts — conversations and working alone — from claude.ai or the Claude app on your phone.",
486
+ "ui.settings.remote.on": "Turn it on?",
487
+ "ui.settings.remote.off": "Turn it off?",
488
+ "ui.settings.archives": "Archives: your logins, downloaders asking first",
489
+ "ui.alone.title": "The agent working alone",
490
+ "ui.alone.minutes": "Time for one task: {minutes} min",
491
+ "ui.alone.minutes.ask": "How many minutes may one task take? (5–600; 0 goes back)",
492
+ "ui.alone.minutes.bad": "Please a number of minutes from 5 to 600.",
493
+ "ui.alone.gate": "Working on and on under a condition: {gate}",
494
+ "ui.alone.gate.about": "A condition is asked before each session while the agent works on and on by itself; the menu offers that only with one set. Tasks you start yourself are your choice.",
495
+ "ui.alone.gate.pick": "Which condition?",
496
+ "ui.alone.gate.claude": "By how many points should the week's usage of your Claude subscription stay behind the part of the week gone? (a day ≈ 14.3; 0: just not ahead of the week)",
497
+ "ui.alone.gate.args": "What to give it (Enter: nothing)",
498
+ "ui.alone.test": "What would the condition say now?",
499
+ "ui.alone.asking": "Asking the condition…",
500
+ "ui.alone.said.go": "It would start now{reason}",
501
+ "ui.alone.said.wait": "It would wait until {at}{reason}",
502
+ "ui.alone.said.stop": "It would stop{reason}",
503
+ "ui.alone.said.error": "No proper answer{reason}",
504
+ "ui.hooks.title": "Hooks — a number turns one on or off:",
505
+ "ui.hooks.on": "{name}: on",
506
+ "ui.hooks.off": "{name}: off",
507
+ "ui.hooks.none": "No hooks here yet. A hook is your own program told of what is saved (e.g. a message to your phone) — how: {readme}",
508
+ "ui.archives.title": "Downloaders of the archives here:",
509
+ "ui.archives.empty": "No downloaders here yet — the agent builds one when the research needs an archive.",
510
+ "ui.archives.login.saved": "your login saved {date}",
511
+ "ui.archives.login.none": "can use your account — no login saved",
512
+ "ui.archives.consent": "Downloaders ask you before they run: {state}",
513
+ "ui.archives.consent.about": "Off (usual): downloaders run by themselves — slowly, and only to their archive. On: each needs your yes first, here in the terminal, and so does each archive it reaches.",
514
+ "ui.archives.consent.on": "Turn it on?",
515
+ "ui.archives.consent.off": "Turn it off?",
516
+ "ui.hooks.nothing": "none",
517
+ "ui.menu.noagent": "No AI agent on this computer yet — the research is its work. Item 1 installs one.",
518
+ "ui.menu.getagent": "Install or choose the AI agent",
519
+ "ui.menu.update.where": "A new version of strom is out: {version} — update it in the settings (item {key}).",
520
+ "ui.more.noagent": "The agent takes it up once it is on this computer (item 1 of the menu).",
521
+ "ui.menu.noagent.chosen": "The AI agent this research uses is not on this computer — item 1 chooses another.",
522
+ "ui.waiting.title": "The agent needs you for {n} thing(s):",
523
+ "ui.waiting.do": "What to do: {what}",
524
+ "ui.waiting.ask": "the agent will tell you in a conversation",
525
+ "ui.waiting.images": "Save by hand: images {images} of {book}",
526
+ "ui.waiting.more": "…and {n} more — the agent goes through them with you in a conversation.",
527
+ "ui.waiting.pick": "Have you got it? Choose the number and write what you found:",
528
+ "ui.waiting.answer": "Answer {k}: {what}",
529
+ "ui.waiting.folder": "Open the folder for the images of {k}",
530
+ "ui.waiting.opened": "The folder: {dir} — once the images are there, the next work of the agent takes them in by itself.",
531
+ "ui.waiting.write": "Your answer — a link, a book's number, what you found out (Enter or 0 goes back)",
532
+ "ui.waiting.back": "✓ The task goes back to the agent with your answer.",
533
+ "ui.waiting.chat": "Go on with the agent now?",
534
+ "ui.waiting.say": "I answered what you asked of me: {what} — let us go on with it.",
535
+ "ui.intake.empty": "There is nothing in it to take in.",
536
+ "ui.intake.ours": "{path} is strom's own folder (the research itself), not material of the family.",
537
+ "ui.research.exists": "This research is there already: “{name}” — the agent goes on with it.",
538
+ "ui.alone.gate.bad": "Please a number of points from 0 to 100 (e.g. 10).",
539
+ "ui.waiting.long": "That is long for an answer here (at most {n} characters) — a link or a sentence; the rest tell the agent in a conversation.",
540
+ "ui.settings.remote.chats": "conversations yes, working alone no",
541
+ "ui.settings.remote.own": "Conversations with the agent you can watch from your phone already — Claude Code does it itself (its own setting). The agent working alone (item 2 of the menu) only once this is on too.",
542
+ "ui.settings.remote.both": "conversations and working alone",
543
+ "ui.settings.remote.onalone": "Turn it on for working alone too?",
544
+ "ui.archives.login.nobody": "None of them needs a login: the archives are open. A login shows here only for a downloader of a portal with an account (perhaps a paid one).",
545
+ "ui.archives.login.how": "Your login is typed in here and kept on this computer only; strom puts it into the downloader's requests — neither the downloader nor the agent sees it.",
546
+ "ui.archives.login.save": "Save your login to {title}",
547
+ "ui.archives.login.change": "Change your login to {title}",
548
+ "ui.home.what": "The research is in {from} (family trees: {trees}), with the shared folder — downloaders, hooks, the inbox, big images. It moves to {to} as a whole.",
549
+ "ui.home.sure": "Move it there?",
550
+ "ui.home.moved": "✓ The research is in {to} now.",
551
+ "ui.home.copied": "✓ The research is copied to {to} (another disk) and works from there. The old folder {from} is left as it was — delete it once you have seen all is well.",
552
+ "ui.home.stays": "The research stays in {from}.",
553
+ "ui.home.inside": "{to} is inside the research's folder {from} (or the other way round) — pick a folder beside it.",
554
+ "ui.home.notEmpty": "{to} holds something already — the research moves only into an empty or new folder.",
555
+ "ui.home.busy": "Somebody is at work in {trees} right now (an agent, a run, or the Strom app following it live) — the research can move once that has finished.",
556
+ "ui.home.failed": "It could not be moved ({reason}) — nothing changed.",
557
+ "ui.app.notinstalled": "The Strom app is not installed on this computer yet. Install it now as an app from the browser (free; strom shows where to click)?",
558
+ "ui.browser.agent": "{names}: through the browser — that only Claude Code can (the Claude in Chrome extension); this research is done by {agent}. Save those images by hand, or let Claude Code do the research (strom agents use claude).",
559
+ "ui.browser.noext": "{names}: through the browser — Chrome needs the Claude in Chrome extension, signed in to the account Claude Code uses; it is not in a browser here: {url}",
560
+ "ui.doc.browser": "browser (Claude in Chrome)",
561
+ "ui.doc.browser.ok": "the extension in {browsers} — Chrome must run while the agent works",
562
+ "ui.doc.browser.noext": "the extension is not installed (needed by {names})",
563
+ "ui.doc.browser.agent": "{names} go through the browser — only Claude Code can (now: {agent})",
564
+ "ui.archives.via.direct": "images directly, paced by strom",
565
+ "ui.archives.via.browser": "images through your browser",
566
+ "ui.archives.via.manual": "images saved by hand (the archive allows no automation)",
567
+ "ui.archives.browser.ok": "Through the browser: the Claude in Chrome extension is in {browsers}; Chrome must run while the agent works, signed in to the account Claude Code uses.",
568
+ "ui.archives.to.browser": "{title}: fetch through your browser instead",
569
+ "ui.archives.to.direct": "{title}: fetch directly instead",
570
+ "ui.archives.browser.about": "Through your browser the agent fetches the images in your own Chrome (where your login to the archive is), with the Claude in Chrome extension — only Claude Code can. strom still plans and paces every request.",
571
+ "ui.archives.direct.about": "Directly, strom fetches the images itself, paced, without a browser — any agent can.",
572
+ "ui.archives.switch": "Switch it?",
573
+ "ui.archives.now.browser": "✓ {title}: through your browser from now on.",
574
+ "ui.archives.now.direct": "✓ {title}: directly from now on.",
575
+ "ui.setup.home.again": "Where to keep the research (0 keeps it where it is)",
576
+ "ui.home.env": "The research's folder {from} is set from outside (STROM_HOME) — it stays; change it there.",
577
+ "ui.menu.failed": "That did not work: {reason}",
578
+ "ui.review.next.human": "The agent takes them up in a conversation or when it works alone.",
579
+ "ui.review.wake.human": " to go on with it, tell the agent in a conversation",
580
+ "ui.waiting.gone": "This one no longer waits for you — the agent has taken it up meanwhile.",
411
581
  };
412
582
  /** A text for the person, in their language. */
413
583
  export function ui(lang, key, values = {}) {