strom-research 1.6.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/assets/lang/cs.json +150 -4
- package/assets/lang/de.json +150 -4
- package/assets/method/narrate.md +5 -1
- package/assets/plugins/gates/claude-usage/gate.ts +2 -1
- package/dist/agents/files.js +6 -4
- package/dist/cli/main.js +21 -3
- package/dist/cli/menu-links.js +35 -1
- package/dist/cli/menu-research.js +41 -2
- package/dist/cli/menu-settings.js +17 -1
- package/dist/cli/menu.js +47 -9
- package/dist/cli/ui.js +90 -3
- package/dist/commands/connectors.js +12 -8
- package/dist/commands/guide.js +6 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/links.js +39 -3
- package/dist/commands/media.js +11 -3
- package/dist/commands/pack.js +134 -0
- package/dist/commands/research.js +42 -1
- package/dist/commands/session.js +79 -9
- package/dist/commands/setup.js +3 -0
- package/dist/commands/start.js +6 -3
- package/dist/commands/story.js +47 -6
- package/dist/commands/tasks.js +15 -6
- package/dist/commands/trees.js +82 -2
- package/dist/core/changelog.js +223 -0
- package/dist/core/clock.js +20 -0
- package/dist/core/config.js +2 -0
- package/dist/core/connector.js +25 -1
- package/dist/core/directions.js +41 -0
- package/dist/core/links.js +17 -4
- package/dist/core/live.js +393 -50
- package/dist/core/pack.js +342 -0
- package/dist/core/phrases.js +61 -0
- package/dist/core/queue.js +7 -2
- package/dist/core/relocate.js +4 -4
- package/dist/core/session.js +8 -2
- package/dist/core/trash.js +72 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/views.js +10 -2
- package/dist/core/workers.js +28 -3
- package/dist/core/zip.js +226 -0
- package/dist/runners/claude.js +2 -1
- package/dist/runners/jsonl.js +2 -2
- package/dist/runners/runner.js +16 -3
- package/dist/runners/script.js +2 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import fs from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { agentReady, droppedPaths, outOfAnswers, pause, pickPerson, subMenu, translator } from "./menu-parts.js";
|
|
8
8
|
import { Tree } from "../core/tree.js";
|
|
9
|
+
import { directionOf, scopes } from "../core/directions.js";
|
|
9
10
|
import { displayName, lifespan } from "../core/people.js";
|
|
10
11
|
import { DEATH_AFTER_YEARS, unprovenPeople } from "../core/review.js";
|
|
11
12
|
import { planSync, readTreeFile, receivedSince } from "../core/sync.js";
|
|
@@ -26,10 +27,12 @@ export async function addToResearch(ctx, run, lang, root) {
|
|
|
26
27
|
items.push({ key: "3", label: t("ui.menu.review"), act: async () => void (await reviewPerson(ctx, run, lang, root)) });
|
|
27
28
|
items.push({ key: "4", label: t(noApp(ctx) ? "ui.more.sync.noapp" : "ui.more.sync"), act: async () => void (await syncTree(ctx, run, lang, root)) });
|
|
28
29
|
}
|
|
30
|
+
if (tree.count("research") > 0)
|
|
31
|
+
items.push({ key: "5", label: t("ui.more.directions"), act: async () => void (await directions(ctx, run, lang, root)) });
|
|
29
32
|
// only when there are some: the people no record of their own proves — last, it shows only sometimes
|
|
30
33
|
const unproven = unprovenPeople(tree).filter((u) => !u.living).length;
|
|
31
34
|
if (unproven)
|
|
32
|
-
items.push({ key:
|
|
35
|
+
items.push({ key: String(items.length + 1), label: t("ui.menu.unproven", { n: unproven }), act: async () => void (await reviewUnproven(ctx, run, lang, root)) });
|
|
33
36
|
return { title: t("ui.more.title"), items };
|
|
34
37
|
});
|
|
35
38
|
}
|
|
@@ -175,6 +178,13 @@ export async function startDirection(ctx, run, lang, root, direction, who, name,
|
|
|
175
178
|
: undefined;
|
|
176
179
|
if (same)
|
|
177
180
|
return void ctx.io.stdout(t("ui.research.exists", { name: same.name }) + "\n");
|
|
181
|
+
// …paused or ended: taken up again rather than made twice
|
|
182
|
+
const stopped = focus && direction !== "question" ? Tree.open(root, ctx.env).list("research").find((r) => r.focus === focus && r.direction === direction) : undefined;
|
|
183
|
+
if (stopped) {
|
|
184
|
+
if (await ctx.confirm(t("ui.research.resume", { name: stopped.name, state: t(`ui.dirs.${stopped.state}`) })))
|
|
185
|
+
await setDirection(ctx, run, lang, root, stopped, "resume");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
178
188
|
const suggested = question ?? t(direction === "ancestors" ? "ui.research.title.ancestors" : "ui.research.title.descendants", { name });
|
|
179
189
|
const title = (await ctx.ask(t("ui.research.title"), suggested)).trim();
|
|
180
190
|
if (title === "0")
|
|
@@ -193,7 +203,36 @@ export async function startDirection(ctx, run, lang, root, direction, who, name,
|
|
|
193
203
|
if (knows && knows !== "0")
|
|
194
204
|
await run(["intake", `--text=${knows}`, "--research", made.id], true);
|
|
195
205
|
ctx.io.stdout(t("ui.research.done", { name: made.name }) + "\n");
|
|
196
|
-
await offerChat(ctx, run, lang, root, t("ui.research.chat"), t("ui.research.say", { name: made.name }));
|
|
206
|
+
await offerChat(ctx, run, lang, root, t("ui.research.chat"), t("ui.research.say", { name: made.name, id: made.id }));
|
|
207
|
+
}
|
|
208
|
+
/** A direction's tasks not finished: open, at work, put aside or waiting for the user. */
|
|
209
|
+
export function directionTasks(tree, research) {
|
|
210
|
+
const all = scopes(tree);
|
|
211
|
+
return tree.list("task").filter((x) => ["open", "doing", "parked", "waiting"].includes(x.state) && directionOf(tree, x, all) === research).length;
|
|
212
|
+
}
|
|
213
|
+
/** The directions of the research, each with how it goes and its tasks: one paused, ended or taken up again. */
|
|
214
|
+
async function directions(ctx, run, lang, root) {
|
|
215
|
+
const t = translator(lang);
|
|
216
|
+
const tree = Tree.open(root, ctx.env);
|
|
217
|
+
const all = tree.list("research");
|
|
218
|
+
const label = (r) => t("ui.dirs.item", { name: r.name, state: t(`ui.dirs.${r.state}`), n: directionTasks(tree, r.id) });
|
|
219
|
+
const i = await ctx.choose(t("ui.dirs.pick"), all.map((r) => ({ label: label(r) })), all.length, { back: t("ui.browse.back") });
|
|
220
|
+
if (i === undefined)
|
|
221
|
+
return;
|
|
222
|
+
const r = all[i];
|
|
223
|
+
const acts = r.state === "active" ? ["pause", "done"] : r.state === "paused" ? ["resume", "done"] : ["resume"];
|
|
224
|
+
const a = await ctx.choose(t("ui.dirs.what", { name: r.name }), acts.map((x) => ({ label: t(`ui.dirs.do.${x}`) })), acts.length, { back: t("ui.browse.back") });
|
|
225
|
+
if (a === undefined)
|
|
226
|
+
return;
|
|
227
|
+
await setDirection(ctx, run, lang, root, r, acts[a]);
|
|
228
|
+
}
|
|
229
|
+
/** A direction paused, ended or taken up again (with the person's reason), and what that means for its tasks. */
|
|
230
|
+
export async function setDirection(ctx, run, lang, root, r, act, reason) {
|
|
231
|
+
const t = translator(lang);
|
|
232
|
+
if ((await run(["research", act, r.id, ...(reason ? [`--reason=${reason}`] : [])], true)) !== 0)
|
|
233
|
+
return;
|
|
234
|
+
const state = act === "pause" ? "paused" : act === "resume" ? "active" : "done";
|
|
235
|
+
ctx.io.stdout(t(`ui.dirs.${state}.now`, { name: r.name, n: directionTasks(Tree.open(root, ctx.env), r.id) }) + "\n");
|
|
197
236
|
}
|
|
198
237
|
/** The person said no to the Strom app: it is not named. */
|
|
199
238
|
const noApp = (ctx) => stromAppState(ctx.settings) === "no";
|
|
@@ -226,7 +226,9 @@ async function archives(ctx, run, lang, shared, root) {
|
|
|
226
226
|
const saved = loadLogins(ctx.env);
|
|
227
227
|
const connectors = listConnectors(shared);
|
|
228
228
|
const consent = ctx.settings.connectorsConsent();
|
|
229
|
-
const
|
|
229
|
+
const treeConfig = root ? Tree.open(root, ctx.env).config : undefined;
|
|
230
|
+
const agent = ctx.settings.agent(treeConfig).value;
|
|
231
|
+
const always = ctx.settings.resolve("agent.browser", treeConfig)?.value === "always";
|
|
230
232
|
const via = (c) => (c.manifest.policy.automation === "manual" ? "manual" : routeOf(ctx.env, c).via);
|
|
231
233
|
// Each downloader on a line of its own: how its images come, and its login where it can use one.
|
|
232
234
|
const lines = connectors.map((c) => {
|
|
@@ -260,6 +262,20 @@ async function archives(ctx, run, lang, shared, root) {
|
|
|
260
262
|
await run(["config", "set", "connectors.consent", consent ? "off" : "on"], true);
|
|
261
263
|
},
|
|
262
264
|
},
|
|
265
|
+
// The browser for the agent in every session: Claude Code's alone, shown only when it does the research and is here.
|
|
266
|
+
...(claudeHere(ctx, treeConfig)
|
|
267
|
+
? [
|
|
268
|
+
{
|
|
269
|
+
key: "b",
|
|
270
|
+
label: t("ui.archives.agentbrowser", { state: t(always ? "ui.archives.agentbrowser.always" : "ui.archives.agentbrowser.archives") }),
|
|
271
|
+
act: async () => {
|
|
272
|
+
out(t("ui.archives.agentbrowser.about"));
|
|
273
|
+
if (await ctx.confirm(t(always ? "ui.archives.agentbrowser.off" : "ui.archives.agentbrowser.on"), false))
|
|
274
|
+
await run(always ? ["config", "unset", "agent.browser"] : ["config", "set", "agent.browser", "always"], true);
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
]
|
|
278
|
+
: []),
|
|
263
279
|
...switchable.map((c) => {
|
|
264
280
|
const next = via(c) === "browser" ? "direct" : "browser";
|
|
265
281
|
const name = c.manifest.title ?? c.name;
|
package/dist/cli/menu.js
CHANGED
|
@@ -25,7 +25,9 @@ import { liveWorkers, runsAtWork } from "../core/workers.js";
|
|
|
25
25
|
import { askStromApp } from "./wizard.js";
|
|
26
26
|
import { openForUser } from "../core/open.js";
|
|
27
27
|
import { displayName } from "../core/people.js";
|
|
28
|
-
import { agentReady, guarded, pause as partsPause, pickPerson, subMenu } from "./menu-parts.js";
|
|
28
|
+
import { agentReady, droppedPaths, guarded, pause as partsPause, pickPerson, subMenu } from "./menu-parts.js";
|
|
29
|
+
import { packPlan } from "../core/pack.js";
|
|
30
|
+
import { expandHome } from "../core/paths.js";
|
|
29
31
|
import { offerAgent } from "./fixes.js";
|
|
30
32
|
import { addToResearch } from "./menu-research.js";
|
|
31
33
|
import { waitingForYou } from "./menu-waiting.js";
|
|
@@ -201,8 +203,11 @@ export async function runMenu(ctx, run) {
|
|
|
201
203
|
ctx.settings.config.stromAppFollow = follow ? "yes" : "no";
|
|
202
204
|
ctx.settings.save();
|
|
203
205
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
// opened when the first session starts: a run may wait for its gate first
|
|
207
|
+
if (follow) {
|
|
208
|
+
args.push("--follow");
|
|
209
|
+
out(t("ui.run.follow.later"));
|
|
210
|
+
}
|
|
206
211
|
}
|
|
207
212
|
out(t("ui.run.start"));
|
|
208
213
|
await run(["run", ...args]);
|
|
@@ -244,13 +249,15 @@ export async function runMenu(ctx, run) {
|
|
|
244
249
|
await run(["chat", "--agent", others[i].id]);
|
|
245
250
|
},
|
|
246
251
|
};
|
|
252
|
+
// Another tree, right after the research's own items.
|
|
253
|
+
items.push({ key: "6", label: known.length > 1 ? t("ui.menu.trees") : t("ui.menu.newtree"), act: async () => pickTree(ctx, run, lang) });
|
|
247
254
|
// 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.
|
|
248
255
|
const app = stromAppState(ctx.settings);
|
|
249
256
|
if (app !== "no") {
|
|
250
257
|
const atWork = liveWorkers(root).length > 0 || runsAtWork(root).length > 0;
|
|
251
258
|
const hasApp = app !== "unknown" || Boolean(installedStromApp(ctx.env, process.platform, stromAppUrl(ctx.settings)));
|
|
252
259
|
appItem = {
|
|
253
|
-
key: "
|
|
260
|
+
key: "7",
|
|
254
261
|
label: t(!hasApp ? "ui.menu.app.new" : atWork ? "ui.menu.app.watch" : "ui.menu.app"),
|
|
255
262
|
act: async () => openStromApp(ctx, run, lang, hasApp),
|
|
256
263
|
};
|
|
@@ -262,12 +269,9 @@ export async function runMenu(ctx, run) {
|
|
|
262
269
|
// Several trees and none chosen: pick one first.
|
|
263
270
|
items.push({ key: "1", label: t("ui.menu.trees"), act: async () => pickTree(ctx, run, lang) });
|
|
264
271
|
}
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
const settings = { key: "6", label: t("ui.menu.settings"), act: async () => settingsMenu(ctx, run, lang, root, newer) };
|
|
272
|
+
// The settings of this computer (with the check); at most nine: the newer strom is taken in the settings, the line above says where.
|
|
273
|
+
const settings = { key: "8", label: t("ui.menu.settings"), act: async () => settingsMenu(ctx, run, lang, root, newer) };
|
|
268
274
|
items.push(settings);
|
|
269
|
-
if (root)
|
|
270
|
-
items.push({ key: "8", label: known.length > 1 ? t("ui.menu.trees") : t("ui.menu.newtree"), act: async () => pickTree(ctx, run, lang) });
|
|
271
275
|
// Only with several agents here: last, so that it moves no other number.
|
|
272
276
|
if (other)
|
|
273
277
|
items.push(other);
|
|
@@ -423,6 +427,10 @@ async function pickTree(ctx, run, lang) {
|
|
|
423
427
|
}
|
|
424
428
|
const at = known.findIndex((k) => k.root === here);
|
|
425
429
|
const options = [...known.map((k) => ({ label: `${k.name} (${ctx.display(k.root)})` })), { label: ui(lang, "ui.trees.new") }];
|
|
430
|
+
// handing a research over: this one packed, one someone sent unpacked
|
|
431
|
+
const packAt = at >= 0 ? options.push({ label: ui(lang, "ui.trees.pack", { name: known[at].name }) }) - 1 : -1;
|
|
432
|
+
const unpackAt = options.push({ label: ui(lang, "ui.trees.unpack") }) - 1;
|
|
433
|
+
const removeAt = known.length ? options.push({ label: ui(lang, "ui.trees.remove") }) - 1 : -1;
|
|
426
434
|
const back = at >= 0 ? { back: ui(lang, "ui.back.stay", { name: known[at].name }) } : {};
|
|
427
435
|
const i = await ctx.choose(ui(lang, "ui.trees.pick"), options, Math.max(0, at), back);
|
|
428
436
|
if (i === undefined || i === at)
|
|
@@ -431,6 +439,36 @@ async function pickTree(ctx, run, lang) {
|
|
|
431
439
|
await run(["trees", "use", known[i].root], true);
|
|
432
440
|
return;
|
|
433
441
|
}
|
|
442
|
+
if (i === packAt) {
|
|
443
|
+
const tree = Tree.open(known[at].root, ctx.env);
|
|
444
|
+
const shared = ctx.settings.shared().value;
|
|
445
|
+
const size = (all) => (packPlan(tree, shared, all).media.reduce((s, m) => s + m.size, 0) / 1e6).toFixed(0);
|
|
446
|
+
const which = await ctx.choose(ui(lang, "ui.pack.which"), [{ label: ui(lang, "ui.pack.opt.records", { mb: size(false) }) }, { label: ui(lang, "ui.pack.opt.all", { mb: size(true) }) }], 0, { back: ui(lang, "ui.browse.back") });
|
|
447
|
+
if (which === undefined)
|
|
448
|
+
return;
|
|
449
|
+
await run(["pack", "--tree", tree.root, ...(which === 1 ? ["--all-images"] : [])]);
|
|
450
|
+
await partsPause(ctx, lang);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (i === removeAt) {
|
|
454
|
+
const which = await ctx.choose(ui(lang, "ui.remove.which"), known.map((k) => ({ label: `${k.name} (${ctx.display(k.root)})` })), known.length, { back: ui(lang, "ui.browse.back") }); // Enter goes back
|
|
455
|
+
if (which === undefined)
|
|
456
|
+
return;
|
|
457
|
+
await run(["trees", "remove", known[which].root]);
|
|
458
|
+
await partsPause(ctx, lang);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (i === unpackAt) {
|
|
462
|
+
const answer = (await ctx.ask(ui(lang, "ui.unpack.file"))).trim();
|
|
463
|
+
if (!answer || answer === "0")
|
|
464
|
+
return;
|
|
465
|
+
const resolve = (p) => path.resolve(ctx.cwd, expandHome(p, ctx.env));
|
|
466
|
+
const dropped = droppedPaths(answer).map(resolve);
|
|
467
|
+
const file = dropped.length === 1 && fs.existsSync(dropped[0]) ? dropped[0] : resolve(answer);
|
|
468
|
+
await run(["unpack", file]);
|
|
469
|
+
await partsPause(ctx, lang);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
434
472
|
const name = (await ctx.ask(ui(lang, "ui.tree.name.new"), ui(lang, "ui.tree.default"))).trim();
|
|
435
473
|
if (name === "0")
|
|
436
474
|
return;
|
package/dist/cli/ui.js
CHANGED
|
@@ -41,8 +41,8 @@ export const UI = {
|
|
|
41
41
|
"ui.menu.waitlist": "What waits for you",
|
|
42
42
|
"ui.menu.settings": "Settings: the agent, working alone, messages, archives, the check",
|
|
43
43
|
"ui.menu.doctor": "Check the installation",
|
|
44
|
-
"ui.menu.trees": "Another family tree",
|
|
45
|
-
"ui.menu.newtree": "Create a family tree",
|
|
44
|
+
"ui.menu.trees": "Another family tree · send or receive one",
|
|
45
|
+
"ui.menu.newtree": "Create a family tree · send or receive one",
|
|
46
46
|
"ui.menu.quit": "Quit",
|
|
47
47
|
"ui.menu.bye": "See you next time. Start again with: strom",
|
|
48
48
|
"ui.run.how": "How many tasks? (it stops earlier when there is nothing to do; 0 goes back)",
|
|
@@ -104,6 +104,7 @@ export const UI = {
|
|
|
104
104
|
"ui.menu.app.live": "The agent is at work — you can watch it live in the Strom app: choice {key}.",
|
|
105
105
|
"ui.app.open.now": "Once it is installed: open the research in it?",
|
|
106
106
|
"ui.run.follow": "Watch the work live in the Strom app meanwhile?",
|
|
107
|
+
"ui.run.follow.later": "The Strom app opens when the agent begins its first session.",
|
|
107
108
|
"ui.app.opened": "Opening the Strom app ({where}).",
|
|
108
109
|
"ui.app.browser": "in your browser",
|
|
109
110
|
"ui.app.url": "Open it here: {url}",
|
|
@@ -162,6 +163,7 @@ export const UI = {
|
|
|
162
163
|
"ui.run.stop.resets": " (resets {at})",
|
|
163
164
|
"ui.run.stop.auth": "the agent is not logged in — run {command} once and log in",
|
|
164
165
|
"ui.run.stop.failed": "the agent failed: {detail}",
|
|
166
|
+
"ui.run.finishing": "■ Ctrl-C: session {session} is asked to finish — the agent writes down what it found and closes it, then the run stops (no next session). Ctrl-C again stops it now.",
|
|
165
167
|
"ui.run.stopping": "■ stopping — the session is closed and its task goes back to the queue; Ctrl-C again to quit at once",
|
|
166
168
|
"ui.run.gone": "· {session} closed — its run had stopped{back}",
|
|
167
169
|
"ui.run.back": "; {task} is back in the queue",
|
|
@@ -515,6 +517,14 @@ export const UI = {
|
|
|
515
517
|
"ui.consent.gate.set": "Let the condition “{name}” decide when the agent working alone goes on?",
|
|
516
518
|
"ui.consent.gate.unset": "Remove the condition “{name}” — the agent working alone no longer asks it?",
|
|
517
519
|
"ui.consent.gate.skip": "Let the agent work on its own without the condition “{name}”?",
|
|
520
|
+
"ui.run.browser.always": "· the browser (Claude in Chrome) in every session, as you set it (agent.browser always) — without asking for {hosts}; Chrome with the Claude extension must be running",
|
|
521
|
+
"ui.consent.browser": "Give the agent browser tools (Claude in Chrome) in every research session? It works in your own Chrome — where you are signed in — for the sites of the research's archives without asking; in a conversation it asks about any other site.",
|
|
522
|
+
"ui.archives.agentbrowser": "The browser for the agent (Claude in Chrome): {state}",
|
|
523
|
+
"ui.archives.agentbrowser.archives": "only for archives through the browser",
|
|
524
|
+
"ui.archives.agentbrowser.always": "in every session",
|
|
525
|
+
"ui.archives.agentbrowser.about": "In every session, the agent can work in your own Chrome — where you are signed in — so you can ask it for anything through the browser. The sites of the research's archives without asking; in a conversation it asks about any other site, working alone it uses none other. Chrome with the Claude extension must be running.",
|
|
526
|
+
"ui.archives.agentbrowser.on": "Give the agent the browser in every session?",
|
|
527
|
+
"ui.archives.agentbrowser.off": "Back to the browser only for archives through the browser?",
|
|
518
528
|
"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?",
|
|
519
529
|
"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.",
|
|
520
530
|
"ui.consent.hook.off": "Stop telling the hook “{name}” what is saved?",
|
|
@@ -546,6 +556,16 @@ export const UI = {
|
|
|
546
556
|
"ui.link.what.chat.person": "a conversation with the agent ({agent}) about {person}.",
|
|
547
557
|
"ui.link.chat.say": "Let us look at {person}: tell me briefly what the research knows of this person, and ask me what I want to find out.",
|
|
548
558
|
"ui.link.what.review.person": "look at {person} again.",
|
|
559
|
+
"ui.link.what.direction.pause": "pause the direction “{name}”: its tasks ({n}) wait out of the queue and nothing new is proposed for it. Nothing is deleted.",
|
|
560
|
+
"ui.link.what.direction.done": "end the direction “{name}”: its tasks ({n}) leave the queue. Nothing is deleted, it can be taken up again.",
|
|
561
|
+
"ui.link.what.direction.resume": "take the direction “{name}” up again: its tasks ({n}) come back to the queue and the research proposes what is missing.",
|
|
562
|
+
"ui.link.direction.none": "Direction {id} is not in this research. Nothing was done.",
|
|
563
|
+
"ui.link.direction.cannot": "The direction “{name}” is {state} already. Nothing was done.",
|
|
564
|
+
"ui.link.direction.why": "Why? (a few words, kept with the direction; Enter: no reason)",
|
|
565
|
+
"ui.link.what.chat.research": "a conversation with the agent ({agent}) about the direction “{name}”, only its tasks.",
|
|
566
|
+
"ui.link.chat.research.say": "Let us work on the direction “{name}” ({id}), on its tasks.",
|
|
567
|
+
"ui.link.what.finish": "ask {agent} to finish session {session} ({task}): it writes down what it found and closes the session; working alone, it starts no next one.",
|
|
568
|
+
"ui.link.finish.none": "Session {session} is not at work (it has closed meanwhile). Nothing was done.",
|
|
549
569
|
"ui.link.what.review.family": "look at {person} again, with the partners and children.",
|
|
550
570
|
"ui.link.what.review.line": "look at {person} again, with the ancestors.",
|
|
551
571
|
"ui.link.what.research.ancestors": "search for the ancestors of {person} (a new direction of the research).",
|
|
@@ -635,7 +655,7 @@ export const UI = {
|
|
|
635
655
|
"ui.research.knows": "What do you know of it? Dates, places, stories of the family help the agent (Enter: nothing)",
|
|
636
656
|
"ui.research.done": "✓ The research “{name}” is started.",
|
|
637
657
|
"ui.research.chat": "Start on it with the agent now?",
|
|
638
|
-
"ui.research.say": "I started a new research, “{name}” — let us begin with it.",
|
|
658
|
+
"ui.research.say": "I started a new research, “{name}” ({id}) — let us begin with it, with its tasks.",
|
|
639
659
|
"ui.settings.title": "Settings (on this computer)",
|
|
640
660
|
"ui.settings.wizard": "Language, agent, model, what the agent may do alone, stories… (the setup)",
|
|
641
661
|
"ui.settings.alone": "The agent working alone: {minutes} min a task · condition: {gate}",
|
|
@@ -697,6 +717,20 @@ export const UI = {
|
|
|
697
717
|
"ui.intake.empty": "There is nothing in it to take in.",
|
|
698
718
|
"ui.intake.ours": "{path} is strom's own folder (the research itself), not material of the family.",
|
|
699
719
|
"ui.research.exists": "This research is there already: “{name}” — the agent goes on with it.",
|
|
720
|
+
"ui.more.directions": "The directions of the research: pause, end or take one up again",
|
|
721
|
+
"ui.dirs.pick": "Which direction?",
|
|
722
|
+
"ui.dirs.item": "{name} — {state} · tasks: {n}",
|
|
723
|
+
"ui.dirs.active": "going on",
|
|
724
|
+
"ui.dirs.paused": "paused",
|
|
725
|
+
"ui.dirs.done": "ended",
|
|
726
|
+
"ui.dirs.what": "“{name}”:",
|
|
727
|
+
"ui.dirs.do.pause": "Pause it — its tasks wait, the agent takes others",
|
|
728
|
+
"ui.dirs.do.done": "End it — its tasks leave the queue (nothing is deleted)",
|
|
729
|
+
"ui.dirs.do.resume": "Take it up again — its tasks back in the queue",
|
|
730
|
+
"ui.dirs.paused.now": "⏸ “{name}” is paused: its tasks ({n}) wait until you take it up again (here).",
|
|
731
|
+
"ui.dirs.done.now": "■ “{name}” is ended: its tasks ({n}) are out of the queue — nothing is deleted, it can be taken up again here.",
|
|
732
|
+
"ui.dirs.active.now": "▶ “{name}” goes on: its tasks ({n}) are back in the queue.",
|
|
733
|
+
"ui.research.resume": "This research is there already, {state}: “{name}”. Take it up again?",
|
|
700
734
|
"ui.alone.gate.bad": "Please a number of points from 0 to 100 (e.g. 10).",
|
|
701
735
|
"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.",
|
|
702
736
|
"ui.settings.remote.chats": "conversations yes, working alone no",
|
|
@@ -740,6 +774,59 @@ export const UI = {
|
|
|
740
774
|
"ui.review.next.human": "The agent takes them up in a conversation or when it works alone.",
|
|
741
775
|
"ui.review.wake.human": " to go on with it, tell the agent in a conversation",
|
|
742
776
|
"ui.waiting.gone": "This one no longer waits for you — the agent has taken it up meanwhile.",
|
|
777
|
+
"ui.trees.pack": "pack \"{name}\" to send to someone (a ZIP file)",
|
|
778
|
+
"ui.trees.unpack": "a family tree someone sent you (a ZIP file)",
|
|
779
|
+
"ui.pack.which": "Which images go along?",
|
|
780
|
+
"ui.pack.opt.records": "the ones its records stand on — {mb} MB (the pages searched through the archive gives again)",
|
|
781
|
+
"ui.pack.opt.all": "all of them — {mb} MB",
|
|
782
|
+
"ui.pack.done": "Packed \"{name}\": {file} ({mb} MB)",
|
|
783
|
+
"ui.pack.saved": " the family tree as last saved, with its history",
|
|
784
|
+
"ui.pack.images": " images: {n} ({mb} MB) — the ones its records stand on and the ones nobody can fetch again",
|
|
785
|
+
"ui.pack.images.all": " images: all {n} ({mb} MB)",
|
|
786
|
+
"ui.pack.left": " left out: {n} images ({mb} MB) — pages searched through; strom fetches them from the archive again when they are needed",
|
|
787
|
+
"ui.pack.connectors": " connectors: {list}",
|
|
788
|
+
"ui.pack.noconnector": "⚠ some images left out came through a connector that is not on this computer — nobody could fetch them again: strom pack --all-images",
|
|
789
|
+
"ui.pack.missing": "⚠ {n} image(s) the family tree names are not on this computer — not in the package",
|
|
790
|
+
"ui.pack.send": "Send the file. The one who gets it unpacks it and double-clicks Windows.cmd or macOS.command inside: strom is installed where it is missing and the research opens (with strom already: strom unpack \"<the file>\").",
|
|
791
|
+
"ui.pack.readme.file": "HOW-TO-CONTINUE.txt",
|
|
792
|
+
"ui.pack.readme": "The research \"{name}\" - how to go on with it\n=============================================\n\n1. Unpack the whole ZIP file: on Windows right click it - Extract All; on a Mac double click it.\n\n2. In the unpacked folder start, by a double click:\n Windows: Windows.cmd\n macOS: macOS.command (the first time: right click - Open; when macOS refuses:\n System Settings - Privacy & Security - Open Anyway)\n Linux: in a terminal: bash Linux.sh\n It installs Strom Research where it is not yet (the official installer), puts the\n research in place - the family tree among yours, the images into your shared folder,\n all of it checked - takes it over on your computer and opens strom.\n\n3. Go on: in strom's menu, the conversation with the agent.\n\nWithout the launchers: install strom yourself\n Windows (PowerShell): {win}\n macOS, Linux (Terminal): {unix}\nthen: strom unpack \"<this ZIP file>\" (type strom unpack and a space, drag the file into\nthe terminal, press Enter) - or in strom's menu: another family tree - a family tree someone sent you.\n\nThe images the records stand on are in the package. The other pages of the books\n(searched through, nothing found on them) are not, for size - strom fetches them from\nthe archive again when they are needed.\n",
|
|
793
|
+
"ui.unpack.file": "Drag the ZIP file here (or type where it is; 0 goes back):",
|
|
794
|
+
"ui.unpack.what": "\"{name}\" (sent from strom {strom}, saved {date}): {images} image(s){connectors} → {root}",
|
|
795
|
+
"ui.unpack.ask": "Put it here and take it over on this computer?",
|
|
796
|
+
"ui.unpack.done": "\"{name}\" is here: {root}",
|
|
797
|
+
"ui.unpack.images": " {n} image(s) into the shared folder{had}",
|
|
798
|
+
"ui.unpack.had": " ({n} were here already)",
|
|
799
|
+
"ui.unpack.connectors": " connectors: {list}",
|
|
800
|
+
"ui.unpack.kept": " connector {name} was here already — yours stays",
|
|
801
|
+
"ui.unpack.left": " {n} pages searched through are not in it: strom fetches them from the archive when they are needed",
|
|
802
|
+
"ui.unpack.adopted": " taken over on this computer",
|
|
803
|
+
"ui.unpack.next": "Go on: strom (the menu) or strom chat.",
|
|
804
|
+
"ui.unpack.same": "this research is here already: {root}",
|
|
805
|
+
"ui.unpack.person": "only the person at the computer unpacks a research (it adds connectors)",
|
|
806
|
+
"ui.pack.launch.extract": "First unpack the whole ZIP file - on Windows: right click, Extract All - then start this again from the unpacked folder.",
|
|
807
|
+
"ui.pack.launch.failed": "Strom Research could not be installed. Install it by the line in HOW-TO-CONTINUE.txt and run this again.",
|
|
808
|
+
"ui.trees.remove": "take a family tree off this computer",
|
|
809
|
+
"ui.session.finish.done": "Session {session} (task {task}) is asked to finish: its agent sees it at its next strom command, writes down what it found and closes the session.",
|
|
810
|
+
"ui.session.finish.none": "no session is at work in this research",
|
|
811
|
+
"ui.session.finish.which": "several sessions are at work ({sessions}) — say which one",
|
|
812
|
+
"ui.session.finish.closed": "session {session} is closed already",
|
|
813
|
+
"ui.remove.which": "Which family tree to take off this computer?",
|
|
814
|
+
"ui.remove.person": "only the person at the computer takes a family tree off it",
|
|
815
|
+
"ui.remove.busy": "somebody is at work on \"{name}\" right now — it can go once the work is over",
|
|
816
|
+
"ui.remove.what": "\"{name}\" ({root}) · people: {persons}",
|
|
817
|
+
"ui.remove.backup": "Pack it first as a backup, with all its images ({mb} MB)? It comes back any time with strom unpack.",
|
|
818
|
+
"ui.remove.backup.done": " backup: {file}",
|
|
819
|
+
"ui.remove.images": "Into the trash too the images no other family tree uses ({n}, {mb} MB)?",
|
|
820
|
+
"ui.remove.type": "To confirm, type the name of the family tree ({name}) — anything else takes nothing off:",
|
|
821
|
+
"ui.remove.cancelled": "Nothing was taken off.",
|
|
822
|
+
"ui.remove.done": "\"{name}\" is in the trash.",
|
|
823
|
+
"ui.remove.live": " the Strom app no longer follows it live",
|
|
824
|
+
"ui.remove.images.done": " its images in the trash: {n} ({mb} MB)",
|
|
825
|
+
"ui.remove.images.folder": "{name} – images",
|
|
826
|
+
"ui.remove.backup.back": " it comes back from the backup: strom unpack \"{file}\"",
|
|
827
|
+
"ui.remove.trash.alone": " it comes back from the system's trash, until it is emptied",
|
|
828
|
+
"ui.remove.next": "Now you work on \"{name}\" — another one: Another family tree in the menu.",
|
|
829
|
+
"ui.remove.trash": " or from the system's trash, until it is emptied",
|
|
743
830
|
};
|
|
744
831
|
/** A text for the person, in their language. */
|
|
745
832
|
export function ui(lang, key, values = {}) {
|
|
@@ -1094,14 +1094,16 @@ async function fetchWith(ctx, c, request, recordset) {
|
|
|
1094
1094
|
});
|
|
1095
1095
|
if (recordset && request.cmd === "fetch") {
|
|
1096
1096
|
// what is registered already is not asked for again (a part of an image is not the image)
|
|
1097
|
-
|
|
1097
|
+
// …unless its file is not here (a research handed over without its images): fetched again, put back
|
|
1098
|
+
const here = (m) => fs.existsSync(path.join(shared(ctx), m.file));
|
|
1099
|
+
const have = new Set(tree.list("media").filter((m) => m.recordset === recordset && !m.part && m.image !== undefined && here(m)).map((m) => m.image));
|
|
1098
1100
|
const asked = request.images;
|
|
1099
1101
|
request.images = asked.filter((n) => !have.has(n));
|
|
1100
1102
|
if (!request.images.length)
|
|
1101
1103
|
return { text: `images ${runs(asked)} of ${recordset} are registered already — nothing fetched`, data: { added: [], again: asked } };
|
|
1102
1104
|
}
|
|
1103
1105
|
if (recordset && request.cmd === "part") {
|
|
1104
|
-
const had = tree.list("media").find((m) => m.recordset === recordset && m.image === request.image && m.part && sameRegion(m.part, request.region));
|
|
1106
|
+
const had = tree.list("media").find((m) => m.recordset === recordset && m.image === request.image && m.part && sameRegion(m.part, request.region) && fs.existsSync(path.join(shared(ctx), m.file)));
|
|
1105
1107
|
if (had)
|
|
1106
1108
|
return { text: `part ${regionText(request.region)} of image ${request.image} of ${recordset} is registered already: ${had.id} — nothing fetched`, data: { added: [], again: [had.id] } };
|
|
1107
1109
|
}
|
|
@@ -1158,16 +1160,18 @@ async function fetchWith(ctx, c, request, recordset) {
|
|
|
1158
1160
|
? lines(`part ${regionText(m.part)} of image ${request.image} of ${recordset} fetched and registered: ${m.id}${m.width ? ` · ${m.width}×${m.height} px` : ""}${gain ? ` · ${gain.toFixed(1)}× the detail of the whole image` : ""} · ${took}`, gain !== undefined && gain < 1.2 ? " no sharper than the whole image: the portal gives no more detail than that" : undefined, `look at it: strom media view ${recordset}:${request.image} --crop ${regionText(m.part)} (a view of the image uses the part by itself)`)
|
|
1159
1161
|
: res.again.length && whole && res.again.includes(whole.id) && r.images.every((i) => isWhole(i.region))
|
|
1160
1162
|
? lines(`the portal's sharpest of image ${request.image} is the whole scan, registered already: ${whole.id}${whole.width ? ` · ${whole.width}×${whole.height} px` : ""} — no part needed`, `look at it: strom media view ${recordset}:${request.image} --crop ${regionText(request.region)}`)
|
|
1161
|
-
: res.
|
|
1162
|
-
?
|
|
1163
|
-
:
|
|
1163
|
+
: res.restored.length
|
|
1164
|
+
? `part ${regionText(request.region)} of image ${request.image} of ${recordset} fetched again and put back (its file was not here; the same scan): ${res.restored.join(" ")} · ${took}`
|
|
1165
|
+
: res.again.length
|
|
1166
|
+
? lines(`that part is registered already: ${res.again.join(" ")}`, ...res.clashes.map((x) => `⚠ ${x}`))
|
|
1167
|
+
: "no part fetched";
|
|
1164
1168
|
data = { ...data, added: res.added.map((x) => ({ id: x.id, image: x.image, part: x.part })), again: res.again };
|
|
1165
1169
|
}
|
|
1166
1170
|
else if (recordset && r.images.length) {
|
|
1167
1171
|
const res = registerImages(tree, shared(ctx), r.images.map((i) => ({ file: i.file, image: i.n, url: i.url, from: from(i.file), fetched })), recordset);
|
|
1168
1172
|
const nums = res.added.map((m) => m.image).filter((n) => n !== undefined);
|
|
1169
|
-
text = lines(`${res.added.length} image(s) of ${recordset}${nums.length ? ` (images ${Math.min(...nums)}–${Math.max(...nums)})` : ""} fetched and registered · ${took}
|
|
1170
|
-
data = { ...data, added: res.added.map((m) => ({ id: m.id, image: m.image })), again: res.again, woken: res.woken, clashes: res.clashes };
|
|
1173
|
+
text = lines(res.added.length || !res.restored.length ? `${res.added.length} image(s) of ${recordset}${nums.length ? ` (images ${Math.min(...nums)}–${Math.max(...nums)})` : ""} fetched and registered · ${took}` : undefined, res.restored.length ? `${res.restored.length} image(s) of ${recordset} fetched again and put back (their file was not here; the same scan): ${res.restored.slice(0, 5).join(" ")}${res.restored.length > 5 ? " …" : ""} · ${took}` : undefined, res.again.length ? `${res.again.length} already registered` : undefined, ...res.clashes.slice(0, 10).map((x) => `⚠ ${x}`), res.woken.length ? `back in the queue (they waited for these images): ${res.woken.join(" ")}` : undefined);
|
|
1174
|
+
data = { ...data, added: res.added.map((m) => ({ id: m.id, image: m.image })), again: res.again, restored: res.restored, woken: res.woken, clashes: res.clashes };
|
|
1171
1175
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
1172
1176
|
}
|
|
1173
1177
|
else if (r.images.length) {
|
|
@@ -1307,7 +1311,7 @@ async function takeOver(ctx, c, result) {
|
|
|
1307
1311
|
if (plan.recordset) {
|
|
1308
1312
|
const res = registerImages(tree, shared(ctx), mine.map((t) => ({ file: t.file, image: t.item.n, url: t.item.url, from: `connector ${c.name} · your browser · ${path.basename(t.file)}`, part: t.item.region, fetched })), plan.recordset);
|
|
1309
1313
|
added.push(...res.added.map((m) => ({ id: m.id, ...(m.image !== undefined ? { image: m.image } : {}) })));
|
|
1310
|
-
again.push(...res.again);
|
|
1314
|
+
again.push(...res.again, ...res.restored);
|
|
1311
1315
|
notes.push(...res.clashes);
|
|
1312
1316
|
woken.push(...res.woken);
|
|
1313
1317
|
for (const t of mine)
|
package/dist/commands/guide.js
CHANGED
|
@@ -59,6 +59,10 @@ conversation opened anywhere, a bot on its own server)
|
|
|
59
59
|
then tell the user what was found (strom person card …) and what comes next (strom plan), and ask
|
|
60
60
|
before the next session. The next step is strom's queue: what the user wants instead becomes a
|
|
61
61
|
task first (strom task add …, or strom research new …) — never search outside a session and its task.
|
|
62
|
+
The user names a research (G…) to work on: its tasks only — strom session start --research G….
|
|
63
|
+
A side of the family no direction reaches (a spouse's line) is a direction of its own: strom research
|
|
64
|
+
new --person <them>. The user wants a direction to wait or to end: strom research pause|done G… (its
|
|
65
|
+
tasks leave the queue, nothing is deleted; strom research resume G… brings them back).
|
|
62
66
|
In a conversation the next task is best begun in a fresh context (Claude Code: /clear) — suggest
|
|
63
67
|
it to the user; many tasks waiting: the agent working on its own (strom run, the user starts it
|
|
64
68
|
from strom's menu), every task in a fresh session
|
|
@@ -153,7 +157,8 @@ THE STROM APP'S EDITS, ANOTHER FAMILY TREE (the user changed the tree in the Str
|
|
|
153
157
|
STORIES OF THE ANCESTORS (the setting stories — on by default; strom shows it)
|
|
154
158
|
Once records tell a person's life (a baptism and more facts from records), strom proposes a narrate
|
|
155
159
|
task: write the story for the family book — plain words, the research language, every statement on a
|
|
156
|
-
recorded fact (strom story set P… --text @notes/story-P….md --fact E…
|
|
160
|
+
recorded fact (strom story set P… --text @notes/story-P….md --title "…" --fact E… …; paragraphs,
|
|
161
|
+
"## " subheadings, "- " bullets, **bold**, *italic* — nothing else shows in the app). More facts later: it
|
|
157
162
|
proposes adding to it. Stories wait behind all the research; working alone
|
|
158
163
|
(strom run), every few sessions one comes first. In a conversation the user leads: write one when they
|
|
159
164
|
want it, or offer it when the research has a pause. A story is a draft until the user approves it (strom story approve P…; the Strom app asks it too). When strom says to tell
|
package/dist/commands/index.js
CHANGED
package/dist/commands/links.js
CHANGED
|
@@ -80,6 +80,10 @@ function who(p) {
|
|
|
80
80
|
function aboutPerson(lang, p) {
|
|
81
81
|
return ui(lang, "ui.link.chat.say", { person: who(p) });
|
|
82
82
|
}
|
|
83
|
+
/** The conversation's first message about one direction: its name and ID, from the research's own record. */
|
|
84
|
+
function aboutDirection(lang, r) {
|
|
85
|
+
return ui(lang, "ui.link.chat.research.say", { name: r.name, id: r.id });
|
|
86
|
+
}
|
|
83
87
|
/** The conversation's first message about a conflict: its ID and title from the research itself. */
|
|
84
88
|
function aboutConflict(root, ctx, id) {
|
|
85
89
|
const tree = Tree.open(root, ctx.env);
|
|
@@ -121,7 +125,8 @@ async function inTerminal(ctx, link) {
|
|
|
121
125
|
// commands, which ask themselves
|
|
122
126
|
case "open":
|
|
123
127
|
case "app":
|
|
124
|
-
|
|
128
|
+
case "live":
|
|
129
|
+
return { text: "", data: { done: (await run(link.action === "open" ? [] : link.action === "live" ? ["app", "--live"] : ["app"])) === 0, action: link.action, tree: root } };
|
|
125
130
|
case "update":
|
|
126
131
|
case "setup":
|
|
127
132
|
return end((await run([link.action])) === 0);
|
|
@@ -158,6 +163,14 @@ async function inTerminal(ctx, link) {
|
|
|
158
163
|
const { undoSending } = await import("../cli/menu-links.js");
|
|
159
164
|
return end(await undoSending(ctx, run, treeLang, root, link.intake));
|
|
160
165
|
}
|
|
166
|
+
case "direction": {
|
|
167
|
+
const { directionDo } = await import("../cli/menu-links.js");
|
|
168
|
+
return end(await directionDo(ctx, run, treeLang, root, link.id, link.do));
|
|
169
|
+
}
|
|
170
|
+
case "finish": {
|
|
171
|
+
const { finishSession } = await import("../cli/menu-links.js");
|
|
172
|
+
return end(await finishSession(ctx, run, treeLang, root, link.session));
|
|
173
|
+
}
|
|
161
174
|
}
|
|
162
175
|
const named = "person" in link ? link.person : undefined;
|
|
163
176
|
const person = named ? personOf(root, ctx, named) : undefined;
|
|
@@ -166,11 +179,20 @@ async function inTerminal(ctx, link) {
|
|
|
166
179
|
const { asks } = await import("../cli/menu-links.js");
|
|
167
180
|
switch (link.action) {
|
|
168
181
|
case "chat": {
|
|
182
|
+
// one direction: its tasks only (the research's own name and ID in the first message)
|
|
183
|
+
const direction = link.research ? tree.get(link.research) : undefined;
|
|
184
|
+
if (link.research && direction?.type !== "research")
|
|
185
|
+
return said("ui.link.direction.none", { id: link.research });
|
|
169
186
|
// the agent costs: said and asked first, then the conversation as strom chat starts it
|
|
170
|
-
const what =
|
|
187
|
+
const what = direction
|
|
188
|
+
? ui(treeLang, "ui.link.what.chat.research", { agent, name: direction.name })
|
|
189
|
+
: person
|
|
190
|
+
? ui(treeLang, "ui.link.what.chat.person", { agent, person: who(person) })
|
|
191
|
+
: ui(treeLang, "ui.link.what.chat", { agent });
|
|
171
192
|
if (!(await asks(ctx, treeLang, root, what, ui(treeLang, "ui.link.cost"))))
|
|
172
193
|
return { text: "", data: { done: false, action: link.action, tree: root } };
|
|
173
|
-
|
|
194
|
+
const first = direction ? aboutDirection(treeLang, direction) : person ? aboutPerson(treeLang, person) : undefined;
|
|
195
|
+
return { text: "", data: { done: (await run(["chat", ...(first ? [`--say=${first}`] : [])])) === 0, action: link.action, tree: root } };
|
|
174
196
|
}
|
|
175
197
|
case "story": {
|
|
176
198
|
const { approveStory } = await import("../cli/menu-links.js");
|
|
@@ -206,6 +228,9 @@ async function withoutTerminal(ctx, link, root) {
|
|
|
206
228
|
const run = await runnerOf(ctx)(root);
|
|
207
229
|
if (link.action === "app")
|
|
208
230
|
return { text: "", data: { done: (await run(["app"], true)) === 0, action: "app", tree: root } };
|
|
231
|
+
// followed live: the bridge started (or the one running), its address into the app's window — strom app --live
|
|
232
|
+
if (link.action === "live")
|
|
233
|
+
return { text: "", data: { done: (await run(["app", "--live"], true)) === 0, action: "live", tree: root } };
|
|
209
234
|
if (link.action !== "chat" && !(link.action === "conflict" && link.do === "agent"))
|
|
210
235
|
return undefined;
|
|
211
236
|
const tree = Tree.open(root, ctx.env);
|
|
@@ -217,6 +242,12 @@ async function withoutTerminal(ctx, link, root) {
|
|
|
217
242
|
if (!first)
|
|
218
243
|
return say(ctx, "ui.link.conflict.none", { id: link.id }, { tree: link.tree });
|
|
219
244
|
}
|
|
245
|
+
else if (link.action === "chat" && link.research) {
|
|
246
|
+
const direction = tree.get(link.research);
|
|
247
|
+
if (direction?.type !== "research")
|
|
248
|
+
return say(ctx, "ui.link.direction.none", { id: link.research }, { tree: link.tree });
|
|
249
|
+
first = aboutDirection(tree.lang, direction);
|
|
250
|
+
}
|
|
220
251
|
else if (link.action === "chat" && link.person) {
|
|
221
252
|
const person = personOf(root, ctx, link.person);
|
|
222
253
|
if (!person)
|
|
@@ -251,6 +282,7 @@ register({
|
|
|
251
282
|
"strom-research://excerpt?tree=<id>&source=S0042&clip=<its mark> — the excerpt from the scan, not made smaller, in the\n" +
|
|
252
283
|
"viewer of the system (the page in the online archive when the scan is not here). Nothing is written.\n" +
|
|
253
284
|
"strom-research://app?tree=<id> — the research as it is now into the app's open window (strom app).\n" +
|
|
285
|
+
"strom-research://live?tree=<id> — the research followed live in the app's window (strom app --live).\n" +
|
|
254
286
|
"strom-research://open?tree=<id> — the research's menu in a terminal.\n" +
|
|
255
287
|
"strom-research://chat?tree=<id>[&person=P0012] — a conversation with the agent (strom chat; of one person: its first\n" +
|
|
256
288
|
"message names only who). strom-research://task?tree=<id>&task=T0007 — the task that waits for the user, answered.\n" +
|
|
@@ -260,6 +292,10 @@ register({
|
|
|
260
292
|
"strom-research://conflict?tree=<id>&id=X0007&do=decide|agent — decided by the user (which claim, why) or left to the agent.\n" +
|
|
261
293
|
"strom-research://story?tree=<id>&person=P0012&do=final — the story approved (strom story approve).\n" +
|
|
262
294
|
"strom-research://sync-undo?tree=<id>&intake=I0042 — a sending from the app taken back (strom sync undo).\n" +
|
|
295
|
+
"strom-research://direction?tree=<id>&id=G0002&do=pause|done|resume — a direction paused, ended (the reason asked) or\n" +
|
|
296
|
+
"taken up again (strom research pause|done|resume).\n" +
|
|
297
|
+
"strom-research://chat?tree=<id>&research=G0002 — a conversation about one direction, its tasks only.\n" +
|
|
298
|
+
"strom-research://finish?tree=<id>&session=N0012 — the session at work asked to finish (strom session finish).\n" +
|
|
263
299
|
"strom-research://update|sessions|setup?tree=<id> — strom update, the agent's sessions and their cost, strom setup.\n" +
|
|
264
300
|
"strom-research://new?app=<the app's mark> — a tree of the app becomes a new research: named, made, the app hands the\n" +
|
|
265
301
|
"tree to its bridge (?adopt=, GET/POST /adopt), taken in as leads, sent back to the app.\n" +
|
package/dist/commands/media.js
CHANGED
|
@@ -26,12 +26,20 @@ export function registerImages(tree, shared, items, recordset) {
|
|
|
26
26
|
const known = new Map(tree.list("media").filter((m) => !m.retracted).map((m) => [m.sha, m]));
|
|
27
27
|
const added = [];
|
|
28
28
|
const again = [];
|
|
29
|
+
const restored = [];
|
|
29
30
|
const clashes = [];
|
|
30
31
|
const copies = [];
|
|
31
32
|
tree.withTreeLock(() => {
|
|
32
33
|
for (const it of items) {
|
|
33
34
|
const sha = fileSha256(it.file);
|
|
34
35
|
const k = known.get(sha);
|
|
36
|
+
if (k && !tree.dryRun && !fs.existsSync(path.join(shared, k.file))) {
|
|
37
|
+
// its file is not here (a research handed over without its images): the same scan, put back where it was
|
|
38
|
+
fs.mkdirSync(path.dirname(path.join(shared, k.file)), { recursive: true });
|
|
39
|
+
fs.copyFileSync(it.file, path.join(shared, k.file));
|
|
40
|
+
restored.push(k.id);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
35
43
|
if (k) {
|
|
36
44
|
again.push(k.id);
|
|
37
45
|
// the same scan as another image: a portal serving another book's images, or a file given the wrong number
|
|
@@ -85,7 +93,7 @@ export function registerImages(tree, shared, items, recordset) {
|
|
|
85
93
|
});
|
|
86
94
|
woken.push(t.id);
|
|
87
95
|
}
|
|
88
|
-
return { added, again, woken, clashes, copies };
|
|
96
|
+
return { added, again, restored, woken, clashes, copies };
|
|
89
97
|
}
|
|
90
98
|
function sharedDir(ctx) {
|
|
91
99
|
return ctx.settings.shared().value;
|
|
@@ -264,7 +272,7 @@ register({
|
|
|
264
272
|
hint: "rename the files by the image number the viewer shows (9.jpg), or --image <n> for one file",
|
|
265
273
|
});
|
|
266
274
|
}
|
|
267
|
-
const { added, again, woken, clashes, copies } = registerImages(tree, shared, items, recordset);
|
|
275
|
+
const { added, again, restored, woken, clashes, copies } = registerImages(tree, shared, items, recordset);
|
|
268
276
|
if (opts.inbox && !tree.dryRun) {
|
|
269
277
|
for (const f of files)
|
|
270
278
|
fs.rmSync(f, { force: true }); // now in the store
|
|
@@ -275,7 +283,7 @@ register({
|
|
|
275
283
|
const nums = added.map((m) => m.image).filter((n) => n !== undefined);
|
|
276
284
|
const text = lines(added.length
|
|
277
285
|
? `${added.length} image(s) registered: ${added[0].id}${added.length > 1 ? `–${added.at(-1).id}` : ""}${recordset ? ` of ${recordset}` : ""}${nums.length ? ` (images ${runs(nums)})` : ""}`
|
|
278
|
-
: "no new images", again.length ? `${again.length} already registered (same content): ${again.slice(0, 5).join(" ")}${again.length > 5 ? " …" : ""}` : undefined, ...clashes.slice(0, 10).map((c) => `⚠ ${c}`), ...copies.slice(0, 10), copies.length > 10 ? `… and ${copies.length - 10} more images with a sharper copy` : undefined, woken.length ? `back in the queue (they waited for these images): ${woken.join(" ")}` : undefined, tree.dryRun ? "(dry run — nothing written)" : undefined, added[0] ? `\nlook at one: strom media view ${recordset && added[0].image !== undefined ? `${recordset}:${added[0].image}` : added[0].id} --grid` : undefined);
|
|
286
|
+
: restored.length ? undefined : "no new images", restored.length ? `${restored.length} image(s) put back (their file was not here; the same scan): ${restored.slice(0, 5).join(" ")}${restored.length > 5 ? " …" : ""}` : undefined, again.length ? `${again.length} already registered (same content): ${again.slice(0, 5).join(" ")}${again.length > 5 ? " …" : ""}` : undefined, ...clashes.slice(0, 10).map((c) => `⚠ ${c}`), ...copies.slice(0, 10), copies.length > 10 ? `… and ${copies.length - 10} more images with a sharper copy` : undefined, woken.length ? `back in the queue (they waited for these images): ${woken.join(" ")}` : undefined, tree.dryRun ? "(dry run — nothing written)" : undefined, added[0] ? `\nlook at one: strom media view ${recordset && added[0].image !== undefined ? `${recordset}:${added[0].image}` : added[0].id} --grid` : undefined);
|
|
279
287
|
return { text, data: { added: added.map((m) => ({ id: m.id, image: m.image })), again, woken, clashes, copies } };
|
|
280
288
|
},
|
|
281
289
|
}, {
|