kimal3 0.2.2 → 0.2.3
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 +134 -102
- package/dist/anmeldung.js +1 -1
- package/dist/befehle/skill-install.js +50 -0
- package/dist/befehle/wache.js +185 -0
- package/dist/befehle-katalog.js +20 -0
- package/dist/einstellungen-mischen.js +124 -0
- package/dist/einstellungen-schreiben.js +88 -0
- package/dist/kimal3.js +35 -1
- package/dist/wache-rechnung.js +139 -0
- package/dist/wache-schalter.js +149 -0
- package/package.json +27 -27
- package/skill/SKILL.md +381 -347
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { WACHE_BEFEHL, mischeEinstellungen } from "./einstellungen-mischen.js";
|
|
5
|
+
export function einstellungenDatei() {
|
|
6
|
+
return join(homedir(), ".claude", "settings.json");
|
|
7
|
+
}
|
|
8
|
+
export function sicherungsDatei() {
|
|
9
|
+
return einstellungenDatei() + ".kimal3-sicherung";
|
|
10
|
+
}
|
|
11
|
+
export function trageWacheEin() {
|
|
12
|
+
const datei = einstellungenDatei();
|
|
13
|
+
const gabEsSchon = existsSync(datei);
|
|
14
|
+
// --- Schritt 1: lesen ---
|
|
15
|
+
let alt = null;
|
|
16
|
+
if (gabEsSchon) {
|
|
17
|
+
try {
|
|
18
|
+
alt = readFileSync(datei, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
/*
|
|
22
|
+
🔴 NICHT als "keine Datei" behandeln. Eine Datei, die es gibt und die
|
|
23
|
+
wir nicht lesen koennen, ist etwas anderes als eine, die es nicht gibt
|
|
24
|
+
-- wer beide gleich behandelt, legt eine neue an und der Mensch
|
|
25
|
+
verliert alles, was drinstand.
|
|
26
|
+
*/
|
|
27
|
+
return { art: "gescheitert", grund: `nicht lesbar (${e.message})` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const ergebnis = mischeEinstellungen(alt, WACHE_BEFEHL);
|
|
31
|
+
if (ergebnis.art === "abgelehnt")
|
|
32
|
+
return { art: "abgelehnt", grund: ergebnis.grund };
|
|
33
|
+
if (ergebnis.art === "unveraendert")
|
|
34
|
+
return { art: "stand-schon" };
|
|
35
|
+
// --- Schritt 2: Sicherungskopie ---
|
|
36
|
+
if (gabEsSchon) {
|
|
37
|
+
try {
|
|
38
|
+
copyFileSync(datei, sicherungsDatei());
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
/*
|
|
42
|
+
🔴 OHNE SICHERUNG WIRD NICHT GESCHRIEBEN. Der ganze Sinn von Schritt 4
|
|
43
|
+
ist, im Fehlerfall zurueckzukoennen -- ohne Kopie gibt es kein Zurueck,
|
|
44
|
+
und dann ist Schreiben die schlechtere Wahl als Nichtstun.
|
|
45
|
+
*/
|
|
46
|
+
return { art: "gescheitert", grund: `keine Sicherungskopie moeglich (${e.message})` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// --- Schritt 3: schreiben ---
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(join(homedir(), ".claude"), { recursive: true });
|
|
52
|
+
writeFileSync(datei, ergebnis.inhalt, "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
zurueck(gabEsSchon);
|
|
56
|
+
return { art: "gescheitert", grund: `nicht schreibbar (${e.message})` };
|
|
57
|
+
}
|
|
58
|
+
// --- Schritt 4: zuruecklesen und erneut pruefen ---
|
|
59
|
+
try {
|
|
60
|
+
const zurueckgelesen = readFileSync(datei, "utf8");
|
|
61
|
+
const geparst = JSON.parse(zurueckgelesen);
|
|
62
|
+
if (!zurueckgelesen.includes(WACHE_BEFEHL) || geparst === null) {
|
|
63
|
+
throw new Error("unser Eintrag steht nicht in der zurueckgelesenen Datei");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
zurueck(gabEsSchon);
|
|
68
|
+
return { art: "gescheitert", grund: `Kontrolle nach dem Schreiben fehlgeschlagen (${e.message})` };
|
|
69
|
+
}
|
|
70
|
+
return { art: "eingetragen" };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Die Sicherung zurueckspielen.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ Gab es vorher KEINE Datei, gibt es auch nichts zurueckzuspielen -- dann
|
|
76
|
+
* bleibt die halb geschriebene stehen. Das ist die harmlose Seite: Sie
|
|
77
|
+
* enthielt vorher nichts, was jemandem gehoerte.
|
|
78
|
+
*/
|
|
79
|
+
function zurueck(gabEsSchon) {
|
|
80
|
+
if (!gabEsSchon)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
copyFileSync(sicherungsDatei(), einstellungenDatei());
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* Mehr koennen wir nicht tun -- der Grund steht in der Meldung oben. */
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/kimal3.js
CHANGED
|
@@ -15,6 +15,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
15
15
|
import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
|
|
16
16
|
import { gemerkterHinweis, lokalerKontextStandJetzt, NichtAngemeldet } from "./api.js";
|
|
17
17
|
import { leseGesehen, merkeGesehen } from "./gesehen.js";
|
|
18
|
+
import { ausfallMeldung, hinweisAnhaengen } from "./wache-rechnung.js";
|
|
19
|
+
import { leseWacheDatei, sitzungJetzt, wacheAusgefallen } from "./wache-schalter.js";
|
|
18
20
|
import { formatiere, heuteAus, skillMeldung, skillZustand, sollZeigen, } from "./nachricht.js";
|
|
19
21
|
import { eigeneVersion } from "./version.js";
|
|
20
22
|
import { hilfeText } from "./befehle-katalog.js";
|
|
@@ -29,6 +31,7 @@ import { logout } from "./befehle/logout.js";
|
|
|
29
31
|
import { projektListe } from "./befehle/projekt-liste.js";
|
|
30
32
|
import { projektNeu } from "./befehle/projekt-neu.js";
|
|
31
33
|
import { skillDatei, skillInstall } from "./befehle/skill-install.js";
|
|
34
|
+
import { wache, wacheSchalten } from "./befehle/wache.js";
|
|
32
35
|
// 📌 Kein handgeschriebener Block mehr (2026-08-18): Die Liste wohnt in
|
|
33
36
|
// `befehle-katalog.ts` und wird von dort AUCH im Pro-Bereich der App angezeigt.
|
|
34
37
|
// Zwei Listen ueber dieselben Befehle driften — die eine bekaeme den naechsten
|
|
@@ -146,6 +149,28 @@ function hinweisText() {
|
|
|
146
149
|
return { text: skillMeldung(), kennung: "skill-veraltet", tag: heute };
|
|
147
150
|
}
|
|
148
151
|
}
|
|
152
|
+
/*
|
|
153
|
+
Weg 3: hat die Wache fuer diese Claude-Sitzung ueberhaupt nachgesehen?
|
|
154
|
+
|
|
155
|
+
🔴 WARUM DAS DER LETZTE WEG IST, nicht der erste: Die beiden
|
|
156
|
+
oberen nennen etwas, das JETZT zu tun ist -- eine Nachricht von uns, ein
|
|
157
|
+
veralteter Befehlssatz. Dies hier ist eine Aussage ueber das Werkzeug
|
|
158
|
+
selbst. Es steht deshalb hinten und erscheint an den meisten Tagen als
|
|
159
|
+
Einziges, weil sonst nichts anliegt.
|
|
160
|
+
|
|
161
|
+
WARUM ES DAS UEBERHAUPT GIBT: Ein Startbefehl, dessen Programm fehlt
|
|
162
|
+
oder abstuerzt, laermt nicht -- gemessen mit Gegenprobe (M-0). Ohne
|
|
163
|
+
diese Zeilen sieht ein Tag mit toter Wache genauso aus wie einer, an dem
|
|
164
|
+
alles in Ordnung ist. *Eine Wache, die still stirbt, kauft Vertrauen und
|
|
165
|
+
deckt nichts mehr.*
|
|
166
|
+
*/
|
|
167
|
+
if (wacheAusgefallen(leseWacheDatei(), sitzungJetzt())) {
|
|
168
|
+
// Auch dieser Hinweis hoechstens einmal am Tag, unter fester Kennung.
|
|
169
|
+
const gesehen = leseGesehen();
|
|
170
|
+
if (gesehen["wache-ausgefallen"] !== heute) {
|
|
171
|
+
return { text: ausfallMeldung(), kennung: "wache-ausgefallen", tag: heute };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
149
174
|
}
|
|
150
175
|
catch {
|
|
151
176
|
/* Ein Nebenschauplatz darf den Befehl nicht umwerfen. */
|
|
@@ -182,7 +207,10 @@ function endeMitHinweis(code) {
|
|
|
182
207
|
verbrauchte den EINEN Platz des Tages. Wer sich vertippt und danach
|
|
183
208
|
richtig tippt, saehe die Nachricht dann heute gar nicht mehr.
|
|
184
209
|
*/
|
|
185
|
-
|
|
210
|
+
// 🔴 `hinweisAnhaengen` entscheidet, ob dieser Befehl den allgemeinen
|
|
211
|
+
// Kanal ueberhaupt bekommt -- `kimal3 wache` bekommt ihn nicht, weil
|
|
212
|
+
// sie ihre Zeilen selbst und in der richtigen Schreibweise sagt.
|
|
213
|
+
const raus = code === 0 && hinweisAnhaengen(process.argv.slice(2)) ? hinweisText() : null;
|
|
186
214
|
/*
|
|
187
215
|
🔴 BEIDE KANAELE LEEREN, NICHT NUR STDERR. Die erste Fassung dieser
|
|
188
216
|
Funktion sicherte stderr -- und liess den haeufigeren Weg (kein Hinweis)
|
|
@@ -306,6 +334,12 @@ async function haupt() {
|
|
|
306
334
|
return kontextHoch(argv.slice(2));
|
|
307
335
|
if (argv[0] === "kontext" && argv[1] === "fragen")
|
|
308
336
|
return kontextFragen(argv.slice(2));
|
|
337
|
+
if (wort === "wache")
|
|
338
|
+
return wache();
|
|
339
|
+
if (wort === "wache aus")
|
|
340
|
+
return wacheSchalten(false);
|
|
341
|
+
if (wort === "wache an")
|
|
342
|
+
return wacheSchalten(true);
|
|
309
343
|
console.error(`Den Befehl „${wort}" kenne ich nicht.\n${HILFE}`);
|
|
310
344
|
return 1;
|
|
311
345
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { istAelter, istVersionsstand } from "./nachricht.js";
|
|
2
|
+
import { BEFEHLE } from "./befehle-katalog.js";
|
|
3
|
+
/**
|
|
4
|
+
* Den Befehl in der Schreibweise holen, in der man ihn dort eintippt, wo man
|
|
5
|
+
* ihn liest -- die Wache spricht immer durch Claude Code, also mit `!`.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 KEIN ERSATZWERT AUS DER LUFT. Steht der Befehl nicht im Katalog, ist das
|
|
8
|
+
* ein Fehler beim Bauen und keiner, den der Kunde ausbaden soll; check:wache
|
|
9
|
+
* faengt ihn, weil die Zeile dann den erwarteten Text nicht enthaelt.
|
|
10
|
+
*/
|
|
11
|
+
function tippen(befehl) {
|
|
12
|
+
const eintrag = BEFEHLE.find((b) => b.befehl === befehl);
|
|
13
|
+
return eintrag ? eintrag.tippen : befehl;
|
|
14
|
+
}
|
|
15
|
+
export function wacheZeilen(lage) {
|
|
16
|
+
// 🔴 Ausgeloggt: NUR diese eine Zeile. Die anderen beiden waeren durchaus
|
|
17
|
+
// beantwortbar (npm fragt ohne Anmeldung, der Skill-Stempel liegt auf der
|
|
18
|
+
// Platte) -- sie werden trotzdem unterdrueckt. Wer ausgeloggt ist, hat genau
|
|
19
|
+
// eine Aufgabe; zwei weitere Ratschlaege daneben begraben sie.
|
|
20
|
+
if (!lage.zugangGilt) {
|
|
21
|
+
return [
|
|
22
|
+
"Dein Zugang ist abgelaufen — das passiert nach längerer Pause. " +
|
|
23
|
+
`Neu anmelden: ${tippen("kimal3 login")}`,
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
const zeilen = [];
|
|
27
|
+
if (lage.nachricht !== null) {
|
|
28
|
+
zeilen.push(`Nachricht von kimal3: „${lage.nachricht}“`);
|
|
29
|
+
}
|
|
30
|
+
// Beide Nummern muessen da UND gueltig sein. Fehlt eine, wird geschwiegen --
|
|
31
|
+
// nicht geraten.
|
|
32
|
+
if (lage.eigeneVersion !== null && lage.neuesteVersion !== null &&
|
|
33
|
+
istVersionsstand(lage.eigeneVersion) && istVersionsstand(lage.neuesteVersion) &&
|
|
34
|
+
istAelter(lage.eigeneVersion, lage.neuesteVersion)) {
|
|
35
|
+
zeilen.push(`Es gibt eine neuere Version: ${lage.neuesteVersion} (du hast ${lage.eigeneVersion}). ` +
|
|
36
|
+
`Holen mit: ! npm install -g kimal3@${lage.neuesteVersion}`);
|
|
37
|
+
}
|
|
38
|
+
if (lage.skillVeraltet) {
|
|
39
|
+
zeilen.push("Deine /k3-Befehle in Claude sind aus einer älteren Version. " +
|
|
40
|
+
`Auffrischen: ${tippen("kimal3 skill install")}`);
|
|
41
|
+
}
|
|
42
|
+
return zeilen;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Zieht den Hinweis aus der Antwort von `/api/cli/wache` -- unter dem Feld
|
|
46
|
+
* `nachricht`, nicht `hinweis` wie beim allgemeinen Update-Kanal in `api.ts`.
|
|
47
|
+
*
|
|
48
|
+
* 🔑 WARUM EIN EIGENES FELD UND KEINE ZWEITVERWENDUNG VON `hinweisAusAntwort`:
|
|
49
|
+
* Die Wache ist ein EIGENER, zweiter Kanal (US-1 der Wache-Spec) -- ihre
|
|
50
|
+
* Antwort traegt die Nachricht deshalb absichtlich unter einem anderen
|
|
51
|
+
* Namen als der Kanal, den `ruf()` bei jedem anderen Befehl mitliest. Zwei
|
|
52
|
+
* Kanaele, die zufaellig denselben Feldnamen teilten, koennten sich sonst
|
|
53
|
+
* gegenseitig fuellen.
|
|
54
|
+
*/
|
|
55
|
+
export function nachrichtAusAntwort(koerper) {
|
|
56
|
+
if (!koerper || typeof koerper !== "object")
|
|
57
|
+
return null;
|
|
58
|
+
const roh = koerper.nachricht;
|
|
59
|
+
if (!roh || typeof roh !== "object")
|
|
60
|
+
return null;
|
|
61
|
+
const h = roh;
|
|
62
|
+
if (typeof h.id !== "string" || typeof h.text !== "string")
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
id: h.id,
|
|
66
|
+
text: h.text,
|
|
67
|
+
gilt_unter_version: typeof h.gilt_unter_version === "string" ? h.gilt_unter_version : null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Was der HTTP-Status von `/api/cli/wache` bedeutet -- getrennt von allem,
|
|
72
|
+
* was Netz oder Zeitgrenze braucht, damit sich die drei Faelle ohne Warten
|
|
73
|
+
* oder Verbiegen pruefen lassen: gilt, widerrufen, Serverfehler.
|
|
74
|
+
*
|
|
75
|
+
* 🔴 NUR `401` HEISST "WIDERRUFEN". Jeder andere Nicht-Erfolg (`500`, ein
|
|
76
|
+
* verstuemmeltes JSON, ein Feld, das fehlt) heisst "wir wissen es nicht" und
|
|
77
|
+
* bekommt deshalb `zugangGilt: true` -- dieselbe Regel wie bei `istAelter`:
|
|
78
|
+
* Im Zweifel schweigen ist bei einem Nebenschauplatz die richtige Vorgabe.
|
|
79
|
+
* Wuerde stattdessen `false` zurueckkommen, zeigte ein simpler Aussetzer des
|
|
80
|
+
* EIGENEN Servers dieselbe Zeile wie ein wirklich widerrufener Ausweis --
|
|
81
|
+
* und `wacheZeilen` wuerde dabei zusaetzlich die Fassungs- und Skill-Zeile
|
|
82
|
+
* mit unterdruecken, die von diesem Ausfall gar nicht betroffen sind.
|
|
83
|
+
*/
|
|
84
|
+
export function wacheAntwortAusHttp(status, koerper) {
|
|
85
|
+
if (status === 401)
|
|
86
|
+
return { zugangGilt: false, hinweis: null };
|
|
87
|
+
if (status !== 200)
|
|
88
|
+
return { zugangGilt: true, hinweis: null };
|
|
89
|
+
return { zugangGilt: true, hinweis: nachrichtAusAntwort(koerper) };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Die Zeile, die erscheint, wenn die Wache fuer diese Claude-Sitzung nicht
|
|
93
|
+
* nachgesehen hat.
|
|
94
|
+
*
|
|
95
|
+
* 🔴 SIE NENNT BEIDE WEGE -- den zurueck UND den hinaus. Eine Meldung, die
|
|
96
|
+
* man nicht abstellen kann, wird nach der dritten Wiederholung zur Wand, und
|
|
97
|
+
* dann liest niemand mehr die, auf die es ankommt.
|
|
98
|
+
*
|
|
99
|
+
* 🔑 MIT AUSRUFEZEICHEN: Diese Zeile erscheint ausschliesslich in Claude --
|
|
100
|
+
* sie wird ja nur gezeigt, wenn eine Sitzungsnummer da ist. Wer sie liest,
|
|
101
|
+
* sitzt in dem Fenster, in dem `!` der richtige Anfang ist.
|
|
102
|
+
*/
|
|
103
|
+
export function ausfallMeldung() {
|
|
104
|
+
return ("Die tägliche kimal3-Prüfung hat für diese Claude-Sitzung nicht nachgesehen. " +
|
|
105
|
+
`Neu eintragen: ${tippen("kimal3 skill install")}` +
|
|
106
|
+
/*
|
|
107
|
+
🔴 `kimal3 wache aus` steht NICHT im Katalog -- dort steht nur
|
|
108
|
+
`kimal3 wache`. `tippen()` gibt bei einem unbekannten Befehl den Text
|
|
109
|
+
unveraendert zurueck, also OHNE das Ausrufezeichen: Der Mensch tippt in
|
|
110
|
+
Claude etwas ab, das dort nicht laeuft. Gefunden am 2026-09-08 in der
|
|
111
|
+
Live-Messung, NICHT von der Pruefung -- die fragte nur, ob irgendwo ein
|
|
112
|
+
`! kimal3` vorkommt, und das stimmte wegen der Zeile darueber.
|
|
113
|
+
Deshalb: die Schreibweise vom Katalog holen, das Wort `aus` anhaengen.
|
|
114
|
+
*/
|
|
115
|
+
` · Dauerhaft abstellen: ${tippen("kimal3 wache")} aus`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Soll der allgemeine Update-Hinweis an diesen Befehl angehaengt werden?
|
|
119
|
+
*
|
|
120
|
+
* 🔴 BEI `kimal3 wache` NICHT -- gefunden in der Live-Messung am 2026-09-08,
|
|
121
|
+
* nachdem die Route live ging. Die Wache sagt bei abgelaufenem Zugang
|
|
122
|
+
* absichtlich GENAU EINE Zeile ("wer ausgeloggt ist, hat genau eine Aufgabe;
|
|
123
|
+
* zwei weitere Ratschlaege daneben begraben sie"). Der allgemeine Kanal haengt
|
|
124
|
+
* trotzdem seine eigene Zeile darunter -- und macht damit genau das kaputt,
|
|
125
|
+
* wofuer die Beschraenkung gebaut wurde.
|
|
126
|
+
*
|
|
127
|
+
* ⚠️ UND ER SCHREIBT SIE IN DER FALSCHEN FORM. Der allgemeine Hinweis ist fuer
|
|
128
|
+
* ein Terminalfenster gedacht und nennt `kimal3 skill install` ohne
|
|
129
|
+
* Ausrufezeichen. Die Wache laeuft aber ausschliesslich in Claude, wo dieselbe
|
|
130
|
+
* Zeile `! kimal3 skill install` heissen muss. *Der Mensch haette etwas
|
|
131
|
+
* abgetippt, das dort nicht laeuft.*
|
|
132
|
+
*
|
|
133
|
+
* 🔑 Und es geht nichts verloren: Die beiden Faelle des allgemeinen Kanals --
|
|
134
|
+
* neue Fassung, veralteter Skill -- sagt die Wache ohnehin selbst, in der
|
|
135
|
+
* richtigen Schreibweise und mit besserem Wortlaut.
|
|
136
|
+
*/
|
|
137
|
+
export function hinweisAnhaengen(argv) {
|
|
138
|
+
return argv[0] !== "wache";
|
|
139
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Der Ausschalter und der Tages-Merkzettel der Wache: "aus" (fuer immer
|
|
6
|
+
* schweigen) und "zuletzt" (heute schon gelaufen) wohnen in DERSELBEN Datei.
|
|
7
|
+
*
|
|
8
|
+
* 🔑 WARUM BEIDES ZUSAMMEN: Beides sind reine Zustandsfragen ueber denselben
|
|
9
|
+
* Merkzettel -- eine zweite Datei wuerde nur eine zweite Stelle schaffen, an
|
|
10
|
+
* der beide synchron bleiben muessten.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 ALLE VIER FUNKTIONEN MIT DATEIZUGRIFF WERFEN NIE (Vorbild: gesehen.ts).
|
|
13
|
+
* Ein kaputter, fehlender oder schreibgeschuetzter Merkzettel darf keinen
|
|
14
|
+
* Claude-Start umwerfen.
|
|
15
|
+
*/
|
|
16
|
+
const ORDNER = join(homedir(), ".kimal3");
|
|
17
|
+
const DATEI = join(ORDNER, "wache.json");
|
|
18
|
+
/** Der rohe Inhalt des Merkzettels, oder null. Wirft nie. */
|
|
19
|
+
export function leseWacheDatei() {
|
|
20
|
+
try {
|
|
21
|
+
const roh = JSON.parse(readFileSync(DATEI, "utf8"));
|
|
22
|
+
if (!roh || typeof roh !== "object" || Array.isArray(roh))
|
|
23
|
+
return null;
|
|
24
|
+
const inhalt = roh;
|
|
25
|
+
const raus = {};
|
|
26
|
+
if (typeof inhalt.aus === "boolean")
|
|
27
|
+
raus.aus = inhalt.aus;
|
|
28
|
+
if (typeof inhalt.zuletzt === "string")
|
|
29
|
+
raus.zuletzt = inhalt.zuletzt;
|
|
30
|
+
if (typeof inhalt.sitzung === "string")
|
|
31
|
+
raus.sitzung = inhalt.sitzung;
|
|
32
|
+
return raus;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function schreibeWacheDatei(inhalt) {
|
|
39
|
+
try {
|
|
40
|
+
// ⚠️ `mode: 0o700` wie in gesehen.ts -- `mkdirSync` mit `recursive: true`
|
|
41
|
+
// aendert die Rechte eines schon bestehenden Ordners NICHT. Wer diesen
|
|
42
|
+
// Ordner zuerst anlegt, legt seine Rechte fuer immer fest.
|
|
43
|
+
mkdirSync(ORDNER, { recursive: true, mode: 0o700 });
|
|
44
|
+
writeFileSync(DATEI, JSON.stringify(inhalt, null, 2), "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* Merkzettel nicht schreibbar -- der Befehl laeuft trotzdem durch. */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Ist die Wache dauerhaft abgeschaltet? Wirft nie. */
|
|
51
|
+
export function wacheIstAus() {
|
|
52
|
+
return leseWacheDatei()?.aus === true;
|
|
53
|
+
}
|
|
54
|
+
/** Wache an- oder abschalten. Der Tages-Merkzettel ("zuletzt") bleibt erhalten. */
|
|
55
|
+
export function setzeWache(an) {
|
|
56
|
+
const bisher = leseWacheDatei() ?? {};
|
|
57
|
+
schreibeWacheDatei({ ...bisher, aus: !an });
|
|
58
|
+
}
|
|
59
|
+
/** Ist die Wache heute schon gelaufen? Wirft nie. */
|
|
60
|
+
export function schonGelaufenAm(heute) {
|
|
61
|
+
return leseWacheDatei()?.zuletzt === heute;
|
|
62
|
+
}
|
|
63
|
+
/** Merken, dass die Wache heute gelaufen ist. Der Ausschalter bleibt erhalten. */
|
|
64
|
+
export function merkeLauf(heute) {
|
|
65
|
+
const bisher = leseWacheDatei() ?? {};
|
|
66
|
+
schreibeWacheDatei({ ...bisher, zuletzt: heute });
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Laeuft die Wache heute?
|
|
70
|
+
*
|
|
71
|
+
* 🔴 REIN und ohne Dateisystem, damit sie geprueft werden kann -- "heute schon
|
|
72
|
+
* gelaufen" und "abgeschaltet" gibt es in der echten Welt nur durch Warten
|
|
73
|
+
* oder Verbiegen.
|
|
74
|
+
*
|
|
75
|
+
* 🔴 Bei Muell im Merkzettel lautet die Antwort JA. Der umgekehrte Fehler
|
|
76
|
+
* waere teurer: Eine Wache, die wegen einer kaputten Datei fuer immer
|
|
77
|
+
* schweigt, sieht genauso aus wie eine, bei der alles in Ordnung ist.
|
|
78
|
+
*/
|
|
79
|
+
export function sollHeuteLaufen(inhalt, heute) {
|
|
80
|
+
if (inhalt !== null && inhalt.aus === true)
|
|
81
|
+
return false;
|
|
82
|
+
if (inhalt !== null && typeof inhalt.zuletzt === "string" && inhalt.zuletzt === heute) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Die Nummer der Claude-Sitzung, in der dieser Aufruf gerade laeuft -- oder
|
|
89
|
+
* `null`, wenn kein Claude im Spiel ist.
|
|
90
|
+
*
|
|
91
|
+
* 🔑 WOFUER: Claude gibt jeder Sitzung eine eigene Nummer und reicht sie an
|
|
92
|
+
* jedes Programm weiter, das es startet. Damit laesst sich exakt beantworten,
|
|
93
|
+
* ob die Wache FUER DIESE SITZUNG nachgesehen hat. Ohne sie muesste man den
|
|
94
|
+
* TAG vergleichen -- und "zuletzt vor drei Tagen" hat zwei Ursachen: Die
|
|
95
|
+
* Wache ist kaputt, oder der Mensch hat einfach nichts gemacht. *Ein Wert,
|
|
96
|
+
* der zwei Wirklichkeiten traegt, ist genau die Krankheit, gegen die die
|
|
97
|
+
* Wache gebaut ist.*
|
|
98
|
+
*
|
|
99
|
+
* 📌 Gemessen am 2026-09-08 (docs/qa/2026-09-08-wache-messungen.md, M-0b):
|
|
100
|
+
* Der Startbefehl sieht dieselbe Nummer wie die laufende Sitzung, eine zweite
|
|
101
|
+
* Sitzung bekommt eine andere, und der getippte Weg mit `!` sieht sie auch.
|
|
102
|
+
* ⚠️ Gemessen auf einem Rechner, in einer Claude-Fassung. Fehlt sie beim
|
|
103
|
+
* Kunden, ist die Antwort `null` -- und dann schweigt die Erkennung, statt zu
|
|
104
|
+
* raten. Die harmlose Seite.
|
|
105
|
+
*/
|
|
106
|
+
export function sitzungJetzt() {
|
|
107
|
+
const roh = process.env.CLAUDE_CODE_SESSION_ID;
|
|
108
|
+
return typeof roh === "string" && roh.trim() !== "" ? roh : null;
|
|
109
|
+
}
|
|
110
|
+
/** Merken, fuer welche Claude-Sitzung zuletzt nachgesehen wurde. Wirft nie. */
|
|
111
|
+
export function merkeSitzung(sitzung) {
|
|
112
|
+
if (sitzung === null)
|
|
113
|
+
return;
|
|
114
|
+
const bisher = leseWacheDatei() ?? {};
|
|
115
|
+
schreibeWacheDatei({ ...bisher, sitzung });
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Ist die Wache fuer diese Claude-Sitzung ausgefallen?
|
|
119
|
+
*
|
|
120
|
+
* 🔴 REIN und ohne Umgebung, damit sie geprueft werden kann -- "eine andere
|
|
121
|
+
* Sitzung hat zuletzt nachgesehen" gibt es in der Wirklichkeit nur durch
|
|
122
|
+
* Claude-Neustarts.
|
|
123
|
+
*
|
|
124
|
+
* WARUM ES DIESE FRAGE UEBERHAUPT GIBT: Ein Startbefehl, dessen Programm
|
|
125
|
+
* fehlt oder abstuerzt, laermt nicht (gemessen, M-0). Der Bildschirm sieht
|
|
126
|
+
* dann aus wie an einem guten Tag. *Eine Wache, die still stirbt, ist
|
|
127
|
+
* schlimmer als keine: Sie kauft Vertrauen und deckt nichts mehr.*
|
|
128
|
+
*
|
|
129
|
+
* 🔴 DREI MAL SCHWEIGEN, EINMAL SPRECHEN:
|
|
130
|
+
* - Keine Sitzungsnummer -> der Mensch sitzt im Terminalfenster, die Wache
|
|
131
|
+
* hatte nie einen Anlass zu laufen. "Nicht gelaufen" heisst hier nichts.
|
|
132
|
+
* - Abgeschaltet -> wer `kimal3 wache aus` getippt hat, will nicht taeglich
|
|
133
|
+
* daran erinnert werden. Sonst waere der Ausschalter keiner.
|
|
134
|
+
* - Dieselbe Nummer -> sie hat fuer diese Sitzung nachgesehen.
|
|
135
|
+
* Nur wenn eine Sitzung laeuft, die Wache angeschaltet ist und trotzdem
|
|
136
|
+
* niemand fuer sie nachgesehen hat, ist wirklich etwas kaputt.
|
|
137
|
+
*/
|
|
138
|
+
export function wacheAusgefallen(
|
|
139
|
+
// Der GANZE Merkzettel, nicht nur die zwei Felder, die diese Rechnung liest.
|
|
140
|
+
// `zuletzt` interessiert sie nicht -- aber ein Merkzettel aus einer aelteren
|
|
141
|
+
// Fassung traegt genau das und sonst nichts, und dieser Fall gehoert
|
|
142
|
+
// geprueft. Ein Typ, der ihn verbietet, verbietet auch seine Pruefung.
|
|
143
|
+
inhalt, jetzt) {
|
|
144
|
+
if (jetzt === null)
|
|
145
|
+
return false;
|
|
146
|
+
if (inhalt !== null && inhalt.aus === true)
|
|
147
|
+
return false;
|
|
148
|
+
return inhalt === null || inhalt.sitzung !== jetzt;
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kimal3",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"kimal3": "dist/kimal3.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"skill",
|
|
12
|
-
"README.md"
|
|
13
|
-
],
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=20"
|
|
16
|
-
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsc -p tsconfig.json",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
-
},
|
|
21
|
-
"keywords": [
|
|
22
|
-
"kimal3",
|
|
23
|
-
"cli"
|
|
24
|
-
],
|
|
25
|
-
"homepage": "https://app.kimal3.de/pro",
|
|
26
|
-
"license": "SEE LICENSE IN README.md"
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "kimal3",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kimal3": "dist/kimal3.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skill",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"kimal3",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://app.kimal3.de/pro",
|
|
26
|
+
"license": "SEE LICENSE IN README.md"
|
|
27
|
+
}
|