strom-research 1.2.0 → 1.4.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 (46) hide show
  1. package/README.md +4 -1
  2. package/assets/lang/cs.json +164 -7
  3. package/assets/lang/de.json +164 -7
  4. package/assets/plugins/README.md +8 -1
  5. package/assets/plugins/gates/README.md +1 -1
  6. package/assets/plugins/hooks/README.md +91 -0
  7. package/dist/agents/files.js +142 -99
  8. package/dist/agents/global.js +83 -6
  9. package/dist/agents/launch.js +12 -5
  10. package/dist/agents/profiles.js +13 -2
  11. package/dist/cli/main.js +17 -1
  12. package/dist/cli/menu-parts.js +140 -0
  13. package/dist/cli/menu-research.js +194 -0
  14. package/dist/cli/menu-settings.js +273 -0
  15. package/dist/cli/menu-waiting.js +79 -0
  16. package/dist/cli/menu.js +91 -97
  17. package/dist/cli/registry.js +1 -1
  18. package/dist/cli/ui.js +162 -6
  19. package/dist/cli/wizard.js +48 -2
  20. package/dist/commands/browse.js +2 -2
  21. package/dist/commands/connectors.js +25 -2
  22. package/dist/commands/hooks.js +141 -0
  23. package/dist/commands/index.js +1 -0
  24. package/dist/commands/intake.js +2 -2
  25. package/dist/commands/meta.js +6 -2
  26. package/dist/commands/research.js +55 -16
  27. package/dist/commands/session.js +15 -7
  28. package/dist/commands/setup.js +51 -2
  29. package/dist/commands/start.js +9 -4
  30. package/dist/commands/tasks.js +41 -4
  31. package/dist/core/browser.js +50 -0
  32. package/dist/core/config.js +7 -1
  33. package/dist/core/hooks.js +210 -0
  34. package/dist/core/install.js +3 -2
  35. package/dist/core/media.js +21 -7
  36. package/dist/core/phrases.js +1 -0
  37. package/dist/core/relocate.js +111 -0
  38. package/dist/core/shortcut.js +1 -4
  39. package/dist/core/tree.js +19 -2
  40. package/dist/core/which.js +18 -0
  41. package/dist/runners/claude.js +3 -0
  42. package/dist/runners/codex.js +14 -5
  43. package/dist/runners/grok.js +108 -0
  44. package/dist/runners/index.js +2 -0
  45. package/dist/runners/opencode.js +8 -4
  46. package/package.json +1 -1
@@ -0,0 +1,79 @@
1
+ // The menu's "What waits for you": each task that waits for the person, what
2
+ // to do for it (in their language, as the agent wrote it), the folder for the
3
+ // images they save by hand — and their answer, taken in here (strom task wake
4
+ // --answer): the task goes back to the agent with it.
5
+ import path from "node:path";
6
+ import { subMenu, translator } from "./menu-parts.js";
7
+ import { offerChat } from "./menu-research.js";
8
+ import { truncate } from "./format.js";
9
+ import { Tree } from "../core/tree.js";
10
+ import { openForUser } from "../core/open.js";
11
+ /** At most this many are listed to answer (the menu's nine); the rest after them. */
12
+ const SHOWN = 9;
13
+ /** An answer is a note on the task (500 characters, with what it asked). */
14
+ const ANSWER_MAX = 300;
15
+ export async function waitingForYou(ctx, run, lang, root) {
16
+ const t = translator(lang);
17
+ const out = (line) => ctx.io.stdout(line + "\n");
18
+ const shared = ctx.settings.shared()?.value;
19
+ const inbox = (x) => (shared && x.awaits ? path.join(shared, "inbox", x.awaits.folder) : undefined);
20
+ await subMenu(ctx, lang, () => {
21
+ const tree = Tree.open(root, ctx.env);
22
+ const waiting = tree.list("task").filter((x) => x.state === "waiting");
23
+ if (!waiting.length)
24
+ return { title: t("ui.waiting.none"), items: [] };
25
+ const shown = waiting.slice(0, SHOWN);
26
+ const text = [t("ui.waiting.title", { n: waiting.length })];
27
+ for (const [k, x] of shown.entries()) {
28
+ text.push("", ` ${k + 1}. ${x.what}`);
29
+ if (x.awaits) {
30
+ const b = tree.get(x.awaits.recordset);
31
+ text.push(` ${t("ui.waiting.images", { images: x.awaits.images, book: b?.title ?? x.awaits.recordset })}${b?.url ? ` · ${b.url}` : ""}`);
32
+ text.push(` ${t("ui.wait.save", { dir: `${ctx.display(inbox(x) ?? `inbox/${x.awaits.folder}`)}${path.sep}` })}`);
33
+ }
34
+ else
35
+ text.push(` ${t("ui.waiting.do", { what: x.waitingOn || t("ui.waiting.ask") })}`);
36
+ }
37
+ if (shown.some((x) => x.awaits))
38
+ text.push("", t("ui.wait.how1"), t("ui.wait.how2"));
39
+ if (waiting.length > SHOWN)
40
+ text.push("", t("ui.waiting.more", { n: waiting.length - SHOWN }));
41
+ const items = shown.map((x, k) => x.awaits
42
+ ? {
43
+ key: String(k + 1),
44
+ label: t("ui.waiting.folder", { k: k + 1 }),
45
+ act: async () => {
46
+ const dir = inbox(x);
47
+ if (dir)
48
+ openForUser(dir, ctx.env);
49
+ out(t("ui.waiting.opened", { dir: dir ? ctx.display(dir) : `inbox/${x.awaits.folder}` }));
50
+ },
51
+ }
52
+ : {
53
+ key: String(k + 1),
54
+ label: t("ui.waiting.answer", { k: k + 1, what: truncate(x.what, 60) }),
55
+ act: async () => {
56
+ let answer = "";
57
+ for (;;) {
58
+ answer = (await ctx.ask(t("ui.waiting.write"))).trim();
59
+ if (!answer || answer === "0")
60
+ return;
61
+ // kept as a note on the task, with what it asked: short — a longer story is for a conversation
62
+ if (answer.length <= ANSWER_MAX)
63
+ break;
64
+ out(t("ui.waiting.long", { n: ANSWER_MAX }));
65
+ if (ctx.io.answers !== undefined && ctx.io.answers.length === 0)
66
+ return;
67
+ }
68
+ // meanwhile taken up by an agent (done, dropped, back in the queue): said, nothing written
69
+ if (Tree.open(root, ctx.env).get(x.id)?.state !== "waiting")
70
+ return void out(t("ui.waiting.gone"));
71
+ if ((await run(["task", "wake", x.id, `--answer=${answer}`], true)) !== 0)
72
+ return;
73
+ out(t("ui.waiting.back"));
74
+ await offerChat(ctx, run, lang, root, t("ui.waiting.chat"), t("ui.waiting.say", { what: truncate(x.what, 80) }));
75
+ },
76
+ });
77
+ return { title: `${text.join("\n")}\n\n${t("ui.waiting.pick")}`, items };
78
+ });
79
+ }
package/dist/cli/menu.js CHANGED
@@ -24,7 +24,12 @@ import { chromiumBrowser } from "../core/chromium.js";
24
24
  import { liveWorkers, runsAtWork } from "../core/workers.js";
25
25
  import { askStromApp } from "./wizard.js";
26
26
  import { openForUser } from "../core/open.js";
27
- 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";
28
33
  import { mainPerson } from "../core/kin.js";
29
34
  import { AGENTS, findAgent } from "../core/which.js";
30
35
  import { agentsHere } from "../core/apps.js";
@@ -72,10 +77,13 @@ export async function runMenu(ctx, run) {
72
77
  if (outOfAnswers())
73
78
  return;
74
79
  const name = await ctx.ask(t("ui.tree.name"), t("ui.tree.default"));
75
- if (!(await createTree(ctx, run, lang, name)))
80
+ const made = await createTree(ctx, run, lang, name);
81
+ if (!made)
76
82
  return;
77
83
  reload();
78
- 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"]);
79
87
  if (outOfAnswers())
80
88
  return;
81
89
  continue;
@@ -83,26 +91,48 @@ export async function runMenu(ctx, run) {
83
91
  const items = [];
84
92
  let appItem;
85
93
  let appTip;
94
+ let other;
86
95
  out();
87
96
  out(t("ui.menu.title", { version: VERSION }));
88
97
  const newer = await newerVersion(ctx.settings, ctx.env);
89
- if (newer)
90
- out(t("ui.menu.update.line", { version: newer }));
91
98
  if (root) {
92
99
  const tree = Tree.open(root, ctx.env);
93
100
  const waiting = waitingForUser(tree).length;
94
101
  const started = tree.list("research").length > 0;
95
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"));
96
107
  if (waiting)
97
108
  out(t("ui.menu.waiting", { count: waiting }));
98
109
  const runs = runsAtWork(root).length;
99
110
  if (runs)
100
111
  out(t(runs > 1 ? "ui.menu.working.more" : "ui.menu.working", { n: runs }));
101
- 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
+ }, {
102
126
  key: "2",
103
127
  label: t("ui.menu.run"),
104
128
  act: async () => {
105
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
+ }
106
136
  const who = PROFILES[ctx.settings.agent(tree.config).value];
107
137
  if (who && !findAgent(who.command, ctx.env)) {
108
138
  out(t("ui.run.needscli", { agent: who.name }));
@@ -182,44 +212,28 @@ export async function runMenu(ctx, run) {
182
212
  key: "3",
183
213
  label: `${t("ui.menu.waitlist")}${waiting ? ` (${waiting})` : ""}`,
184
214
  act: async () => {
185
- if (!waiting)
186
- out(t("ui.waiting.none"));
187
- else {
188
- await run(["task", "list", "--state", "waiting"]);
189
- const inbox = ctx.settings.shared() ? path.join(ctx.settings.shared().value, "inbox") : undefined;
190
- if (inbox && (await ctx.confirm(t("ui.waiting.open"), false)))
191
- openForUser(inbox, ctx.env);
192
- }
215
+ if (waiting)
216
+ return waitingForYou(ctx, run, lang, root);
217
+ out(t("ui.waiting.none"));
193
218
  await pause(lang);
194
219
  },
220
+ }, {
221
+ key: "4",
222
+ label: t("ui.menu.more"),
223
+ act: async () => addToResearch(ctx, run, lang, root),
195
224
  }, {
196
225
  key: "4",
197
226
  label: t("ui.menu.browse"),
198
227
  act: async () => browse(ctx, run, lang, root),
199
- }, ...(tree.count("person") === 0 ? [] : [{
200
- key: "4",
201
- label: t("ui.menu.review"),
202
- act: async () => {
203
- // one person, looked at again: what the tree says of them elsewhere, what to read whole, what to check
204
- const person = await pickPerson(ctx, lang, root, t("ui.review.who"));
205
- if (!person)
206
- return;
207
- const family = await ctx.confirm(t("ui.review.family"), false);
208
- await run(["review", person.id, ...(family ? ["--scope", "family"] : [])]);
209
- const research = Tree.open(root, ctx.env).list("research").find((r) => r.direction === "person" && r.focus === person.id && r.review);
210
- const open = research ? Tree.open(root, ctx.env).list("task").filter((x) => x.research === research.id && x.state === "open").length : 0;
211
- if (research && open && (await ctx.confirm(t("ui.review.run", { n: open }), false)))
212
- await run(["run", "--research", research.id, "--max", String(open)]);
213
- await pause(lang);
214
- },
215
- }]));
228
+ });
216
229
  // Several agents on this computer: this conversation with another one (the default stays).
217
230
  const current = ctx.settings.agent(tree.config).value;
218
231
  const others = agentsHere(ctx.env)
219
232
  .filter((a) => a.id !== current)
220
233
  .map((a) => AGENTS.find((x) => x.id === a.id));
221
- if (others.length)
222
- items.push({
234
+ other = !others.length
235
+ ? undefined
236
+ : {
223
237
  key: "9",
224
238
  label: t("ui.menu.otheragent"),
225
239
  act: async () => {
@@ -228,7 +242,7 @@ export async function runMenu(ctx, run) {
228
242
  if (i !== undefined)
229
243
  await run(["chat", "--agent", others[i].id]);
230
244
  },
231
- });
245
+ };
232
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.
233
247
  const app = stromAppState(ctx.settings);
234
248
  if (app !== "no") {
@@ -237,13 +251,7 @@ export async function runMenu(ctx, run) {
237
251
  appItem = {
238
252
  key: "5",
239
253
  label: t(!hasApp ? "ui.menu.app.new" : atWork ? "ui.menu.app.watch" : "ui.menu.app"),
240
- act: async () => {
241
- if (hasApp)
242
- return void (await run(["app"]));
243
- const answer = await askStromApp(ctx, lang);
244
- if (answer === "install" && (await ctx.confirm(t("ui.app.open.now"), true)))
245
- await run(["app"]);
246
- },
254
+ act: async () => openStromApp(ctx, run, lang, hasApp),
247
255
  };
248
256
  appTip = !hasApp ? "ui.menu.app.tip" : atWork ? "ui.menu.app.live" : undefined;
249
257
  items.push(appItem);
@@ -253,31 +261,28 @@ export async function runMenu(ctx, run) {
253
261
  // Several trees and none chosen: pick one first.
254
262
  items.push({ key: "1", label: t("ui.menu.trees"), act: async () => pickTree(ctx, run, lang) });
255
263
  }
256
- items.push({ key: "6", label: t("ui.menu.settings"), act: async () => void (await run(["setup"])) }, {
257
- key: "7",
258
- label: t("ui.menu.doctor"),
259
- act: async () => {
260
- const code = await run(["doctor"]);
261
- if (code !== 0 && (await ctx.confirm(t("ui.menu.fix"), true)))
262
- await run(["doctor", "--fix"]);
263
- await pause(lang);
264
- },
265
- });
266
- if (newer)
267
- 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);
268
268
  if (root)
269
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);
270
273
  items.push({ key: "0", label: t("ui.menu.quit"), act: async () => true });
271
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.
272
275
  const numbered = items.filter((it) => it.key !== "0");
273
276
  if (appItem && appTip)
274
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 }));
275
280
  // Numbered in the order shown; 0 always quits.
276
281
  let n = 0;
277
282
  const i = await ctx.choose("", items.map((it) => ({ key: it.key === "0" ? "0" : String(++n), label: it.label })), 0);
278
283
  if (i === undefined)
279
284
  return;
280
- const quit = await items[i].act();
285
+ const quit = await guarded(ctx, lang, items[i].act);
281
286
  if (quit === true) {
282
287
  out(t("ui.menu.bye"));
283
288
  return;
@@ -316,27 +321,6 @@ export function pickNumbers(answer, max) {
316
321
  }
317
322
  return out.length ? out : undefined;
318
323
  }
319
- /** Ask for a person by name or ID; several match: pick one. Nobody (Enter, or no answers left): undefined. */
320
- async function pickPerson(ctx, lang, root, question, suggested) {
321
- const tree = Tree.open(root, ctx.env);
322
- for (;;) {
323
- if (ctx.io.answers !== undefined && ctx.io.answers.length === 0)
324
- return undefined;
325
- const who = (await ctx.ask(question, suggested)).trim();
326
- if (!who || who === "0")
327
- return undefined;
328
- const byId = /^P\d+$/i.test(who) ? tree.get(who.toUpperCase()) : undefined;
329
- const hits = byId && !byId.retracted ? [byId] : findPersons(tree, who);
330
- if (!hits.length)
331
- ctx.io.stdout(ui(lang, "ui.review.none", { who }) + "\n");
332
- else if (hits.length === 1)
333
- return hits[0];
334
- else {
335
- 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") });
336
- return i === undefined ? undefined : hits[i];
337
- }
338
- }
339
- }
340
324
  /**
341
325
  * Look through the family tree: the research at a glance, what is new, what the agent does next, one person, the
342
326
  * ancestors of one, and the result files — each an ordinary command (stats, recent, plan, person card, pedigree),
@@ -345,28 +329,27 @@ async function pickPerson(ctx, lang, root, question, suggested) {
345
329
  async function browse(ctx, run, lang, root) {
346
330
  const t = (key, values = {}) => ui(lang, key, values);
347
331
  const out = (line = "") => ctx.io.stdout(line + "\n");
348
- const pause = async () => {
349
- if (!(ctx.io.answers !== undefined && ctx.io.answers.length === 0))
350
- 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);
351
336
  };
352
- for (;;) {
353
- if (ctx.io.answers !== undefined && ctx.io.answers.length === 0)
354
- return;
337
+ await subMenu(ctx, lang, () => {
355
338
  const tree = Tree.open(root, ctx.env);
356
339
  const people = tree.count("person") > 0;
357
340
  const items = [
358
341
  ...(people
359
342
  ? [
360
- { key: "1", label: t("ui.browse.stats"), act: async () => void (await run(["stats"])) },
361
- { key: "2", label: t("ui.browse.recent"), act: async () => void (await run(["recent"])) },
362
- { 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"]) },
363
346
  {
364
347
  key: "3",
365
348
  label: t("ui.browse.card"),
366
349
  act: async () => {
367
350
  const p = await pickPerson(ctx, lang, root, t("ui.review.who"));
368
351
  if (p)
369
- await run(["person", "card", p.id]);
352
+ await show(["person", "card", p.id]);
370
353
  },
371
354
  },
372
355
  {
@@ -376,7 +359,7 @@ async function browse(ctx, run, lang, root) {
376
359
  const main = mainPerson(tree);
377
360
  const p = await pickPerson(ctx, lang, root, t("ui.browse.whose"), main ? displayName(tree.get(main)) : undefined);
378
361
  if (p)
379
- await run(["pedigree", p.id]);
362
+ await show(["pedigree", p.id]);
380
363
  },
381
364
  },
382
365
  ]
@@ -404,18 +387,28 @@ async function browse(ctx, run, lang, root) {
404
387
  }
405
388
  },
406
389
  },
407
- ...(stromAppState(ctx.settings) === "no" ? [] : [{ key: "6", label: t("ui.menu.app"), act: async () => void (await run(["app"])) }]),
408
- { 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") }]),
409
391
  ];
410
- out();
411
- let n = 0;
412
- const i = await ctx.choose("", items.map((it) => ({ key: it.key === "0" ? "0" : String(++n), label: it.label })), 0);
413
- if (i === undefined)
414
- return;
415
- if ((await items[i].act()) === true)
416
- return;
417
- 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;
418
410
  }
411
+ await run(["app"]);
419
412
  }
420
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. */
421
414
  async function pickTree(ctx, run, lang) {
@@ -444,5 +437,6 @@ async function pickTree(ctx, run, lang) {
444
437
  if (!made)
445
438
  return;
446
439
  await run(["trees", "use", made], true);
447
- await run(["chat"]);
440
+ if (agentReady(ctx, Tree.open(made, ctx.env).config))
441
+ await run(["chat"]);
448
442
  }
@@ -22,7 +22,7 @@ export const GLOBAL_OPTIONS = [
22
22
  { name: "home", type: "string", value: "<dir>", description: "Strom home folder (default from config)" },
23
23
  { name: "shared", type: "string", value: "<dir>", description: "shared data folder (media, catalog, tools)" },
24
24
  { name: "trees", type: "string", value: "<dir>", description: "folder holding the trees" },
25
- { name: "agent", type: "string", value: "<agent>", description: "AI agent: claude, codex, antigravity, opencode (default from config)" },
25
+ { name: "agent", type: "string", value: "<agent>", description: "AI agent: claude, codex, antigravity, opencode, grok (default from config)" },
26
26
  { name: "yes", type: "boolean", description: "accept suggested defaults, never ask" },
27
27
  { name: "debug", type: "boolean", description: "show technical details on errors" },
28
28
  { name: "version", type: "boolean", description: "print the version of strom" },
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",
@@ -154,7 +154,7 @@ export const UI = {
154
154
  "ui.run.stop.time": "time limit",
155
155
  "ui.run.stop.empty": "no open tasks",
156
156
  "ui.run.stop.problems": "the evidence has problems — strom check",
157
- "ui.run.stop.denied": "the agent was not allowed to run strom — its permissions are wrong (.claude/settings.json of the tree); nothing more was started",
157
+ "ui.run.stop.denied": "the agent was not allowed to run strom — its permissions or its sandbox kept it away; nothing more was started",
158
158
  "ui.run.stop.limit": "subscription limit{resets}",
159
159
  "ui.run.stop.resets": " (resets {at})",
160
160
  "ui.run.stop.auth": "the agent is not logged in — run {command} once and log in",
@@ -187,6 +187,7 @@ export const UI = {
187
187
  "ui.doc.yes": "yes",
188
188
  "ui.doc.default": "default",
189
189
  "ui.doc.noagent": "no AI agent installed",
190
+ "ui.doc.agent.other": "none strom knows is installed — strom is run by another agent or program now, which does the research through strom",
190
191
  "ui.doc.knows.yes": "yes, in any folder",
191
192
  "ui.doc.knows.no": "not yet: {agents}",
192
193
  "ui.doc.app.yes": "you use it",
@@ -348,9 +349,7 @@ export const UI = {
348
349
  "ui.card.partner": "Partner: {name}",
349
350
  "ui.card.unknown": "not known yet",
350
351
  "ui.card.children": "Children: {names}",
351
- "ui.card.story": "Story ({status}):",
352
- "ui.card.story.draft": "a draft",
353
- "ui.card.story.final": "approved",
352
+ "ui.card.story": "Story:",
354
353
  "ui.stats.title": "{name} – the research at a glance",
355
354
  "ui.stats.counts": "People: {persons} · families: {families} · records from sources: {sources} · scans: {images} · places: {places}",
356
355
  "ui.stats.facts": "Facts: proven {proven} · probable {probable} · possible {possible} · only leads {lead}",
@@ -362,7 +361,7 @@ export const UI = {
362
361
  "ui.stats.sessions": "Sessions of the agent: {n} · the last {last}",
363
362
  "ui.stats.cost": " · cost so far {cost}",
364
363
  "ui.stats.cost.partial": " (without sessions stopped before they said what they cost: {n})",
365
- "ui.stats.stories": "Stories: {n} (approved {final})",
364
+ "ui.stats.stories": "Stories: {n}",
366
365
  "ui.ped.noparents": "parents not known yet",
367
366
  "ui.ped.nofather": "father not known yet",
368
367
  "ui.ped.nomother": "mother not known yet",
@@ -379,6 +378,20 @@ export const UI = {
379
378
  "ui.recent.sessions": "What the agent did:",
380
379
  "ui.recent.more": "… and {n} more",
381
380
  "ui.browse.plan": "What the agent will do next",
381
+ "ui.review.new": "{name} — {n} new task(s):",
382
+ "ui.review.nothing": "{name} — nothing new to review",
383
+ "ui.review.would": " would add: {level} · {what}",
384
+ "ui.review.dry": "(dry run — nothing written)",
385
+ "ui.review.covered": " {name}: already covered by {task} ({state}) — {what}",
386
+ "ui.review.wake": " to go on with it: strom task wake {task}",
387
+ "ui.review.exhausted": " {name}: searched in vain ({tasks}) — what next is your decision",
388
+ "ui.review.open": "{n} task(s) open in {research}, about as many sessions",
389
+ "ui.review.cost": " — a session here costs ${cost} on average",
390
+ "ui.review.next": "next: strom run --research {research} (the agent alone) · or in a conversation: strom session start --research {research}",
391
+ "ui.review.state.open": "in the queue",
392
+ "ui.review.state.doing": "being worked on",
393
+ "ui.review.state.parked": "parked",
394
+ "ui.review.state.waiting": "waiting for you",
382
395
  "ui.plan.title": "What the agent will do next – {name}:",
383
396
  "ui.plan.now": "now",
384
397
  "ui.plan.images": " (needs the scans first)",
@@ -423,6 +436,149 @@ export const UI = {
423
436
  "ui.consent.gate.set": "Let the condition “{name}” decide when the agent working alone goes on?",
424
437
  "ui.consent.gate.unset": "Remove the condition “{name}” — the agent working alone no longer asks it?",
425
438
  "ui.consent.gate.skip": "Let the agent work on its own without the condition “{name}”?",
439
+ "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?",
440
+ "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.",
441
+ "ui.consent.hook.off": "Stop telling the hook “{name}” what is saved?",
442
+ "ui.menu.more": "Add to the research: material, a new direction, one person reviewed",
443
+ "ui.more.title": "Add to the research",
444
+ "ui.more.intake": "Material from the family: documents, photos, a family tree from another program, what you know",
445
+ "ui.more.research": "A new direction: the ancestors or descendants of someone else, or one question",
446
+ "ui.intake.what": "What do you bring?",
447
+ "ui.intake.files": "Files or a folder from this computer",
448
+ "ui.intake.text": "What you know — names, dates, places, family memories",
449
+ "ui.intake.write": "Write what you know (Enter or 0 goes back)",
450
+ "ui.intake.path": "The folder or file — you can drag it into this window (Enter or 0 goes back)",
451
+ "ui.intake.missing": "Not found: {path} — check the path, or drag the folder in.",
452
+ "ui.intake.scans": "The folder {folder} holds {n} images. Are they family documents and photos? (no: scans of a register or another book)",
453
+ "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.",
454
+ "ui.intake.known": "This material is in the research already — nothing added.",
455
+ "ui.intake.done": "✓ Added to the research: {n}. The agent goes through it in its next work.",
456
+ "ui.intake.chat": "Go through it with the agent now?",
457
+ "ui.intake.say": "I added new material to the research — let us go through it.",
458
+ "ui.research.kind": "What do you want to find out?",
459
+ "ui.research.ancestors": "The ancestors of someone",
460
+ "ui.research.descendants": "The descendants of someone",
461
+ "ui.research.question": "One question (e.g. who was Mary's father?)",
462
+ "ui.research.ask": "The question (Enter or 0 goes back)",
463
+ "ui.research.whom": "Who is it about?",
464
+ "ui.research.known": "Someone already in the family tree",
465
+ "ui.research.newperson": "Someone new",
466
+ "ui.research.name": "Their name (e.g. John Smith; 0 goes back)",
467
+ "ui.research.sex": "A man or a woman?",
468
+ "ui.research.male": "A man",
469
+ "ui.research.female": "A woman",
470
+ "ui.research.unknown": "I don't know",
471
+ "ui.research.title": "The research's name",
472
+ "ui.research.title.ancestors": "Ancestors: {name}",
473
+ "ui.research.title.descendants": "Descendants: {name}",
474
+ "ui.research.knows": "What do you know of it? Dates, places, stories of the family help the agent (Enter: nothing)",
475
+ "ui.research.done": "✓ The research “{name}” is started.",
476
+ "ui.research.chat": "Start on it with the agent now?",
477
+ "ui.research.say": "I started a new research, “{name}” — let us begin with it.",
478
+ "ui.settings.title": "Settings (on this computer)",
479
+ "ui.settings.wizard": "Language, agent, model, what the agent may do alone, stories… (the setup)",
480
+ "ui.settings.alone": "The agent working alone: {minutes} min a task · condition: {gate}",
481
+ "ui.settings.gate.none": "none",
482
+ "ui.settings.hooks": "Messages of what is saved (hooks): {state}",
483
+ "ui.settings.on": "on",
484
+ "ui.settings.off": "off",
485
+ "ui.settings.remote": "Watch the agent from your phone (Remote Control): {state}",
486
+ "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.",
487
+ "ui.settings.remote.on": "Turn it on?",
488
+ "ui.settings.remote.off": "Turn it off?",
489
+ "ui.settings.archives": "Archives: your logins, downloaders asking first",
490
+ "ui.alone.title": "The agent working alone",
491
+ "ui.alone.minutes": "Time for one task: {minutes} min",
492
+ "ui.alone.minutes.ask": "How many minutes may one task take? (5–600; 0 goes back)",
493
+ "ui.alone.minutes.bad": "Please a number of minutes from 5 to 600.",
494
+ "ui.alone.gate": "Working on and on under a condition: {gate}",
495
+ "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.",
496
+ "ui.alone.gate.pick": "Which condition?",
497
+ "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)",
498
+ "ui.alone.gate.args": "What to give it (Enter: nothing)",
499
+ "ui.alone.test": "What would the condition say now?",
500
+ "ui.alone.asking": "Asking the condition…",
501
+ "ui.alone.said.go": "It would start now{reason}",
502
+ "ui.alone.said.wait": "It would wait until {at}{reason}",
503
+ "ui.alone.said.stop": "It would stop{reason}",
504
+ "ui.alone.said.error": "No proper answer{reason}",
505
+ "ui.hooks.title": "Hooks — a number turns one on or off:",
506
+ "ui.hooks.on": "{name}: on",
507
+ "ui.hooks.off": "{name}: off",
508
+ "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}",
509
+ "ui.archives.title": "Downloaders of the archives here:",
510
+ "ui.archives.empty": "No downloaders here yet — the agent builds one when the research needs an archive.",
511
+ "ui.archives.login.saved": "your login saved {date}",
512
+ "ui.archives.login.none": "can use your account — no login saved",
513
+ "ui.archives.consent": "Downloaders ask you before they run: {state}",
514
+ "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.",
515
+ "ui.archives.consent.on": "Turn it on?",
516
+ "ui.archives.consent.off": "Turn it off?",
517
+ "ui.hooks.nothing": "none",
518
+ "ui.menu.noagent": "No AI agent on this computer yet — the research is its work. Item 1 installs one.",
519
+ "ui.menu.getagent": "Install or choose the AI agent",
520
+ "ui.menu.update.where": "A new version of strom is out: {version} — update it in the settings (item {key}).",
521
+ "ui.more.noagent": "The agent takes it up once it is on this computer (item 1 of the menu).",
522
+ "ui.menu.noagent.chosen": "The AI agent this research uses is not on this computer — item 1 chooses another.",
523
+ "ui.waiting.title": "The agent needs you for {n} thing(s):",
524
+ "ui.waiting.do": "What to do: {what}",
525
+ "ui.waiting.ask": "the agent will tell you in a conversation",
526
+ "ui.waiting.images": "Save by hand: images {images} of {book}",
527
+ "ui.waiting.more": "…and {n} more — the agent goes through them with you in a conversation.",
528
+ "ui.waiting.pick": "Have you got it? Choose the number and write what you found:",
529
+ "ui.waiting.answer": "Answer {k}: {what}",
530
+ "ui.waiting.folder": "Open the folder for the images of {k}",
531
+ "ui.waiting.opened": "The folder: {dir} — once the images are there, the next work of the agent takes them in by itself.",
532
+ "ui.waiting.write": "Your answer — a link, a book's number, what you found out (Enter or 0 goes back)",
533
+ "ui.waiting.back": "✓ The task goes back to the agent with your answer.",
534
+ "ui.waiting.chat": "Go on with the agent now?",
535
+ "ui.waiting.say": "I answered what you asked of me: {what} — let us go on with it.",
536
+ "ui.intake.empty": "There is nothing in it to take in.",
537
+ "ui.intake.ours": "{path} is strom's own folder (the research itself), not material of the family.",
538
+ "ui.research.exists": "This research is there already: “{name}” — the agent goes on with it.",
539
+ "ui.alone.gate.bad": "Please a number of points from 0 to 100 (e.g. 10).",
540
+ "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.",
541
+ "ui.settings.remote.chats": "conversations yes, working alone no",
542
+ "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.",
543
+ "ui.settings.remote.both": "conversations and working alone",
544
+ "ui.settings.remote.onalone": "Turn it on for working alone too?",
545
+ "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).",
546
+ "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.",
547
+ "ui.archives.login.save": "Save your login to {title}",
548
+ "ui.archives.login.change": "Change your login to {title}",
549
+ "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.",
550
+ "ui.home.sure": "Move it there?",
551
+ "ui.home.moved": "✓ The research is in {to} now.",
552
+ "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.",
553
+ "ui.home.stays": "The research stays in {from}.",
554
+ "ui.home.inside": "{to} is inside the research's folder {from} (or the other way round) — pick a folder beside it.",
555
+ "ui.home.notEmpty": "{to} holds something already — the research moves only into an empty or new folder.",
556
+ "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.",
557
+ "ui.home.failed": "It could not be moved ({reason}) — nothing changed.",
558
+ "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)?",
559
+ "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).",
560
+ "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}",
561
+ "ui.doc.browser": "browser (Claude in Chrome)",
562
+ "ui.doc.browser.ok": "the extension in {browsers} — Chrome must run while the agent works",
563
+ "ui.doc.browser.noext": "the extension is not installed (needed by {names})",
564
+ "ui.doc.browser.agent": "{names} go through the browser — only Claude Code can (now: {agent})",
565
+ "ui.archives.via.direct": "images directly, paced by strom",
566
+ "ui.archives.via.browser": "images through your browser",
567
+ "ui.archives.via.manual": "images saved by hand (the archive allows no automation)",
568
+ "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.",
569
+ "ui.archives.to.browser": "{title}: fetch through your browser instead",
570
+ "ui.archives.to.direct": "{title}: fetch directly instead",
571
+ "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.",
572
+ "ui.archives.direct.about": "Directly, strom fetches the images itself, paced, without a browser — any agent can.",
573
+ "ui.archives.switch": "Switch it?",
574
+ "ui.archives.now.browser": "✓ {title}: through your browser from now on.",
575
+ "ui.archives.now.direct": "✓ {title}: directly from now on.",
576
+ "ui.setup.home.again": "Where to keep the research (0 keeps it where it is)",
577
+ "ui.home.env": "The research's folder {from} is set from outside (STROM_HOME) — it stays; change it there.",
578
+ "ui.menu.failed": "That did not work: {reason}",
579
+ "ui.review.next.human": "The agent takes them up in a conversation or when it works alone.",
580
+ "ui.review.wake.human": " to go on with it, tell the agent in a conversation",
581
+ "ui.waiting.gone": "This one no longer waits for you — the agent has taken it up meanwhile.",
426
582
  };
427
583
  /** A text for the person, in their language. */
428
584
  export function ui(lang, key, values = {}) {