strom-research 1.13.2-beta.12 → 1.13.2-beta.13
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/assets/lang/cs.json +112 -23
- package/assets/lang/de.json +112 -23
- package/assets/method/core.md +9 -0
- package/assets/method/locate.md +3 -2
- package/assets/method/reading.md +8 -0
- package/assets/method/request.md +12 -3
- package/assets/method/waiting.md +66 -0
- package/dist/agents/files.js +8 -2
- package/dist/brief/brief.js +23 -3
- package/dist/brief/sheet.js +3 -1
- package/dist/cli/human.js +28 -3
- package/dist/cli/menu-archives.js +73 -76
- package/dist/cli/menu-links.js +90 -1
- package/dist/cli/menu-research.js +1 -1
- package/dist/cli/menu-waiting.js +487 -112
- package/dist/cli/menu.js +6 -8
- package/dist/cli/ui.js +102 -20
- package/dist/commands/archives.js +9 -2
- package/dist/commands/guide.js +8 -2
- package/dist/commands/intake.js +45 -11
- package/dist/commands/links.js +11 -0
- package/dist/commands/media.js +20 -3
- package/dist/commands/meta.js +15 -1
- package/dist/commands/read.js +6 -3
- package/dist/commands/readings.js +25 -5
- package/dist/commands/session.js +18 -1
- package/dist/commands/start.js +37 -5
- package/dist/commands/tasks.js +199 -13
- package/dist/core/archivewait.js +4 -2
- package/dist/core/changelog.js +7 -0
- package/dist/core/conflicts.js +1 -1
- package/dist/core/impersonal.js +69 -0
- package/dist/core/links.js +6 -2
- package/dist/core/live.js +191 -146
- package/dist/core/media.js +2 -0
- package/dist/core/model.js +2 -0
- package/dist/core/originals.js +5 -5
- package/dist/core/phrases.js +9 -2
- package/dist/core/reader.js +19 -2
- package/dist/core/schema.js +4 -1
- package/dist/core/stromapp.js +2 -1
- package/dist/core/taskfiles.js +64 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/waiting.js +350 -0
- package/dist/core/waitkind.js +276 -0
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Method: a conversation about what waits for the user
|
|
2
|
+
|
|
3
|
+
The user picked a task that waits for them (strom's menu, What waits; the Strom
|
|
4
|
+
app) and the conversation begins with it: its ID, what it waits for, where it is
|
|
5
|
+
dealt with, and the other tasks waiting at the same place. The user is here to
|
|
6
|
+
get it done with your help — lead them through it, one step at a time, in the
|
|
7
|
+
research language.
|
|
8
|
+
|
|
9
|
+
- First `strom task show T…` for the task and for each of the others named
|
|
10
|
+
with it: what it waits for, the book and its images, the folder for images
|
|
11
|
+
saved by hand, whom a request goes to, its text in `notes/`.
|
|
12
|
+
- Take the tasks of one place one after another, in the order given; say which
|
|
13
|
+
one you are on.
|
|
14
|
+
- The user's answer is theirs to give in strom's menu or the Strom app — never
|
|
15
|
+
record it with `strom task wake T… --answer`. What you find together you
|
|
16
|
+
record with the usual commands (a source with its transcript, the facts
|
|
17
|
+
citing it; what the user tells you: `strom intake --text "…"`).
|
|
18
|
+
|
|
19
|
+
## Images saved by hand
|
|
20
|
+
|
|
21
|
+
An archive that allows no automation (or whose connector cannot fetch images):
|
|
22
|
+
the user saves the images, you guide them. You open nothing and download
|
|
23
|
+
nothing yourself — not through a browser either.
|
|
24
|
+
|
|
25
|
+
1. The book: its title and its address on the archive's site (the task shows
|
|
26
|
+
them). An archive that needs an account: the user logs in with their own.
|
|
27
|
+
2. Which images: the numbers the task names, as the archive's viewer counts
|
|
28
|
+
them (the viewer's own image number, not a folio or a page printed in the
|
|
29
|
+
book).
|
|
30
|
+
3. How: the viewer's download or export, as JPEG; where the viewer offers
|
|
31
|
+
none, the browser's save of the image itself.
|
|
32
|
+
4. The name of each file: its image number (`9.jpg`; a zoomed-in part of it
|
|
33
|
+
`9a.jpg`).
|
|
34
|
+
5. Where: the exact folder of the shared inbox the task shows — say it whole.
|
|
35
|
+
|
|
36
|
+
When the user says the images are saved:
|
|
37
|
+
`strom media add --inbox "<the folder>" --recordset B…` registers them and the
|
|
38
|
+
task comes back to the queue (it says which images are still missing — ask for
|
|
39
|
+
those). Then `strom session start T…`: its brief says how to read and record.
|
|
40
|
+
Read them, record what they say, tell the user what was found
|
|
41
|
+
(`strom person card P…`) and what is needed next — all in this conversation.
|
|
42
|
+
|
|
43
|
+
## A letter or the family
|
|
44
|
+
|
|
45
|
+
1. Help write the request: short, in the language of whoever reads it; for an
|
|
46
|
+
office or an archive whose record, which type, the place, the years and what
|
|
47
|
+
is known already (method of requests). Save the text in `notes/` and name it
|
|
48
|
+
on the task (the family: `--kind family`):
|
|
49
|
+
`strom task edit T… --kind letter --to "<the addressee>" --request notes/<file>.md`
|
|
50
|
+
2. Say to whom it goes and how — e-mail, a form on the office's site, the post,
|
|
51
|
+
a call to a relative — and what to attach.
|
|
52
|
+
3. Once the user says it went out (never before):
|
|
53
|
+
`strom task sent T…` — the task still waits for the reply.
|
|
54
|
+
4. A reply that came as a file: `strom intake <file> --answers T…` — the file
|
|
55
|
+
becomes material of the task's people, the task comes back to the queue
|
|
56
|
+
with it (no intake task of its own). A reply the user tells you:
|
|
57
|
+
`strom intake --text "…"`, then `strom task wake T…`. The work goes on in a
|
|
58
|
+
session.
|
|
59
|
+
|
|
60
|
+
## A site
|
|
61
|
+
|
|
62
|
+
Say which site, where on it and what to look for there; the user looks. What
|
|
63
|
+
they find is recorded (an online record read with them is a source; a hint is a
|
|
64
|
+
lead: `strom intake --text "…"`), then `strom task wake T…`. A check of a site
|
|
65
|
+
that keeps automated tools off (`strom task wait --check`) is the user's alone
|
|
66
|
+
to answer, in strom's menu or the Strom app: say what to look at, nothing more.
|
package/dist/agents/files.js
CHANGED
|
@@ -69,6 +69,10 @@ the researcher; \`strom\` is your only way to read and change the research.
|
|
|
69
69
|
either — check with \`strom net web --check <url>\`. Another way to the
|
|
70
70
|
same records first; a task for the user (\`strom task wait T… --check
|
|
71
71
|
<url> --on "…"\`) only if the source stays necessary.
|
|
72
|
+
What a task waits for (\`--on\`) is a sentence the user reads in strom's
|
|
73
|
+
menu: impersonal, what is needed and from whom, never addressed to them;
|
|
74
|
+
say its kind (\`--kind letter|family|web|other\`) and, when known,
|
|
75
|
+
\`--to\`, \`--request notes/<file>.md\`, \`--url\`.
|
|
72
76
|
11. What strom or this tree denies you (a command, a site, a file) stays
|
|
73
77
|
denied: never offer the user to change your permissions or settings,
|
|
74
78
|
strom's or the tree's, to get round it — go another way, or say what was
|
|
@@ -98,7 +102,7 @@ says how it ends.)
|
|
|
98
102
|
there, strom says how the user clears your context (Claude Code: /clear)
|
|
99
103
|
— nothing is lost, strom keeps it all; suggest it in a sentence, go on
|
|
100
104
|
here if they prefer. Working alone (strom run) each task starts fresh by
|
|
101
|
-
itself and
|
|
105
|
+
itself and its closing message gives no such advice;
|
|
102
106
|
- what only they can do: when strom needs their consent it opens a window
|
|
103
107
|
on their screen — tell them to answer it, you cannot; images a portal gives
|
|
104
108
|
only by hand — strom says which ones and where to save them.
|
|
@@ -125,7 +129,9 @@ says how it ends.)
|
|
|
125
129
|
would take — tell them, write it on their yes (\`--apply\`); a sync is
|
|
126
130
|
undone with \`strom sync undo I…\`. Straight from the app: \`strom sync --app\`.
|
|
127
131
|
- **What waits for them** (\`strom\` shows it): tell them plainly what to do
|
|
128
|
-
and where, one thing at a time.
|
|
132
|
+
and where, one thing at a time. A conversation that begins with a task that
|
|
133
|
+
waits for them (they picked it in the menu or the Strom app): \`strom guide
|
|
134
|
+
waiting\` says how to lead it.
|
|
129
135
|
- **Results and the Strom app**: \`output/tree-strom.ged\` is the family tree
|
|
130
136
|
for the Strom app (${appSite(app)}) — strom's companion: a free family
|
|
131
137
|
tree app, no account, their data stay on their computer; it shows the tree,
|
package/dist/brief/brief.js
CHANGED
|
@@ -753,6 +753,21 @@ function archiveLine(tree, b, repo, shared, installed, qs = []) {
|
|
|
753
753
|
}
|
|
754
754
|
return parts.length ? ` archive ${a.host}: ${parts.join(" · ")}` : "";
|
|
755
755
|
}
|
|
756
|
+
/**
|
|
757
|
+
* The files that came for what the task waited for (Task.files: strom intake --answers, the Strom app's file to the
|
|
758
|
+
* task) — that they came, never the person's words: read each as the task's next step.
|
|
759
|
+
*/
|
|
760
|
+
function filesCame(task) {
|
|
761
|
+
const all = task.files ?? [];
|
|
762
|
+
if (!all.length)
|
|
763
|
+
return [];
|
|
764
|
+
const who = (by) => (by === "app" ? "from the Strom app" : by === "user" ? "from the user" : `by ${by}`);
|
|
765
|
+
return [
|
|
766
|
+
...all.slice(-5).map((f) => `file came (${f.at.slice(0, 10)}, ${who(f.by)}; asked: ${f.asked}): ${f.input} — strom ${f.input.startsWith("M") ? "media show" : "input show"} ${f.input}`),
|
|
767
|
+
all.length > 5 ? `(files: the last 5 of ${all.length} — all: strom task show ${task.id})` : "",
|
|
768
|
+
"read what came first: it is what the task waited for (what it says is recorded as from any material; strom input done <I…> once used)",
|
|
769
|
+
].filter(Boolean);
|
|
770
|
+
}
|
|
756
771
|
/**
|
|
757
772
|
* What the person answered for the task (Task.answers: only theirs, through strom task wake --answer/--check in their
|
|
758
773
|
* terminal, a window or strom's menu) — apart from the notes, which anybody writes — and the way on from a check.
|
|
@@ -833,7 +848,11 @@ export function buildBrief(tree, opts) {
|
|
|
833
848
|
research?.question ? `Research question: ${research.question}` : "",
|
|
834
849
|
...(research?.notes ?? []).slice(-3).map((n) => `From the user: ${n.text}`),
|
|
835
850
|
`Research language: ${langName(lang)} — ${alone ? "" : "talk to the user and "}write notes, tasks and summaries in ${langName(lang)}; transcripts stay in the original language.`,
|
|
836
|
-
|
|
851
|
+
// the user may read a run afterwards (its output, its log): every word in the research language, impersonal (N0288,
|
|
852
|
+
// N0292, N0295: a last message in English, "it waits for you")
|
|
853
|
+
alone
|
|
854
|
+
? `Working alone (strom run): nobody answers here, but the user may read this session afterwards (the run's output, its log) — every message of yours, the last one too, in ${langName(lang)}, about the work and never addressed to the user ("waits for a check", never "waits for you"); a step only the user can take goes into strom task wait.`
|
|
855
|
+
: "",
|
|
837
856
|
STROM_ONLY,
|
|
838
857
|
webRule(new Settings(tree.env, {}).webPerHost(tree.config)),
|
|
839
858
|
opts.deadline !== undefined ? briefClock(opts.deadline) : "",
|
|
@@ -863,6 +882,7 @@ export function buildBrief(tree, opts) {
|
|
|
863
882
|
...task.notes.slice(-3).map((n) => `note: ${n.text}`),
|
|
864
883
|
task.notes.length > 3 ? `(notes: the last 3 of ${task.notes.length} — all: strom task show ${task.id})` : "",
|
|
865
884
|
...personAnswers(tree, task),
|
|
885
|
+
...filesCame(task),
|
|
866
886
|
].filter(Boolean).join("\n"),
|
|
867
887
|
});
|
|
868
888
|
// 2. a review: what each item of the task is, as the tree has it
|
|
@@ -1265,12 +1285,12 @@ export function buildBrief(tree, opts) {
|
|
|
1265
1285
|
// there are impersonal, so is what it shows of the agent's
|
|
1266
1286
|
alone
|
|
1267
1287
|
? // working alone: every task starts in a fresh session by itself; nobody reads a closing message
|
|
1268
|
-
|
|
1288
|
+
`- the summary: plain words of the research for the record (menu, Strom app), never addressed to the user, no advice (/clear, starting anything); after strom session close you are done: a last message of one line at most, in ${langName(lang)}, never addressed to the user`
|
|
1269
1289
|
: "- the user reads the summary (menu, Strom app): plain words, never addressed to them",
|
|
1270
1290
|
// nobody answers a question in a run: what only the user can decide waits for them where they look (the menu's
|
|
1271
1291
|
// What waits for you, the Strom app), their answer comes back on the task (strom task wake --answer)
|
|
1272
1292
|
alone && task
|
|
1273
|
-
? `- a decision or a step only the user can take: strom task wait ${task.id} --on "<the question, impersonal, in the research language>" — they see it in the menu and the Strom app; a question in the summary gets no answer`
|
|
1293
|
+
? `- a decision or a step only the user can take: strom task wait ${task.id} --on "<the question, impersonal, in the research language>" — they see it in the menu and the Strom app; a question in the summary gets no answer; its kind: --kind letter|family|web|other`
|
|
1274
1294
|
: "",
|
|
1275
1295
|
"- a command you need: strom help <command> (short, with examples) — the full guide: strom guide",
|
|
1276
1296
|
].filter(Boolean).join("\n"),
|
package/dist/brief/sheet.js
CHANGED
|
@@ -18,7 +18,7 @@ const LEVELS = {
|
|
|
18
18
|
enrich: [...ALL, ...SCANS, ...ENTRY, "hypothesis add", "task wait"],
|
|
19
19
|
locate: [...ALL, "place add", "place jurisdiction", "repo add", "recordset add", "fetch", "task edit", "task wait"],
|
|
20
20
|
intake: [...ALL, "input sort", "input skip", "media view", ...ENTRY.filter((c) => c !== "event edit"), "person merge", "recordset add", "conflict add"],
|
|
21
|
-
request: [...ALL, "task wait"],
|
|
21
|
+
request: [...ALL, "task wait", "task sent"],
|
|
22
22
|
narrate: ["task add", "note add", "story set"],
|
|
23
23
|
// a task that links hypotheses to the tree (METHOD_LINKS): it reads the hypotheses and their people, and links
|
|
24
24
|
links: ["hypothesis show", "hypothesis link", "hypothesis argue", "hypothesis variant", "person show", "family show", "task add", "note add", "search add"],
|
|
@@ -34,6 +34,8 @@ const RARE = {
|
|
|
34
34
|
"source edit": ["translation", "media"],
|
|
35
35
|
"recordset add": ["images"],
|
|
36
36
|
grep: ["regex", "files"],
|
|
37
|
+
// what a waiting task waits for is said when it is set waiting (task wait); edit fills it for an older one
|
|
38
|
+
"task edit": ["on", "kind", "to", "request", "url"],
|
|
37
39
|
};
|
|
38
40
|
/** Where a source is cited — written once as +cite, the same in every command that takes all of it. */
|
|
39
41
|
const CITE = ["cite", "locator", "quote", "information"];
|
package/dist/cli/human.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { MONTHS } from "../core/gdate.js";
|
|
6
6
|
import { ui } from "./ui.js";
|
|
7
7
|
import { displayName } from "../core/people.js";
|
|
8
|
+
import { foldText } from "../core/text.js";
|
|
8
9
|
/**
|
|
9
10
|
* English reads "31 Aug 1830"; the others their own numeric form ("31. 8. 1830", "31.8.1830"). A month and
|
|
10
11
|
* year alone are named ("srpen 1830") — after a word ("před 3/1853") in numbers, which no grammatical case bends.
|
|
@@ -121,8 +122,32 @@ export function humanPlace(place, house, lang) {
|
|
|
121
122
|
return place ?? "";
|
|
122
123
|
return ui(lang, "ui.card.house", { place: place ?? "", house }).replace(/^[,\s]+/, "");
|
|
123
124
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
125
|
+
/** The words of a person's name, folded: what a text that names them may decline ("Václava" for Václav). */
|
|
126
|
+
function nameWords(p) {
|
|
127
|
+
return foldText(displayName(p)).split(/[^\p{L}\p{M}\p{N}]+/u).filter((w) => w.length >= 2);
|
|
128
|
+
}
|
|
129
|
+
/** Does a word name one of these (its stem: the first letters but the last, five at most)? */
|
|
130
|
+
function namesOne(word, names) {
|
|
131
|
+
return names.some((n) => {
|
|
132
|
+
const k = Math.min(5, Math.max(2, n.length - 1));
|
|
133
|
+
return word.length >= k && word.slice(0, k) === n.slice(0, k);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* What the agent wrote into a task, for the user: the records it names by ID by their names ("S0001" → its title).
|
|
138
|
+
* `asWritten`: a person's ID right after their name in the text stays the bare ID in brackets ("Václava Nováka P0001" →
|
|
139
|
+
* "Václava Nováka [P0001]"), never the name said twice — the short lines of What waits.
|
|
140
|
+
*/
|
|
141
|
+
export function humanTask(tree, text, lang, o = {}) {
|
|
142
|
+
const named = (id, before) => {
|
|
143
|
+
const p = tree.get(id);
|
|
144
|
+
if (p?.type !== "person")
|
|
145
|
+
return false;
|
|
146
|
+
// the two words before the ID (letters only, none of them an ID)
|
|
147
|
+
const words = foldText(before.slice(-60)).split(/[^\p{L}\p{M}\p{N}]+/u).filter(Boolean).slice(-2).filter((w) => !/^[pfsbn]\d{4,}$/u.test(w));
|
|
148
|
+
const names = nameWords(p);
|
|
149
|
+
return words.some((w) => namesOne(w, names));
|
|
150
|
+
};
|
|
126
151
|
const name = (id) => {
|
|
127
152
|
const r = tree.get(id);
|
|
128
153
|
if (!r)
|
|
@@ -136,5 +161,5 @@ export function humanTask(tree, text, lang) {
|
|
|
136
161
|
return r.title;
|
|
137
162
|
};
|
|
138
163
|
// A list of IDs as a list of names, each once (two sessions of one day read as one).
|
|
139
|
-
return text.replace(/\b[PFSBN]\d{4,}\b(?:, [PFSBN]\d{4,}\b)*/g, (ids) => [...new Set(ids.split(", ").map(name))].join(", "));
|
|
164
|
+
return text.replace(/\b[PFSBN]\d{4,}\b(?:, [PFSBN]\d{4,}\b)*/g, (ids, at) => o.asWritten && !ids.includes(",") && named(ids, text.slice(0, at)) ? `[${ids}]` : [...new Set(ids.split(", ").map(name))].join(", "));
|
|
140
165
|
}
|
|
@@ -12,64 +12,16 @@ import { Tree } from "../core/tree.js";
|
|
|
12
12
|
import { isArchive } from "../core/mode.js";
|
|
13
13
|
import { bareHost, listConnectors, routeOf, routesOf } from "../core/connector.js";
|
|
14
14
|
import { claimOf, loadArchives, rank } from "../core/terms.js";
|
|
15
|
-
import {
|
|
15
|
+
import { connectorFor, levelWords, tasksStanding, tasksWaitingOn, termsText, waysOffered, wokenLines } from "../core/archiveasks.js";
|
|
16
16
|
import { wakeForArchive } from "../core/archivewait.js";
|
|
17
|
-
import { archivesOverview, levelsOf, paceFor } from "../commands/archives.js";
|
|
18
|
-
/** A pace in the person's words: "one image at most every 10 s, at most 120 an hour". */
|
|
17
|
+
import { archivesOverview, capWords, levelsOf, paceFor } from "../commands/archives.js";
|
|
18
|
+
/** A pace in the person's words: "one image at most every 10 s, at most 120 requests … an hour" — or no hourly cap. */
|
|
19
19
|
export function paceWords(lang, p) {
|
|
20
|
-
|
|
21
|
-
return ui(lang, "ui.archive.pace", { seconds: secondsIn(lang, p.minIntervalMs), n });
|
|
20
|
+
return ui(lang, "ui.archive.pace", { seconds: secondsIn(lang, p.minIntervalMs), cap: capWords(lang, p.perHour, true) });
|
|
22
21
|
}
|
|
23
22
|
function connectorsHere(ctx) {
|
|
24
23
|
return listConnectors(ctx.settings.shared()?.value, ctx.env);
|
|
25
24
|
}
|
|
26
|
-
/** The question's lines in What waits: the archive, its terms (strom's quote, or the connector's reading), the tasks. */
|
|
27
|
-
function permissionLines(ctx, t, lang, tree, q) {
|
|
28
|
-
const tasks = tree ? tasksStanding(tree, q) : [];
|
|
29
|
-
return [
|
|
30
|
-
` ${termsText(lang, ctx.env, q)}`,
|
|
31
|
-
...(tasks.length ? [` ${t("ui.archive.item.tasks", { ids: tasks.join(", ") })}`] : []),
|
|
32
|
-
// since when it waits (the first time asked) and how many times a command needed it
|
|
33
|
-
` ${t("ui.archive.item.since", { date: humanDay(q.at, lang), tries: q.tries ?? 1 })}`,
|
|
34
|
-
];
|
|
35
|
-
}
|
|
36
|
-
/** A question the menu can put: its archive named (a permission, a milder reading), the connectors taken over. */
|
|
37
|
-
const answerable = (q) => (q.kind === "takeover" ? !!q.connectors?.length : !!q.host && (q.kind === "permission" || !!q.connector));
|
|
38
|
-
/** One question in What waits: its text, the label of its item, what choosing it does. */
|
|
39
|
-
export function archiveItems(ctx, run, lang, root, start, max) {
|
|
40
|
-
const t = translator(lang);
|
|
41
|
-
const tree = Tree.open(root, ctx.env);
|
|
42
|
-
const qs = archiveQuestions(tree).filter(answerable);
|
|
43
|
-
const text = [];
|
|
44
|
-
const items = [];
|
|
45
|
-
const connectors = qs.length ? connectorsHere(ctx) : [];
|
|
46
|
-
for (const q of qs) {
|
|
47
|
-
if (items.length >= max)
|
|
48
|
-
break;
|
|
49
|
-
const k = start + items.length + 1;
|
|
50
|
-
if (q.kind === "permission" && q.host) {
|
|
51
|
-
const title = q.title ?? q.host;
|
|
52
|
-
text.push("", ` ${k}. ${t("ui.archive.item", { title, host: q.host })}`, ...permissionLines(ctx, t, lang, tree, q));
|
|
53
|
-
items.push({ key: String(k), label: t("ui.archive.item.read", { k, title }), act: async () => void (await decideArchive(ctx, run, lang, root, q.host, q)) });
|
|
54
|
-
}
|
|
55
|
-
else if (q.kind === "takeover" && q.connectors?.length) {
|
|
56
|
-
const names = q.connectors.map((n) => connectors.find((c) => c.name === n)?.manifest.title ?? n);
|
|
57
|
-
text.push("", ` ${k}. ${t("ui.archive.takeover.item", { n: names.length, names: names.join(", ") })}`);
|
|
58
|
-
items.push({ key: String(k), label: t("ui.archive.takeover.read", { k }), act: () => decideTakeover(ctx, run, lang, q) });
|
|
59
|
-
}
|
|
60
|
-
else if (q.kind === "milder" && q.host && q.connector) {
|
|
61
|
-
const c = connectors.find((x) => x.name === q.connector);
|
|
62
|
-
const title = c?.manifest.title ?? q.title ?? q.connector;
|
|
63
|
-
text.push("", ` ${k}. ${t("ui.archive.milder.item", { title, host: q.host, to: levelWords(lang, q.to ?? "allowed"), from: levelWords(lang, q.from ?? "restricted") })}`);
|
|
64
|
-
items.push({ key: String(k), label: t("ui.archive.milder.read", { k, title }), act: () => decideMilder(ctx, run, lang, q, title) });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return { text, items, count: qs.length };
|
|
68
|
-
}
|
|
69
|
-
/** How many questions about archives wait with this research (none in an archive). */
|
|
70
|
-
export function archiveQuestionCount(tree) {
|
|
71
|
-
return archiveQuestions(tree).filter(answerable).length;
|
|
72
|
-
}
|
|
73
25
|
/** Every research on this computer, the one worked on first: a decision about an archive is the person's for all. */
|
|
74
26
|
export function rootsHere(ctx, root) {
|
|
75
27
|
return [...new Set([...(root ? [root] : []), ...ctx.knownTrees().map((k) => k.root)])];
|
|
@@ -78,11 +30,18 @@ export function rootsHere(ctx, root) {
|
|
|
78
30
|
function levelHere(ctx, c, host) {
|
|
79
31
|
return levelsOf(ctx, c, true).find((a) => a.host === host);
|
|
80
32
|
}
|
|
33
|
+
/** Can a downloader give images at all — directly (fetch, part) or through the browser (locate)? Else they go only by hand. */
|
|
34
|
+
function givesImages(c) {
|
|
35
|
+
const can = c.manifest.can;
|
|
36
|
+
const routes = routesOf(c);
|
|
37
|
+
return (routes.includes("direct") && (can.includes("fetch") || can.includes("part"))) || (routes.includes("browser") && (can.includes("locate") || can.includes("fetch")));
|
|
38
|
+
}
|
|
81
39
|
/**
|
|
82
40
|
* One archive: what its terms say and the ways to get its images — a question that waited (What waits, a link), or
|
|
83
41
|
* the person's look at it in the settings (the current way suggested, its permission to be forgotten). True: decided.
|
|
42
|
+
* `all`: the archive's hosts (its downloader's, the same state on each) — said once, decided for all of them.
|
|
84
43
|
*/
|
|
85
|
-
export async function decideArchive(ctx, run, lang, root, host, q) {
|
|
44
|
+
export async function decideArchive(ctx, run, lang, root, host, q, all) {
|
|
86
45
|
const t = translator(lang);
|
|
87
46
|
const out = (line = "") => ctx.io.stdout(line + "\n");
|
|
88
47
|
const c = connectorFor(connectorsHere(ctx), host, q?.connector);
|
|
@@ -93,12 +52,18 @@ export async function decideArchive(ctx, run, lang, root, host, q) {
|
|
|
93
52
|
const a = levelHere(ctx, c, host);
|
|
94
53
|
const title = a?.title ?? q?.title ?? c.manifest.title ?? host;
|
|
95
54
|
const tree = root ? Tree.open(root, ctx.env) : undefined;
|
|
55
|
+
const hosts = all && all.length > 1 ? all : [host];
|
|
56
|
+
// the command's target: the downloader for all its hosts at once, else the host
|
|
57
|
+
const target = hosts.length > 1 ? c.name : host;
|
|
96
58
|
out("");
|
|
97
|
-
out(` ${t("ui.archive.item", { title, host })}`);
|
|
59
|
+
out(` ${t("ui.archive.item", { title, host: hosts.join(", ") })}`);
|
|
98
60
|
out(` ${termsText(lang, ctx.env, { host, ...(a?.quote ? { quote: a.quote } : q?.quote ? { quote: q.quote } : {}), ...(a?.terms ?? q?.terms ? { terms: (a?.terms ?? q?.terms) } : {}), ...(a?.quoteBy ? { quoteBy: a.quoteBy } : q?.quoteBy ? { quoteBy: q.quoteBy } : {}) })}`);
|
|
99
|
-
const tasks = tree && !isArchive(tree) ? (q?.kind === "permission" ? tasksStanding(tree, q) : tasksWaitingOn(tree,
|
|
61
|
+
const tasks = tree && !isArchive(tree) ? (q?.kind === "permission" ? tasksStanding(tree, q) : [...new Set(hosts.flatMap((h) => tasksWaitingOn(tree, h).map((x) => x.id)))]) : [];
|
|
100
62
|
if (tasks.length)
|
|
101
63
|
out(` ${t("ui.archive.item.tasks", { ids: tasks.join(", ") })}`);
|
|
64
|
+
// a question that waits: since when (the first time asked) and how many times a command needed it
|
|
65
|
+
if (q?.kind === "permission")
|
|
66
|
+
out(` ${t("ui.archive.item.since", { date: humanDay(q.at, lang), tries: q.tries ?? 1 })}`);
|
|
102
67
|
// a server that refused strom's client: its answer, not the archive's terms — only the person lifts it (--unblock)
|
|
103
68
|
if (a?.level === "forbidden" && a.refused && !a.refused.unreadable) {
|
|
104
69
|
const h = a.refused.host;
|
|
@@ -106,9 +71,10 @@ export async function decideArchive(ctx, run, lang, root, host, q) {
|
|
|
106
71
|
await pause(ctx, lang);
|
|
107
72
|
return false;
|
|
108
73
|
}
|
|
109
|
-
// a level no permission opens (by hand only, nothing at all): said, and who lifts it
|
|
74
|
+
// a level no permission opens (by hand only, nothing at all): said, and who lifts it — only where lifting it would
|
|
75
|
+
// open a way (its downloader gives images, the archive refuses strom nothing); else the images go by hand
|
|
110
76
|
if (a && (a.level === "manual" || a.level === "forbidden")) {
|
|
111
|
-
out(` ${t("ui.archive.closed", { title, level: levelWords(lang, a.level, false, a.from), host })}`);
|
|
77
|
+
out(` ${t(givesImages(c) && !a.refused ? "ui.archive.closed" : "ui.archive.closed.hand", { title, level: levelWords(lang, a.level, false, a.from), host: target })}`);
|
|
112
78
|
await pause(ctx, lang);
|
|
113
79
|
return false;
|
|
114
80
|
}
|
|
@@ -125,7 +91,7 @@ export async function decideArchive(ctx, run, lang, root, host, q) {
|
|
|
125
91
|
const now = a?.permission;
|
|
126
92
|
const choices = ways.map((w) => ({
|
|
127
93
|
way: w,
|
|
128
|
-
label: t(w === "slow" ? "ui.archive.way.slow" : w === "browser" ? "ui.archive.way.browser" : "ui.archive.way.manual", { seconds: secondsIn(lang, slow.minIntervalMs),
|
|
94
|
+
label: t(w === "slow" ? "ui.archive.way.slow" : w === "browser" ? "ui.archive.way.browser" : "ui.archive.way.manual", { seconds: secondsIn(lang, slow.minIntervalMs), cap: capWords(lang, slow.perHour) }),
|
|
129
95
|
}));
|
|
130
96
|
if (now)
|
|
131
97
|
choices.push({ label: t("ui.archive.way.revoke") });
|
|
@@ -136,14 +102,14 @@ export async function decideArchive(ctx, run, lang, root, host, q) {
|
|
|
136
102
|
return false;
|
|
137
103
|
const chosen = choices[i].way;
|
|
138
104
|
if (!chosen) {
|
|
139
|
-
if ((await run(["allow", "archive",
|
|
105
|
+
if ((await run(["allow", "archive", target, "--revoke"], true)) !== 0)
|
|
140
106
|
return false;
|
|
141
107
|
out(t("ui.archive.revoked", { title }));
|
|
142
108
|
return true;
|
|
143
109
|
}
|
|
144
110
|
// what the decision does to the tasks that waited (the command wakes them, in every research here): said after it
|
|
145
|
-
const plan = wakeForArchive(ctx.env, rootsHere(ctx, root),
|
|
146
|
-
if ((await run(["allow", "archive",
|
|
111
|
+
const plan = hosts.flatMap((h) => wakeForArchive(ctx.env, rootsHere(ctx, root), h, { ...(ctx.settings.shared()?.value ? { shared: ctx.settings.shared().value } : {}), way: chosen, dry: true }));
|
|
112
|
+
if ((await run(["allow", "archive", target, "--way", chosen], true)) !== 0)
|
|
147
113
|
return false;
|
|
148
114
|
// the connector's route follows the way chosen: through the browser, or straight through strom
|
|
149
115
|
const via = routeOf(ctx.env, c).via;
|
|
@@ -157,7 +123,7 @@ export async function decideArchive(ctx, run, lang, root, host, q) {
|
|
|
157
123
|
return true;
|
|
158
124
|
}
|
|
159
125
|
/** The connectors taken over from an older strom: confirmed all at once, or gone through one by one. */
|
|
160
|
-
async function decideTakeover(ctx, run, lang, q) {
|
|
126
|
+
export async function decideTakeover(ctx, run, lang, q) {
|
|
161
127
|
const t = translator(lang);
|
|
162
128
|
const out = (line = "") => ctx.io.stdout(line + "\n");
|
|
163
129
|
const all = connectorsHere(ctx).filter((c) => q.connectors?.includes(c.name));
|
|
@@ -202,7 +168,7 @@ async function decideTakeover(ctx, run, lang, q) {
|
|
|
202
168
|
}
|
|
203
169
|
}
|
|
204
170
|
/** A connector that says now an archive allows more than strom keeps: accepted, or kept as it is. */
|
|
205
|
-
async function decideMilder(ctx, run, lang, q, title) {
|
|
171
|
+
export async function decideMilder(ctx, run, lang, q, title) {
|
|
206
172
|
const t = translator(lang);
|
|
207
173
|
const out = (line = "") => ctx.io.stdout(line + "\n");
|
|
208
174
|
const c = connectorsHere(ctx).find((x) => x.name === q.connector);
|
|
@@ -227,13 +193,15 @@ function onlyBrowser(t, c, robots) {
|
|
|
227
193
|
export function browserOnlyHere(ctx, c) {
|
|
228
194
|
return archivesOverview(ctx).some((r) => r.connector === c.name && r.browserOnly && r.level !== "manual" && r.level !== "forbidden");
|
|
229
195
|
}
|
|
230
|
-
/**
|
|
231
|
-
|
|
196
|
+
/**
|
|
197
|
+
* What holds for a downloader's archive, on its line in the settings: its level, the person's way and since when, the
|
|
198
|
+
* pace — once for all its hosts where they are the same (`host`: that host alone).
|
|
199
|
+
*/
|
|
200
|
+
export function archiveState(ctx, lang, c, host) {
|
|
232
201
|
const t = translator(lang);
|
|
233
|
-
const rows = archivesOverview(ctx).filter((r) => r.connector === c.name);
|
|
202
|
+
const rows = archivesOverview(ctx).filter((r) => r.connector === c.name && (!host || r.host === host));
|
|
234
203
|
const store = loadArchives(ctx.env).archives;
|
|
235
|
-
|
|
236
|
-
.map((r) => {
|
|
204
|
+
const said = rows.map((r) => {
|
|
237
205
|
const rec = store[r.host];
|
|
238
206
|
const level = r.level;
|
|
239
207
|
const closed = level === "manual" || level === "forbidden";
|
|
@@ -242,23 +210,52 @@ export function archiveState(ctx, lang, c) {
|
|
|
242
210
|
const only = !closed && !r.way && r.browserOnly ? onlyBrowser(t, c, !!r.robots) : undefined;
|
|
243
211
|
const way = only ?? (!closed && r.way ? t("ui.archives.state.way", { way: t(`ui.archive.is.${r.way}`), date: rec?.at ? humanDay(rec.at, lang) : "–" }) : levelWords(lang, level, !!r.robots, r.from));
|
|
244
212
|
const pace = !closed && r.way !== "manual" && (level === "allowed" || r.way) ? `; ${paceWords(lang, r.pace)}` : "";
|
|
245
|
-
return
|
|
246
|
-
})
|
|
247
|
-
|
|
213
|
+
return { host: r.host, text: `${way}${pace}` };
|
|
214
|
+
});
|
|
215
|
+
// said once where every host of the archive is the same, per host only where they differ
|
|
216
|
+
return said.every((x) => x.text === said[0].text) ? (said[0]?.text ?? "") : said.map((x) => `${x.host}: ${x.text}`).join(" · ");
|
|
217
|
+
}
|
|
218
|
+
/** Each host of a downloader with what holds for it. */
|
|
219
|
+
function hostStates(ctx, lang, c) {
|
|
220
|
+
return [...new Set(archivesOverview(ctx).filter((r) => r.connector === c.name).map((r) => r.host))].map((host) => ({ host, text: archiveState(ctx, lang, c, host) }));
|
|
248
221
|
}
|
|
249
222
|
/** Settings → Archives → permissions: each archive of a downloader here, then the same choices as in What waits. */
|
|
250
223
|
export async function archivePermissions(ctx, run, lang, root) {
|
|
251
224
|
const t = translator(lang);
|
|
225
|
+
const pending = (hosts) => loadArchives(ctx.env).pending.find((p) => p.kind === "permission" && hosts.includes(p.host ?? ""));
|
|
252
226
|
await subMenu(ctx, lang, () => {
|
|
253
227
|
const rows = archivesOverview(ctx);
|
|
254
|
-
const hosts = [...new Map(rows.map((r) => [r.host, r])).values()];
|
|
255
228
|
const connectors = connectorsHere(ctx);
|
|
256
|
-
|
|
257
|
-
|
|
229
|
+
// one line per archive (its downloader) with its hosts inside it — a host two downloaders reach under the first
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
const archives = [];
|
|
232
|
+
for (const name of [...new Set(rows.map((r) => r.connector))]) {
|
|
233
|
+
const c = connectors.find((x) => x.name === name);
|
|
234
|
+
const hosts = [...new Set(rows.filter((r) => r.connector === name && !seen.has(r.host)).map((r) => r.host))];
|
|
235
|
+
if (!c || !hosts.length)
|
|
236
|
+
continue;
|
|
237
|
+
for (const h of hosts)
|
|
238
|
+
seen.add(h);
|
|
239
|
+
archives.push({ c, hosts });
|
|
240
|
+
}
|
|
241
|
+
const items = archives.slice(0, 9).map(({ c, hosts }, k) => {
|
|
242
|
+
const states = hostStates(ctx, lang, c).filter((x) => hosts.includes(x.host));
|
|
243
|
+
const same = states.every((x) => x.text === states[0]?.text);
|
|
244
|
+
const title = c.manifest.title ?? c.name;
|
|
258
245
|
return {
|
|
259
246
|
key: String(k + 1),
|
|
260
|
-
label: `${
|
|
261
|
-
act: async () =>
|
|
247
|
+
label: `${title} (${hosts.join(", ")}) – ${same ? (states[0]?.text ?? "") : states.map((x) => `${x.host}: ${x.text}`).join(" · ")}`,
|
|
248
|
+
act: async () => {
|
|
249
|
+
// the same on every host: decided for the archive at once; else for the host the person picks
|
|
250
|
+
if (same || hosts.length === 1) {
|
|
251
|
+
const q = pending(hosts);
|
|
252
|
+
return void (await decideArchive(ctx, run, lang, root, q?.host ?? hosts[0], q, hosts));
|
|
253
|
+
}
|
|
254
|
+
const i = await ctx.choose(title, states.map((x) => ({ label: `${x.host} – ${x.text}` })), states.length, { back: t("ui.browse.back") });
|
|
255
|
+
if (i === undefined)
|
|
256
|
+
return;
|
|
257
|
+
await decideArchive(ctx, run, lang, root, states[i].host, pending([states[i].host]));
|
|
258
|
+
},
|
|
262
259
|
};
|
|
263
260
|
});
|
|
264
261
|
return { title: t(items.length ? "ui.archives.permissions.title" : "ui.archives.empty"), items };
|
package/dist/cli/menu-links.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { outOfAnswers, translator } from "./menu-parts.js";
|
|
9
|
-
import { directionTasks, offerChat, setDirection } from "./menu-research.js";
|
|
9
|
+
import { askPaths, directionTasks, offerChat, setDirection } from "./menu-research.js";
|
|
10
10
|
import { humanCost, humanDay, humanTask, humanWhen } from "./human.js";
|
|
11
11
|
import { lines, truncate } from "./format.js";
|
|
12
12
|
import { isArchive } from "../core/mode.js";
|
|
@@ -36,6 +36,10 @@ import { forgetTree } from "../commands/trees.js";
|
|
|
36
36
|
import { archiveQuestions } from "../core/archiveasks.js";
|
|
37
37
|
import { bareHost, listConnectors } from "../core/connector.js";
|
|
38
38
|
import { decideArchive } from "./menu-archives.js";
|
|
39
|
+
import { inboxOf, requestFile, sameGroup } from "../core/waiting.js";
|
|
40
|
+
import { SENDABLE, waitGroupOf, waitKindOf } from "../core/waitkind.js";
|
|
41
|
+
import { openForUser } from "../core/open.js";
|
|
42
|
+
import { fileRefused } from "../core/taskfiles.js";
|
|
39
43
|
/** What the Strom app asks, said before anything is done; true when the person goes on. */
|
|
40
44
|
export async function asks(ctx, lang, root, what, note, suggested = true) {
|
|
41
45
|
const name = Tree.open(root, ctx.env).config.name;
|
|
@@ -74,6 +78,91 @@ export async function taskDo(ctx, run, lang, root, id, what) {
|
|
|
74
78
|
out(ctx, t(`ui.link.task.${what}.done`));
|
|
75
79
|
return true;
|
|
76
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* What a link of the Strom app does with a task that waits: a conversation about it and what waits with it at the same
|
|
83
|
+
* place (said and asked first: the agent costs), its request's text or its folder of images opened (nothing written),
|
|
84
|
+
* its request marked sent on the person's word (Enter: no). True: done.
|
|
85
|
+
*
|
|
86
|
+
* `menu`: the same chosen in the menu's What waits — the person picked it there, nothing said as the app's: a
|
|
87
|
+
* conversation starts at once, sent is asked in a line of its own (Enter: no).
|
|
88
|
+
*/
|
|
89
|
+
export async function waitingDo(ctx, run, lang, root, id, what, agent, menu = false) {
|
|
90
|
+
const t = translator(lang);
|
|
91
|
+
const tree = Tree.open(root, ctx.env);
|
|
92
|
+
const task = tree.get(id);
|
|
93
|
+
if (!task || task.type !== "task" || task.retracted)
|
|
94
|
+
return stop(ctx, t("ui.link.task.none", { task: id }));
|
|
95
|
+
if (task.state !== "waiting")
|
|
96
|
+
return stop(ctx, t("ui.link.notask"));
|
|
97
|
+
const shown = humanTask(tree, task.what, lang);
|
|
98
|
+
if (what === "chat") {
|
|
99
|
+
if (menu)
|
|
100
|
+
return (await run(["chat", "--task", task.id])) === 0;
|
|
101
|
+
const more = sameGroup(tree, task, ctx.settings.shared()?.value).length;
|
|
102
|
+
const said = `${t("ui.link.what.task.chat", { agent, what: shown })}${more ? t("ui.link.what.task.chat.more", { n: more }) : ""}`;
|
|
103
|
+
if (!(await asks(ctx, lang, root, said, t("ui.link.cost"))))
|
|
104
|
+
return false;
|
|
105
|
+
return (await run(["chat", "--task", task.id])) === 0;
|
|
106
|
+
}
|
|
107
|
+
if (what === "note") {
|
|
108
|
+
const file = requestFile(tree, task);
|
|
109
|
+
if (!file)
|
|
110
|
+
return stop(ctx, t("ui.link.note.none", { task: task.id }));
|
|
111
|
+
openForUser(file, ctx.env);
|
|
112
|
+
out(ctx, t("ui.link.note.opened", { file: ctx.display(file) }));
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (what === "folder") {
|
|
116
|
+
const inbox = inboxOf(ctx.settings.shared()?.value, task);
|
|
117
|
+
if (!inbox)
|
|
118
|
+
return stop(ctx, t("ui.link.folder.none", { task: task.id }));
|
|
119
|
+
openForUser(inbox.dir, ctx.env);
|
|
120
|
+
out(ctx, t("ui.waiting.opened", { dir: ctx.display(inbox.dir) }));
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// a file that came for it: dragged in, a short note — an input of its people, the task back in the queue (Enter or 0
|
|
124
|
+
// at the file, 0 at the note: nothing written)
|
|
125
|
+
if (what === "file") {
|
|
126
|
+
if (fileRefused(task, task.id))
|
|
127
|
+
return stop(ctx, t("ui.link.file.cannot", { task: task.id }));
|
|
128
|
+
if (!menu) {
|
|
129
|
+
out(ctx, t("ui.link.asks", { what: t("ui.link.what.task.file", { what: shown }), name: tree.config.name }));
|
|
130
|
+
if (task.waitingOn)
|
|
131
|
+
out(ctx, ` ${t("ui.waiting.do", { what: task.waitingOn })}`);
|
|
132
|
+
}
|
|
133
|
+
const paths = await askPaths(ctx, lang, root);
|
|
134
|
+
if (!paths?.length)
|
|
135
|
+
return false;
|
|
136
|
+
const note = (await ctx.ask(t("ui.link.file.note"))).trim();
|
|
137
|
+
if (note === "0")
|
|
138
|
+
return false;
|
|
139
|
+
if ((await run(["intake", ...paths, `--answers=${task.id}`, ...(note ? [`--note=${note}`] : [])], true)) !== 0)
|
|
140
|
+
return false;
|
|
141
|
+
out(ctx, t("ui.link.task.file.done"));
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
// sent: a fact the person gives, never a consent — the task still waits for the reply
|
|
145
|
+
if (!SENDABLE.includes(waitKindOf(task)))
|
|
146
|
+
return stop(ctx, t("ui.link.sent.cannot", { task: task.id }));
|
|
147
|
+
if (task.sent)
|
|
148
|
+
return stop(ctx, t("ui.link.sent.already", { task: task.id, day: humanDay(task.sent.at, lang) }));
|
|
149
|
+
const group = waitGroupOf(tree, task, lang);
|
|
150
|
+
const to = task.wait?.to?.trim() || (group.kind === "office" || group.kind === "family" ? group.name : undefined);
|
|
151
|
+
if (menu) {
|
|
152
|
+
if (!(await ctx.confirm(t("ui.wait.sent.sure", { to: to ? ` (${to})` : "" }), false)))
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
if (task.waitingOn)
|
|
157
|
+
out(ctx, ` ${t("ui.waiting.do", { what: task.waitingOn })}`);
|
|
158
|
+
if (!(await asks(ctx, lang, root, t("ui.link.what.task.sent", { what: shown, to: to ? ` (${to})` : "" }), undefined, false)))
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if ((await run(["task", "sent", task.id], true)) !== 0)
|
|
162
|
+
return false;
|
|
163
|
+
out(ctx, t("ui.link.task.sent.done"));
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
77
166
|
/**
|
|
78
167
|
* The question about an archive's images a link of the Strom app opens: only an archive that waits for the person's
|
|
79
168
|
* decision, or one a downloader here reaches (its permission looked at again) — anything else only said.
|
|
@@ -122,7 +122,7 @@ export async function offerChat(ctx, run, lang, root, question, say) {
|
|
|
122
122
|
await run(["chat", "--say", say]);
|
|
123
123
|
}
|
|
124
124
|
/** Paths typed or dropped in, each one there and none of strom's own; asked again until they are, nothing (Enter, 0): undefined. */
|
|
125
|
-
async function askPaths(ctx, lang, root) {
|
|
125
|
+
export async function askPaths(ctx, lang, root) {
|
|
126
126
|
const t = translator(lang);
|
|
127
127
|
const own = [root, ctx.settings.home()?.value].filter((d) => Boolean(d));
|
|
128
128
|
const inside = (p, dir) => {
|