kimal3 0.1.6 → 0.2.1
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 +3 -0
- package/dist/api.js +112 -0
- package/dist/arbeitsplatz-dateien.js +84 -22
- package/dist/argumente.js +150 -0
- package/dist/befehle/arbeitsplatz.js +139 -17
- package/dist/befehle/kontext-fragen.js +182 -0
- package/dist/befehle/kontext-gemeinsam.js +185 -0
- package/dist/befehle/kontext-hoch.js +236 -0
- package/dist/befehle/kontext-runter.js +305 -0
- package/dist/befehle/skill-install.js +51 -17
- package/dist/befehle/werkbank.js +128 -17
- package/dist/befehle-katalog.js +58 -2
- package/dist/firmen-liste.js +4 -2
- package/dist/fragen-lesen.js +178 -0
- package/dist/kimal3.js +29 -2
- package/dist/kontext-dateien.js +248 -0
- package/dist/kontext-stand.js +870 -0
- package/dist/nachricht.js +137 -2
- package/package.json +1 -1
- package/skill/SKILL.md +106 -11
- package/skill/commands/k3.md +59 -18
- package/skill/commands/k3kontext.md +89 -0
- /package/skill/commands/{k3firma.md → k3analyse.md} +0 -0
package/dist/befehle-katalog.js
CHANGED
|
@@ -42,7 +42,13 @@ export const BEFEHLE = [
|
|
|
42
42
|
tippen: "! kimal3 skill install",
|
|
43
43
|
terminal: "kimal3 skill install",
|
|
44
44
|
details: [
|
|
45
|
-
|
|
45
|
+
// ⚠️ KEINE Anzahl hier (Review-Runde 5): Hier stand „deine vier
|
|
46
|
+
// Slash-Befehle“, und die Zahl war ab `/k3kontext` falsch — auf der
|
|
47
|
+
// Pro-Seite UND in der Terminal-Hilfe, denn dieser Text wird an beiden
|
|
48
|
+
// Stellen ausgegeben. Dieselbe Lehre wie bei „13 Pruefungen“ in
|
|
49
|
+
// `check:arbeitsplatz`: Eine Anzahl im Fliesstext ist eine Behauptung,
|
|
50
|
+
// die verrottet. Welche es sind, sagt `/k3` beim Laufen.
|
|
51
|
+
"Der Befehl, der aus den anderen erst etwas macht: Er legt 🤖 Claudes Anleitung ab (den „Skill“) und 🧑 deine Slash-Befehle.",
|
|
46
52
|
"Er läuft NICHT automatisch — einmal nach der Installation, und nach jedem Update noch einmal. Sonst arbeitet Claude weiter mit der alten Anleitung.",
|
|
47
53
|
"Geklappt hat es, wenn er „Skill angelegt“ meldet und die Slash-Befehle aufzählt. Beides kennt Claude ab der NÄCHSTEN Sitzung — die laufende einmal neu starten.",
|
|
48
54
|
],
|
|
@@ -84,6 +90,56 @@ export const BEFEHLE = [
|
|
|
84
90
|
"Gib deinen Vornamen mit — der Ordner trägt ihn: aus sabine wird werkbank-sabine. Ohne Angabe bildet der Server ihn aus deiner Anmelde-Adresse. So sieht man später, wem welche Werkbank gehört.",
|
|
85
91
|
],
|
|
86
92
|
},
|
|
93
|
+
/*
|
|
94
|
+
🔴 Auch diese zwei Eintraege stehen direkt NEBEN `werkbank neu` und nicht
|
|
95
|
+
am Ende der Liste, aus demselben Grund wie oben bei `arbeitsplatz neu`:
|
|
96
|
+
Wer hier liest, sucht den naechsten Schritt, nicht das Alphabet. Wer
|
|
97
|
+
einen KI-Arbeitsplatz gerade angelegt oder betreten hat, fragt sich als
|
|
98
|
+
Naechstes, wie der Firmenkontext (CLAUDE.md + kontext/*.md) aktuell
|
|
99
|
+
bleibt -- nicht, wie man ein Projekt eintraegt.
|
|
100
|
+
*/
|
|
101
|
+
{
|
|
102
|
+
befehl: "kimal3 kontext runter",
|
|
103
|
+
beschreibung: "Den Firmenkontext holen — CLAUDE.md und die fünf Dateien in kontext/",
|
|
104
|
+
wer: "du",
|
|
105
|
+
rhythmus: "wann immer die Firma etwas geändert haben könnte",
|
|
106
|
+
tippen: "! kimal3 kontext runter",
|
|
107
|
+
terminal: "kimal3 kontext runter",
|
|
108
|
+
details: [
|
|
109
|
+
"Holt CLAUDE.md und die fünf Dateien in kontext/ von deiner Firma und legt sie in deinem KI-Arbeitsplatz ab. Was du selbst lokal geändert hast, fasst er nicht an — er sagt dir, wie du es einbringst.",
|
|
110
|
+
"Hast du eine Datei lokal geändert UND hat die Firma sie inzwischen auch geändert, sichert er deine Fassung zuerst nach kontext/vorher-<datum>/, bevor er sie ersetzt — nichts geht verloren.",
|
|
111
|
+
"Geklappt hat es, wenn er die aktualisierten Dateien nennt oder „Dein Firmenkontext ist aktuell“ sagt.",
|
|
112
|
+
"⚠️ Wie werkbank neu braucht er ein eigenes Pro-Konto. Ohne das kommt Rückgabewert 4 — dann nicht wiederholen.",
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
befehl: "kimal3 kontext fragen",
|
|
117
|
+
beschreibung: "Die Fragen über deine Firma beantworten — Claude führt dich durch",
|
|
118
|
+
wer: "claude",
|
|
119
|
+
ausloeser: "beantworten wir die Fragen über meine Firma",
|
|
120
|
+
tippen: "/k3kontext",
|
|
121
|
+
terminal: "kimal3 kontext fragen",
|
|
122
|
+
details: [
|
|
123
|
+
"Liest die fünf Dateien in kontext/ und gibt jede Frage aus, unter der noch nichts steht — mit Datei, Frage und Hinweis. Sind alle beantwortet, sagt er das in einem Satz.",
|
|
124
|
+
"Er liest nur: kein Netz, keine Anmeldung, kein Schreiben. Deshalb ist er der richtige erste Schritt — Claude stellt dir die Fragen dann einzeln und trägt deine Antworten in die Dateien ein.",
|
|
125
|
+
"Danach schickt kimal3 kontext hoch sie an die Firma. Das darf nur der Admin; alle anderen geben ihre Antworten an ihn weiter.",
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
befehl: "kimal3 kontext hoch",
|
|
130
|
+
beschreibung: "Deine lokalen Änderungen an den fünf Firmen-Texten hochschicken",
|
|
131
|
+
wer: "du",
|
|
132
|
+
rhythmus: "nachdem du kontext/*.md bearbeitet hast — nur als Admin",
|
|
133
|
+
tippen: "! kimal3 kontext hoch",
|
|
134
|
+
terminal: "kimal3 kontext hoch",
|
|
135
|
+
details: [
|
|
136
|
+
"Schickt alle fünf Firmen-Texte an deine Firma — die Dateien in kontext/ (kurzprofil.md, firma.md, marke.md, systeme.md, sicherheit.md). Damit kommen sie beim nächsten kontext runter auch bei den Kolleginnen und Kollegen an.",
|
|
137
|
+
"🔴 Nur der Firmen-Admin darf das. Bist du keiner, sagt er es dir — dann gib deine Ergänzung an den Admin weiter, ein zweiter Versuch ändert daran nichts.",
|
|
138
|
+
"⚠️ Klappt etwas nicht, zählt die MELDUNG, nicht die Zahl dahinter: Dieselbe 1 steht für „du bist kein Admin“, „hier steht kein Arbeitsplatz“, „jemand anderes hat inzwischen geändert“ (Ausweg --ueberschreiben) und mehreres mehr. Sie steht sogar unter einer Meldung, die mit „Die Firma hat deine Änderungen gespeichert“ beginnt und nur sagt, dass eine Datei auf deiner Platte nicht geschrieben werden konnte — DAS ist ein Erfolg: alles ist oben angekommen. Lies den Satz, nicht die Zahl.",
|
|
139
|
+
"Sieht eine der fünf Dateien nicht mehr aus wie ihre Frageliste — etwa weil etwas Fremdes darüberkopiert wurde —, bricht er ab und schickt gar nichts, auch die vier anderen nicht. kimal3 kontext runter holt die Datei zurück.",
|
|
140
|
+
"Geklappt hat es, wenn er „✓ hochgeschickt“ mit den Dateinamen meldet.",
|
|
141
|
+
],
|
|
142
|
+
},
|
|
87
143
|
{
|
|
88
144
|
befehl: "kimal3 projekt neu …",
|
|
89
145
|
beschreibung: "Ein Projekt ins Erfolgsprofil eintragen",
|
|
@@ -113,7 +169,7 @@ export const BEFEHLE = [
|
|
|
113
169
|
beschreibung: "Die Daten deiner Firma holen, damit Claude sie auswerten kann",
|
|
114
170
|
wer: "claude",
|
|
115
171
|
ausloeser: "werte meine Firma aus",
|
|
116
|
-
tippen: "/
|
|
172
|
+
tippen: "/k3analyse <deine Frage>",
|
|
117
173
|
terminal: 'kimal3 firma daten --firma "Nordwind Handel GmbH"',
|
|
118
174
|
details: [
|
|
119
175
|
"Holt Kennzahlen, Menschen und Projekte deiner Firma — auswerten tut Claude. Frag einfach los: „Welche drei Projekte bringen den meisten messbaren Ertrag?“",
|
package/dist/firmen-liste.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Die Firmen-Auswahl, die eine Absage dem Menschen anbietet.
|
|
3
3
|
*
|
|
4
4
|
* 🔑 Eine reine Rechnung, damit `check:cli` sie ohne Server pruefen kann --
|
|
5
|
-
* dieselbe Ueberlegung wie bei `fehler.ts`.
|
|
5
|
+
* dieselbe Ueberlegung wie bei `fehler.ts`. `alsWort` unten ist ebenfalls eine
|
|
6
|
+
* reine Rechnung, der Import bricht das also nicht.
|
|
6
7
|
*/
|
|
8
|
+
import { alsWort } from "./argumente.js";
|
|
7
9
|
/**
|
|
8
10
|
* Aus den Namen der eigenen Firmen die Zeilen bauen, die unter „Deine Firmen:"
|
|
9
11
|
* stehen.
|
|
@@ -42,7 +44,7 @@ export function firmenZeilen(namen) {
|
|
|
42
44
|
// Fertiger Befehl statt blosser Liste: Der naechste Schritt ist dann
|
|
43
45
|
// Kopieren statt Abtippen -- und ein Name mit Leerzeichen braucht
|
|
44
46
|
// Anfuehrungszeichen, an die sonst niemand denkt.
|
|
45
|
-
zeilen.push(` kimal3 firma daten --firma
|
|
47
|
+
zeilen.push(` kimal3 firma daten --firma ${alsWort(name, true)}`);
|
|
46
48
|
}
|
|
47
49
|
else {
|
|
48
50
|
zeilen.push(` „${name}" — kommt ${wieOft}× vor und ist über den Namen nicht wählbar.`);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Die Fragen aus einer Kontext-Datei lesen -- die reine Rechnung hinter
|
|
3
|
+
`kimal3 kontext fragen` und hinter dem Abbruch von `kimal3 kontext hoch`.
|
|
4
|
+
|
|
5
|
+
🔑 WOHER DIE FRAGEN KOMMEN -- die eine Entscheidung dieser Datei:
|
|
6
|
+
**Aus der Datei selbst, nicht aus einem Katalog.** Jede ausgelieferte
|
|
7
|
+
Kontext-Datei traegt ihre Fragen bereits vollstaendig bei sich: einen Anker
|
|
8
|
+
(`<!-- k3:marke_anrede -->`), darunter die Frage als `## `-Ueberschrift,
|
|
9
|
+
darunter unsere Hinweiszeilen mit `>`, darunter die Antwort des Menschen.
|
|
10
|
+
Das Format beschreibt sich selbst.
|
|
11
|
+
|
|
12
|
+
🔴 WARUM DAS NICHT NUR BEQUEM, SONDERN DAS EINZIG EHRLICHE IST: Die CLI kann
|
|
13
|
+
nichts aus `src/` importieren (eigenes Paket, eigenes tsconfig -- s. Kopf von
|
|
14
|
+
`kontext-stand.ts`). Ein Katalog in `cli/src/` waere eine ZWEITE Fassung der
|
|
15
|
+
17 Fragen, und sie friert in dem Moment ein, in dem `npm publish` laeuft: Der
|
|
16
|
+
Kunde hat dann eine CLI von gestern und Dateien von heute. Die Datei auf
|
|
17
|
+
seiner Platte kommt dagegen IMMER vom Server -- sie ist die einzige Quelle,
|
|
18
|
+
die nicht gegenueber dem abweichen kann, was er wirklich vor sich hat.
|
|
19
|
+
|
|
20
|
+
⚠️ DIESE DATEI IMPORTIERT NICHTS -- dieselbe Auflage wie `kontext-stand.ts`.
|
|
21
|
+
Nur so kann `scripts/check-kontext.mts` sie neben `FRAGEN` (aus der App) in
|
|
22
|
+
denselben Lauf holen und beide gegeneinander messen.
|
|
23
|
+
|
|
24
|
+
📌 Der Preis, ausgesprochen: Wer den ANKER einer Frage aus seiner Datei
|
|
25
|
+
loescht, die Ueberschrift aber stehen laesst, hat eine Frage, die hier nicht
|
|
26
|
+
mehr auftaucht. Die App faengt diesen Fall ab (sie erkennt die Frage am
|
|
27
|
+
Wortlaut) und setzt den Anker beim naechsten Speichern wieder ein. Die
|
|
28
|
+
Ausgabe von `kontext fragen` sagt deshalb "Fragen in deinen Dateien" und
|
|
29
|
+
nicht "alle Fragen deiner Firma" -- sie behauptet nur, was sie messen kann.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* `<!-- k3:anrede -->` bzw. `<!-- k3:unwoerter optional -->`.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ Woertlich dasselbe Muster wie `ANKER_MUSTER` in
|
|
35
|
+
* `src/app/firma/kontext-zerlegen.ts`. Es steht hier ein zweites Mal, weil
|
|
36
|
+
* diese Datei nichts importieren darf (s. Kopf) -- dass beide dasselbe meinen,
|
|
37
|
+
* misst `check:kontext`, indem er die ECHTEN Vorlagen durch beide Seiten
|
|
38
|
+
* schickt und die Ergebnisse vergleicht.
|
|
39
|
+
*/
|
|
40
|
+
const ANKER_MUSTER = /^<!--\s*k3:([a-z0-9_]+)(\s+optional)?\s*-->$/;
|
|
41
|
+
function istHinweis(zeile) {
|
|
42
|
+
return zeile.trimStart().startsWith(">");
|
|
43
|
+
}
|
|
44
|
+
function istUeberschrift(zeile) {
|
|
45
|
+
return zeile.startsWith("## ");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Alle Fragen, die in `text` stehen -- in der Reihenfolge der Datei.
|
|
49
|
+
*
|
|
50
|
+
* Der Durchgang ist zeilenweise und haelt drei Dinge auseinander:
|
|
51
|
+
* 1. **Anker** -- er merkt sich, wessen Frage als Naechstes kommt.
|
|
52
|
+
* 2. **Ueberschrift** -- steht ein Anker offen, beginnt hier seine Frage.
|
|
53
|
+
* 3. **Darunter** -- die unmittelbar folgenden `>`-Zeilen sind UNSER Hinweis,
|
|
54
|
+
* alles Weitere bis zur naechsten Ueberschrift ist die Antwort.
|
|
55
|
+
*
|
|
56
|
+
* 🔑 DER HINWEIS ZAEHLT NUR UNMITTELBAR UNTER DER UEBERSCHRIFT -- genau wie in
|
|
57
|
+
* `scripts/arbeitsplatz-inhalt.mts`, wo derselbe Katalog fuer die App entsteht.
|
|
58
|
+
* Sonst kaeme eine `>`-Zeile, die ein Mensch mitten in seine Antwort setzt,
|
|
59
|
+
* als unser Hinweis heraus.
|
|
60
|
+
*
|
|
61
|
+
* 🔴 AUS DER ANTWORT FALLEN TROTZDEM ALLE `>`-ZEILEN HERAUS, auch spaetere --
|
|
62
|
+
* das ist die Regel, nach der auch die App zerlegt (`zerlege`). Ein Zitatblock
|
|
63
|
+
* gehoert uns; ginge er als Antwort durch, stuende unser eigener Satz als
|
|
64
|
+
* Aussage der Firma in der Datenbank.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ Ein Anker OHNE Ueberschrift darunter zaehlt nicht -- er wird vom naechsten
|
|
67
|
+
* Anker oder von der naechsten Ueberschrift ueberschrieben. *Ein Anker, der auf
|
|
68
|
+
* eine Ueberschrift drei Absaetze weiter zeigt, ist kein Anker, sondern ein
|
|
69
|
+
* Zufall, der aussieht wie Absicht.*
|
|
70
|
+
*/
|
|
71
|
+
export function fragenAus(text) {
|
|
72
|
+
const zeilen = text.split(/\r?\n/);
|
|
73
|
+
const fragen = [];
|
|
74
|
+
let offenerAnker = null;
|
|
75
|
+
let aktuell = null;
|
|
76
|
+
let hinweisNochOffen = false;
|
|
77
|
+
const hinweise = [];
|
|
78
|
+
const antwort = [];
|
|
79
|
+
/** Die laufende Frage abschliessen -- Hinweis und Antwort festschreiben. */
|
|
80
|
+
const schliesse = () => {
|
|
81
|
+
if (aktuell === null)
|
|
82
|
+
return;
|
|
83
|
+
aktuell.hinweis = hinweise.join(" ").trim();
|
|
84
|
+
aktuell.antwort = antwort.join("\n").trim();
|
|
85
|
+
aktuell = null;
|
|
86
|
+
hinweise.length = 0;
|
|
87
|
+
antwort.length = 0;
|
|
88
|
+
};
|
|
89
|
+
for (const zeile of zeilen) {
|
|
90
|
+
const treffer = ANKER_MUSTER.exec(zeile.trim());
|
|
91
|
+
if (treffer) {
|
|
92
|
+
schliesse();
|
|
93
|
+
offenerAnker = { anker: treffer[1], pflicht: !treffer[2] };
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (istUeberschrift(zeile)) {
|
|
97
|
+
schliesse();
|
|
98
|
+
if (offenerAnker) {
|
|
99
|
+
aktuell = {
|
|
100
|
+
anker: offenerAnker.anker,
|
|
101
|
+
ueberschrift: zeile.slice(3).trim(),
|
|
102
|
+
hinweis: "",
|
|
103
|
+
pflicht: offenerAnker.pflicht,
|
|
104
|
+
antwort: "",
|
|
105
|
+
};
|
|
106
|
+
hinweisNochOffen = true;
|
|
107
|
+
fragen.push(aktuell);
|
|
108
|
+
}
|
|
109
|
+
offenerAnker = null;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
/*
|
|
113
|
+
🔴 **RULING K-14: EIN ANKER, UNTER DEM NICHT UNMITTELBAR SEINE FRAGE
|
|
114
|
+
STEHT, ZAEHLT NICHT.** Leerzeilen sind erlaubt, Text nicht. Ohne diese
|
|
115
|
+
drei Zeilen machte ein verwaister Anker die naechstbeste Ueberschrift zur
|
|
116
|
+
Frage -- und `kimal3 kontext fragen` stellte einem Menschen eine Frage,
|
|
117
|
+
die niemand geschrieben hat. Dieselbe Regel wie im Erzeuger
|
|
118
|
+
(`frageZeileNach` in `scripts/arbeitsplatz-inhalt.mts`).
|
|
119
|
+
|
|
120
|
+
⚠️ Die App ist an dieser Stelle NACHSICHTIGER: `zerlege` haengt einen
|
|
121
|
+
verwaisten Anker an die naechste Ueberschrift, wie weit sie auch weg ist.
|
|
122
|
+
Der Unterschied ist gewollt und folgenlos, solange keine ausgelieferte
|
|
123
|
+
Datei einen verwaisten Anker hat -- und genau das prueft
|
|
124
|
+
`check:arbeitsplatz` fuer jede Vorlage.
|
|
125
|
+
*/
|
|
126
|
+
if (offenerAnker && zeile.trim() !== "")
|
|
127
|
+
offenerAnker = null;
|
|
128
|
+
if (aktuell === null)
|
|
129
|
+
continue;
|
|
130
|
+
if (istHinweis(zeile)) {
|
|
131
|
+
// Nur der Block DIREKT unter der Ueberschrift ist unser Hinweis;
|
|
132
|
+
// spaetere `>`-Zeilen fallen weg, gehoeren aber auch nicht zur Antwort.
|
|
133
|
+
if (hinweisNochOffen)
|
|
134
|
+
hinweise.push(zeile.trimStart().slice(1).trim());
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// ⚠️ SCHON EINE LEERZEILE BEENDET DEN HINWEISBLOCK -- woertlich dieselbe
|
|
138
|
+
// Grenze wie im Erzeuger (`katalogAus` in `scripts/arbeitsplatz-inhalt.mts`).
|
|
139
|
+
// Zoege sie hier weiter, kaeme aus einer Datei mit einem Zitat weiter unten
|
|
140
|
+
// ein anderer Hinweis heraus als in der App, und `check:kontext` haette
|
|
141
|
+
// recht, wenn er rot wird.
|
|
142
|
+
hinweisNochOffen = false;
|
|
143
|
+
antwort.push(zeile);
|
|
144
|
+
}
|
|
145
|
+
schliesse();
|
|
146
|
+
return fragen;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Sieht dieser Text aus wie die Datei, die er sein soll?
|
|
150
|
+
*
|
|
151
|
+
* 🔴 **DER ABBRUCH VON `kimal3 kontext hoch` HAENGT AN DIESER ZEILE** (Spec,
|
|
152
|
+
* Abschnitt 11). Ohne sie schickt der Befehl JEDEN Dateiinhalt hoch, den er an
|
|
153
|
+
* der Stelle findet. Die App zerlegt ihn dann gegen den Katalog DIESER Datei,
|
|
154
|
+
* findet keine einzige Antwort -- und legt den kompletten Text als "Zusatz"
|
|
155
|
+
* ab. Die Firma haette danach einen Zusatz statt eines Firmentextes, und die
|
|
156
|
+
* CLI haette dazu einen Haken gemeldet. *Ein Fehlerfall darf nie denselben
|
|
157
|
+
* Namen tragen wie ein Erfolgsfall.*
|
|
158
|
+
*
|
|
159
|
+
* 🔑 **WORAN ERKANNT WIRD: an mindestens einem Anker, der zu DIESER Datei
|
|
160
|
+
* gehoert.** Die Anker sind je Datei benannt (`marke_anrede`, `firma_angebot`,
|
|
161
|
+
* `kurz_kunden` …), und `FIRMEN_TEXTE` (`kontext-stand.ts`) fuehrt je Datei
|
|
162
|
+
* ihren Praefix. Damit faellt nicht nur die voellig fremde Datei durch, sondern
|
|
163
|
+
* auch der haeufigere Unfall: `marke.md` ueber `firma.md` kopiert. Ein blosses
|
|
164
|
+
* "irgendein Anker steht drin" haette genau den durchgewunken.
|
|
165
|
+
*
|
|
166
|
+
* ⚠️ **Dass die Praefixe wirklich passen, ist keine Verabredung, sondern eine
|
|
167
|
+
* Messung:** `check:kontext` haelt jeden Anker aus `FRAGEN` (der App) gegen den
|
|
168
|
+
* Praefix, den `FIRMEN_TEXTE` fuer seine Datei nennt.
|
|
169
|
+
*
|
|
170
|
+
* 📌 **Der Preis, ausgesprochen:** Wer ALLE Anker einer Datei loescht und nur
|
|
171
|
+
* die Ueberschriften stehen laesst, bekommt hier ein Nein, obwohl die App den
|
|
172
|
+
* Text noch am Wortlaut erkennen wuerde. Das ist die sichere Seite: Es geht
|
|
173
|
+
* nichts verloren, die Meldung sagt warum, und `kimal3 kontext runter` stellt
|
|
174
|
+
* die Datei wieder her. Die andere Seite waere der stille Datenverlust oben.
|
|
175
|
+
*/
|
|
176
|
+
export function traegtFragenVon(text, ankerPraefix) {
|
|
177
|
+
return fragenAus(text).some((f) => f.anker.startsWith(ankerPraefix));
|
|
178
|
+
}
|
package/dist/kimal3.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
|
|
16
|
-
import { gemerkterHinweis, NichtAngemeldet } from "./api.js";
|
|
16
|
+
import { gemerkterHinweis, lokalerKontextStandJetzt, NichtAngemeldet } from "./api.js";
|
|
17
17
|
import { leseGesehen, merkeGesehen } from "./gesehen.js";
|
|
18
18
|
import { formatiere, heuteAus, skillMeldung, skillZustand, sollZeigen, } from "./nachricht.js";
|
|
19
19
|
import { eigeneVersion } from "./version.js";
|
|
@@ -21,6 +21,9 @@ import { hilfeText } from "./befehle-katalog.js";
|
|
|
21
21
|
import { arbeitsplatzNeu } from "./befehle/arbeitsplatz.js";
|
|
22
22
|
import { werkbankNeu } from "./befehle/werkbank.js";
|
|
23
23
|
import { firmaDaten } from "./befehle/firma-daten.js";
|
|
24
|
+
import { kontextFragen } from "./befehle/kontext-fragen.js";
|
|
25
|
+
import { kontextRunter } from "./befehle/kontext-runter.js";
|
|
26
|
+
import { kontextHoch } from "./befehle/kontext-hoch.js";
|
|
24
27
|
import { login } from "./befehle/login.js";
|
|
25
28
|
import { logout } from "./befehle/logout.js";
|
|
26
29
|
import { projektListe } from "./befehle/projekt-liste.js";
|
|
@@ -96,8 +99,26 @@ function hinweisText() {
|
|
|
96
99
|
if (eigene === null)
|
|
97
100
|
return null;
|
|
98
101
|
const heute = heuteAus(new Date());
|
|
102
|
+
/*
|
|
103
|
+
🔴 HIER, UND NICHT FRUEHER (offener Punkt A-4). Diese Zeile liest den
|
|
104
|
+
Merkzettel auf der Platte in genau dem Augenblick, in dem der Hinweis
|
|
105
|
+
ausgegeben werden soll -- also NACH dem Befehl.
|
|
106
|
+
|
|
107
|
+
Genau darin lag der Fehler: Den Stand, den der Server fuer seinen
|
|
108
|
+
Hinweis vergleicht, schickt die CLI im Kopf mit, und der wird VOR dem
|
|
109
|
+
Netzaufruf gelesen (`kontextStandKopf` in `api.ts`). Direkt nach
|
|
110
|
+
`kimal3 kontext runter` stand deshalb "Dein Firmenkontext ist aktuell."
|
|
111
|
+
und darunter der Rat, `kimal3 kontext runter` auszufuehren. Der zweite
|
|
112
|
+
Aufruf schwieg dann von selbst -- die Meldung kam schlicht einen Aufruf
|
|
113
|
+
zu spaet.
|
|
114
|
+
|
|
115
|
+
🔑 `sollZeigen` VERLANGT diesen Wert (kein optionales Argument): Wer ihn
|
|
116
|
+
hier vergisst, bekommt einen Uebersetzungsfehler statt einer Rechnung,
|
|
117
|
+
die niemand mehr benutzt.
|
|
118
|
+
*/
|
|
119
|
+
const standJetzt = lokalerKontextStandJetzt();
|
|
99
120
|
const h = gemerkterHinweis();
|
|
100
|
-
if (h && sollZeigen(h, eigene, leseGesehen(), heute)) {
|
|
121
|
+
if (h && sollZeigen(h, eigene, leseGesehen(), heute, standJetzt)) {
|
|
101
122
|
// Hoechstens EIN Hinweis je Aufruf -- zwei sind eine Wand.
|
|
102
123
|
return { text: formatiere(h), kennung: h.id, tag: heute };
|
|
103
124
|
}
|
|
@@ -279,6 +300,12 @@ async function haupt() {
|
|
|
279
300
|
return arbeitsplatzNeu(argv.slice(2));
|
|
280
301
|
if (argv[0] === "werkbank" && argv[1] === "neu")
|
|
281
302
|
return werkbankNeu(argv.slice(2));
|
|
303
|
+
if (argv[0] === "kontext" && argv[1] === "runter")
|
|
304
|
+
return kontextRunter(argv.slice(2));
|
|
305
|
+
if (argv[0] === "kontext" && argv[1] === "hoch")
|
|
306
|
+
return kontextHoch(argv.slice(2));
|
|
307
|
+
if (argv[0] === "kontext" && argv[1] === "fragen")
|
|
308
|
+
return kontextFragen(argv.slice(2));
|
|
282
309
|
console.error(`Den Befehl „${wort}" kenne ich nicht.\n${HILFE}`);
|
|
283
310
|
return 1;
|
|
284
311
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { istUnsichererPfad } from "./arbeitsplatz-dateien.js";
|
|
4
|
+
import { MERKZETTEL_PFAD, VERWALTETE_DATEIEN, arbeitsplatzAus, leseMerkzettel, schreibeMerkzettel, sicherungsZiel, } from "./kontext-stand.js";
|
|
5
|
+
/**
|
|
6
|
+
* Das Dateisystem hinter `kimal3 kontext runter` -- Ordner finden, die
|
|
7
|
+
* verwalteten Dateien lesen, sichern, schreiben, den Merkzettel lesen und
|
|
8
|
+
* schreiben.
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **Ruling T8-1: Die reine Fundregel `arbeitsplatzAus` wird hier NICHT noch
|
|
11
|
+
* einmal geschrieben** -- sie steht seit Task 1 in `kontext-stand.ts` und wird
|
|
12
|
+
* hier nur VERBRAUCHT (`findeArbeitsplatz` unten fuellt sie mit echten
|
|
13
|
+
* Kandidaten vom Dateisystem). Zwei Fassungen derselben Fundregel waeren
|
|
14
|
+
* dieselbe zweite Wahrheit, die dieses Projekt schon bei `werkbankAusDateien`
|
|
15
|
+
* teuer bezahlt hat.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ **Dieselben Netze wie in `arbeitsplatz-dateien.ts`**, aus denselben,
|
|
18
|
+
* teuer bezahlten Gruenden: `existsSync`/`statSync`/`readFileSync` gehoeren
|
|
19
|
+
* in ein EIGENES `try` -- ein `EACCES` fluege sonst bis in den Auffang-Block
|
|
20
|
+
* von `kimal3.ts`, der "kimal3 ist gerade nicht erreichbar" druckt und `3`
|
|
21
|
+
* zurueckgibt. Der Mensch suchte den Fehler dann beim Server, obwohl er auf
|
|
22
|
+
* seiner eigenen Platte sitzt -- und `3` heisst "gleich noch einmal", was
|
|
23
|
+
* hier nie helfen kann. Ein Schreibfehler (Platte voll, `EACCES`) gibt
|
|
24
|
+
* deshalb `1` zurueck, nicht `3`.
|
|
25
|
+
*/
|
|
26
|
+
/** Ist `pfad` (relativ zu `wurzel`) ein lesbarer ORDNER? Unlesbar zaehlt als "nein". */
|
|
27
|
+
function istLesbarerOrdner(pfad) {
|
|
28
|
+
try {
|
|
29
|
+
return existsSync(pfad) && statSync(pfad).isDirectory();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Ist `pfad` (relativ zu `wurzel`) eine lesbare DATEI? Unlesbar zaehlt als "nein". */
|
|
36
|
+
function istLesbareDatei(pfad) {
|
|
37
|
+
try {
|
|
38
|
+
return existsSync(pfad) && statSync(pfad).isFile();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Von `start` aufwaerts nach dem Arbeitsplatz suchen -- die Kandidaten kommen
|
|
46
|
+
* von UNTEN NACH OBEN, `start` zuerst.
|
|
47
|
+
*
|
|
48
|
+
* 🔑 Diese Funktion FUELLT nur die Kandidatenliste vom Dateisystem; die
|
|
49
|
+
* eigentliche Entscheidung ("welcher Ordner gewinnt") trifft `arbeitsplatzAus`
|
|
50
|
+
* in `kontext-stand.ts` (Ruling P-4/T8-1).
|
|
51
|
+
*
|
|
52
|
+
* Die Schleife hat eine Sicherheitsgrenze (200 Ebenen): `dirname()` konvergiert
|
|
53
|
+
* auf jedem realen Dateisystem zur Wurzel, aber ein Netzlaufwerk mit einem
|
|
54
|
+
* kaputten Elternverweis soll trotzdem nie eine Endlosschleife erzeugen.
|
|
55
|
+
*/
|
|
56
|
+
export function findeArbeitsplatz(start) {
|
|
57
|
+
const kandidaten = [];
|
|
58
|
+
let aktuell = resolve(start);
|
|
59
|
+
for (let i = 0; i < 200; i++) {
|
|
60
|
+
kandidaten.push({
|
|
61
|
+
pfad: aktuell,
|
|
62
|
+
hatKontext: istLesbarerOrdner(join(aktuell, "kontext")),
|
|
63
|
+
hatClaude: istLesbareDatei(join(aktuell, "CLAUDE.md")),
|
|
64
|
+
});
|
|
65
|
+
const eltern = dirname(aktuell);
|
|
66
|
+
if (eltern === aktuell)
|
|
67
|
+
break;
|
|
68
|
+
aktuell = eltern;
|
|
69
|
+
}
|
|
70
|
+
return arbeitsplatzAus(kandidaten);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Eine der verwalteten Dateien lesen -- fehlend wird zu `inhalt: null`,
|
|
74
|
+
* NICHT zu einem Fehler (ein frisch geklonter Ordner hat oft noch keine).
|
|
75
|
+
*/
|
|
76
|
+
export function leseEineDatei(ordner, pfad) {
|
|
77
|
+
const ort = join(ordner, ...pfad.split("/"));
|
|
78
|
+
try {
|
|
79
|
+
if (!existsSync(ort))
|
|
80
|
+
return { ok: true, inhalt: null };
|
|
81
|
+
if (!statSync(ort).isFile()) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
meldung: `„${pfad}" existiert, ist aber keine Datei -- ich fasse das nicht an. Bitte pruefen.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, inhalt: readFileSync(ort, "utf8") };
|
|
88
|
+
}
|
|
89
|
+
catch (fehler) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
meldung: `Ich komme an „${pfad}" nicht heran: ${fehler instanceof Error ? fehler.message : String(fehler)}. ` +
|
|
93
|
+
"Das liegt an diesem Rechner, nicht am Server -- Rechte pruefen.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Den lokalen Merkzettel lesen. Unlesbar oder nicht vorhanden -> leeres
|
|
99
|
+
* Objekt, GENAU wie `leseMerkzettel(null)` -- der sichere Fall aus §7b: "wer
|
|
100
|
+
* nichts weiss, loescht nichts". Eine unlesbare Datei fuehrt also zur
|
|
101
|
+
* Sicherung vor dem Ueberschreiben, nie zum stillen Verlust.
|
|
102
|
+
*/
|
|
103
|
+
export function leseMerkzettelDatei(ordner) {
|
|
104
|
+
try {
|
|
105
|
+
const ort = join(ordner, MERKZETTEL_PFAD);
|
|
106
|
+
const roh = existsSync(ort) ? readFileSync(ort, "utf8") : null;
|
|
107
|
+
return leseMerkzettel(roh);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return leseMerkzettel(null);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Den Merkzettel schreiben -- MIT dem Stand, der zum ausgelieferten Inhalt passt. */
|
|
114
|
+
export function schreibeMerkzettelDatei(ordner, m) {
|
|
115
|
+
try {
|
|
116
|
+
const ort = join(ordner, MERKZETTEL_PFAD);
|
|
117
|
+
writeFileSync(ort, schreibeMerkzettel(m), "utf8");
|
|
118
|
+
return { ok: true };
|
|
119
|
+
}
|
|
120
|
+
catch (fehler) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
meldung: `Der Merkzettel liess sich nicht schreiben: ${fehler instanceof Error ? fehler.message : String(fehler)}. Das liegt an diesem Rechner, nicht am Server -- Platz und Rechte pruefen.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Der naechste NICHT belegte Sicherungspfad -- reine Rechnung, kein
|
|
129
|
+
* Dateisystem. `istBelegt(pfad)` sagt, ob dieser Pfad schon existiert (der
|
|
130
|
+
* Aufrufer entscheidet, WIE das geprueft wird); diese Funktion zaehlt vom
|
|
131
|
+
* Basisnamen aus hoch, bis eine freie Stelle gefunden ist.
|
|
132
|
+
*
|
|
133
|
+
* 🔴 **Ruling T8-2 (Review-Fund 1, Fix-Runde 1).** Vorher gab `sichereDatei`
|
|
134
|
+
* bei einem belegten Ziel `{ uebersprungen: true }` zurueck und schrieb dabei
|
|
135
|
+
* GAR NICHTS -- eine zweite Aenderung am selben Tag verschwand beim
|
|
136
|
+
* anschliessenden Ueberschreiben spurlos, waehrend die Ausgabe im Befehl
|
|
137
|
+
* "gesichert" behauptete. **Diese Funktion darf deshalb NIE einen belegten
|
|
138
|
+
* Pfad als Endergebnis zurueckgeben und NIE einen Pfad, der schon existiert,
|
|
139
|
+
* unkommentiert ueberschreiben lassen** -- sie liefert IMMER einen Pfad, der
|
|
140
|
+
* (laut `istBelegt`) frei ist.
|
|
141
|
+
*
|
|
142
|
+
* ⚠️ **Die erste Sicherung eines Tages -- der Basisname -- bleibt fuer immer
|
|
143
|
+
* unangetastet.** Das ist US-7 woertlich: "Laeuft der Befehl am selben Tag
|
|
144
|
+
* ein zweites Mal, wird die erste Sicherung NICHT ueberschrieben." Wer diese
|
|
145
|
+
* Funktion vereinfacht ("dann eben doch den Basisnamen nehmen, wenn `nummer`
|
|
146
|
+
* gross wird") loescht genau die Sicherung, die US-7 ausdruecklich schuetzt.
|
|
147
|
+
* Eine dritte, vierte, ... Aenderung bekommt stattdessen einen weiteren,
|
|
148
|
+
* eigenen Namen daneben -- nie einen schon vergebenen.
|
|
149
|
+
*/
|
|
150
|
+
export function naechstesFreiesSicherungsziel(basisZielRel, istBelegt) {
|
|
151
|
+
if (!istBelegt(basisZielRel))
|
|
152
|
+
return basisZielRel;
|
|
153
|
+
for (let nummer = 2; nummer <= 1000; nummer++) {
|
|
154
|
+
const kandidat = nummeriertesZiel(basisZielRel, nummer);
|
|
155
|
+
if (!istBelegt(kandidat))
|
|
156
|
+
return kandidat;
|
|
157
|
+
}
|
|
158
|
+
/*
|
|
159
|
+
Praktisch unerreichbar (1000 Sicherungen derselben Datei am selben Tag).
|
|
160
|
+
Diese reine Funktion wirft nicht -- sie liefert den letzten geprueften
|
|
161
|
+
Namen, und der Aufrufer (`sichereDatei`) trifft beim tatsaechlichen
|
|
162
|
+
Schreiben erneut auf `existsSync` und meldet dort einen echten Fehler,
|
|
163
|
+
statt dass eine reine Rechnung selbst eine Ausnahme auslöst.
|
|
164
|
+
*/
|
|
165
|
+
return nummeriertesZiel(basisZielRel, 1000);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* `kontext/vorher-2026-08-21/firma.md` + 2 -> `.../firma-2.md`.
|
|
169
|
+
*
|
|
170
|
+
* Nur der DATEINAME bekommt die Nummer, nicht der Ordner -- alle
|
|
171
|
+
* verwalteten Dateien landen weiter FLACH im selben `vorher-<datum>/`
|
|
172
|
+
* (unveraendert gegenueber `sicherungsZiel`).
|
|
173
|
+
*/
|
|
174
|
+
function nummeriertesZiel(basisZielRel, nummer) {
|
|
175
|
+
const teile = basisZielRel.split("/");
|
|
176
|
+
const dateiname = teile.pop() ?? "";
|
|
177
|
+
const punkt = dateiname.lastIndexOf(".");
|
|
178
|
+
const name = punkt === -1 ? dateiname : dateiname.slice(0, punkt);
|
|
179
|
+
const endung = punkt === -1 ? "" : dateiname.slice(punkt);
|
|
180
|
+
return [...teile, `${name}-${nummer}${endung}`].join("/");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Die lokale Fassung von `pfad` nach `kontext/vorher-<datum>/` sichern --
|
|
184
|
+
* FLACH, mit dem Basisnamen (`sicherungsZiel`), oder -- ist der schon belegt
|
|
185
|
+
* -- unter einem durchnummerierten Namen DANEBEN (`naechstesFreiesSicherungsziel`,
|
|
186
|
+
* Ruling T8-2).
|
|
187
|
+
*
|
|
188
|
+
* 🔴 **Es wird NIE uebersprungen und NIE ueberschrieben.** Beides waere eine
|
|
189
|
+
* stille Datenvernichtung: Ueberspringen ohne Kopie loescht die zweite
|
|
190
|
+
* Aenderung eines Tages beim anschliessenden Ueberschreiben der Arbeitsdatei;
|
|
191
|
+
* Ueberschreiben der ersten Sicherung verstoesst woertlich gegen US-7.
|
|
192
|
+
* `nebenVorhandener: true` im Ergebnis sagt dem Aufrufer, dass HEUTE schon
|
|
193
|
+
* eine andere Sicherung derselben Datei existiert -- er muss das in der
|
|
194
|
+
* Ausgabe unterscheiden (§8: kein Fehlerfall traegt den Namen eines
|
|
195
|
+
* Erfolgsfalls, und hier sind es zwei verschiedene ECHTE Erfolgsfaelle).
|
|
196
|
+
*/
|
|
197
|
+
export function sichereDatei(ordner, pfad, inhalt, datum) {
|
|
198
|
+
const basisZielRel = sicherungsZiel(datum, pfad);
|
|
199
|
+
const istBelegt = (zielRel) => existsSync(join(ordner, ...zielRel.split("/")));
|
|
200
|
+
const zielRel = naechstesFreiesSicherungsziel(basisZielRel, istBelegt);
|
|
201
|
+
const ort = join(ordner, ...zielRel.split("/"));
|
|
202
|
+
try {
|
|
203
|
+
mkdirSync(dirname(ort), { recursive: true });
|
|
204
|
+
writeFileSync(ort, inhalt, "utf8");
|
|
205
|
+
return { ok: true, ziel: zielRel, nebenVorhandener: zielRel !== basisZielRel };
|
|
206
|
+
}
|
|
207
|
+
catch (fehler) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
meldung: `Sichern von „${pfad}" ist fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}. Das liegt an diesem Rechner, nicht am Server -- Platz und Rechte pruefen.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Eine der verwalteten Dateien schreiben.
|
|
216
|
+
*
|
|
217
|
+
* 🔴 **Positivliste, keine Verbotsliste** (Auflage aus dem Brief): `pfad` muss
|
|
218
|
+
* WOERTLICH einer der Eintraege aus `VERWALTETE_DATEIEN` sein, sonst wird
|
|
219
|
+
* NICHTS geschrieben. Diese Funktion bekommt `pfad` im echten Ablauf immer aus
|
|
220
|
+
* der eigenen Iteration ueber `VERWALTETE_DATEIEN` (nie roh aus der Server-
|
|
221
|
+
* Antwort) -- die Pruefung hier ist trotzdem ausdruecklich verlangt und bleibt
|
|
222
|
+
* als zweites Netz stehen, falls sich der Aufrufer einmal aendert.
|
|
223
|
+
*
|
|
224
|
+
* ⚠️ `istUnsichererPfad` zusaetzlich, aus demselben Grund wie in
|
|
225
|
+
* `dateienSchreiben` (`arbeitsplatz-dateien.ts`): Ein Pfad kommt letztlich aus
|
|
226
|
+
* einer Server-Antwort, und eine Pruefung, die nur dort gilt, wo man sie
|
|
227
|
+
* gerade braucht, ist keine Pruefung dieser Funktion.
|
|
228
|
+
*/
|
|
229
|
+
export function schreibeVerwalteteDatei(ordner, pfad, inhalt) {
|
|
230
|
+
if (!VERWALTETE_DATEIEN.includes(pfad)) {
|
|
231
|
+
return { ok: false, meldung: `„${pfad}" ist keine verwaltete Datei -- wird nicht geschrieben. Bitte melden.` };
|
|
232
|
+
}
|
|
233
|
+
if (istUnsichererPfad(pfad)) {
|
|
234
|
+
return { ok: false, meldung: `„${pfad}" ist kein sicherer Pfad -- wird nicht geschrieben. Bitte melden.` };
|
|
235
|
+
}
|
|
236
|
+
const ort = join(ordner, ...pfad.split("/"));
|
|
237
|
+
try {
|
|
238
|
+
mkdirSync(dirname(ort), { recursive: true });
|
|
239
|
+
writeFileSync(ort, inhalt, "utf8");
|
|
240
|
+
return { ok: true };
|
|
241
|
+
}
|
|
242
|
+
catch (fehler) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
meldung: `Schreiben von „${pfad}" ist fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}. Das liegt an diesem Rechner, nicht am Server -- Platz und Rechte pruefen.`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|