dante-watch 0.1.16 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/package.json +3 -2
- package/src/bib/ear.mjs +1 -1
- package/src/bib/router.mjs +1 -1
- package/src/me/conv.mjs +15 -0
- package/src/me/pick.mjs +54 -2
- package/src/session.mjs +37 -11
- package/src/ticker.mjs +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npx dante-watch # open a session: talk to Dante in German, English or Spanish
|
|
7
|
+
npm i -g dante-watch && dante # install once, then summon him with one word (careful: `npx dante` is another package)
|
|
7
8
|
npx dante-watch --version # which version runs
|
|
8
9
|
npx dante-watch --ssh root@host --key ~/.ssh/id # check a server you own, once
|
|
9
10
|
```
|
|
@@ -152,6 +153,26 @@ scary report — it is a fast, grounded check you can run on your own box and be
|
|
|
152
153
|
- **Check me against another AI.** If Dante tells you something is open, verify it against an
|
|
153
154
|
independent model. If that check disagrees, write us: info@nm-streetwear.space.
|
|
154
155
|
|
|
156
|
+
## How it got built (and why we tell you)
|
|
157
|
+
|
|
158
|
+
We use AI. All the way, on purpose. We have no investors and no team, and we chose to go after a field where funded companies are
|
|
159
|
+
already running, so the only fair fight was speed - with proof.
|
|
160
|
+
|
|
161
|
+
In about three days (first commit 25 Oct 2026):
|
|
162
|
+
|
|
163
|
+
- **7,600 lines** of Dante across 77 files, plus **2,800 lines of tests** - 289 of them, green on macOS, Linux and a real Windows Server.
|
|
164
|
+
- **259,000 checked sentences** in German, English and Spanish, so he understands what you type - the way a person types it.
|
|
165
|
+
- **1,111 secret formats** (cloud keys, tokens, private keys) he looks for in your code, and **86 systems** he can tell apart.
|
|
166
|
+
- One real run of Dante on a real Windows Server, with answers typed like a person, on every change.
|
|
167
|
+
|
|
168
|
+
A normal team at a normal pace needs, on our own conservative assumptions - 75 finished and tested lines per developer-day, 300 labelled
|
|
169
|
+
sentences per hour, half an hour per secret format - roughly **320 person-days**: a person-year and a half. In three calendar days that
|
|
170
|
+
is a factor of about **100**. This is our estimate; the assumptions are written down so you can change them and do the sum yourself.
|
|
171
|
+
|
|
172
|
+
What the AI does not get to decide: every claim Dante makes has to show the real line it came from, "safe" is only said with proof,
|
|
173
|
+
and nothing changes on your machine without your *yes*. The tests, the equality checks against the real `find`/`grep`, and an opponent
|
|
174
|
+
that attacks Dante with hostile input are there to catch exactly the mistakes a fast machine makes. If you find one anyway, tell us.
|
|
175
|
+
|
|
155
176
|
## Credits
|
|
156
177
|
|
|
157
178
|
Dante is the pilgrim who walked through Hell and came back with the map - companion to
|
|
@@ -163,9 +184,7 @@ Where the knowledge comes from, and what was *not* taken:
|
|
|
163
184
|
was learned from the public documentation and check lists of PowerSploit/PowerUp, SharpUp (GhostPack), WinPEAS and HardeningKitty,
|
|
164
185
|
and from Microsoft's own documentation of `icacls`, `schtasks` and the registry. We read what they look for; **we copied no code**.
|
|
165
186
|
- **Technique names** in the explanation cards follow MITRE ATT&CK where we are sure of the id.
|
|
166
|
-
-
|
|
167
|
-
Spanish training lines of the language library were generated with DeepSeek and filtered against hand-written test sentences.
|
|
168
|
-
Every claim Dante makes still has to show a real line - that is the part no model gets to write.
|
|
187
|
+
- - How the code and the library were made, and how fast: see **How it got built** above.
|
|
169
188
|
- Runs on [Node.js](https://nodejs.org) and your system's own `ssh`, `git` and `curl`; nothing else is installed.
|
|
170
189
|
|
|
171
190
|
MIT License
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dante-watch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Dante watches your own server and never lies to itself: every verdict carries the real line it came from.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"dante-watch": "src/cli.mjs"
|
|
7
|
+
"dante-watch": "src/cli.mjs",
|
|
8
|
+
"dante": "src/cli.mjs"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"src",
|
package/src/bib/ear.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export const bibDate = () => load().date;
|
|
|
20
20
|
// einig - act only when both ears are sure on their own and say the same; else ask back
|
|
21
21
|
// kombi - the combiner's joint probability decides
|
|
22
22
|
// The ranked list a human sees ("Meinst du ...?") always comes from the joint probability.
|
|
23
|
-
// Measured per ear
|
|
23
|
+
// Measured per ear on the held-out sentences: ohr bayes 131/138 · 1 wrong · einig 130 · 0 wrong · kombi 131 · 3 wrong
|
|
24
24
|
// janein bayes 30/33 · einig 30/33 · kombi 33/33 (it can never say yes)
|
|
25
25
|
const WAYS = { ohr: "einig", janein: "kombi", system: "einig", muster: "einig", wissen: "einig" };
|
|
26
26
|
export const setWay = (w) => { WAYS.ohr = WAYS.janein = WAYS.system = WAYS.muster = WAYS.wissen = w; };
|
package/src/bib/router.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const SHARP = 2; // measured on the held-out set: 1 -> too shy, ≥2.5 ->
|
|
|
10
10
|
export const SURE = 0.6; // below this the router asks instead of acting
|
|
11
11
|
// Risk-weighted: an intent that only ANSWERS (a question, small talk, a greeting) may speak from
|
|
12
12
|
// SURE_ANSWER on - a wrong answer is wrong text, never a deed. Actions keep SURE.
|
|
13
|
-
// Measured
|
|
13
|
+
// Measured on the held-out sentences: 135 of 146 question misses had the right intent on top, only under 60 %.
|
|
14
14
|
export const ANSWER_ONLY = new Set(["frage_luecke", "frage_urteil", "frage_gehackt", "frage_dante", "smalltalk", "gruss", "hilfe", "unklar"]);
|
|
15
15
|
export let SURE_ANSWER = Number(process.env.DANTE_SURE_ANSWER || 0.4);
|
|
16
16
|
export const sureFor = (intent) => (ANSWER_ONLY.has(intent) ? SURE_ANSWER : SURE);
|
package/src/me/conv.mjs
CHANGED
|
@@ -49,3 +49,18 @@ export const FALLBACK = {
|
|
|
49
49
|
long: { de: "Das ist viel auf einmal. Sag es mir in einem Satz, was ich tun soll — oder „hilfe“.", en: "That is a lot at once. Tell me in one sentence what I should do — or “help”.", es: "Es mucho de golpe. Dime en una frase qué debo hacer — o «ayuda»." },
|
|
50
50
|
again: { de: "Ich habe dich nicht verstanden, und raten bringt uns nicht weiter. Sag es anders, oder „hilfe“.", en: "I did not understand you, and guessing gets us nowhere. Put it another way, or “help”.", es: "No te he entendido y adivinar no ayuda. Dilo de otra forma, o «ayuda»." },
|
|
51
51
|
};
|
|
52
|
+
|
|
53
|
+
// "wie mache ich den Schritt?" - the person means the open step of the last check, not a new check.
|
|
54
|
+
export const wantsHowFix = (line) => W("wie (?:mach(?:e|t|st)?|erledige|setze|führe|fuehre) ich (?:den|das|die|es|dies\\w*)|wie (?:behebe|beheb|löse|loese|schließe|schliesse|fixe|repariere) ich|wie (?:kann|soll|muss) ich (?:das|den|es|die) (?:beheben|lösen|loesen|schließen|schliessen|machen|beseitigen)|was (?:muss|soll|kann) ich (?:jetzt |nun )?(?:tun|machen)|was (?:nun|jetzt) tun|how (?:do|can|should) i (?:do|fix|close|solve|resolve|handle) (?:that|this|it|the step)|how to (?:fix|close|solve) (?:that|this|it)|what (?:should|do|can|must) i do(?: now| about (?:that|this|it))?|c[oó]mo (?:lo |la )?(?:arreglo|soluciono|resuelvo|hago|cierro)|qu[eé] (?:hago|debo hacer|tengo que hacer)").test(line);
|
|
55
|
+
|
|
56
|
+
// "prüfe die 4" after a numbered list was shown: the number belongs to THAT list. -> the number, or null
|
|
57
|
+
export function menuPick(line, size) {
|
|
58
|
+
const t = String(line).trim();
|
|
59
|
+
const m = /(?:^|\s)(?:pr[uü]f\w*|check|revisa|nimm|nehme?|take|mach|nummer|nr\.?|number|n[uú]mero|option|die|den|the|el|la|#)\s*(?:die|den|das|nummer|nr\.?|number|the|el|la|n[uú]mero|#)?\s*#?(\d{1,2})\b/i.exec(t);
|
|
60
|
+
const n = m ? Number(m[1]) : null;
|
|
61
|
+
return n && n >= 1 && n <= size && !/@|\./.test(t) ? n : null; // an address or a version number is not a menu pick
|
|
62
|
+
}
|
|
63
|
+
export const HOWFIX = {
|
|
64
|
+
head: { de: "Zum offenen Schritt, ohne etwas zu ändern:", en: "About the open step, without changing anything:", es: "Sobre el paso abierto, sin cambiar nada:" },
|
|
65
|
+
tail: { de: "Ich mache nichts, bevor du es willst. Sag „fix“, dann zeige ich den Plan noch einmal und frage nach deinem Ja.", en: "I do nothing until you want it. Say “fix” and I show the plan again and ask for your yes.", es: "No hago nada hasta que lo quieras. Di «fix» y te muestro el plan otra vez y pido tu sí." },
|
|
66
|
+
};
|
package/src/me/pick.mjs
CHANGED
|
@@ -27,15 +27,67 @@ export function options({ servers = [], hints = [] }) {
|
|
|
27
27
|
if (!servers.length) for (const h of hints.slice(0, 6)) out.push({ kind: "hint", host: h });
|
|
28
28
|
return out;
|
|
29
29
|
}
|
|
30
|
+
// "der zweite", "the last one", "el tercero": the place in a list, said in words. Cardinals ("drei") only count in a short answer
|
|
31
|
+
// to "Nummer oder Name"; ordinals count anywhere in a line. Two different places in one line ("die drei, nein die zwei") are no answer.
|
|
32
|
+
const ORD = [[/^(erst(e|en|er|es)?|first|primer[oa]?|1st)$/, 1], [/^(zweit(e|en|er|es)?|second|segund[oa]|2nd)$/, 2], [/^(dritt(e|en|er|es)?|third|tercer[oa]?|3rd)$/, 3], [/^(viert(e|en|er|es)?|fourth|cuart[oa]|4th)$/, 4], [/^(f[uü]nft(e|en|er|es)?|fuenft(e|en|er|es)?|fifth|quint[oa]|5th)$/, 5]];
|
|
33
|
+
const LAST = /^(letzt(e|en|er|es)?|last|[uú]ltim[oa])$/;
|
|
34
|
+
const CARD = { eins: 1, one: 1, uno: 1, zwei: 2, two: 2, dos: 2, drei: 3, three: 3, tres: 3, vier: 4, four: 4, cuatro: 4, "fünf": 5, fuenf: 5, five: 5, cinco: 5 };
|
|
35
|
+
export function placeIn(text, size, { cardinals = false } = {}) {
|
|
36
|
+
const found = new Set();
|
|
37
|
+
for (const w of String(text).toLowerCase().split(/[^\p{L}\d]+/u).filter(Boolean)) {
|
|
38
|
+
const o = ORD.find(([re]) => re.test(w));
|
|
39
|
+
if (o) found.add(o[1]); else if (LAST.test(w)) found.add(size); else if (cardinals && CARD[w]) found.add(CARD[w]);
|
|
40
|
+
}
|
|
41
|
+
const [n] = found;
|
|
42
|
+
return found.size === 1 && n >= 1 && n <= size ? n : null;
|
|
43
|
+
}
|
|
44
|
+
const esc = (x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45
|
+
const hostPart = (t) => String(t || "").replace(/^[^@]*@/, "");
|
|
46
|
+
// a server named somewhere in the line: its alias or its address as a whole word ("nimm db", "203.0.113.12 halt")
|
|
47
|
+
const namedIn = (a0, opts) => {
|
|
48
|
+
const a = a0.replace(/[.,;:!?]+(?=\s|$)/g, " "); // "check db." - the full stop ends the sentence, it is not part of a host name
|
|
49
|
+
const hits = opts.filter((o) => o.kind === "server" || o.kind === "hint").filter((o) => [o.alias, hostPart(o.target), o.host].some((x) => x && x.length >= 2 && new RegExp(`(^|[^\\w.-])${esc(x.toLowerCase())}($|[^\\w.-])`).test(a)));
|
|
50
|
+
return hits.length === 1 ? hits[0] : null;
|
|
51
|
+
};
|
|
52
|
+
|
|
30
53
|
/** a number, "mac"/"hier", "alle", or part of a server's name or address. Several matches → { kind: "many", list }. */
|
|
31
54
|
export function choose(answer, opts) {
|
|
32
|
-
const a = String(answer || "").trim().toLowerCase();
|
|
55
|
+
const a = String(answer || "").trim().toLowerCase().replace(/[\s!?.,;:)]+$/, ""); // "3!!", "2." are a number
|
|
33
56
|
if (!a) return null;
|
|
34
57
|
if (/^\d{1,3}$/.test(a)) return opts[Number(a) - 1] || null;
|
|
35
58
|
if (/^(alle|alles|all|todos|todo|everything|jeden)$/.test(a)) return opts.find((o) => o.kind === "all") || null;
|
|
36
59
|
if (LOCAL_RE.test(a) || /^(dieser|diesen|este|this)\b/.test(a)) return opts[0];
|
|
37
60
|
const hits = opts.filter((o) => o.kind === "server" || o.kind === "hint").filter((o) => [o.alias, o.target, o.host].some((x) => x && x.toLowerCase().includes(a)));
|
|
38
|
-
return hits.length === 1 ? hits[0] :
|
|
61
|
+
if (hits.length) return hits.length === 1 ? hits[0] : { kind: "many", list: hits };
|
|
62
|
+
const named = namedIn(a, opts);
|
|
63
|
+
if (named) return named;
|
|
64
|
+
const n = placeIn(a, opts.length, { cardinals: a.split(/\s+/).length <= 4 });
|
|
65
|
+
return n ? opts[n - 1] : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// "mach mal bei dem zweiten", "nimm db", "the last one" after a list of machines: which one of the list? Only for a line that is about
|
|
69
|
+
// a machine (short, or with a machine word or a check verb), never for "der erste Schritt" or "the second finding".
|
|
70
|
+
const MACHINE = /server|servidor|rechner|maschine|machine|kiste|box|host|equipo|m[aá]quina|computer|ordenador|nimm|nehm|take|toma|coge|pr[uü]f|check|revisa|mira|schau|mach|haz|do\b/i;
|
|
71
|
+
const NOT_A_MACHINE = /schritt|step|paso|fund|befund|finding|hallazgo|punkt|point|punto|frage|question|pregunta|wort|word|palabra|befehl|command|comando|antwort|answer|respuesta|satz|sentence|frase|liste?\b|list\b/i;
|
|
72
|
+
const OTHER = /(^|[^\p{L}])(ander(e|en|er|es)?|other|otr[oa])(?![\p{L}])/u;
|
|
73
|
+
// A question about a machine ("welche ip hat db?", "cuál es, el segundo o el último") wants an answer, not a check.
|
|
74
|
+
const QUESTION = /(^|[^\p{L}])(cu[aá]l|welche\w*|which|qu[eé]|what|was|wo|where|d[oó]nde|warum|wieso|why|por qu[eé]|is there|gibt es|hay)(?![\p{L}])/u;
|
|
75
|
+
// a place counts with its article ("den zweiten", "the last one", "el otro"), so "revisa este equipo primero" (first = beforehand) does not
|
|
76
|
+
const ART = "(?:der|die|den|dem|das|the|el|la|lo|los|las)";
|
|
77
|
+
const PLACE_WORD = /^(erst|zweit|dritt|viert|f[uü]nft|fuenft|letzt)(e|en|er|es)?$|^(first|second|third|fourth|fifth|last)$|^(primer|segund|tercer|cuart|quint|[uú]ltim)[oa]?$/;
|
|
78
|
+
const withArticle = (l) => l.split(/[^\p{L}\d]+/u).filter(Boolean).some((w, i, ws) => PLACE_WORD.test(w) && i > 0 && new RegExp(`^${ART}$`).test(ws[i - 1]));
|
|
79
|
+
export function pickInLine(line, choices, current = null) {
|
|
80
|
+
const l = String(line || "").toLowerCase().trim(), words = l.split(/\s+/).length;
|
|
81
|
+
if (!choices || !choices.length || NOT_A_MACHINE.test(l) || QUESTION.test(l) || (l.includes("?") && words > 4)) return null;
|
|
82
|
+
if (!(words <= 6 || MACHINE.test(l))) return null;
|
|
83
|
+
// "und der andere": of two machines, the one that is not the current one
|
|
84
|
+
const servers = choices.filter((c) => c.kind === "server");
|
|
85
|
+
if (OTHER.test(l) && new RegExp(`${ART}\\s+(ander|other|otr)`).test(l) && current && servers.length === 2 && servers.some((c) => c.target === current)) return servers.find((c) => c.target !== current);
|
|
86
|
+
// a name counts with a check verb or alone ("nimm db", "check db", "db")
|
|
87
|
+
const named = namedIn(l, choices);
|
|
88
|
+
if (named && named.kind === "server" && (words <= 3 || /nimm|nehm|take|toma|coge|pr[uü]f|check|revisa|mira|schau|scan/.test(l))) return named;
|
|
89
|
+
const n = (words <= 2 || withArticle(l)) ? placeIn(l, choices.length) : null;
|
|
90
|
+
return n && choices[n - 1].kind !== "hint" ? choices[n - 1] : null;
|
|
39
91
|
}
|
|
40
92
|
|
|
41
93
|
const label = (M, o) => (o.kind === "local" ? M.pickLocal : o.kind === "all" ? M.pickAll(o.count) : o.kind === "server" ? M.pickServer(o) : M.pickHint(o.host));
|
package/src/session.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { activeBattery, checkById } from "./checks/battery.mjs";
|
|
|
14
14
|
import { demo } from "./checks/demo.mjs";
|
|
15
15
|
import { run, firstLine } from "./core/sh.mjs";
|
|
16
16
|
import { slots } from "./bib/router.mjs";
|
|
17
|
-
import { newConv, note, understood, fallback, FALLBACK, wantsRecap, recap, wantsCodeProof, CODE_PROOF } from "./me/conv.mjs";
|
|
17
|
+
import { newConv, note, understood, fallback, FALLBACK, wantsRecap, recap, wantsCodeProof, CODE_PROOF, wantsHowFix, menuPick, HOWFIX } from "./me/conv.mjs";
|
|
18
18
|
import { hearMain, hearConsent, hearSystem, hearMuster, hearWissen, hearDomain, langOf, SETUP_WORDS } from "./bib/ear.mjs";
|
|
19
19
|
import { cardsOf, findCard, answer as knowAnswer } from "./pack/know.mjs";
|
|
20
20
|
import { FAMILY_OF } from "./bib/system.mjs";
|
|
@@ -30,7 +30,7 @@ import * as schedule from "./me/schedule.mjs";
|
|
|
30
30
|
import * as deepSchedule from "./me/schedule-deep.mjs";
|
|
31
31
|
import { deep as deepRun } from "./me/deep.mjs";
|
|
32
32
|
import { holder } from "./me/lock.mjs";
|
|
33
|
-
import { wantsLocal, wantsServerPick, badTarget, options as pickOptions, pickFlow } from "./me/pick.mjs";
|
|
33
|
+
import { wantsLocal, wantsServerPick, badTarget, options as pickOptions, pickFlow, pickInLine } from "./me/pick.mjs";
|
|
34
34
|
import { pool } from "./me/pool.mjs";
|
|
35
35
|
import { wantsServerList, wantsHealth, wantsFind, wantsStatus, wantsMore, wantsServersCheck, serversFlow, findFlow } from "./me/servers.mjs";
|
|
36
36
|
import { wantsSysInfo, wantsGoingOn, wantsAllSystems } from "./me/sysq.mjs";
|
|
@@ -41,7 +41,7 @@ import { script as sysScript, nativeOutput as sysNative } from "./me/sysid.mjs";
|
|
|
41
41
|
import { report as sysReport } from "./me/sysreport.mjs";
|
|
42
42
|
import { LOCAL_RE } from "./me/pick.mjs";
|
|
43
43
|
// words that open a yes, and words that spoil one; words that ask for an explanation instead of answering
|
|
44
|
-
const LEADING_YES = /^\s*["'¡]*(?:ja+|jap|jo|jawohl|klar|gern(?:e)?|na klar|okay|ok|yes+|yep|yeah|yup|sure|please|of course|go ahead|go on|s[ií]+|sip|claro|vale|adelante|dale|por favor|bitte|mach(?: ruhig)?|los|schau|look)(?![\p{L}])/iu;
|
|
44
|
+
const LEADING_YES = /^\s*["'¡]*(?:ja+|j|jap|jo|jawohl|meinetwegen|von mir aus|na gut|klar|gern(?:e)?|na klar|okay|ok|yes+|yep|yeah|yup|sure|please|of course|go ahead|go on|s[ií]+|sip|claro|vale|adelante|dale|por favor|bitte|mach(?: ruhig)?|los|schau|look)(?![\p{L}])/iu;
|
|
45
45
|
const NEGATION = /(?<![\p{L}])(?:nein|nee|nicht|nie|no|nope|don'?t|do not|never|stop|halt|nunca|ni|aber nicht|lieber nicht)(?![\p{L}])/iu;
|
|
46
46
|
const LEADING_NO = /^\s*["'¡]*(?:nein|nee|ne|nö|nope|no|nicht|lieber nicht|nada|nunca)(?![\p{L}])/iu;
|
|
47
47
|
const ASKS_WHAT = /\?|(?<![\p{L}])(?:was (?:ist|heißt|bedeutet|macht)|erkl[aä]r\p{L}*|what(?:'s| is| does)|explain|qu[eé] (?:es|significa)|no s[eé] qu[eé]|ich wei(?:ß|ss) nicht was)(?![\p{L}])/iu;
|
|
@@ -210,6 +210,8 @@ export async function session() {
|
|
|
210
210
|
const a = await ask(check.id, t("run"), { plan: r.fix, rl });
|
|
211
211
|
if (a === "no") { say(" " + C("2", t("nothing"))); return; }
|
|
212
212
|
if (a === "always") { grantAutomode(check.id); }
|
|
213
|
+
// DANTE_FIX_DRY=1 (tests, the lab): the plan is shown and agreed, nothing runs
|
|
214
|
+
if (process.env.DANTE_FIX_DRY) { for (const s of r.fix.steps) say(" " + C("2", "(dry) " + tx(s.desc) + " $ " + s.script)); return; }
|
|
213
215
|
for (const s of r.fix.steps) {
|
|
214
216
|
const o = await waitFor(tx(s.desc), job({ kind: "exec", scripts: [s.script], timeout: 120000 }, target$()));
|
|
215
217
|
const res = o.reached ? o.outs[0] : { ok: false, out: "", err: o.line };
|
|
@@ -456,18 +458,25 @@ export async function session() {
|
|
|
456
458
|
if (wordsServer && u && (u.servers || []).length) return pickMove({ sys: mode });
|
|
457
459
|
return sysOne(null, "localhost", mode);
|
|
458
460
|
};
|
|
459
|
-
const
|
|
461
|
+
const serverChoices = () => (((readUser() || {}).servers) || []).map((s) => ({ kind: "server", alias: s.alias, target: s.target, key: s.key || null, os: s.os || "" }));
|
|
462
|
+
let lastMenu = null, turnNo = 0; // the numbered list that was last on screen, and when: "prüfe die 4" belongs to it
|
|
463
|
+
const applyPick = async (o, opts = {}) => {
|
|
460
464
|
const u = readUser();
|
|
461
|
-
const choices = pickOptions({ servers: (u && u.servers) || [], hints: knownHosts(homedir(), ((u && u.servers) || []).map((s) => s.alias)) });
|
|
462
|
-
const o = await pickFlow(io, choices);
|
|
463
|
-
if (!o) { say(" " + C("2", mt().pickNone)); return; }
|
|
464
465
|
if (o.kind === "all") return opts.sys ? sysAll((u && u.servers) || [], opts.sys) : fleetMove((u && u.servers) || []);
|
|
465
466
|
if (o.kind === "local") { target = null; configureTarget(null); say(" " + C("2", t("target") + "localhost")); return opts.sys ? sysOne(null, "localhost", opts.sys) : runChecks(false); }
|
|
466
467
|
if (o.key) key = o.key.replace(/^~(?=\/)/, homedir());
|
|
467
468
|
target = o.target; configureTarget({ host: target, key }); say(" " + C("2", t("target")) + targetLabel());
|
|
468
469
|
return opts.sys ? sysOne(target$(), targetLabel(), opts.sys) : runChecks(false);
|
|
469
470
|
};
|
|
470
|
-
|
|
471
|
+
const pickMove = async (opts = {}) => {
|
|
472
|
+
const u = readUser();
|
|
473
|
+
const choices = pickOptions({ servers: (u && u.servers) || [], hints: knownHosts(homedir(), ((u && u.servers) || []).map((s) => s.alias)) });
|
|
474
|
+
lastMenu = { choices, opts, turn: turnNo };
|
|
475
|
+
const o = await pickFlow(io, choices);
|
|
476
|
+
if (!o) { say(" " + C("2", mt().pickNone)); return; }
|
|
477
|
+
return applyPick(o, opts);
|
|
478
|
+
};
|
|
479
|
+
// What such a helper may order Dante to do on its own: only answers and looks. Anything that changes something still asks, as everywhere.
|
|
471
480
|
const viaZentrale = async (z, line) => {
|
|
472
481
|
const ok = new Set(["gruss", "hilfe", "smalltalk", "karte", "frage_dante", "frage_luecke", "frage_urteil", "frage_gehackt", "pruef"]);
|
|
473
482
|
if (z.ear === "muster") { await musterMove(z.intent); return true; }
|
|
@@ -532,7 +541,7 @@ export async function session() {
|
|
|
532
541
|
|
|
533
542
|
// The call home of the test builds (src/lab is not in the published package: then this stays null and changes nothing).
|
|
534
543
|
let turn = null;
|
|
535
|
-
const closeTurn = () => { if (turn) { note(conv, turn.line, turn.intent, turn.route); if (turn.route !== "unclear") understood(conv); if (lab) lab.log(turn); } turn = null; };
|
|
544
|
+
const closeTurn = () => { if (turn) { note(conv, turn.line, turn.intent, turn.route); if (turn.route !== "unclear") understood(conv); if (lab) lab.log({ ...turn, ctx: { menuAge: lastMenu ? turnNo - lastMenu.turn : null, open: last.filter(({ r }) => r.verdict !== SAFE).length, topic: lastTopic, target: !!target, asked: conv.asked } }); } turn = null; };
|
|
536
545
|
let pending = null; // the guess Dante asked back about; a plain "ja" takes it
|
|
537
546
|
const conv = newConv(); // what this conversation remembers (memory only, never written)
|
|
538
547
|
for (;;) {
|
|
@@ -560,7 +569,14 @@ export async function session() {
|
|
|
560
569
|
// (user.md, then known_hosts) instead of asking blind.
|
|
561
570
|
let forceLocal = false;
|
|
562
571
|
const prevTopic = lastTopic; lastTopic = null; // what the last answer was about, for "welche noch?"
|
|
563
|
-
|
|
572
|
+
turnNo++;
|
|
573
|
+
let menuN = null, linePick = null;
|
|
574
|
+
const menuNow = lastMenu && turnNo - lastMenu.turn <= 8 ? lastMenu : null;
|
|
575
|
+
if (!exact && menuNow && (menuN = menuPick(line, menuNow.choices.length)) && menuNow.choices[menuN - 1].kind !== "hint") heard.intent = "menupick";
|
|
576
|
+
// "mach mal bei dem zweiten", "nimm db", "und der andere?": a machine from the list on screen, or from the known servers in the order he shows them
|
|
577
|
+
else if (!exact && !ziel && (linePick = pickInLine(line, menuNow ? menuNow.choices : serverChoices(), target))) heard.intent = "linepick";
|
|
578
|
+
else if (!exact && last.some(({ r }) => r.verdict !== SAFE && r.fix) && wantsHowFix(line)) heard.intent = "howfix";
|
|
579
|
+
else if (!exact && wantsRecap(line)) heard.intent = "recap";
|
|
564
580
|
else if (!exact && wantsCodeProof(line)) heard.intent = "codebeweis";
|
|
565
581
|
else if (!exact && prevTopic === "servers" && wantsMore(line)) heard.intent = "servermore";
|
|
566
582
|
else if (!exact && wantsServersCheck(line, !!ziel)) heard.intent = "serverscheck";
|
|
@@ -583,6 +599,16 @@ export async function session() {
|
|
|
583
599
|
turn = { line, lang: lang(), route: exact ? "cmd" : heard.intent.startsWith("server") || /^(sysinfo|waslos|systemealle|mitm|mitmauto|keyfind)$/.test(heard.intent) ? "rule" : "main", intent: heard.intent, p: heard.ranked && heard.ranked[0] ? Number(heard.ranked[0].p.toFixed(2)) : 1 };
|
|
584
600
|
switch (heard.intent) {
|
|
585
601
|
case "raus": say(" " + C("2", t("bye"))); footerStop(); rl.close(); return;
|
|
602
|
+
case "menupick": await applyPick(lastMenu.choices[menuN - 1], lastMenu.opts); break;
|
|
603
|
+
case "linepick": await applyPick(linePick, menuNow ? menuNow.opts : {}); break;
|
|
604
|
+
case "howfix": {
|
|
605
|
+
say(" " + C("2", HOWFIX.head[lang()]));
|
|
606
|
+
for (const { r } of last.filter(({ r }) => r.verdict !== SAFE && r.fix).slice(0, 3)) {
|
|
607
|
+
say(" " + C("1", tx(r.title)) + " - " + tx(r.fix.why || ""));
|
|
608
|
+
r.fix.steps.forEach((st, i) => say(" " + (i + 1) + ") " + tx(st.desc) + " " + C("2", "$ " + st.script)));
|
|
609
|
+
}
|
|
610
|
+
say(" " + C("33", HOWFIX.tail[lang()])); break;
|
|
611
|
+
}
|
|
586
612
|
case "recap": say(recap(conv, lang()).map((l) => " " + l).join("\n")); break;
|
|
587
613
|
case "codebeweis": say(CODE_PROOF[lang()].map((l) => " " + l).join("\n")); break;
|
|
588
614
|
case "gruss": say(" " + pick(t("hello"))); break;
|
|
@@ -655,7 +681,7 @@ export async function session() {
|
|
|
655
681
|
warmDomain();
|
|
656
682
|
const dom = hearDomain(line);
|
|
657
683
|
if (dom) { turn = { ...turn, route: "domain:" + dom.ear, intent: dom.intent }; }
|
|
658
|
-
//
|
|
684
|
+
// Optional hook for test builds (absent in the published package): a line nobody understood can be sorted into a template by a helper service.
|
|
659
685
|
const zen = !dom && lab ? await waitFor("…", lab.ask({ line, lang: lang(), heard: { intent: heard.intent, ranked: (heard.ranked || []).slice(0, 3) }, version: VERSION })) : null;
|
|
660
686
|
if (zen && (await viaZentrale(zen, line))) { turn = { ...turn, route: "zentrale", intent: zen.intent, ear: zen.ear }; break; }
|
|
661
687
|
if (dom) { if (dom.ear === "muster") await musterMove(dom.intent); else if (dom.ear === "system") await systemMove(dom.intent); else knowMove(dom.intent, line); break; }
|
package/src/ticker.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export function age(now = Date.now()) {
|
|
|
49
49
|
return Math.max(0, Math.floor((now - Date.parse(knowledge + "T00:00:00Z")) / 864e5));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// "bib 25.10. · 0d" - the date of the knowledge and how old it is; "bib 20d · !" once it is stale.
|
|
52
|
+
// "bib 25.10. · 0d" (example) - the date of the knowledge and how old it is; "bib 20d · !" once it is stale.
|
|
53
53
|
export function clockText() {
|
|
54
54
|
const days = age(), v = VERSION ? ` · v${VERSION}` : "";
|
|
55
55
|
return (days > STALE ? `bib ${days}d · !` : `bib ${knowledge.slice(8, 10)}.${knowledge.slice(5, 7)}. · ${days}d`) + v;
|