strom-research 1.15.0-beta.6 → 1.15.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/CHANGELOG.cs.md +9 -4
- package/CHANGELOG.de.md +10 -4
- package/CHANGELOG.md +9 -4
- package/assets/method/core.md +2 -1
- package/assets/method/reading.md +2 -1
- package/assets/plugins/gates/README.md +13 -3
- package/assets/plugins/gates/claude-usage/gate.json +1 -1
- package/assets/plugins/gates/claude-usage/gate.ts +83 -21
- package/dist/agents/profiles.js +12 -1
- package/dist/brief/brief.js +3 -3
- package/dist/brief/sheet.js +4 -3
- package/dist/cli/execute.js +39 -13
- package/dist/cli/format.js +11 -0
- package/dist/cli/help.js +9 -1
- package/dist/cli/main.js +4 -1
- package/dist/cli/registry.js +14 -1
- package/dist/commands/analysis.js +25 -7
- package/dist/commands/connectors.js +163 -42
- package/dist/commands/guide.js +3 -1
- package/dist/commands/media.js +7 -0
- package/dist/commands/read.js +57 -37
- package/dist/commands/readings.js +99 -8
- package/dist/commands/session.js +1 -2
- package/dist/commands/sources.js +60 -6
- package/dist/commands/story.js +38 -2
- package/dist/core/actions.js +1 -1
- package/dist/core/age.js +114 -18
- package/dist/core/media.js +21 -7
- package/dist/core/reader.js +30 -15
- package/dist/core/samebook.js +58 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/views.js +3 -1
- package/package.json +1 -4
package/CHANGELOG.cs.md
CHANGED
|
@@ -7,7 +7,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
7
7
|
|
|
8
8
|
## 1.15.0
|
|
9
9
|
|
|
10
|
-
*
|
|
10
|
+
*16. 10. 2026 · patří k aplikaci Strom 3.12.0.*
|
|
11
11
|
|
|
12
12
|
- **Archiv Stromu podle volby, i s AI agentem.** Průvodce nastavením se ptá, zda nové rodokmeny budou výzkumem
|
|
13
13
|
s agentem, nebo jen Archivem Stromu; v nastavení jde volbu změnit pro jeden rodokmen nebo pro nové.
|
|
@@ -23,9 +23,14 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
23
23
|
přečtené snímky jsou řečeny přesně a jen ty se počítají jako prohledané.
|
|
24
24
|
- **Rodokmeny z jiných programů.** Soubor GEDCOM uložený se značkou BOM nebo v UTF-16 (tak ho ukládají některé programy
|
|
25
25
|
ve Windows) se čte jako rodokmen; rodokmen jiného výzkumu se převezme jako cizí.
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
- **Čistě na jednom počítači.** Živé sledování končí čistě na každém systému, i ve Windows, a aplikace Strom se dozví,
|
|
27
|
+
že skončilo. Jiný strom na stejném počítači (z npm nebo sestavený ze zdrojů) nikdy nepřevezme odkazy aplikace Strom
|
|
28
|
+
ani kanál aktualizací.
|
|
29
|
+
- **Opravy ze skutečných výzkumů.** Denní příděl práce o samotě pustí nový týden hned, v posledních dvou hodinách
|
|
30
|
+
týdne využije, co zbývá, místo čekání přes obnovení, a říká, kdy se týden obnoví. Věk zapsaný zlomkem (½ roku, 1 1/2
|
|
31
|
+
léta, půl roku) se přečte a seznam snímků (`strom read --images 5-19,21-39`) se čte tak, jak je zadaný. Knihu
|
|
32
|
+
stahuje sezení, které ji čte, nikdy předem pro pozdější; `strom fetch --recordset` najde knihu sám; počty a příkazy
|
|
33
|
+
jsou řečeny tak, jak se píšou.
|
|
29
34
|
- **Co je nového.** Po `strom update` a u `strom --version` odkaz na tento přehled v jazyce výzkumu. Aplikace Strom
|
|
30
35
|
ukazuje body nové verze výzkumu ve svém okně „Co je nového“.
|
|
31
36
|
|
package/CHANGELOG.de.md
CHANGED
|
@@ -7,7 +7,7 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
7
7
|
|
|
8
8
|
## 1.15.0
|
|
9
9
|
|
|
10
|
-
*
|
|
10
|
+
*16.10.2026 · gehört zur Strom-App 3.12.0.*
|
|
11
11
|
|
|
12
12
|
- **Ein Strom-Archiv nach Wahl, auch mit einem KI-Agenten.** Die Einrichtung fragt, ob neue Stammbäume Forschungen mit
|
|
13
13
|
dem Agenten sind oder nur ein Strom-Archiv; in den Einstellungen lässt sich das für einen Stammbaum oder für die
|
|
@@ -25,9 +25,15 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
25
25
|
und nur sie gelten als durchsucht.
|
|
26
26
|
- **Stammbäume aus anderen Programmen.** Eine GEDCOM-Datei mit Byte-Order-Mark oder in UTF-16 (so speichern sie manche
|
|
27
27
|
Windows-Programme) wird als Stammbaum gelesen; der Stammbaum einer anderen Forschung wird als fremder übernommen.
|
|
28
|
-
- **Die Live-Ansicht endet sauber
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
- **Sauber auf einem Computer.** Die Live-Ansicht endet sauber auf jedem System, auch unter Windows, und die Strom-App
|
|
29
|
+
erfährt, dass sie endete. Ein anderer strom auf demselben Computer (aus npm oder aus den Quellen gebaut) übernimmt nie
|
|
30
|
+
die Links der Strom-App oder den Update-Kanal.
|
|
31
|
+
- **Korrekturen aus echten Forschungen.** Die tägliche Ration des Alleinarbeitens lässt eine neue Woche sofort
|
|
32
|
+
beginnen, nutzt in den letzten zwei Stunden der Woche, was übrig ist, statt über die Erneuerung hinaus zu warten,
|
|
33
|
+
und sagt, wann sich die Woche erneuert. Ein Alter als Bruch (½ Jahr, 1 1/2 Jahre, ein halbes Jahr) wird gelesen, und
|
|
34
|
+
eine Liste von Bildern (`strom read --images 5-19,21-39`) wird so gelesen, wie sie angegeben ist. Ein Buch lädt die
|
|
35
|
+
Sitzung, die es liest, nie im Voraus für eine spätere; `strom fetch --recordset` findet das Buch selbst; Zahlen und
|
|
36
|
+
Befehle werden so genannt, wie sie geschrieben werden.
|
|
31
37
|
- **Was ist neu.** Nach `strom update` und bei `strom --version` ein Link zu dieser Übersicht in der Sprache der
|
|
32
38
|
Forschung. Die Strom-App zeigt die Punkte einer neuen Forschungsversion in ihrem Fenster „Was ist neu“.
|
|
33
39
|
|
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,7 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
7
7
|
|
|
8
8
|
## 1.15.0
|
|
9
9
|
|
|
10
|
-
*
|
|
10
|
+
*2026-10-16 · goes with the Strom app 3.12.0.*
|
|
11
11
|
|
|
12
12
|
- **A Strom archive by choice, with an AI agent too.** The setup asks whether new family trees are researches with the
|
|
13
13
|
agent or only a Strom archive; the settings change it for one family tree or for the new ones.
|
|
@@ -23,9 +23,14 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
23
23
|
(`strom read --until-found`): the images read are said exactly, and only they count as searched.
|
|
24
24
|
- **Family trees from other programs.** A GEDCOM file saved with a byte order mark or in UTF-16 (as some Windows
|
|
25
25
|
programs save it) is read as a family tree; a family tree of another research is taken in as a foreign one.
|
|
26
|
-
- **The live view ends cleanly
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
- **Cleaner on one computer.** The live view ends cleanly on every system, Windows too, and the Strom app learns that
|
|
27
|
+
it ended. Another strom on the same computer (from npm, or built from the sources) never takes over the links of the
|
|
28
|
+
Strom app or the update channel.
|
|
29
|
+
- **Fixes from real researches.** The daily ration of working alone lets a new week begin at once, uses what is left
|
|
30
|
+
in the week's last two hours instead of waiting past the reset, and says when the week resets. Ages written as
|
|
31
|
+
fractions (½ roku, 1 1/2 years, ein halbes Jahr) are read, and a list of images (`strom read --images 5-19,21-39`)
|
|
32
|
+
is read as given. A book is fetched by the session that reads it, never ahead for a later one;
|
|
33
|
+
`strom fetch --recordset` finds the book itself; counts and commands are said as typed.
|
|
29
34
|
- **What's new.** After `strom update` and with `strom --version`, a link to this list in the research language. The
|
|
30
35
|
Strom app shows the points of a new research version in its own “What's new” window.
|
|
31
36
|
|
package/assets/method/core.md
CHANGED
|
@@ -104,7 +104,8 @@ conclusion so the next researcher can follow it.
|
|
|
104
104
|
usual (about ±5 around the target, an index in batches) — fewer turns.
|
|
105
105
|
`strom read` may follow a fetch at work: sooner, never more images.
|
|
106
106
|
Images you will read later in the session: `--background` fetches them
|
|
107
|
-
while you work on, `strom read` reads them as they come.
|
|
107
|
+
while you work on, `strom read` reads them as they come. Nothing for a
|
|
108
|
+
later session: the session that reads a book fetches it.
|
|
108
109
|
<!-- end -->
|
|
109
110
|
- **What strom or the tree denies stays denied.** Never offer the user to
|
|
110
111
|
change your permissions or settings, strom's or the tree's, to get round it:
|
package/assets/method/reading.md
CHANGED
|
@@ -51,7 +51,8 @@ never open again a view cleared from your context — your notes hold it.
|
|
|
51
51
|
- **Browsing a book or a range is a reader's job**: `strom read B0001
|
|
52
52
|
--images 40-69 --question "…"` (small script: `--half both`) — readers in
|
|
53
53
|
batches of ten, each with the full question, their reports in
|
|
54
|
-
notes/readings/.
|
|
54
|
+
notes/readings/. For every agent, also one with subagents of its own: a
|
|
55
|
+
third of their cost, every reading kept. It ends with what they found, compact: the found and
|
|
55
56
|
unclear entries whole, what was illegible where nothing was found (a
|
|
56
57
|
possible match often hides there — look at it), the gaps in the book.
|
|
57
58
|
Later: `strom readings` (`--match <surname>`, `--list`; one image's block:
|
|
@@ -110,9 +110,19 @@ usage is at least the number you give it behind the week gone:
|
|
|
110
110
|
strom config set run.gate "claude-usage 10" gone − used ≥ 10
|
|
111
111
|
|
|
112
112
|
Without a number: 0 — no faster than the week goes; 14: about a day in hand.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
|
|
114
|
+
Early in the week the number grows with it: at least half the week gone, the
|
|
115
|
+
full number from twice it on (at 15: from 30 % gone). Right after the reset a
|
|
116
|
+
session starts at once, and the first day uses at most half of its share.
|
|
117
|
+
In the week's last 2 hours what is left would go with the reset: a session
|
|
118
|
+
starts when the room left (to 100 %, or to the cap) holds one — counted high,
|
|
119
|
+
10 points.
|
|
120
|
+
|
|
121
|
+
Not enough in hand: it waits until the first of — the week has caught up, the
|
|
122
|
+
week's last 2 hours (with room for a session), the reset itself (a new week
|
|
123
|
+
starts at once); never past the reset. Every answer says when the week resets
|
|
124
|
+
("weekly reset in 1.7 h"). A used-up session: until it resets. Another agent
|
|
125
|
+
than Claude Code: it lets the work go on.
|
|
116
126
|
|
|
117
127
|
**A cap** — your own hard stop for the week:
|
|
118
128
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"interface": 1,
|
|
3
3
|
"title": "Claude usage",
|
|
4
|
-
"description": "Keeps working alone to the daily ration of your Claude subscription: a session starts only while the week's usage is at least n points behind the share of the week gone (a day ≈ 14.3): strom config set run.gate \"claude-usage 10\" (without a number: 0). A cap: \"claude-usage 10 --cap 95\" — at 95 % of the week no session starts, in any run, and no \"start anyway\" goes past it.",
|
|
4
|
+
"description": "Keeps working alone to the daily ration of your Claude subscription: a session starts only while the week's usage is at least n points behind the share of the week gone (a day ≈ 14.3; early in the week half the share gone, so a new week starts at once; in its last 2 hours what is left goes when it holds a session): strom config set run.gate \"claude-usage 10\" (without a number: 0). A cap: \"claude-usage 10 --cap 95\" — at 95 % of the week no session starts, in any run, and no \"start anyway\" goes past it.",
|
|
5
5
|
"command": [
|
|
6
6
|
"node",
|
|
7
7
|
"gate.ts"
|
|
@@ -8,8 +8,19 @@
|
|
|
8
8
|
// (a day is about 14.3 %) against the week's usage. With the number you give
|
|
9
9
|
// it (strom config set run.gate "claude-usage 10") a session starts only while
|
|
10
10
|
// usage is at least that many points behind the week gone: gone − used ≥ 10.
|
|
11
|
-
// Without a number: 0 — no faster than the week goes.
|
|
12
|
-
//
|
|
11
|
+
// Without a number: 0 — no faster than the week goes.
|
|
12
|
+
//
|
|
13
|
+
// Early in the week the number grows with it: at least min(n, gone / 2) — right
|
|
14
|
+
// after the reset (0 % gone, 0 % used) a session may start, and until 2·n % of
|
|
15
|
+
// the week is gone usage may reach half of it; from there on the full number.
|
|
16
|
+
// So the first day never spends the week (at most half its share), and a new
|
|
17
|
+
// week is not idle for n % of it (a day and more at 15) for nothing.
|
|
18
|
+
// Late in the week what is left goes with the reset: in its last END_WINDOW a
|
|
19
|
+
// session starts when what is left (to 100 %, or to the cap) holds one session
|
|
20
|
+
// (SESSION_POINTS).
|
|
21
|
+
// Else it waits until the first of: the week has caught up, its last
|
|
22
|
+
// END_WINDOW (with room for a session), its reset (a new week starts at once) —
|
|
23
|
+
// never past the reset. A full session: until it resets.
|
|
13
24
|
//
|
|
14
25
|
// A cap (strom config set run.gate "claude-usage 15 --cap 95"): at 95 % of the
|
|
15
26
|
// week used no session starts — the user's own hard stop, asked before every
|
|
@@ -28,15 +39,32 @@ const FRESH_MS = 15 * 60_000;
|
|
|
28
39
|
/** Under a cap, a session at work is asked to finish at this much of the week. */
|
|
29
40
|
const FINISH_AT = 0.99;
|
|
30
41
|
const WEEK = 7 * DAY;
|
|
42
|
+
/**
|
|
43
|
+
* The last stretch of the week in which what is left is spent rather than kept: about one session of `strom run`
|
|
44
|
+
* (60 minutes by default) with its wrap-up and the gate's next asking. What is not used by the reset is gone with
|
|
45
|
+
* it; what a person keeps in hand for their own work is missed for this long at most.
|
|
46
|
+
*/
|
|
47
|
+
const END_WINDOW = 2 * 3_600_000;
|
|
48
|
+
/** What one session may take of the week, counted high (points of the week's %): the room it needs before the reset. */
|
|
49
|
+
const SESSION_POINTS = 10;
|
|
50
|
+
/** After the reset the gate asks again this much later, so the new week is what Claude Code says. */
|
|
51
|
+
const AFTER_RESET = 60_000;
|
|
31
52
|
const lang = (process.env.STROM_LANG ?? "en").slice(0, 2);
|
|
32
53
|
|
|
33
54
|
const T: Record<string, Record<string, string>> = {
|
|
34
55
|
en: {
|
|
35
56
|
other: "the agent is not Claude Code — nothing to check",
|
|
36
57
|
api: "Claude Code runs on an API key, not a subscription — no plan limits",
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
head: "week {used} % used, {gone} % gone, {behind} in hand (at least {min} needed{early})",
|
|
59
|
+
early: " — half the week gone, early in the week",
|
|
60
|
+
resetIn: "; weekly reset in {reset}",
|
|
61
|
+
catch: " — until the week catches up; weekly reset in {reset}",
|
|
62
|
+
last: " — until the last {window} before the weekly reset in {reset}",
|
|
63
|
+
reset: " — until the weekly reset in {reset}",
|
|
64
|
+
end: "week {used} % used, weekly reset in {reset} — the {room} % left is enough for a session",
|
|
65
|
+
session: "the session is used up — until it resets; weekly reset in {reset}",
|
|
66
|
+
hours: "{n} h",
|
|
67
|
+
days: "{n} days",
|
|
40
68
|
bad: "the number must be 0 to 99, not \"{arg}\"",
|
|
41
69
|
badcap: "the cap must be 1 to 100 (\"--cap 95\"), not \"{arg}\"",
|
|
42
70
|
cap: "week {used} % used — the cap of {cap} % reached; it resets {resets}",
|
|
@@ -47,9 +75,16 @@ const T: Record<string, Record<string, string>> = {
|
|
|
47
75
|
cs: {
|
|
48
76
|
other: "agent není Claude Code — není co hlídat",
|
|
49
77
|
api: "Claude Code běží na API klíči, ne na předplatném — žádné limity plánu",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
78
|
+
head: "týden: využito {used} %, uplynulo {gone} %, náskok {behind} (potřeba aspoň {min}{early})",
|
|
79
|
+
early: " – na začátku týdne polovina uplynulého",
|
|
80
|
+
resetIn: "; týden se obnoví za {reset}",
|
|
81
|
+
catch: " — dokud čas nedožene; týden se obnoví za {reset}",
|
|
82
|
+
last: " — do posledních {window} před obnovením týdne za {reset}",
|
|
83
|
+
reset: " — do obnovení týdne za {reset}",
|
|
84
|
+
end: "týden: využito {used} %, obnoví se za {reset} — zbylých {room} % stačí na sezení",
|
|
85
|
+
session: "sezení je vyčerpané — do jeho obnovení; týden se obnoví za {reset}",
|
|
86
|
+
hours: "{n} h",
|
|
87
|
+
days: "{n} dne",
|
|
53
88
|
bad: "číslo musí být 0 až 99, ne \"{arg}\"",
|
|
54
89
|
badcap: "strop musí být 1 až 100 (\"--cap 95\"), ne \"{arg}\"",
|
|
55
90
|
cap: "týden: využito {used} % – strop {cap} % dosažen; obnoví se {resets}",
|
|
@@ -60,9 +95,16 @@ const T: Record<string, Record<string, string>> = {
|
|
|
60
95
|
de: {
|
|
61
96
|
other: "der Agent ist nicht Claude Code — nichts zu prüfen",
|
|
62
97
|
api: "Claude Code läuft mit einem API-Schlüssel, nicht im Abo — keine Planlimits",
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
head: "Woche: {used} % genutzt, {gone} % vergangen, Vorsprung {behind} (mindestens {min} nötig{early})",
|
|
99
|
+
early: " – am Wochenanfang die Hälfte des Vergangenen",
|
|
100
|
+
resetIn: "; Wochen-Reset in {reset}",
|
|
101
|
+
catch: " — bis die Zeit aufholt; Wochen-Reset in {reset}",
|
|
102
|
+
last: " — bis zu den letzten {window} vor dem Wochen-Reset in {reset}",
|
|
103
|
+
reset: " — bis zum Wochen-Reset in {reset}",
|
|
104
|
+
end: "Woche: {used} % genutzt, Wochen-Reset in {reset} — die übrigen {room} % reichen für eine Sitzung",
|
|
105
|
+
session: "die Sitzung ist aufgebraucht — bis sie zurückgesetzt wird; Wochen-Reset in {reset}",
|
|
106
|
+
hours: "{n} h",
|
|
107
|
+
days: "{n} Tagen",
|
|
66
108
|
bad: "die Zahl muss 0 bis 99 sein, nicht \"{arg}\"",
|
|
67
109
|
badcap: "die Obergrenze muss 1 bis 100 sein (\"--cap 95\"), nicht \"{arg}\"",
|
|
68
110
|
cap: "Woche: {used} % genutzt – Obergrenze {cap} % erreicht; zurückgesetzt {resets}",
|
|
@@ -135,19 +177,39 @@ if (cap !== undefined && week.used >= cap)
|
|
|
135
177
|
answer(2, say("cap", { used: Math.floor(week.used), cap, resets: week.resets.toLocaleString(lang, { weekday: "short", day: "numeric", month: "numeric", hour: "2-digit", minute: "2-digit" }) }), undefined, true);
|
|
136
178
|
|
|
137
179
|
const now = Date.now();
|
|
138
|
-
const
|
|
180
|
+
const resets = week.resets.getTime();
|
|
181
|
+
const start = resets - WEEK;
|
|
139
182
|
const gone = Math.max(0, Math.min(100, ((now - start) / WEEK) * 100));
|
|
140
183
|
const behind = gone - week.used;
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
// the number needed in hand: half the week gone early in the week, the full number from 2·n % gone on
|
|
185
|
+
const need = Math.min(min, gone / 2);
|
|
186
|
+
const early = need < min ? say("early") : "";
|
|
187
|
+
const reset = inTime(resets - now);
|
|
188
|
+
// shown rounded down: never "15 in hand (at least 15 needed)" for 14.6 — and gone − used stays what it says; the
|
|
189
|
+
// need early in the week is a fraction: rounded down when it goes, up when it waits, so the two never seem to disagree
|
|
190
|
+
const v = { used: Math.floor(week.used), gone: Math.floor(gone), behind: Math.floor(behind), early, reset };
|
|
191
|
+
if (session && session.used >= 100) answer(1, say("session", { reset }), session.resets ? new Date(session.resets.getTime() + AFTER_RESET) : undefined);
|
|
192
|
+
if (behind >= need) answer(0, say("head", { ...v, min: Math.floor(need) }) + say("resetIn", { reset }));
|
|
193
|
+
// what is left before the reset: (to the cap, else to 100 %) enough for a session — in the week's last stretch it goes
|
|
194
|
+
const room = (cap ?? 100) - week.used;
|
|
195
|
+
const fits = room >= SESSION_POINTS;
|
|
196
|
+
if (fits && resets - now <= END_WINDOW) answer(0, say("end", { used: v.used, reset, room: Math.floor(room) }));
|
|
197
|
+
// else it waits for the first of: the week caught up (gone − used ≥ the need: used < n → gone = 2·used, else
|
|
198
|
+
// used + n), the week's last stretch (with room for a session), the reset — a new week at 0 % goes at once, so
|
|
199
|
+
// never past it
|
|
200
|
+
const caught = start + ((week.used < min ? 2 * week.used : week.used + min) / 100) * WEEK;
|
|
201
|
+
const last = fits ? resets - END_WINDOW : Infinity;
|
|
202
|
+
const after = resets + AFTER_RESET;
|
|
203
|
+
const at = Math.min(caught, last, after);
|
|
204
|
+
const tail = at === caught ? say("catch", { reset }) : at === last ? say("last", { reset, window: say("hours", { n: END_WINDOW / 3_600_000 }) }) : say("reset", { reset });
|
|
205
|
+
answer(1, say("head", { ...v, min: Math.ceil(need) }) + tail, new Date(Math.max(at, now + 5 * 60_000)));
|
|
206
|
+
|
|
207
|
+
/** A stretch of time as a person reads it: hours to two days, then days — one decimal, in the research language. */
|
|
208
|
+
function inTime(ms: number): string {
|
|
209
|
+
const h = Math.max(0, ms) / 3_600_000;
|
|
210
|
+
const n = (x: number) => x.toLocaleString(lang, { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
|
211
|
+
return h < 48 ? say("hours", { n: n(h) }) : say("days", { n: n(h / 24) });
|
|
149
212
|
}
|
|
150
|
-
answer(0, say("go", v));
|
|
151
213
|
|
|
152
214
|
/**
|
|
153
215
|
* The limits Claude Code said in this run (strom passes them: [{kind, used 0–1, resetsAt, at}]), when the week's is
|
package/dist/agents/profiles.js
CHANGED
|
@@ -30,6 +30,9 @@ function delegationRules(n) {
|
|
|
30
30
|
return `- Browsing a book, an index or a range of images ("is our surname on this page?")
|
|
31
31
|
goes to \`strom read\` (by halves: \`--half both\`); a delegate reads only crops
|
|
32
32
|
of the entries found. Anything self-contained that returns little is delegated.
|
|
33
|
+
\`strom read\` and not subagents of your own for a range: about a third of
|
|
34
|
+
their cost on the same images, every reading kept (\`strom readings\`), the
|
|
35
|
+
places its readers found unclear held against a negative.
|
|
33
36
|
- A delegate other than strom-scan-reader (text, print, a search) gets no brief:
|
|
34
37
|
begin its prompt with this, word for word — "${DELEGATE_SHEET}"
|
|
35
38
|
- About ${first} scans (images B…:n) per delegate, four views each (halves and
|
|
@@ -56,6 +59,13 @@ function delegationRules(n) {
|
|
|
56
59
|
export function selfReading(n = DEFAULT_READING_NUMBERS) {
|
|
57
60
|
return `## Reading scans (Codex, Antigravity, OpenCode, Grok)
|
|
58
61
|
|
|
62
|
+
Browsing a book, an index or a range of images goes to \`strom read\`
|
|
63
|
+
(\`strom read B0001 --images 40-69 --question "…"\`), also for an agent with
|
|
64
|
+
sub-agents of its own: cheaper than reading the range in your context or by
|
|
65
|
+
a sub-agent, every reading kept (\`strom readings\`), the places its readers
|
|
66
|
+
found unclear held against a negative. Where strom says no reader can start
|
|
67
|
+
here, and for the entries found:
|
|
68
|
+
|
|
59
69
|
Read the images yourself, with your strongest model — never hand old
|
|
60
70
|
handwriting to a faster or cheaper model, subagent or pass. Read them
|
|
61
71
|
in batches of about ${inWords(n.batch)}: open a batch (one call gives its views —
|
|
@@ -103,7 +113,8 @@ comes back to you. Choose the model by the kind of work:
|
|
|
103
113
|
|
|
104
114
|
| work | model |
|
|
105
115
|
|---|---|
|
|
106
|
-
| handwriting:
|
|
116
|
+
| browsing handwriting: a book, an index, a range of images | \`strom read\` (its readers on the model for handwriting) — not a subagent of yours |
|
|
117
|
+
| handwriting read closely: the entries found, a page of a land book | \`strom-scan-reader\`, ${m.vision ? `on \`${m.vision}\`` : "on your own model"} — never cheaper |
|
|
107
118
|
| print and type: documents of the 20th century, catalogues, web pages, big text files (the lines \`strom grep\` found) | \`${m.text ?? "sonnet"}\` |
|
|
108
119
|
| mechanical: downloads, renaming, counting | \`${m.cheap ?? "haiku"}\` |
|
|
109
120
|
| a text in inputs/ or notes/ | nobody — \`strom grep\`, one call |
|
package/dist/brief/brief.js
CHANGED
|
@@ -21,7 +21,7 @@ import { byHandOnly, listConnectors, missingConsents, readyConnectors, routeOf }
|
|
|
21
21
|
import { clock, hostAllowed, hostState, REFUSED_FOREVER } from "../core/net.js";
|
|
22
22
|
import { archiveLevel } from "../core/terms.js";
|
|
23
23
|
import { onHost, permissionQuestions, setHosts } from "../core/archivewait.js";
|
|
24
|
-
import { runs, shellArg } from "../cli/format.js";
|
|
24
|
+
import { runs, runsArg, shellArg } from "../cli/format.js";
|
|
25
25
|
import { commandSheet } from "./sheet.js";
|
|
26
26
|
import { foldText } from "../core/text.js";
|
|
27
27
|
import { subjectPeople } from "../core/records.js";
|
|
@@ -631,10 +631,10 @@ function imagesLines(tree, b, route, shape, finder, noSharper = false, waiting)
|
|
|
631
631
|
/** The fetches at work for the research (each book: how far), and those in the background that ended lately with images left. */
|
|
632
632
|
function fetchingLines(root) {
|
|
633
633
|
const active = activeFetches(root);
|
|
634
|
-
const out = active.map((m) => `- ${m.recordset}: ${m.registered}/${m.images.length} images registered (${m.background ? `in the background${m.task ? `, for ${m.task}` : ""}` : "in the foreground"}) — strom read ${m.recordset} --images ${
|
|
634
|
+
const out = active.map((m) => `- ${m.recordset}: ${m.registered}/${m.images.length} images registered (${m.background ? `in the background${m.task ? `, for ${m.task}` : ""}` : "in the foreground"}) — strom read ${m.recordset} --images ${runsArg(m.images)} reads them as they come; not fetched again`);
|
|
635
635
|
for (const e of endedFetches(root))
|
|
636
636
|
if (e.missed.length && !active.some((m) => m.recordset === e.recordset))
|
|
637
|
-
out.push(`- ${e.recordset}: a fetch in the background ended with images ${runs(e.missed)} not fetched${e.stopped ? ` (stopped: ${e.stopped})` : ""} — strom fetch ${e.connector} ${shellArg(e.book)} --images ${
|
|
637
|
+
out.push(`- ${e.recordset}: a fetch in the background ended with images ${runs(e.missed)} not fetched${e.stopped ? ` (stopped: ${e.stopped})` : ""} — strom fetch ${e.connector} ${shellArg(e.book)} --images ${runsArg(e.missed)} --recordset ${e.recordset} if they are needed`);
|
|
638
638
|
return out;
|
|
639
639
|
}
|
|
640
640
|
/** What the user put in the shared inbox, waiting to be registered — by folder (one download each). */
|
package/dist/brief/sheet.js
CHANGED
|
@@ -27,10 +27,11 @@ const LEVELS = {
|
|
|
27
27
|
const RARE = {
|
|
28
28
|
"search add": ["by"],
|
|
29
29
|
"task add": ["research", "anyway"],
|
|
30
|
-
"media view": ["scale", "max", "grey", "png", "image"],
|
|
30
|
+
"media view": ["scale", "max", "grey", "png", "image", "whole"],
|
|
31
31
|
read: ["context", "batch", "model", "minutes", "max"],
|
|
32
|
-
// a book the research has already: the refusal says --again where it applies; --status and --stop: the method and the brief name them
|
|
33
|
-
|
|
32
|
+
// a book the research has already: the refusal says --again where it applies; --status and --stop: the method and the brief name them;
|
|
33
|
+
// --task: strom takes the task the agent works on where it is plain, and says --task where it took one
|
|
34
|
+
fetch: ["take", "result", "again", "status", "stop", "task"],
|
|
34
35
|
"source add": ["translation", "media"],
|
|
35
36
|
"source edit": ["translation", "media"],
|
|
36
37
|
"recordset add": ["images", "again"],
|
package/dist/cli/execute.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { UsageError } from "../core/errors.js";
|
|
6
6
|
import { ALL_PREFIXES } from "../core/model.js";
|
|
7
7
|
import { shellArg } from "./format.js";
|
|
8
|
-
import { GLOBAL_OPTIONS, aliasExtends, commands, match, optionsOf, subcommandsOf, usageLine, WRITE_OPTIONS } from "./registry.js";
|
|
8
|
+
import { GLOBAL_OPTIONS, aliasExtends, aliasGroup, commands, match, optionsOf, subcommandsOf, usageLine, WRITE_OPTIONS } from "./registry.js";
|
|
9
9
|
const VALUE_GLOBALS = new Set(GLOBAL_OPTIONS.filter((o) => o.type === "string").map((o) => `--${o.name}`));
|
|
10
10
|
/** The first word of a command line as typed — a command strom knows or not; the values of global options (--lang cs) are none. */
|
|
11
11
|
export function firstWord(argv) {
|
|
@@ -55,12 +55,28 @@ function distance(a, b) {
|
|
|
55
55
|
}
|
|
56
56
|
return d[a.length][b.length];
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* How many typos a word of this many letters may hold and still be meant: one in a short word, two in a longer one,
|
|
60
|
+
* a few more in a command of several words — never so many that another word is offered (--whole is no --home).
|
|
61
|
+
*/
|
|
62
|
+
function typos(letters) {
|
|
63
|
+
return letters <= 6 ? 1 : letters <= 14 ? 2 : Math.floor(letters / 5);
|
|
64
|
+
}
|
|
58
65
|
/** The closest candidates to `input` (a prefix, or a few typos), best first — only the best ones. */
|
|
59
66
|
export function suggest(input, candidates) {
|
|
60
|
-
|
|
67
|
+
// an option's dashes are no letters of it: "--bron" is "bron"
|
|
68
|
+
const bare = (s) => s.toLowerCase().replace(/^-+/u, "");
|
|
69
|
+
const q = bare(input);
|
|
70
|
+
if (!q)
|
|
71
|
+
return [];
|
|
61
72
|
const scored = candidates
|
|
62
|
-
.map((c) =>
|
|
63
|
-
|
|
73
|
+
.map((c) => {
|
|
74
|
+
const b = bare(c);
|
|
75
|
+
// a prefix of at least three letters ("--surnamez" for --surname, "person ad" for person add)
|
|
76
|
+
const prefix = Math.min(b.length, q.length) >= 3 && (b.startsWith(q) || q.startsWith(b));
|
|
77
|
+
return { c, d: prefix ? 0.5 : distance(q, b) };
|
|
78
|
+
})
|
|
79
|
+
.filter((x) => x.d <= typos([...q].length))
|
|
64
80
|
.sort((a, b) => a.d - b.d || a.c.length - b.c.length);
|
|
65
81
|
const best = scored[0]?.d;
|
|
66
82
|
return [...new Set(scored.filter((x) => x.d <= (best ?? 0) + 0.5).map((x) => x.c))].slice(0, 3);
|
|
@@ -78,11 +94,14 @@ export function resolveCommand(argv) {
|
|
|
78
94
|
return { def: found.def, rest };
|
|
79
95
|
}
|
|
80
96
|
// A group ("person") with a wrong or missing subcommand.
|
|
81
|
-
const
|
|
97
|
+
const typedGroup = found ? [...found.def.path, ...leftover] : words;
|
|
98
|
+
// another name of a group (strom book → strom recordset): that group
|
|
99
|
+
const group = aliasGroup(typedGroup) ?? typedGroup;
|
|
82
100
|
if (group.length && subcommandsOf(group).length) {
|
|
83
101
|
if (!extra.length)
|
|
84
102
|
throw new GroupOnly(group);
|
|
85
|
-
|
|
103
|
+
// said as typed (strom book B1, never "strom recordset B1"), looked up in the group it stands for
|
|
104
|
+
throw unknownCommand([...group, extra[0]], [...typedGroup, extra[0]]);
|
|
86
105
|
}
|
|
87
106
|
throw unknownCommand([...words, ...extra].slice(0, Math.max(words.length + 1, 2)));
|
|
88
107
|
}
|
|
@@ -94,15 +113,22 @@ export class GroupOnly extends Error {
|
|
|
94
113
|
this.group = group;
|
|
95
114
|
}
|
|
96
115
|
}
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
/**
|
|
117
|
+
* An unknown command, said as typed (`said`: another name of a group — strom book … — when `words` name the group it
|
|
118
|
+
* stands for) with the commands near it; a group's ID after it (strom book B1) offers the group's show.
|
|
119
|
+
*/
|
|
120
|
+
function unknownCommand(words, said = words) {
|
|
121
|
+
const typed = said.join(" ");
|
|
99
122
|
const all = commands().filter((c) => c.path.length > 0).map((c) => c.path.join(" "));
|
|
100
123
|
// A known group narrows the choice ("person ad" → "person add", not "lesson add").
|
|
101
124
|
const inGroup = all.filter((c) => c.startsWith(`${words[0]} `));
|
|
102
125
|
const same = (inGroup.length ? inGroup : all).filter((c) => c.split(" ").length === words.length);
|
|
103
|
-
const hits = suggest(
|
|
104
|
-
const
|
|
105
|
-
const
|
|
126
|
+
const hits = suggest(words.join(" "), same.length ? same : all);
|
|
127
|
+
const show = !hits.length && words.length === 2 && /^\p{Lu}\p{N}+$/u.test(words[1]) && inGroup.includes(`${words[0]} show`) ? `${said[0]} show ${said[1]}` : undefined;
|
|
128
|
+
const more = show ? [show] : hits.length ? hits : suggest(words[0] ?? "", [...new Set(all.map((c) => c.split(" ")[0]))]);
|
|
129
|
+
// in the words typed: the group's other name kept (strom book show, not strom recordset show)
|
|
130
|
+
const asTyped = (c) => (said[0] !== words[0] && c.split(" ")[0] === words[0] ? [said[0], ...c.split(" ").slice(1)].join(" ") : c);
|
|
131
|
+
const near = more.map((c) => `strom ${asTyped(c)}`).join(" · ");
|
|
106
132
|
return new UsageError(`unknown command "strom ${typed}"`, {
|
|
107
133
|
hint: more.length ? `similar command: ${near}` : "strom help (all commands)",
|
|
108
134
|
...(more.length ? { code: "command.near", params: { cmd: typed, near } } : { code: "command.unknown", params: { cmd: typed } }),
|
|
@@ -275,9 +301,9 @@ export function parseOptions(def, rest, program = "strom") {
|
|
|
275
301
|
const own = [...(def.options ?? []), ...(def.writes ? optionsOf(def).filter((o) => o.name === "dry-run" || o.name === "reason") : [])].filter((o) => !o.hidden).map((o) => `--${o.name}`);
|
|
276
302
|
if (e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
277
303
|
// "strom brief --task T0003": the task is an argument there, not an option
|
|
278
|
-
const arg = def.args?.find((a) => `--${a.name}` === opt);
|
|
304
|
+
const arg = def.args?.find((a) => `--${a.name}` === opt || a.mistaken?.includes(opt.replace(/^-+/u, "")));
|
|
279
305
|
if (arg)
|
|
280
|
-
throw new UsageError(`${opt.slice(2)} is an argument of ${cmd}, not an option`, { hint: `strom ${def.path.join(" ")} <${arg.name}>`, code: "option.is-arg", params: { opt, cmd, arg: arg.name }, usage });
|
|
306
|
+
throw new UsageError(`--${arg.name}` === opt ? `${opt.slice(2)} is an argument of ${cmd}, not an option` : `${opt} is no option of ${cmd}: <${arg.name}> is its argument`, { hint: `strom ${def.path.join(" ")} <${arg.name}>`, code: "option.is-arg", params: { opt, cmd, arg: arg.name }, usage });
|
|
281
307
|
// a status asked of a name or a family (N0191): it is a fact's only — said as cite says it (the person's
|
|
282
308
|
// language: the plain unknown option, its code kept)
|
|
283
309
|
if (opt === "--status" && (def.path[0] === "name" || def.path[0] === "family"))
|
package/dist/cli/format.js
CHANGED
|
@@ -38,6 +38,10 @@ export function truncate(text, max) {
|
|
|
38
38
|
const one = text.replace(/\s+/g, " ").trim();
|
|
39
39
|
return [...one].length <= max ? one : [...one].slice(0, max - 1).join("") + "…";
|
|
40
40
|
}
|
|
41
|
+
/** A count with its noun as English says it: "1 image", "2 images" (`many` where the plural is not one more s). */
|
|
42
|
+
export function counted(n, one, many = `${one}s`) {
|
|
43
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
44
|
+
}
|
|
41
45
|
export function lines(...parts) {
|
|
42
46
|
return parts.filter((p) => typeof p === "string").join("\n");
|
|
43
47
|
}
|
|
@@ -54,6 +58,13 @@ export function runs(nums) {
|
|
|
54
58
|
}
|
|
55
59
|
return out.join(", ");
|
|
56
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Runs of numbers as a command takes them, one word of the shell: [1, 2, 3, 7] → "1-3,7" (no spaces, no en dash — a
|
|
63
|
+
* hint copied as runs() says it for a person, "1–3, 7", would be three arguments).
|
|
64
|
+
*/
|
|
65
|
+
export function runsArg(nums) {
|
|
66
|
+
return runs(nums).replace(/–/g, "-").replace(/,\s*/g, ",");
|
|
67
|
+
}
|
|
57
68
|
/** A number as a language writes it (1,4 in Czech, 1.4 in English); a language Intl does not know: English. */
|
|
58
69
|
export function numberIn(lang, n, o = { maximumFractionDigits: 1 }) {
|
|
59
70
|
try {
|
package/dist/cli/help.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Help text generated from the registry. Short by design: examples first,
|
|
2
2
|
// then arguments and options.
|
|
3
|
-
import { GROUPS, GLOBAL_OPTIONS, WRITE_OPTIONS, commands, descriptionOf, match, subcommandsOf, usageLine } from "./registry.js";
|
|
3
|
+
import { GROUPS, GROUP_ALIASES, GLOBAL_OPTIONS, WRITE_OPTIONS, aliasGroup, commands, descriptionOf, match, subcommandsOf, usageLine } from "./registry.js";
|
|
4
4
|
import { table } from "./format.js";
|
|
5
5
|
import { suggest } from "./execute.js";
|
|
6
6
|
import { UI, ui } from "./ui.js";
|
|
@@ -77,12 +77,14 @@ export function groupHelp(prefix, archive = false) {
|
|
|
77
77
|
});
|
|
78
78
|
if (!subs.length)
|
|
79
79
|
return `strom ${prefix.join(" ")}: not in an archive (research is switched on by the user: strom mode research)`;
|
|
80
|
+
const also = prefix.length === 1 ? Object.entries(GROUP_ALIASES).filter(([, g]) => g === prefix[0]).map(([a]) => `strom ${a} <command>`) : [];
|
|
80
81
|
return [
|
|
81
82
|
`strom ${prefix.join(" ")} <command>`,
|
|
82
83
|
"",
|
|
83
84
|
table(subs),
|
|
84
85
|
"",
|
|
85
86
|
`Details: strom help ${prefix.join(" ")} <command>`,
|
|
87
|
+
...(also.length ? [`Also: ${also.join(" · ")}`] : []),
|
|
86
88
|
].join("\n");
|
|
87
89
|
}
|
|
88
90
|
/** Words joined with " · ", wrapped to lines of about `width`, the next ones indented. */
|
|
@@ -130,6 +132,9 @@ export function helpFor(input, archive, appUrl) {
|
|
|
130
132
|
return commandHelp(found.def, archive, appUrl);
|
|
131
133
|
if (subcommandsOf(words).length > 0)
|
|
132
134
|
return groupHelp(words, archive);
|
|
135
|
+
const real = aliasGroup(words);
|
|
136
|
+
if (real)
|
|
137
|
+
return groupHelp(real, archive);
|
|
133
138
|
const all = commands().filter((c) => c.path.length > 0).map((c) => c.path.join(" "));
|
|
134
139
|
const near = suggest(words.join(" "), all.filter((c) => c.split(" ").length === words.length));
|
|
135
140
|
return `unknown command "${words.join(" ")}"${near.length ? ` — similar command: ${near.map((c) => `strom help ${c}`).join(" · ")}` : ""}\nall commands: strom help`;
|
|
@@ -205,6 +210,9 @@ export function helpAs(input, as) {
|
|
|
205
210
|
return humanCommand(found.def, as);
|
|
206
211
|
if (subcommandsOf(words).length > 0)
|
|
207
212
|
return humanGroup(words, as);
|
|
213
|
+
const real = aliasGroup(words);
|
|
214
|
+
if (real)
|
|
215
|
+
return humanGroup(real, as);
|
|
208
216
|
const all = commands().filter((c) => c.path.length > 0).map((c) => c.path.join(" "));
|
|
209
217
|
const near = suggest(words.join(" "), all.filter((c) => c.split(" ").length === words.length));
|
|
210
218
|
return [ui(as.lang, "ui.help.unknown", { cmd: words.join(" ") }), ...near.map((c) => ` strom help ${c} --human`)].join("\n");
|
package/dist/cli/main.js
CHANGED
|
@@ -30,6 +30,7 @@ import { refreshGlobal } from "../agents/global.js";
|
|
|
30
30
|
import { expandFromLine, refreshLinks } from "../core/links.js";
|
|
31
31
|
import { clockLine, FINISH_LINE, finishLimitLine, finishOf, timeLine } from "../core/clock.js";
|
|
32
32
|
import { currentSession } from "../core/session.js";
|
|
33
|
+
import { backgroundOf } from "../core/fetching.js";
|
|
33
34
|
import { callMistake, checkArgs, GroupOnly, parseOptions, resolveCommand, firstWord, severalRecords, splitPassthrough, usageOf } from "./execute.js";
|
|
34
35
|
import { placeholders, UI, ui } from "./ui.js";
|
|
35
36
|
import { catalog, localized, sayCommandAs } from "../core/phrases.js";
|
|
@@ -409,7 +410,9 @@ export async function main(argv, io, env, cwd) {
|
|
|
409
410
|
let lang;
|
|
410
411
|
try {
|
|
411
412
|
// an unknown command or option has no context yet: the person's language all the same
|
|
412
|
-
|
|
413
|
+
// …but a fetch in the background writes into its log, which the agent reads: English, as the rest of it
|
|
414
|
+
if (!backgroundOf(env))
|
|
415
|
+
lang = (ran ?? Context.fromOptions({ env, cwd, io, json, values: {} })).uiLang();
|
|
413
416
|
}
|
|
414
417
|
catch {
|
|
415
418
|
// no language to say it in: English
|
package/dist/cli/registry.js
CHANGED
|
@@ -33,8 +33,16 @@ export const WRITE_OPTIONS = [
|
|
|
33
33
|
{ name: "reason", type: "string", value: "<text>", description: "why (required when changing or retracting facts)" },
|
|
34
34
|
];
|
|
35
35
|
const registry = [];
|
|
36
|
+
/**
|
|
37
|
+
* Other names of a whole group (what agents type for it: "book show" for strom recordset show): each command of the
|
|
38
|
+
* group is taken by it too — an alias of its own, said in its help ("Also: strom book show") and in strom commands
|
|
39
|
+
* --json, never a command or group of its own in the overview.
|
|
40
|
+
*/
|
|
41
|
+
export const GROUP_ALIASES = { book: "recordset" };
|
|
36
42
|
export function register(...defs) {
|
|
37
|
-
for (const
|
|
43
|
+
for (const given of defs) {
|
|
44
|
+
const others = Object.entries(GROUP_ALIASES).filter(([, g]) => given.path.length > 1 && given.path[0] === g).map(([a]) => [a, ...given.path.slice(1)]);
|
|
45
|
+
const def = others.length ? { ...given, aliases: [...(given.aliases ?? []), ...others] } : given;
|
|
38
46
|
const names = [def.path, ...(def.aliases ?? [])].map((p) => p.join(" "));
|
|
39
47
|
if (registry.some((c) => [c.path, ...(c.aliases ?? [])].some((p) => names.includes(p.join(" ")))))
|
|
40
48
|
throw new Error(`duplicate command ${def.path.join(" ")}`);
|
|
@@ -72,6 +80,11 @@ export function subcommandsOf(prefix) {
|
|
|
72
80
|
export function aliasExtends(prefix) {
|
|
73
81
|
return registry.some((c) => (c.aliases ?? []).some((a) => a.length > prefix.length && prefix.every((w, i) => a[i] === w)));
|
|
74
82
|
}
|
|
83
|
+
/** The group another name stands for, when the words are one ("book" → "recordset"), else nothing. */
|
|
84
|
+
export function aliasGroup(prefix) {
|
|
85
|
+
const real = prefix.length === 1 ? GROUP_ALIASES[prefix[0]] : undefined;
|
|
86
|
+
return real && subcommandsOf([real]).length ? [real] : undefined;
|
|
87
|
+
}
|
|
75
88
|
export function usageLine(def) {
|
|
76
89
|
const args = (def.args ?? []).map((a) => {
|
|
77
90
|
const name = a.variadic ? `${a.name}...` : a.name;
|