strom-research 1.13.2-beta.8 → 1.13.2-beta.9
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 +5 -1
- package/assets/lang/de.json +5 -1
- package/assets/method/enrich.md +4 -0
- package/dist/agents/files.js +11 -5
- package/dist/brief/brief.js +2 -1
- package/dist/cli/execute.js +19 -1
- package/dist/cli/main.js +5 -2
- package/dist/cli/ui.js +5 -1
- package/dist/commands/connectors.js +19 -9
- package/dist/commands/grep.js +10 -4
- package/dist/commands/guide.js +4 -2
- package/dist/commands/media.js +84 -15
- package/dist/commands/read.js +6 -3
- package/dist/commands/readings.js +6 -6
- package/dist/commands/session.js +5 -2
- package/dist/commands/setup.js +7 -2
- package/dist/commands/sources.js +13 -1
- package/dist/core/delegate.js +6 -2
- package/dist/core/grep.js +7 -2
- package/dist/core/media.js +26 -1
- package/dist/core/net.js +131 -30
- package/dist/core/self.js +194 -7
- package/dist/core/tree.js +1 -1
- package/dist/core/web.js +15 -6
- package/package.json +1 -1
package/assets/lang/cs.json
CHANGED
|
@@ -382,7 +382,9 @@
|
|
|
382
382
|
"ui.consent.connector.direct": "Pozor: jeho kód se připojuje k síti sám, mimo omezovač stromu.",
|
|
383
383
|
"ui.consent.connector.unfenced": "Pozor: spouští program, který strom neohraničí ({run}) – může číst a měnit všechny soubory tohoto uživatele, spouštět jiné programy a připojit se kamkoli. Po každé změně jeho kódu se dotaz zopakuje.",
|
|
384
384
|
"ui.consent.connector.terms": "Podmínky užití archivu: {terms}",
|
|
385
|
-
"ui.consent.host.unblock": "{host}
|
|
385
|
+
"ui.consent.host.unblock": "Na {host} se nesahá (odmítl požadavky, nebo řekl, že je jeho limit vyčerpán). Zrušit to hned – jen pokud archiv řekl, že je to v pořádku?",
|
|
386
|
+
"ui.host.unblocked": "{host}: už se na něj zase smí – strom se ho bude znovu ptát, pomalu",
|
|
387
|
+
"ui.host.unblock.kept": "Nic se nezměnilo: na {host} se dál nesahá.",
|
|
386
388
|
"ui.menu.otheragent": "Tentokrát mluvit s jiným agentem nebo jinde",
|
|
387
389
|
"ui.o.title": "strom {version} — nástroj pro genealogický výzkum s AI agenty",
|
|
388
390
|
"ui.o.rule": "Tady jsou doklady výzkumu; měnit je jen příkazy strom. Nový agent: strom guide",
|
|
@@ -490,6 +492,8 @@
|
|
|
490
492
|
"ui.doc.innode": "strom {version} v Node {node}",
|
|
491
493
|
"ui.doc.fence": "stahovače v ohrádce",
|
|
492
494
|
"ui.doc.fence.net": "Node {node}: stahovače běží v ohrádce, ale síť jim zavře až Node 25 nebo novější",
|
|
495
|
+
"ui.doc.shim": "strom pro agenty",
|
|
496
|
+
"ui.doc.shim.codepage": "složka instalace má písmena mimo ASCII: hooky agentů a jejich strom přes výzkum závisí na kódové stránce konzole — složka instalace bez nich tomu předejde",
|
|
493
497
|
"ui.doc.oldnode": "Node {node} je příliš starý (potřeba 22.18 nebo novější)",
|
|
494
498
|
"ui.doc.missing": "chybí",
|
|
495
499
|
"ui.doc.notsetup": "zatím nenastaveno",
|
package/assets/lang/de.json
CHANGED
|
@@ -382,7 +382,9 @@
|
|
|
382
382
|
"ui.consent.connector.direct": "Achtung: sein Code greift selbst auf das Netz zu, am Begrenzer von strom vorbei.",
|
|
383
383
|
"ui.consent.connector.unfenced": "Achtung: er startet ein Programm, das strom nicht eingrenzt ({run}) – es kann alle Dateien dieses Benutzers lesen und ändern, andere Programme starten und jede Adresse erreichen. Nach jeder Änderung seines Codes wird erneut gefragt.",
|
|
384
384
|
"ui.consent.connector.terms": "Nutzungsbedingungen des Archivs: {terms}",
|
|
385
|
-
"ui.consent.host.unblock": "{host}
|
|
385
|
+
"ui.consent.host.unblock": "{host} wird in Ruhe gelassen (Anfragen abgewiesen, oder sein Limit ist laut ihm aufgebraucht). Jetzt aufheben – nur wenn das Archiv gesagt hat, dass es in Ordnung ist?",
|
|
386
|
+
"ui.host.unblocked": "{host}: nicht mehr in Ruhe gelassen – strom fragt wieder an, langsam",
|
|
387
|
+
"ui.host.unblock.kept": "Nichts geändert: {host} wird weiter in Ruhe gelassen.",
|
|
386
388
|
"ui.menu.otheragent": "Diesmal mit einem anderen Agenten oder woanders sprechen",
|
|
387
389
|
"ui.o.title": "strom {version} — genealogische Forschung mit KI-Agenten",
|
|
388
390
|
"ui.o.rule": "Hier liegen die Belege der Forschung; ändern NUR mit strom-Befehlen. Neuer Agent: strom guide",
|
|
@@ -490,6 +492,8 @@
|
|
|
490
492
|
"ui.doc.innode": "strom {version} in Node {node}",
|
|
491
493
|
"ui.doc.fence": "Downloader eingegrenzt",
|
|
492
494
|
"ui.doc.fence.net": "Node {node}: Downloader laufen eingegrenzt, aber erst Node 25 oder neuer hält sie auch vom Netz fern",
|
|
495
|
+
"ui.doc.shim": "strom für die Agenten",
|
|
496
|
+
"ui.doc.shim.codepage": "der Ordner der Installation hat Buchstaben außerhalb von ASCII: die Hooks der Agenten und ihr strom über die Forschung hängen von der Codepage der Konsole ab — ein Installationsordner ohne sie vermeidet das",
|
|
493
497
|
"ui.doc.oldnode": "Node {node} ist zu alt (nötig: 22.18 oder neuer)",
|
|
494
498
|
"ui.doc.missing": "fehlt",
|
|
495
499
|
"ui.doc.notsetup": "noch nicht eingerichtet",
|
package/assets/method/enrich.md
CHANGED
|
@@ -20,6 +20,10 @@ recorded, also in vain (`strom search add … --method web|catalog|full-text`).
|
|
|
20
20
|
- **Within the brief's budget of pages** fetched or read; then record what
|
|
21
21
|
was searched and close. Printed pages go to the user's model for print
|
|
22
22
|
where you can hand work to one (the brief names it) — never handwriting.
|
|
23
|
+
The budget is the task's whole: pages through a connector count in it.
|
|
24
|
+
More pages of one site than the web rule allows go through its connector,
|
|
25
|
+
built right away when the task needs that site (`strom connector new
|
|
26
|
+
<site>`, then `strom fetch`).
|
|
23
27
|
- **The history of a place once in a research**: what is written of it (the
|
|
24
28
|
brief says) is reused, never searched again.
|
|
25
29
|
- **Read the source itself** — the page, the scan. A snippet, an OCR line,
|
package/dist/agents/files.js
CHANGED
|
@@ -19,10 +19,12 @@ import { CHROME_ALLOW, CHROME_DENY, chromeDomain } from "../core/browser.js";
|
|
|
19
19
|
import { claudeScanReader, opencodeScanReader, SCAN_READER, scanReaderModel, treeReading } from "./scanreader.js";
|
|
20
20
|
import { claudeWebHooks, grokWebHooks, webRuleLines } from "../core/web.js";
|
|
21
21
|
import { claudeDelegateHooks } from "../core/delegate.js";
|
|
22
|
-
import {
|
|
22
|
+
import { powershellUtf8Rule, putShim } from "../core/self.js";
|
|
23
23
|
export const MARKER = "<!-- strom: generated above this line (strom agents sync); your own notes below are kept -->";
|
|
24
|
-
function agentsMd(tree) {
|
|
24
|
+
export function agentsMd(tree, platform = process.platform) {
|
|
25
25
|
const lang = langName(tree.config.lang);
|
|
26
|
+
// (Windows only: written on the computer the agent works on, as its other files)
|
|
27
|
+
const ps = powershellUtf8Rule("`", platform);
|
|
26
28
|
// the Strom app's address as strom app opens it now (the beta its beta, strom.app.url where it says another)
|
|
27
29
|
const app = agentAppUrl(tree.env);
|
|
28
30
|
return `# Family research: ${tree.config.name}
|
|
@@ -56,7 +58,7 @@ the researcher; \`strom\` is your only way to read and change the research.
|
|
|
56
58
|
number) — never read a big file in pieces.
|
|
57
59
|
7. Run strom commands on their own — no pipes (\`| head\`, \`| grep\`): output is
|
|
58
60
|
already short, listings take \`--limit\` and \`--page\`, and piped commands may
|
|
59
|
-
be refused by your permissions
|
|
61
|
+
be refused by your permissions.${ps ? `\n ${ps}` : ""}
|
|
60
62
|
8. Other agents may work on this tree too (Claude Code, Codex, Antigravity,
|
|
61
63
|
OpenCode, Grok — the user's choice). \`strom\` shows who is working now; never take a task
|
|
62
64
|
another one has started.
|
|
@@ -400,7 +402,11 @@ export const GROK_HOOKS = path.join(".grok", "hooks", "strom.json");
|
|
|
400
402
|
/** Claude Code's subagent for reading scans, in the tree. */
|
|
401
403
|
export const CLAUDE_SCAN_READER = path.join(".claude", "agents", `${SCAN_READER}.md`);
|
|
402
404
|
export const AGENT_FILES = ["AGENTS.md", "CLAUDE.md", path.join(".claude", "settings.json"), CLAUDE_SCAN_READER, "opencode.json", path.join(".grok", "config.toml"), path.join(".grok", "rules", "strom.md"), GROK_HOOKS];
|
|
403
|
-
|
|
405
|
+
/**
|
|
406
|
+
* strom's files of the agents written for the tree (only what changed: the files written, to commit). `said.shim`: the
|
|
407
|
+
* shim in .strom/bin was written now (never committed: not among the files).
|
|
408
|
+
*/
|
|
409
|
+
export function syncAgentFiles(tree, said = {}) {
|
|
404
410
|
// an archive: no agent works on it, and nothing of one shows in its folder (Milan's decision, 2026-10-03) — strom's own
|
|
405
411
|
// files of the agents go (what a person wrote in them stays); research switched on writes them again
|
|
406
412
|
if (isArchive(tree))
|
|
@@ -433,7 +439,7 @@ export function syncAgentFiles(tree) {
|
|
|
433
439
|
// the strom the agent hooks name (the tree's .strom/bin, never committed) — there also for a conversation strom did not start
|
|
434
440
|
if (!tree.dryRun) {
|
|
435
441
|
try {
|
|
436
|
-
|
|
442
|
+
said.shim = putShim(tree.root).changed.length > 0;
|
|
437
443
|
}
|
|
438
444
|
catch {
|
|
439
445
|
// a hook that finds no strom lets the call go
|
package/dist/brief/brief.js
CHANGED
|
@@ -1090,7 +1090,8 @@ export function buildBrief(tree, opts) {
|
|
|
1090
1090
|
first: true,
|
|
1091
1091
|
pointer: "strom find <place>",
|
|
1092
1092
|
text: [
|
|
1093
|
-
`## Beyond the registers: about ${ENRICH_PAGES} pages fetched or read, then record the searches and close`,
|
|
1093
|
+
`## Beyond the registers: about ${ENRICH_PAGES} pages fetched or read in all (the task's whole budget, strom fetch too), then record the searches and close`,
|
|
1094
|
+
`- over ${Math.min(WEB_SOFT, new Settings(tree.env, {}).webPerHost(tree.config))} pages of one site: through its connector, built right away when the task needs that site (strom connector new <site> --url https://<host>/, then strom fetch)`,
|
|
1094
1095
|
print ? `- print: a subagent on ${print} (model.text), never handwriting` : "",
|
|
1095
1096
|
written.length ? `- of the places, written already (reuse): ${written.join(" · ")}` : "",
|
|
1096
1097
|
].filter(Boolean).join("\n"),
|
package/dist/cli/execute.js
CHANGED
|
@@ -255,7 +255,19 @@ export function parseOptions(def, rest, program = "strom") {
|
|
|
255
255
|
const cmd = `${program} ${def.path.join(" ")}`.trim();
|
|
256
256
|
try {
|
|
257
257
|
const r = parseArgs({ args: rest, options, allowPositionals: true, strict: true, tokens: true });
|
|
258
|
-
|
|
258
|
+
const folded = foldSurplus(def, defs, r.tokens, r.values, r.positionals);
|
|
259
|
+
// where the values of a repeatable option were typed among the arguments (a command of several arguments only)
|
|
260
|
+
if (!def.args?.some((a) => a.variadic))
|
|
261
|
+
return folded;
|
|
262
|
+
const after = {};
|
|
263
|
+
let seen = 0;
|
|
264
|
+
for (const t of r.tokens) {
|
|
265
|
+
if (t.kind === "positional")
|
|
266
|
+
seen++;
|
|
267
|
+
else if (t.kind === "option" && defs.find((d) => d.name === t.name)?.multiple)
|
|
268
|
+
(after[t.name] ??= []).push(seen);
|
|
269
|
+
}
|
|
270
|
+
return { ...folded, after };
|
|
259
271
|
}
|
|
260
272
|
catch (err) {
|
|
261
273
|
const e = err;
|
|
@@ -273,6 +285,12 @@ export function parseOptions(def, rest, program = "strom") {
|
|
|
273
285
|
hint: `a status is a fact's (E…) only, never a name's or a family's: strom cite E… S… --status probable|proven, strom event edit E… --status …`,
|
|
274
286
|
code: "option.unknown", hintCode: "option.unknown.status", params: { opt, cmd, options: own.join(" "), path: def.path.join(" ") }, usage,
|
|
275
287
|
});
|
|
288
|
+
// a name agents type meaning another option (--timeout for --minutes): that one, with what it takes
|
|
289
|
+
const meant = optionsOf(def).find((o) => o.mistaken?.includes(opt.replace(/^-+/u, "")));
|
|
290
|
+
if (meant) {
|
|
291
|
+
const near = `--${meant.name}${meant.value ? ` ${meant.value}` : ""}`;
|
|
292
|
+
throw new UsageError(`unknown option ${opt} for ${cmd}`, { hint: `meant: ${near} — ${meant.description}`, code: "option.near", params: { opt, cmd, near }, usage });
|
|
293
|
+
}
|
|
276
294
|
const near = suggest(opt, optionsOf(def).filter((o) => !o.hidden).map((o) => `--${o.name}`));
|
|
277
295
|
const help = `strom help${def.path.length ? ` ${def.path.join(" ")}` : ""}`;
|
|
278
296
|
throw new UsageError(`unknown option ${opt} for ${cmd}`, {
|
package/dist/cli/main.js
CHANGED
|
@@ -20,7 +20,7 @@ import { isAgent } from "../core/which.js";
|
|
|
20
20
|
import { appUrlShown, noticeStromApp } from "../core/stromapp.js";
|
|
21
21
|
import { lastChannelOf, pendingTransition, updateChannel } from "../core/update.js";
|
|
22
22
|
import { backupBefore, backupSaid } from "./backups.js";
|
|
23
|
-
import { installation } from "../core/self.js";
|
|
23
|
+
import { installation, refreshTreeShim } from "../core/self.js";
|
|
24
24
|
import { refreshGlobal } from "../agents/global.js";
|
|
25
25
|
import { expandFromLine, refreshLinks } from "../core/links.js";
|
|
26
26
|
import { clockLine, FINISH_LINE, finishLimitLine, finishOf } from "../core/clock.js";
|
|
@@ -274,6 +274,9 @@ export async function main(argv, io, env, cwd) {
|
|
|
274
274
|
catch {
|
|
275
275
|
// another computer's seal, a tree at work: its bridge's start settles it
|
|
276
276
|
}
|
|
277
|
+
// …and the strom its agents' hooks and conversations start (.strom/bin) leads to this one — an older strom's, or one
|
|
278
|
+
// whose mark went with the program folder an update replaced; only where a research has one, never an archive
|
|
279
|
+
refreshTreeShim(k.root, env);
|
|
277
280
|
// the bridges the Strom app follows go on with this version at their addresses: one of an older strom started
|
|
278
281
|
// again, one the installer ended to replace the program (Windows: its files are held while it runs) back
|
|
279
282
|
try {
|
|
@@ -301,7 +304,7 @@ export async function main(argv, io, env, cwd) {
|
|
|
301
304
|
assertIntact(ctx.tree());
|
|
302
305
|
let result;
|
|
303
306
|
try {
|
|
304
|
-
result = await (shows ?? def).run(ctx, { args, opts: v, ...(passthrough.length ? { extra: passthrough } : {}) });
|
|
307
|
+
result = await (shows ?? def).run(ctx, { args, opts: v, ...(passthrough.length ? { extra: passthrough } : {}), ...(parsed.after ? { after: parsed.after } : {}) });
|
|
305
308
|
}
|
|
306
309
|
catch (err) {
|
|
307
310
|
// A writing command is a transaction: on failure nothing it wrote remains.
|
package/dist/cli/ui.js
CHANGED
|
@@ -44,7 +44,9 @@ export const UI = {
|
|
|
44
44
|
"ui.consent.connector.direct": "Warning: its code reaches the network itself, past strom's limiter.",
|
|
45
45
|
"ui.consent.connector.unfenced": "Warning: it runs a program strom does not fence in ({run}) — it may read and change any file of this user, start other programs and reach any address. Asked again after every change of its code.",
|
|
46
46
|
"ui.consent.connector.terms": "The archive's terms of use: {terms}",
|
|
47
|
-
"ui.consent.host.unblock": "{host} refused
|
|
47
|
+
"ui.consent.host.unblock": "{host} is left alone (it refused the requests, or said its limit is used up). Lift it now — only if the archive said it is fine?",
|
|
48
|
+
"ui.host.unblocked": "{host}: no longer left alone — strom will ask it again, slowly",
|
|
49
|
+
"ui.host.unblock.kept": "Nothing changed: {host} is still left alone.",
|
|
48
50
|
"ui.consent.elsewhere.connector": "Run the downloader \"{name}\"? An agent asks for it from outside the research's folder ({cwd}).",
|
|
49
51
|
"ui.consent.elsewhere.run": "Start the agent working alone on the research \"{tree}\"? An agent asks for it from outside the research's folder ({cwd}); the work costs money.",
|
|
50
52
|
"ui.consent.connectors.off": "Let downloaders run without asking first?",
|
|
@@ -463,6 +465,8 @@ export const UI = {
|
|
|
463
465
|
"ui.doc.innode": "strom {version} in Node {node}",
|
|
464
466
|
"ui.doc.fence": "downloaders fenced in",
|
|
465
467
|
"ui.doc.fence.net": "Node {node}: downloaders run fenced in, but only Node 25 or newer also keeps them off the network",
|
|
468
|
+
"ui.doc.shim": "strom for the agents",
|
|
469
|
+
"ui.doc.shim.codepage": "the installation's folder has letters outside ASCII: the agents' hooks and their strom through the research rely on the console's code page — an installation folder without them avoids it",
|
|
466
470
|
"ui.doc.oldnode": "Node {node} is too old (22.18 or newer)",
|
|
467
471
|
"ui.doc.missing": "missing",
|
|
468
472
|
"ui.doc.notsetup": "not set up yet",
|
|
@@ -657,10 +657,13 @@ register({
|
|
|
657
657
|
// (strom connector discard) — in the research's own .strom, never in the plugin's folder
|
|
658
658
|
const tree = ctx.hasTree() ? ctx.tree() : undefined;
|
|
659
659
|
const session = tree ? currentSession(tree, ctx.env) : undefined;
|
|
660
|
+
// whether its agent may take it away again (strom connector discard), else it is the user's to remove
|
|
661
|
+
let discardable = false;
|
|
660
662
|
if (tree) {
|
|
661
663
|
const marker = madeMarker(tree.root, name);
|
|
662
664
|
const folder = folderId(dir);
|
|
663
665
|
if (session && folder) {
|
|
666
|
+
discardable = true;
|
|
664
667
|
const made = { session: session.id, tree: tree.config.id, ...(session.research ? { research: session.research } : {}), at: new Date().toISOString(), folder };
|
|
665
668
|
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
666
669
|
fs.writeFileSync(marker, JSON.stringify(made) + "\n");
|
|
@@ -671,7 +674,7 @@ register({
|
|
|
671
674
|
const rel = ctx.display(dir);
|
|
672
675
|
return {
|
|
673
676
|
text: lines(`connector ${name} started in ${rel}`, "", "next:", ` 1. your agent reads ${path.join(rel, "DISCOVERY.md")} and the contract (README.md one folder up),`, " and finds out what the portal allows — before any code", " 2. it writes connector.json (policy) and connector.ts, and tests with a few requests:", ` strom connector test ${name} --find "<place>"`, consentRequired(ctx.env) ? ` 3. the first test needs your consent, in your terminal: strom allow connector ${name}` : undefined, ofSite
|
|
674
|
-
? `\nnote: the connector ${ofSite} is here already for ${webDomain(url.hostname)} (its hosts: ${listConnectors(shared(ctx)).find((c) => c.name === ofSite)?.manifest.hosts.join(", ") ?? "?"}) — more pages of that site go through it (strom connector show ${ofSite}); keep this one only for another portal of the site, else take it away: strom connector discard ${name}`
|
|
677
|
+
? `\nnote: the connector ${ofSite} is here already for ${webDomain(url.hostname)} (its hosts: ${listConnectors(shared(ctx)).find((c) => c.name === ofSite)?.manifest.hosts.join(", ") ?? "?"}) — more pages of that site go through it (strom connector show ${ofSite}); keep this one only for another portal of the site, else take it away: ${discardable ? `strom connector discard ${name}` : `the user, in their terminal: strom connector remove ${name}`}`
|
|
675
678
|
: undefined),
|
|
676
679
|
data: { name, dir, ...(ofSite ? { site: { domain: webDomain(url.hostname), connector: ofSite } } : {}) },
|
|
677
680
|
};
|
|
@@ -1137,7 +1140,7 @@ register({
|
|
|
1137
1140
|
args: [{ name: "host", description: "e.g. digi.example.org", required: true }],
|
|
1138
1141
|
options: [
|
|
1139
1142
|
{ name: "revoke", type: "boolean", description: "take the consent back" },
|
|
1140
|
-
{ name: "unblock", type: "boolean", description: "lift a refusal (401/403) early — only after the archive said it is fine" },
|
|
1143
|
+
{ name: "unblock", type: "boolean", description: "lift a refusal (401/403), or a limit the host said is used up, early — only after the archive said it is fine" },
|
|
1141
1144
|
{ name: "pace", type: "string", value: "<seconds|auto>", description: `your own pause between two requests to the host (at least ${MIN_INTERVAL_MS / 1000} s); auto: the service's again` },
|
|
1142
1145
|
{ name: "per-hour", type: "string", value: "<n|none|auto>", description: "your own hourly cap for the host; none: no cap; auto: the service's again (strom sets none by itself)" },
|
|
1143
1146
|
],
|
|
@@ -1176,7 +1179,7 @@ register({
|
|
|
1176
1179
|
const now = paceAt(ctx, host, c);
|
|
1177
1180
|
return { text: `${host}: ${paceText(now)}${s.own ? " — yours" : " — the service's again"}`, data: { host, pace: { minIntervalMs: now.minIntervalMs, perHour: Number.isFinite(now.perHour) ? now.perHour : null }, own: s.own ?? null } };
|
|
1178
1181
|
}
|
|
1179
|
-
const how = ctx.requireHuman(`${opts.revoke ? "Take back" : opts.unblock ? "Lift the refusal of" : "Allow"} automated access to ${host}?`, `strom allow host ${host}${opts.revoke ? " --revoke" : opts.unblock ? " --unblock" : ""}`, `host:${host}`, ui(lang, opts.revoke ? "ui.consent.host.revoke" : opts.unblock ? "ui.consent.host.unblock" : "ui.consent.host", { host }));
|
|
1182
|
+
const how = ctx.requireHuman(`${opts.revoke ? "Take back" : opts.unblock ? "Lift the refusal or the used-up limit of" : "Allow"} automated access to ${host}?`, `strom allow host ${host}${opts.revoke ? " --revoke" : opts.unblock ? " --unblock" : ""}`, `host:${host}`, ui(lang, opts.revoke ? "ui.consent.host.revoke" : opts.unblock ? "ui.consent.host.unblock" : "ui.consent.host", { host }));
|
|
1180
1183
|
const all = loadConsents(ctx.env);
|
|
1181
1184
|
if (opts.revoke) {
|
|
1182
1185
|
delete all.hosts[host];
|
|
@@ -1184,10 +1187,11 @@ register({
|
|
|
1184
1187
|
return { text: `${host}: consent taken back — no connector reaches it now`, data: { host, allowed: false } };
|
|
1185
1188
|
}
|
|
1186
1189
|
if (opts.unblock) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1190
|
+
// the person's question and answer, in their language (a refusal, or a limit the host said is used up)
|
|
1191
|
+
if (how === "terminal" && !(await ctx.confirm(ui(lang, "ui.consent.host.unblock", { host }), false)))
|
|
1192
|
+
return { text: ui(lang, "ui.host.unblock.kept", { host }) };
|
|
1189
1193
|
clearBlock(netDir(ctx), host);
|
|
1190
|
-
return { text:
|
|
1194
|
+
return { text: ui(lang, "ui.host.unblocked", { host }), data: { host, unblocked: true } };
|
|
1191
1195
|
}
|
|
1192
1196
|
const c = listConnectors(ctx.settings.shared()?.value).find((x) => x.manifest.hosts.some((h) => hostAllowed(host, [h])));
|
|
1193
1197
|
const ok = await askHost(ctx, host, c, how === "window");
|
|
@@ -1570,10 +1574,16 @@ async function fetchWith(ctx, c, request, recordset) {
|
|
|
1570
1574
|
throw new UsageError(`${c.manifest.title} does not allow automated download (its terms, as the connector read them)`, {
|
|
1571
1575
|
hint: `the user saves them by hand: strom task wait T… --images B…:${request.cmd === "part" ? request.image : runs(request.images)} --on "<the book, its link, which images>" (the link: strom fetch ${c.name} ${request.book} --list)`,
|
|
1572
1576
|
});
|
|
1577
|
+
// an archive marked manual (browser only) refuses downloads, not finding books in its catalogue and their links;
|
|
1578
|
+
// one marked forbidden refuses every automated request
|
|
1573
1579
|
const repo = forbiddenBy(ctx, c);
|
|
1574
|
-
if (repo)
|
|
1575
|
-
throw new UsageError(`${repo.id} ${repo.name} is marked automation
|
|
1576
|
-
hint: `strom repo show ${repo.id} —
|
|
1580
|
+
if (repo?.automation === "forbidden")
|
|
1581
|
+
throw new UsageError(`${repo.id} ${repo.name} is marked automation forbidden in this tree — no requests through a connector`, {
|
|
1582
|
+
hint: `strom repo show ${repo.id} — the user saves what the research needs by hand`,
|
|
1583
|
+
});
|
|
1584
|
+
if (repo && (request.cmd === "fetch" || request.cmd === "part"))
|
|
1585
|
+
throw new UsageError(`${repo.id} ${repo.name} is marked automation manual in this tree — no images downloaded through a connector`, {
|
|
1586
|
+
hint: `the user saves them by hand: strom task wait T… --images B…:${request.cmd === "part" ? request.image : runs(request.images)} --on "<the book, its link, which images>" (finding books and their links still works — the link: strom fetch ${c.name} ${request.book} --list)`,
|
|
1577
1587
|
});
|
|
1578
1588
|
if (recordset && request.cmd === "fetch") {
|
|
1579
1589
|
// what is registered already is not asked for again (a part of an image is not the image)
|
package/dist/commands/grep.js
CHANGED
|
@@ -17,11 +17,12 @@ register({
|
|
|
17
17
|
description: "Searches the text files of inputs/ and notes/ (output/ too with --in output or --in all) — never data/ (the\n" +
|
|
18
18
|
"records: strom find), .git or anything outside the tree, never through a symbolic link; images, PDFs and other\n" +
|
|
19
19
|
"binary files are left out. A text is found as it reads: in any case, with accents or without (mlynar finds\n" +
|
|
20
|
-
"Mlynář), a run of spaces as one. Several texts: a line with
|
|
20
|
+
"Mlynář), a run of spaces as one. Several texts: a line with ANY of them (spellings of one name) by default;\n" +
|
|
21
|
+
"--all: a line with EVERY one of them, in any order (a year and a name: 1803 \"Pag. 11\"). --regex: a\n" +
|
|
21
22
|
"JavaScript regular expression, in any case, Unicode (\\p{L} a letter of any script). A line found is shown\n" +
|
|
22
23
|
"file:line: text, a line round it (--context) file-line- text; a long line is cut round its find.\n" +
|
|
23
24
|
`Listings are paged (--limit, --page); the search stops at ${GREP_MAX_HITS} lines and says so.`,
|
|
24
|
-
args: [{ name: "text", description: "what to find (accents and capitals optional); several = a line with any of them", required: true, variadic: true }],
|
|
25
|
+
args: [{ name: "text", description: "what to find (accents and capitals optional); several = a line with any of them (--all: with every one)", required: true, variadic: true }],
|
|
25
26
|
options: [
|
|
26
27
|
{
|
|
27
28
|
name: "in",
|
|
@@ -31,12 +32,14 @@ register({
|
|
|
31
32
|
description: `${GREP_FOLDERS.join(" | ")} | all | a file or folder inside them | an input I… (default: ${GREP_DEFAULT.join(" and ")})`,
|
|
32
33
|
},
|
|
33
34
|
{ name: "context", type: "string", value: "<n>", description: `lines shown before and after each line found (0–${MAX_CONTEXT}, default 0)` },
|
|
35
|
+
{ name: "all", type: "boolean", description: "several texts: a line with every one of them (default: with any)" },
|
|
34
36
|
{ name: "regex", type: "boolean", description: "the text is a regular expression (JavaScript, in any case, Unicode)" },
|
|
35
37
|
{ name: "files", type: "boolean", description: "only the files and how many lines each has found" },
|
|
36
38
|
],
|
|
37
39
|
examples: [
|
|
38
40
|
'strom grep "mlynář"',
|
|
39
41
|
"strom grep Novák Nowak --in inputs --context 2",
|
|
42
|
+
'strom grep 1803 "Pag. 11" --all --in I0002',
|
|
40
43
|
'strom grep "Lhota" --in inputs/rodokmen.md --context 3',
|
|
41
44
|
'strom grep "Nov[aá]k(ov[aá])?" --regex --in notes',
|
|
42
45
|
"strom grep Novák --in I0002 --files",
|
|
@@ -64,7 +67,7 @@ register({
|
|
|
64
67
|
throw new UsageError(error, { hint: `strom grep ${patterns.map(shellArg).join(" ")} --in ${GREP_FOLDERS.join("|")}|all` });
|
|
65
68
|
let match;
|
|
66
69
|
try {
|
|
67
|
-
match = grepMatcher(patterns, regex);
|
|
70
|
+
match = grepMatcher(patterns, regex, !!opts.all);
|
|
68
71
|
}
|
|
69
72
|
catch (e) {
|
|
70
73
|
throw new UsageError(`not a regular expression: ${e.message}`, { hint: "strom grep <text> — without --regex the text is found as it reads" });
|
|
@@ -77,6 +80,7 @@ register({
|
|
|
77
80
|
...wanted.map((w) => `--in ${shellArg(w)}`),
|
|
78
81
|
...(context ? [`--context ${context}`] : []),
|
|
79
82
|
...(regex ? ["--regex"] : []),
|
|
83
|
+
...(opts.all ? ["--all"] : []),
|
|
80
84
|
...(opts.files ? ["--files"] : []),
|
|
81
85
|
].join(" ");
|
|
82
86
|
const tooBig = found.tooBig.length ? `left out, over 64 MB: ${found.tooBig.join(", ")}` : undefined;
|
|
@@ -86,7 +90,9 @@ register({
|
|
|
86
90
|
data: { total: 0, searched: found.searched, hits: [], ...(found.tooBig.length ? { tooBig: found.tooBig } : {}) },
|
|
87
91
|
};
|
|
88
92
|
const total = `${found.hits.length}${found.capped ? "+" : ""}`;
|
|
89
|
-
|
|
93
|
+
// several texts: whether a line has any or every one of them, said (agents expect every one)
|
|
94
|
+
const mode = patterns.length > 1 ? (opts.all ? " with every text" : " with any of the texts (every one: --all)") : "";
|
|
95
|
+
const head = `${total} line(s)${mode} in ${found.counts.size} of ${found.searched} text file(s) of ${where}${found.capped ? ` — stopped at ${GREP_MAX_HITS}: a narrower text or --in` : ""}`;
|
|
90
96
|
if (opts.files) {
|
|
91
97
|
const files = [...found.counts].map(([file, n]) => ({ file, lines: n }));
|
|
92
98
|
const page = paginate(files, ctx.limit, ctx.page);
|
package/dist/commands/guide.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
import { langName } from "../core/lang.js";
|
|
6
6
|
import { appSite } from "../core/stromapp.js";
|
|
7
7
|
import { WEB_PER_HOST, webRuleLines } from "../core/web.js";
|
|
8
|
+
import { powershellUtf8Rule } from "../core/self.js";
|
|
8
9
|
/** The guide; appUrl: the Strom app's address it names — the one `strom app` opens (appUrlShown); perHost: web.perHost here. */
|
|
9
|
-
export function guideText(lang, appUrl, perHost = WEB_PER_HOST) {
|
|
10
|
+
export function guideText(lang, appUrl, perHost = WEB_PER_HOST, platform = process.platform) {
|
|
11
|
+
const ps = powershellUtf8Rule("", platform);
|
|
10
12
|
const language = lang
|
|
11
13
|
? `The research language of this tree is ${langName(lang)} (${lang}): talk to the user in ${langName(lang)} and write notes, tasks and stories in ${langName(lang)}. Keep transcripts of records in their original language.`
|
|
12
14
|
: "Talk to the user in their language. Set it as the research language: strom setup --lang <code> (cs, en, de, pl, …).";
|
|
@@ -231,7 +233,7 @@ THE STROM APP — where the user sees the result
|
|
|
231
233
|
|
|
232
234
|
OUTPUT AND ERRORS
|
|
233
235
|
- One line per record, IDs first. Read the text; add --json only to parse.
|
|
234
|
-
- Listings are paged (--limit, --page). Do not pipe strom output
|
|
236
|
+
- Listings are paged (--limit, --page). Do not pipe strom output.${ps ? `\n- ${ps}` : ""}
|
|
235
237
|
- People can be named by ID (P0001) or name ("Jan Novák", no diacritics ok).
|
|
236
238
|
An ambiguous name lists candidates (exit 2) — then use the ID.
|
|
237
239
|
- Every error ends with "→ <what to run next>".
|
package/dist/commands/media.js
CHANGED
|
@@ -9,7 +9,7 @@ import { register } from "../cli/registry.js";
|
|
|
9
9
|
import { lines, moreLine, paginate, runs, shellArg, table, truncate } from "../cli/format.js";
|
|
10
10
|
import { UsageError } from "../core/errors.js";
|
|
11
11
|
import { makeNote } from "../core/actions.js";
|
|
12
|
-
import { clipText, collectFiles, coveringPart, fileSha256, findImage, imageNumbers, imageOfRef, inboxFolders, inputPath, isWhole, mimeOf, onlyParts, otherCopies, parseImageList, regionText, sharperPart, storeShared, wholeSizeOf } from "../core/media.js";
|
|
12
|
+
import { clipText, collectFiles, coveringPart, fileSha256, findImage, imageNumbers, imageOfRef, imageRange, inboxFolders, inputPath, isWhole, mimeOf, onlyParts, otherCopies, parseImageList, regionText, sharperPart, storeShared, wholeSizeOf } from "../core/media.js";
|
|
13
13
|
import { create, normId, requireRecord, update } from "../core/records.js";
|
|
14
14
|
import { imageSizeOfFile } from "../image/index.js";
|
|
15
15
|
import { imageOf, pageOf } from "../core/calibration.js";
|
|
@@ -305,11 +305,46 @@ function refsText(wants) {
|
|
|
305
305
|
}
|
|
306
306
|
return out.join(" ");
|
|
307
307
|
}
|
|
308
|
+
/** A number of an image or page after a record set (B0001 --page 112 113): it belongs to the image named before it. */
|
|
309
|
+
const isNumberArg = (a) => /^\d+(?:\s*(?:[-–]|\.\.\.?)\s*\d+)?(?:\s*,\s*\d+(?:\s*(?:[-–]|\.\.\.?)\s*\d+)?)*$/u.test(a.trim());
|
|
310
|
+
/**
|
|
311
|
+
* Crops typed among several images (B0001:2 --crop A B0001:3 --crop B): each crop is of the image named before it —
|
|
312
|
+
* never every crop of every image (N0275: 16 views where 4 were meant). Crops all after the last image, or of one
|
|
313
|
+
* image, are of every image as always (undefined).
|
|
314
|
+
*/
|
|
315
|
+
function cropGroups(args, opts, after) {
|
|
316
|
+
const crops = opts.crop === undefined ? [] : Array.isArray(opts.crop) ? opts.crop.map(String) : [String(opts.crop)];
|
|
317
|
+
if (!crops.length || !after || after.length !== crops.length)
|
|
318
|
+
return undefined;
|
|
319
|
+
const refs = args.map((a, i) => (isNumberArg(a) ? -1 : i)).filter((i) => i >= 0);
|
|
320
|
+
if (refs.length < 2 || after.every((n) => n >= args.length))
|
|
321
|
+
return undefined;
|
|
322
|
+
const form = `strom media view ${args[refs[0]]} --crop ${crops[0]} ${args[refs[1]]} --crop …`;
|
|
323
|
+
if (after.some((n) => n === 0))
|
|
324
|
+
throw new UsageError("a --crop before any image: each --crop is of the image named before it", {
|
|
325
|
+
hint: `${form} (the same crops of every image: all of them after the last image)`,
|
|
326
|
+
code: "view.crop-first",
|
|
327
|
+
});
|
|
328
|
+
const groups = refs.map((r, k) => ({ args: args.slice(r, refs[k + 1] ?? args.length), crops: [] }));
|
|
329
|
+
crops.forEach((c, i) => {
|
|
330
|
+
// the image named last before it (a number after a record set is its page or image)
|
|
331
|
+
let k = refs.length - 1;
|
|
332
|
+
while (k > 0 && refs[k] >= after[i])
|
|
333
|
+
k--;
|
|
334
|
+
groups[k].crops.push(c);
|
|
335
|
+
});
|
|
336
|
+
const bare = groups.find((g) => !g.crops.length);
|
|
337
|
+
if (bare)
|
|
338
|
+
throw new UsageError(`${bare.args[0]} has no --crop of its own: each --crop is of the image named before it`, {
|
|
339
|
+
hint: `${form} (the same crops of every image: all of them after the last image)`,
|
|
340
|
+
code: "view.crop-missing",
|
|
341
|
+
});
|
|
342
|
+
return groups;
|
|
343
|
+
}
|
|
308
344
|
/** The images a call names, at most VIEW_MAX_IMAGES and `maxViews` views of them; images not registered said. */
|
|
309
|
-
function resolveTargets(tree, shared, all, args, opts, parts, maxViews = VIEW_MAX_VIEWS) {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
const refs = args.filter((a) => !numbers(a)).map((a) => a.trim());
|
|
345
|
+
function resolveTargets(tree, shared, all, args, opts, parts, maxViews = VIEW_MAX_VIEWS, several = false) {
|
|
346
|
+
const nums = args.filter(isNumberArg);
|
|
347
|
+
const refs = args.filter((a) => !isNumberArg(a)).map((a) => a.trim());
|
|
313
348
|
const pages = listOf(opts.page);
|
|
314
349
|
const images = listOf(opts.image);
|
|
315
350
|
if (pages.length && images.length)
|
|
@@ -320,6 +355,10 @@ function resolveTargets(tree, shared, all, args, opts, parts, maxViews = VIEW_MA
|
|
|
320
355
|
const pageList = pages.length ? [...pages, ...nums] : [];
|
|
321
356
|
const imageList = pages.length ? [] : [...images, ...nums];
|
|
322
357
|
const list = (s, what) => {
|
|
358
|
+
// a range from the higher number, or of hundreds of images: said as such
|
|
359
|
+
for (const p of s.join(",").split(","))
|
|
360
|
+
if (/^\s*\d+\s*(?:[-–]|\.\.\.?)\s*\d+\s*$/u.test(p))
|
|
361
|
+
imageRange(p.trim(), what === "images" ? "image" : what);
|
|
323
362
|
const n = parseImageList(s.join(","));
|
|
324
363
|
if (n)
|
|
325
364
|
return n;
|
|
@@ -377,7 +416,7 @@ function resolveTargets(tree, shared, all, args, opts, parts, maxViews = VIEW_MA
|
|
|
377
416
|
const parts = onlyParts(all, w.b.id, image);
|
|
378
417
|
const m = parts.length > 1 && (opts.crop !== undefined || opts.half !== undefined || opts.split !== undefined) ? findImage(all, w.b.id, image) : imageOfRef(all, w.b.id, image);
|
|
379
418
|
if (!m) {
|
|
380
|
-
if (wants.length === 1)
|
|
419
|
+
if (wants.length === 1 && !several)
|
|
381
420
|
throw new UsageError(`image ${image} of ${w.b.id} is not registered`, { hint: `strom media list --recordset ${w.b.id} · strom media add <files> --recordset ${w.b.id}` });
|
|
382
421
|
missing.push(`${w.b.id}:${image}`);
|
|
383
422
|
continue;
|
|
@@ -393,7 +432,7 @@ function resolveTargets(tree, shared, all, args, opts, parts, maxViews = VIEW_MA
|
|
|
393
432
|
seen.add(t.key);
|
|
394
433
|
targets.push(t);
|
|
395
434
|
}
|
|
396
|
-
if (!targets.length) {
|
|
435
|
+
if (!targets.length && !several) {
|
|
397
436
|
const b = missing[0].split(":")[0];
|
|
398
437
|
throw new UsageError(`none of these images is registered: ${missing.join(", ")}`, { hint: `strom media list --recordset ${b} · strom media add <files> --recordset ${b}` });
|
|
399
438
|
}
|
|
@@ -726,7 +765,7 @@ register({
|
|
|
726
765
|
path: ["media", "view"],
|
|
727
766
|
summary: "Make views of images to look at: a crop, a half page, enlarged, more contrast, a grid — several in one call",
|
|
728
767
|
group: "sources",
|
|
729
|
-
sheet: `at most ${VIEW_MAX_IMAGES} images a call; several crops of an image: --crop … --crop … (no loop)
|
|
768
|
+
sheet: `at most ${VIEW_MAX_IMAGES} images a call; several crops of an image: --crop … --crop … (no loop), of each image: IMG --crop … IMG --crop …; a whole view finds, only a crop is read; a book read through: strom read`,
|
|
730
769
|
tree: true,
|
|
731
770
|
description: `Writes the views to .strom/views/ and prints their paths: open those files with your image reader. A whole image\n` +
|
|
732
771
|
`is at most ${OVERVIEW_MAX} px on the long side, enough to find an entry; a half, a crop and the parts of --split at most\n` +
|
|
@@ -737,11 +776,13 @@ register({
|
|
|
737
776
|
"Several views in one call, then open all the files it lists together: several images (B0001:57 B0001:58, a range\n" +
|
|
738
777
|
"B0001:57-60, B0001 --page 112 113), several parts of each (--half left --half right; --half both = the two pages\n" +
|
|
739
778
|
"of a double page overlapping at the gutter, where they are sharper than one view of it, else the image whole;\n" +
|
|
740
|
-
"--crop repeated; --split 2x3 = a grid of overlapping parts), each view with its image, page and --clip.
|
|
779
|
+
"--crop repeated; --split 2x3 = a grid of overlapping parts), each view with its image, page and --clip. A --crop\n" +
|
|
780
|
+
"typed after an image among others is of that image alone (B0001:2 --crop … B0001:7 --crop …); crops after the last\n" +
|
|
781
|
+
"image are of every image. At most\n" +
|
|
741
782
|
`${VIEW_MAX_IMAGES} images and ${VIEW_MAX_VIEWS} views in one call: every view stays in the context that opens it.`,
|
|
742
783
|
args: [{ name: "image", description: "M0012, B0001:57 (record set:image), a range B0001:57-60, B0001 with --page, or an input I0002; several = several images", required: true, variadic: true }],
|
|
743
784
|
options: [
|
|
744
|
-
{ name: "crop", type: "string", multiple: true, value: "<x,y,w,h>", description: "part of the image: fractions (0.1,0.35,0.4,0.2) or pixels; repeated = several parts" },
|
|
785
|
+
{ name: "crop", type: "string", multiple: true, value: "<x,y,w,h>", description: "part of the image: fractions (0.1,0.35,0.4,0.2) or pixels; repeated = several parts; after one image of several: of that image alone" },
|
|
745
786
|
{ name: "half", type: "string", multiple: true, value: "<side>", description: "left or right page of a double page (top, bottom); both = the two pages, overlapping at the gutter" },
|
|
746
787
|
{ name: "split", type: "string", value: "<c>x<r>", description: "the image (or each half or crop) as a grid of overlapping parts, e.g. 2x3" },
|
|
747
788
|
{ name: "scale", type: "string", value: "<f>", description: "size of the result: 2 = twice the original pixels" },
|
|
@@ -750,7 +791,7 @@ register({
|
|
|
750
791
|
{ name: "grey", type: "boolean", description: "greyscale" },
|
|
751
792
|
{ name: "grid", type: "boolean", description: "overlay a grid of tenths with labels, to point at a place" },
|
|
752
793
|
{ name: "rotate", type: "string", value: "<deg>", description: "90, 180 or 270" },
|
|
753
|
-
{ name: "image", type: "string", multiple: true, value: "<n>", description: "image number(s) with a record set ID: 57, 57-60" },
|
|
794
|
+
{ name: "image", type: "string", multiple: true, value: "<n>", description: "image number(s) with a record set ID: 57, 57-60, 57..60" },
|
|
754
795
|
{ name: "page", type: "string", multiple: true, value: "<n>", description: "page(s) or folio(s), with a calibrated record set ID: 112, 112 113, 112-115" },
|
|
755
796
|
{ name: "png", type: "boolean", description: "lossless PNG instead of JPEG" },
|
|
756
797
|
],
|
|
@@ -761,13 +802,15 @@ register({
|
|
|
761
802
|
"strom media view B0001 --page 112",
|
|
762
803
|
"strom media view B0001:1-3 --half both",
|
|
763
804
|
"strom media view B0001:2 --crop 0.05,0.10,0.45,0.30 --crop 0.05,0.40,0.45,0.30",
|
|
805
|
+
"strom media view B0001:2 --crop 0.05,0.10,0.45,0.30 B0001:3 --crop 0.50,0.60,0.45,0.20",
|
|
764
806
|
"strom media view B0001 --page 112 113 --split 2x2",
|
|
765
807
|
],
|
|
766
|
-
run(ctx, { args, opts }) {
|
|
808
|
+
run(ctx, { args, opts, after }) {
|
|
767
809
|
const tree = ctx.tree();
|
|
768
810
|
const shared = sharedDir(ctx);
|
|
769
811
|
const all = tree.list("media");
|
|
770
812
|
const parts = viewParts(opts);
|
|
813
|
+
const groups = cropGroups(args, opts, after?.crop);
|
|
771
814
|
// a whole image to find the entry on; a part of it (a half, a crop, a grid's part) as big as the model takes it
|
|
772
815
|
const own = agentViewSizes(ctx, tree);
|
|
773
816
|
// what strom tuned for this agent and model (core/tune.ts) — as at the start of the session at work: the numbers
|
|
@@ -775,7 +818,33 @@ register({
|
|
|
775
818
|
const since = currentSession(tree, ctx.env)?.started;
|
|
776
819
|
const tuned = treeTuning(tree, ctx.settings);
|
|
777
820
|
const reading = readingOf(ctx.settings.config, own.key, { since, on: tuningOn(ctx.settings), root: tree.root });
|
|
778
|
-
|
|
821
|
+
// each image with its own crops (cropGroups), else every part of every image
|
|
822
|
+
const partsOf = new Map();
|
|
823
|
+
let targets = [];
|
|
824
|
+
let missing = [];
|
|
825
|
+
if (!groups)
|
|
826
|
+
({ targets, missing } = resolveTargets(tree, shared, all, args, opts, parts, reading.viewsPerCall));
|
|
827
|
+
else {
|
|
828
|
+
for (const g of groups) {
|
|
829
|
+
const own = { ...opts, crop: g.crops };
|
|
830
|
+
const gp = viewParts(own);
|
|
831
|
+
const r = resolveTargets(tree, shared, all, g.args, own, gp, reading.viewsPerCall, true);
|
|
832
|
+
for (const t of r.targets)
|
|
833
|
+
partsOf.set(t, gp);
|
|
834
|
+
targets.push(...r.targets);
|
|
835
|
+
missing.push(...r.missing);
|
|
836
|
+
}
|
|
837
|
+
if (!targets.length) {
|
|
838
|
+
const b = missing[0].split(":")[0];
|
|
839
|
+
throw new UsageError(`none of these images is registered: ${missing.join(", ")}`, { hint: `strom media list --recordset ${b} · strom media add <files> --recordset ${b}` });
|
|
840
|
+
}
|
|
841
|
+
const views = targets.reduce((n, t) => n + partsOf.get(t).reduce((m, p) => m + (p.both ? 2 : 1) * (p.split ? p.split[0] * p.split[1] : 1), 0), 0);
|
|
842
|
+
if (targets.length > VIEW_MAX_IMAGES || views > reading.viewsPerCall)
|
|
843
|
+
throw new UsageError(`${targets.length} image(s), ${views} views — at most ${VIEW_MAX_IMAGES} images and ${reading.viewsPerCall} views in one call: every view stays in the context that opens it`, {
|
|
844
|
+
hint: "fewer images now — write down what they gave before the next call",
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
const partsFor = (t) => partsOf.get(t) ?? parts;
|
|
779
848
|
const spec = viewSpec({ ...opts, half: undefined, crop: undefined });
|
|
780
849
|
// a book read worse than the others: its views bigger (never above what the model takes)
|
|
781
850
|
const sizesOf = (t) => bookViewSizes(own, tuned, t.media?.recordset, since);
|
|
@@ -785,7 +854,7 @@ register({
|
|
|
785
854
|
// where the size came from, for the record of the views (core/metrics.ts)
|
|
786
855
|
const capFromOf = (t) => (spec.max !== undefined || spec.scale !== undefined ? "option" : sizesOf(t).tuned ? `tuned:${sizesOf(t).tuned}` : own.calibrated ? "calibrated" : "default");
|
|
787
856
|
// the old call: one image, one view — said as it always was
|
|
788
|
-
if (targets.length === 1 && !missing.length && parts.length === 1 && !parts[0].both && !parts[0].split) {
|
|
857
|
+
if (!groups && targets.length === 1 && !missing.length && parts.length === 1 && !parts[0].both && !parts[0].split) {
|
|
789
858
|
const t = targets[0];
|
|
790
859
|
const r = oneView(ctx, tree, shared, all, t, { ...spec, max: maxOf(parts[0], t), half: parts[0].half, crop: parts[0].crop }, { capFrom: capFromOf(t) }, noSharperOf(t));
|
|
791
860
|
return {
|
|
@@ -799,7 +868,7 @@ register({
|
|
|
799
868
|
let first;
|
|
800
869
|
for (const t of targets)
|
|
801
870
|
try {
|
|
802
|
-
for (const p of
|
|
871
|
+
for (const p of partsFor(t))
|
|
803
872
|
for (const s of partSpecs(t, p, capOf(t), !!sizesOf(t).halves))
|
|
804
873
|
made.push({
|
|
805
874
|
t,
|
package/dist/commands/read.js
CHANGED
|
@@ -46,10 +46,13 @@ function partCovering(parts, crop) {
|
|
|
46
46
|
}
|
|
47
47
|
/** "40-69" → [40, 69] */
|
|
48
48
|
function range(v) {
|
|
49
|
-
const m = /^(\d+)(
|
|
49
|
+
const m = /^(\d+)(?:\s*(?:[-–]|\.\.\.?)\s*(\d+))?$/u.exec(v.trim());
|
|
50
50
|
if (!m)
|
|
51
51
|
throw new UsageError(`invalid --images "${v}"`, { hint: "e.g. 40-69" });
|
|
52
|
-
|
|
52
|
+
const [lo, hi] = [Number(m[1]), Number(m[2] ?? m[1])];
|
|
53
|
+
if (hi < lo)
|
|
54
|
+
throw new UsageError(`--images ${v.trim()}: a range from the lower number`, { hint: `--images ${hi}-${lo}` });
|
|
55
|
+
return [lo, hi];
|
|
53
56
|
}
|
|
54
57
|
export async function pool(items, size, fn) {
|
|
55
58
|
const results = new Array(items.length);
|
|
@@ -93,7 +96,7 @@ register({
|
|
|
93
96
|
{ name: "batch", type: "string", value: "<n>", description: `views per reader — a double page in halves is two (default ${BATCH}, fewer where strom tuned the reading of this agent and model: strom config get reading.batch; at most ${BATCH_MAX})` },
|
|
94
97
|
{ name: "parallel", type: "string", value: "<n>", description: "readers at the same time (default 3)" },
|
|
95
98
|
{ name: "model", type: "string", value: "<model>", description: "model of the readers (default: model.vision — never weaker for handwriting)" },
|
|
96
|
-
{ name: "minutes", type: "string", value: "<n>", description: READER_MINUTES_HELP },
|
|
99
|
+
{ name: "minutes", type: "string", value: "<n>", description: READER_MINUTES_HELP, mistaken: ["timeout", "time", "time-limit", "max-minutes"] },
|
|
97
100
|
],
|
|
98
101
|
examples: [
|
|
99
102
|
'strom read B0001 --images 40-69 --question "Every baptism of the surname Novák (Nowak, Nowack) in 1820–1824: the child, date, house, both parents and their parents"',
|