strom-research 1.4.1 → 1.6.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/assets/lang/cs.json +160 -1
- package/assets/lang/de.json +160 -1
- package/assets/method/narrate.md +2 -1
- package/assets/method/recording.md +3 -1
- package/dist/agents/files.js +8 -1
- package/dist/cli/human.js +18 -0
- package/dist/cli/main.js +9 -0
- package/dist/cli/menu-links.js +249 -0
- package/dist/cli/menu-parts.js +6 -3
- package/dist/cli/menu-research.js +185 -7
- package/dist/cli/menu-settings.js +17 -3
- package/dist/cli/menu-waiting.js +61 -47
- package/dist/cli/menu.js +3 -2
- package/dist/cli/ui.js +159 -1
- package/dist/cli/wizard.js +34 -1
- package/dist/commands/analysis.js +6 -2
- package/dist/commands/browse.js +5 -19
- package/dist/commands/checks.js +7 -1
- package/dist/commands/guide.js +15 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/intake.js +3 -0
- package/dist/commands/links.js +367 -0
- package/dist/commands/output.js +7 -1
- package/dist/commands/people.js +58 -1
- package/dist/commands/research.js +113 -24
- package/dist/commands/setup.js +13 -2
- package/dist/commands/start.js +6 -2
- package/dist/commands/story.js +17 -1
- package/dist/commands/sync.js +243 -0
- package/dist/core/actions.js +15 -0
- package/dist/core/config.js +5 -0
- package/dist/core/dialog.js +25 -0
- package/dist/core/excerpt.js +53 -5
- package/dist/core/frontier.js +10 -1
- package/dist/core/links.js +357 -0
- package/dist/core/live.js +156 -7
- package/dist/core/phrases.js +1 -0
- package/dist/core/review.js +43 -5
- package/dist/core/schema.js +1 -0
- package/dist/core/self.js +17 -0
- package/dist/core/session.js +6 -0
- package/dist/core/stromapp.js +38 -0
- package/dist/core/sync.js +876 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/uninstall.js +3 -0
- package/dist/core/validate.js +4 -0
- package/dist/gedcom/export.js +66 -1
- package/dist/gedcom/validate.js +13 -4
- package/package.json +1 -1
package/dist/core/live.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
// GET <token>/tree.ged the tree for the Strom app, as it is now, each entry with its image (the settings excerpts.*)
|
|
11
11
|
// GET <token>/images.ged the same (the address an older strom gave)
|
|
12
12
|
// GET <token>/events server-sent events: hello, change, working
|
|
13
|
+
// POST <token>/sync the Strom app sends the user's edited tree back (?send=): kept to be shown
|
|
14
|
+
// and written on the user's word (strom sync) — nothing of the research changes
|
|
15
|
+
// POST <token>/cancel …or says it sends nothing ({"reason": "unchanged" | "cancelled" | "no-tree"})
|
|
13
16
|
//
|
|
14
17
|
// Only pages of the Strom app may read it (CORS: https://stromapp.info, its beta, and a
|
|
15
18
|
// local copy on localhost for its development), and a browser asks first
|
|
@@ -22,12 +25,19 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
22
25
|
import { Tree, VERSION } from "./tree.js";
|
|
23
26
|
import { exportGedcom } from "../gedcom/export.js";
|
|
24
27
|
import { excerptSettings, planExcerpts } from "./excerpt.js";
|
|
28
|
+
import { noteAdoptFailed, noteNothingSent, pendingAdoption, receiveAdopted, receiveTree, SYNC_MAX_BYTES } from "./sync.js";
|
|
29
|
+
import { ui } from "../cli/ui.js";
|
|
25
30
|
/** New images for the app, made for at most so long when the tree changed (the rest the next time). */
|
|
26
31
|
const LIVE_IMAGES_MS = 20_000;
|
|
27
32
|
import { liveWorkers } from "./workers.js";
|
|
28
|
-
import { openSessions } from "./session.js";
|
|
33
|
+
import { monthSpend, openSessions } from "./session.js";
|
|
34
|
+
import { rankTasks } from "./queue.js";
|
|
35
|
+
import { humanTask } from "../cli/human.js";
|
|
36
|
+
import { knownNewerVersion } from "./update.js";
|
|
29
37
|
import { gitProgram } from "./git.js";
|
|
30
|
-
import { isStromAppOrigin } from "./stromapp.js";
|
|
38
|
+
import { appOpensLinks, isStromAppOrigin } from "./stromapp.js";
|
|
39
|
+
import { Settings } from "./config.js";
|
|
40
|
+
import { linkActions, linkHandlerState } from "./links.js";
|
|
31
41
|
import { stromLauncher } from "./self.js";
|
|
32
42
|
/** Where a running bridge of a tree is noted. */
|
|
33
43
|
export function liveFile(root) {
|
|
@@ -120,10 +130,43 @@ function working(root, tree) {
|
|
|
120
130
|
return { who: w.label, since: w.since, ...(s ? { session: s.id, ...(s.task ? { task: task ? `${task.id} ${task.what}` : s.task } : {}) } : {}) };
|
|
121
131
|
});
|
|
122
132
|
}
|
|
133
|
+
/** How long what the system says of the links is taken as true (asking it costs a program started). */
|
|
134
|
+
const LINKS_FRESH_MS = 60_000;
|
|
135
|
+
let linksSeen;
|
|
136
|
+
/** The strom-research:// links this computer takes: said to the app only while the scheme leads to this strom. */
|
|
137
|
+
function links(env) {
|
|
138
|
+
if (!linksSeen || Date.now() - linksSeen.at > LINKS_FRESH_MS)
|
|
139
|
+
linksSeen = { at: Date.now(), actions: linkActions(linkHandlerState(env)) };
|
|
140
|
+
return linksSeen.actions;
|
|
141
|
+
}
|
|
142
|
+
/** At most so many tasks of the queue the app is told (the rest counted): what comes next, then what was put aside. */
|
|
143
|
+
const QUEUE_NEXT = 14;
|
|
144
|
+
const QUEUE_PARKED = 6;
|
|
145
|
+
/** The agent's queue in its order, for the user (records by their names), and how many more there are. */
|
|
146
|
+
function queue(tree, settings) {
|
|
147
|
+
const lang = tree.lang;
|
|
148
|
+
const next = rankTasks(tree, tree.list("task"), settings.strategy(tree.config)).map((r) => r.task);
|
|
149
|
+
const parked = tree.list("task").filter((t) => t.state === "parked");
|
|
150
|
+
const item = (state) => (t) => ({ id: t.id, text: humanTask(tree, t.what, lang), state });
|
|
151
|
+
const shown = [...next.slice(0, QUEUE_NEXT).map(item("next")), ...parked.slice(0, QUEUE_PARKED).map(item("parked"))];
|
|
152
|
+
return { queue: shown, queueMore: next.length + parked.length - shown.length };
|
|
153
|
+
}
|
|
154
|
+
/** The last tree the Strom app sent (through the bridge) that the research took and has not taken back. */
|
|
155
|
+
function lastIntake(tree) {
|
|
156
|
+
const last = tree
|
|
157
|
+
.list("input")
|
|
158
|
+
.filter((i) => i.sync && !i.sync.undone && i.sync.applied.length && i.name.startsWith("strom-app-"))
|
|
159
|
+
.sort((a, b) => a.created.localeCompare(b.created))
|
|
160
|
+
.pop();
|
|
161
|
+
return last ? { id: last.id, at: last.created } : undefined;
|
|
162
|
+
}
|
|
123
163
|
/** What the app shows beside the tree. */
|
|
124
164
|
function status(root, env) {
|
|
125
165
|
const tree = Tree.open(root, env);
|
|
166
|
+
const settings = new Settings(env, {});
|
|
126
167
|
const waiting = tree.list("task").filter((t) => t.state === "waiting");
|
|
168
|
+
const newer = knownNewerVersion(settings, env);
|
|
169
|
+
const intake = lastIntake(tree);
|
|
127
170
|
return {
|
|
128
171
|
strom: VERSION,
|
|
129
172
|
tree: { id: tree.config.id, name: tree.config.name, lang: tree.lang },
|
|
@@ -133,7 +176,13 @@ function status(root, env) {
|
|
|
133
176
|
researches: tree.list("research").map((r) => ({ id: r.id, name: r.name, state: r.state })),
|
|
134
177
|
working: working(root, tree),
|
|
135
178
|
open: openSessions(tree).map((s) => ({ id: s.id, task: s.task, started: s.started })),
|
|
136
|
-
|
|
179
|
+
// since when it waits: its last change is the one that made it wait
|
|
180
|
+
waiting: waiting.map((t) => ({ id: t.id, what: t.what, on: t.waitingOn ?? "", at: t.updated })),
|
|
181
|
+
links: links(env),
|
|
182
|
+
...queue(tree, settings),
|
|
183
|
+
...(newer ? { update: { version: newer } } : {}),
|
|
184
|
+
spend: { ...monthSpend(tree, new Date().toISOString().slice(0, 7)), currency: "USD" },
|
|
185
|
+
...(intake ? { lastIntake: intake } : {}),
|
|
137
186
|
};
|
|
138
187
|
}
|
|
139
188
|
/** Run the bridge of the tree here, until idle or stopped. */
|
|
@@ -153,7 +202,7 @@ export function serveLive(root, env) {
|
|
|
153
202
|
}
|
|
154
203
|
if (req.method === "OPTIONS") {
|
|
155
204
|
if (origin) {
|
|
156
|
-
res.setHeader("Access-Control-Allow-Methods", "GET");
|
|
205
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST");
|
|
157
206
|
res.setHeader("Access-Control-Allow-Headers", String(req.headers["access-control-request-headers"] ?? ""));
|
|
158
207
|
if (req.headers["access-control-request-private-network"])
|
|
159
208
|
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
@@ -162,22 +211,48 @@ export function serveLive(root, env) {
|
|
|
162
211
|
return;
|
|
163
212
|
}
|
|
164
213
|
const [, t, what] = (req.url ?? "").split("?")[0].split("/");
|
|
214
|
+
if (req.method === "POST" && t === token && what === "sync") {
|
|
215
|
+
receive(req, res, origin);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (req.method === "POST" && t === token && what === "cancel") {
|
|
219
|
+
nothing(req, res, origin);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (req.method === "POST" && t === token && what === "adopt") {
|
|
223
|
+
receive(req, res, origin, "adopt");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
165
226
|
if (req.method !== "GET" || t !== token) {
|
|
166
227
|
res.writeHead(404).end();
|
|
167
228
|
return;
|
|
168
229
|
}
|
|
169
230
|
try {
|
|
170
|
-
if (what === "
|
|
231
|
+
if (what === "adopt") {
|
|
232
|
+
// the new research started from the app's link: which tree of the app it waits for (its mark), its name
|
|
233
|
+
const mark = pendingAdoption(root);
|
|
234
|
+
const tree = Tree.open(root, env);
|
|
235
|
+
const body = mark ? { token: mark, name: tree.config.name, tree: tree.config.id } : { error: "this research waits for no tree" };
|
|
236
|
+
res.writeHead(mark ? 200 : 404, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }).end(JSON.stringify(body));
|
|
237
|
+
}
|
|
238
|
+
else if (what === "status") {
|
|
171
239
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }).end(JSON.stringify(status(root, env)));
|
|
172
240
|
}
|
|
173
241
|
else if (what === "tree.ged" || what === "images.ged") {
|
|
174
242
|
const h = head(root);
|
|
175
|
-
if (!ged || ged.head !== h) {
|
|
243
|
+
if (!ged || ged.head !== h || ged.links !== (appOpensLinks(new Settings(env, {})) ? links(env).join(" ") : "")) {
|
|
176
244
|
const tree = Tree.open(root, env);
|
|
177
245
|
// the images from the cache; new ones made for a while at most (the rest the next time the tree changes)
|
|
178
246
|
const set = excerptSettings(tree);
|
|
179
247
|
const images = set ? planExcerpts(tree, set.shared, { quality: set.quality, for: set.for, maxBytes: set.mb * 1024 * 1024, budgetMs: LIVE_IMAGES_MS }) : undefined;
|
|
180
|
-
|
|
248
|
+
// an app that opens strom-research:// links: each excerpt's mark, and the links this computer takes
|
|
249
|
+
const opens = appOpensLinks(new Settings(env, {}));
|
|
250
|
+
const offered = opens ? links(env) : [];
|
|
251
|
+
ged = {
|
|
252
|
+
head: h,
|
|
253
|
+
links: offered.join(" "),
|
|
254
|
+
text: exportGedcom(tree, { for: "strom", ...(h ? { head: h } : {}), ...(images ? { excerpts: images.of } : {}), ...(opens ? { clips: true, research: true } : {}), ...(offered.length ? { links: offered } : {}) }).text,
|
|
255
|
+
};
|
|
181
256
|
}
|
|
182
257
|
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Strom-Head": h }).end(ged.text);
|
|
183
258
|
}
|
|
@@ -194,6 +269,80 @@ export function serveLive(root, env) {
|
|
|
194
269
|
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }).end(e.message);
|
|
195
270
|
}
|
|
196
271
|
});
|
|
272
|
+
/** The user's tree from the app: only from its pages, only so large; the answer in the research's language. */
|
|
273
|
+
const receive = (req, res, origin, as = "sync") => {
|
|
274
|
+
const reply = (code, body) => res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }).end(JSON.stringify(body));
|
|
275
|
+
const lang = () => {
|
|
276
|
+
try {
|
|
277
|
+
return Tree.open(root, env).lang;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return "en";
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
if (!origin) {
|
|
284
|
+
reply(403, { error: "only the Strom app may send a tree here" });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const chunks = [];
|
|
288
|
+
let size = 0;
|
|
289
|
+
let over = false;
|
|
290
|
+
req.on("data", (c) => {
|
|
291
|
+
size += c.length;
|
|
292
|
+
if (size > SYNC_MAX_BYTES)
|
|
293
|
+
over = true;
|
|
294
|
+
else
|
|
295
|
+
chunks.push(c);
|
|
296
|
+
});
|
|
297
|
+
req.on("end", () => {
|
|
298
|
+
if (over) {
|
|
299
|
+
reply(413, { error: ui(lang(), "ui.sync.bridge.large") });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
304
|
+
if (as === "adopt") {
|
|
305
|
+
// kept for the research in the terminal to take in; the app links its tree to this research
|
|
306
|
+
receiveAdopted(root, text);
|
|
307
|
+
reply(200, { tree: Tree.open(root, env).config.id, head: head(root) ?? "" });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const got = receiveTree(root, env, text);
|
|
311
|
+
reply(200, { ok: true, changes: got.changes, file: path.basename(got.file) });
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
const m = e.message;
|
|
315
|
+
// the new research in the terminal waits for it: told, it stops waiting
|
|
316
|
+
if (as === "adopt" && pendingAdoption(root))
|
|
317
|
+
noteAdoptFailed(root, /empty|no people/.test(m) ? "empty" : "other");
|
|
318
|
+
const key = /another research/.test(m) ? "ui.sync.bridge.other" : /does not look like/.test(m) ? "ui.sync.bridge.foreign" : /empty|no people|not a GEDCOM/.test(m) ? "ui.sync.bridge.empty" : undefined;
|
|
319
|
+
reply(400, { error: key ? ui(lang(), key) : m });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
/** The app sends nothing (unchanged, the user said no, no tree of this research): strom sync --app stops waiting. */
|
|
324
|
+
const nothing = (req, res, origin) => {
|
|
325
|
+
const reply = (code, body) => res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }).end(JSON.stringify(body));
|
|
326
|
+
if (!origin) {
|
|
327
|
+
reply(403, { error: "only the Strom app may say so" });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
let body = "";
|
|
331
|
+
req.on("data", (c) => {
|
|
332
|
+
if (body.length < 4096)
|
|
333
|
+
body += c.toString("utf8");
|
|
334
|
+
});
|
|
335
|
+
req.on("end", () => {
|
|
336
|
+
let reason = "cancelled";
|
|
337
|
+
try {
|
|
338
|
+
reason = String(JSON.parse(body).reason ?? reason);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
reason = body.trim() || reason;
|
|
342
|
+
}
|
|
343
|
+
reply(200, { ok: true, reason: noteNothingSent(root, reason) });
|
|
344
|
+
});
|
|
345
|
+
};
|
|
197
346
|
return new Promise((resolve) => {
|
|
198
347
|
server.listen(0, "127.0.0.1", () => {
|
|
199
348
|
const port = server.address().port;
|
package/dist/core/phrases.js
CHANGED
|
@@ -32,6 +32,7 @@ export const PHRASES = {
|
|
|
32
32
|
"story.why": "{count} facts from records tell the life (generation {generation} of {research}); the family reads it in the Strom app's family book",
|
|
33
33
|
"story.done": "strom story set {id} with every fact it rests on (--fact) — a draft until the user approves it",
|
|
34
34
|
"review.research": "Review of {name}",
|
|
35
|
+
"review.unproven.research": "People without a record of their own",
|
|
35
36
|
"review.more": " (and {count} more after these)",
|
|
36
37
|
"review.mentions.what": "What the tree already says of {name} outside their data: {list}{more}",
|
|
37
38
|
"review.mentions.why": "records, notes or the diary name {name}, yet their data do not show it — the cheapest finds of a review ({research})",
|
package/dist/core/review.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Each kind is at most one task per person at a time, and what a task of that
|
|
8
8
|
// kind took up is never proposed again.
|
|
9
9
|
import { dateYears } from "./gdate.js";
|
|
10
|
-
import { RECORD_KINDS, recordsetsCovering, researchPeople } from "./frontier.js";
|
|
10
|
+
import { birthEstimate, RECORD_KINDS, recordsetsCovering, researchPeople } from "./frontier.js";
|
|
11
11
|
import { birthEvent, displayName, familiesAsPartner, lifespan } from "./people.js";
|
|
12
12
|
import { phrase } from "./phrases.js";
|
|
13
13
|
import { foldText } from "./text.js";
|
|
@@ -55,6 +55,11 @@ function citedFor(tree, p) {
|
|
|
55
55
|
for (const n of p.names)
|
|
56
56
|
for (const c of n.citations ?? [])
|
|
57
57
|
out.add(c.source);
|
|
58
|
+
// the record naming a family — a child's baptism names its parents — also without a fact of the family
|
|
59
|
+
for (const f of tree.list("family"))
|
|
60
|
+
if (!f.retracted && (f.partners.includes(p.id) || f.children.some((c) => c.person === p.id)))
|
|
61
|
+
for (const c of f.citations ?? [])
|
|
62
|
+
out.add(c.source);
|
|
58
63
|
for (const other of tree.list("person"))
|
|
59
64
|
for (const e of other.events)
|
|
60
65
|
if (e.participants?.some((x) => x.person === p.id))
|
|
@@ -67,6 +72,36 @@ function citedFor(tree, p) {
|
|
|
67
72
|
out.add(c.source);
|
|
68
73
|
return out;
|
|
69
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The people no record of their own proves: not one fact of theirs proven or
|
|
77
|
+
* probable — the living among them marked. Each with how they stand — a record names them elsewhere, their
|
|
78
|
+
* facts are only leads, or nothing but a note says where they come from.
|
|
79
|
+
*/
|
|
80
|
+
export function unprovenPeople(tree) {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const p of tree.list("person")) {
|
|
83
|
+
if (p.retracted)
|
|
84
|
+
continue;
|
|
85
|
+
const facts = p.events.filter((e) => !e.retracted && e.status !== "retracted" && e.status !== "disproven");
|
|
86
|
+
if (facts.some((e) => e.status === "proven" || e.status === "probable"))
|
|
87
|
+
continue;
|
|
88
|
+
const sources = [...citedFor(tree, p)];
|
|
89
|
+
const kind = facts.some((e) => e.citations.length) ? "leads" : sources.length ? "named" : "none";
|
|
90
|
+
out.push({ person: p, kind, sources, living: livingBorn(tree, p) !== undefined });
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Most likely alive: born (or, by their marriage or eldest child, about) less than DEATH_AFTER_YEARS ago, and no
|
|
96
|
+
* death recorded — the year, else undefined. Their records are the family's; the registers of those years are
|
|
97
|
+
* mostly closed.
|
|
98
|
+
*/
|
|
99
|
+
export function livingBorn(tree, p) {
|
|
100
|
+
if (p.events.some((e) => !e.retracted && e.status !== "disproven" && DEATHS.has(e.kind)))
|
|
101
|
+
return undefined;
|
|
102
|
+
const born = birthEstimate(tree, p, tree.lang).year;
|
|
103
|
+
return born !== undefined && born > new Date().getFullYear() - DEATH_AFTER_YEARS ? born : undefined;
|
|
104
|
+
}
|
|
70
105
|
/** The models that read each source: the sessions that recorded or corrected it (the user's hand counts as "user"). */
|
|
71
106
|
export function readersOf(tree) {
|
|
72
107
|
const model = new Map(tree.list("session").map((s) => [s.id, s.model ?? "unknown"]));
|
|
@@ -207,10 +242,13 @@ export function reviewProposals(tree, research) {
|
|
|
207
242
|
};
|
|
208
243
|
propose("mentions", "enrich", mentionsOf(tree, p, citedFor(tree, p)));
|
|
209
244
|
propose("entries", "enrich", entriesOf(tree, p));
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
propose("
|
|
245
|
+
// the people without a record want one of their own first: checking what their family's facts rest on can wait
|
|
246
|
+
if (!research.review?.unproven) {
|
|
247
|
+
const single = singleReadings(tree, p);
|
|
248
|
+
propose("verify", "verify", single);
|
|
249
|
+
if (reread && readers)
|
|
250
|
+
propose("reread", "verify", rereads(tree, p, reread, readers).filter((e) => !single.includes(e)), { model: reread });
|
|
251
|
+
}
|
|
214
252
|
propose("conflicts", "verify", openConflicts(tree, p));
|
|
215
253
|
out.push(...gaps(tree, research, p, name, tasks));
|
|
216
254
|
}
|
package/dist/core/schema.js
CHANGED
|
@@ -98,6 +98,7 @@ export const SCHEMAS = {
|
|
|
98
98
|
},
|
|
99
99
|
conflict: {
|
|
100
100
|
title: S({ req: true, max: 200 }),
|
|
101
|
+
fact: S({ max: 8 }),
|
|
101
102
|
subject: RS(ANY_SUBJECT, 1),
|
|
102
103
|
claims: { t: "array", req: true, min: 2, of: { source: R("source"), value: S({ req: true, max: 500 }), note: S({ max: 500 }) } },
|
|
103
104
|
state: S({ req: true, enum: ["open", "resolved"] }),
|
package/dist/core/self.js
CHANGED
|
@@ -33,6 +33,23 @@ export function installation() {
|
|
|
33
33
|
}
|
|
34
34
|
return (cached = { kind: "npm" });
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Another strom on disk than the one running (strom update replaced its code or its Node): this process goes on
|
|
38
|
+
* with what it loaded, so a person is told to start strom again. `version`: the running one's.
|
|
39
|
+
*/
|
|
40
|
+
export function replacedOnDisk(version) {
|
|
41
|
+
const inst = installation();
|
|
42
|
+
if (inst.kind !== "installed" || !inst.root)
|
|
43
|
+
return false;
|
|
44
|
+
try {
|
|
45
|
+
const app = JSON.parse(fs.readFileSync(path.join(inst.root, "app", "package.json"), "utf8"));
|
|
46
|
+
const info = JSON.parse(fs.readFileSync(path.join(inst.root, "install.json"), "utf8"));
|
|
47
|
+
return app.version !== version || info.node !== inst.node;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
36
53
|
/** The program and arguments that run strom: this Node and its entry script (cli.ts from sources, cli.js built). */
|
|
37
54
|
export function stromLauncher() {
|
|
38
55
|
const cli = path.resolve(path.dirname(here), "..", here.endsWith(".ts") ? "cli.ts" : "cli.js");
|
package/dist/core/session.js
CHANGED
|
@@ -157,3 +157,9 @@ export function recentSessions(tree, research, n = 2) {
|
|
|
157
157
|
.sort((a, b) => (b.ended ?? b.updated).localeCompare(a.ended ?? a.updated))
|
|
158
158
|
.slice(0, n);
|
|
159
159
|
}
|
|
160
|
+
/** What the agent's sessions of a month ("2026-09", by when they started) cost: how many, the dollars known (a session stopped before it said: its known part). */
|
|
161
|
+
export function monthSpend(tree, month) {
|
|
162
|
+
const of = tree.list("session").filter((s) => s.started.slice(0, 7) === month);
|
|
163
|
+
const amount = of.reduce((sum, s) => sum + (s.metrics?.costUsd ?? 0), 0);
|
|
164
|
+
return { month, sessions: of.length, amount: Math.round(amount * 100) / 100 };
|
|
165
|
+
}
|
package/dist/core/stromapp.js
CHANGED
|
@@ -30,6 +30,44 @@ export const APP_OPENS_RESEARCH = true;
|
|
|
30
30
|
export function appOpensResearch(settings) {
|
|
31
31
|
return APP_OPENS_RESEARCH || stromAppUrl(settings) !== STROM_APP_URL;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The Strom app sends the user's edits back (?send=<the bridge>, POST <bridge>/sync — the app's spec
|
|
35
|
+
* docs/ZADANI_SYNC_Z_APLIKACE.md): the first version of stromapp.info that does (3.3.0, 2026-09-27).
|
|
36
|
+
* Another copy of the app (its beta, its development: strom.app.url) is taken as current.
|
|
37
|
+
*/
|
|
38
|
+
export const APP_SENDS_CHANGES = "3.3.0";
|
|
39
|
+
export function appSendsChanges(settings) {
|
|
40
|
+
return APP_SENDS_CHANGES !== undefined || stromAppUrl(settings) !== STROM_APP_URL;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The Strom app opens strom-research:// links (the app's spec docs/ZADANI_VYZKUM_ODKAZY_Z_APLIKACE.md): it reads
|
|
44
|
+
* each excerpt's mark (_STROM_CLIP) and the links a research offers (_STROM_LINKS in the GEDCOM the bridge serves).
|
|
45
|
+
* The first version of stromapp.info that does (3.4.0, 2026-09-27); another copy of the app (its beta, its
|
|
46
|
+
* development: strom.app.url) is taken as current. An app of a known version (strom.version) goes by it.
|
|
47
|
+
*/
|
|
48
|
+
export const APP_OPENS_LINKS = "3.4.0";
|
|
49
|
+
export function appOpensLinks(settings, version) {
|
|
50
|
+
if (stromAppUrl(settings) !== STROM_APP_URL)
|
|
51
|
+
return true;
|
|
52
|
+
if (!APP_OPENS_LINKS)
|
|
53
|
+
return false;
|
|
54
|
+
if (!version)
|
|
55
|
+
return true;
|
|
56
|
+
const n = (v) => v.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
57
|
+
const [a, b] = [n(version), n(APP_OPENS_LINKS)];
|
|
58
|
+
for (let i = 0; i < 3; i++)
|
|
59
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
60
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/** The address that opens the Strom app to send the user's edited tree to the bridge. */
|
|
64
|
+
export function sendAppUrl(bridge, settings) {
|
|
65
|
+
return `${stromAppUrl(settings)}?send=${encodeURIComponent(bridge)}`;
|
|
66
|
+
}
|
|
67
|
+
/** The address that opens the Strom app to hand one of its trees to a new research (strom-research://new). */
|
|
68
|
+
export function adoptAppUrl(bridge, settings) {
|
|
69
|
+
return `${stromAppUrl(settings)}?adopt=${encodeURIComponent(bridge)}`;
|
|
70
|
+
}
|
|
33
71
|
/** The address that opens the Strom app following a research through its bridge. */
|
|
34
72
|
export function liveAppUrl(bridge, settings) {
|
|
35
73
|
return `${stromAppUrl(settings)}?live=${encodeURIComponent(bridge)}`;
|