kimal3 0.1.5 → 0.1.6
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 +19 -1
- package/dist/api.js +61 -1
- package/dist/arbeitsplatz-dateien.js +268 -0
- package/dist/befehle/arbeitsplatz.js +209 -233
- package/dist/befehle/skill-install.js +110 -4
- package/dist/befehle/werkbank.js +389 -0
- package/dist/befehle-katalog.js +49 -3
- package/dist/gesehen.js +85 -0
- package/dist/kimal3.js +169 -13
- package/dist/nachricht.js +198 -0
- package/dist/version.js +37 -0
- package/dist/werkbank-name.js +217 -0
- package/package.json +1 -1
- package/skill/SKILL.md +60 -5
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { dateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
|
|
4
|
+
import { werkbankOrdnerAus, werkbankUmbenennen } from "../werkbank-name.js";
|
|
5
|
+
/**
|
|
6
|
+
* Legt **nur eine Werkbank** an — in einem Firmen-Ordner, der schon steht.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 **Warum es diesen Befehl gibt** (gefunden von Dennis am 2026-08-20, im
|
|
9
|
+
* Rollenspiel als Angestellter einer Kundenfirma — nicht von einem Prüfer, und
|
|
10
|
+
* nicht in fünf Review-Runden). Gemessen:
|
|
11
|
+
*
|
|
12
|
+
* Im bestehenden Firmen-Ordner nochmal `kimal3 arbeitsplatz neu`
|
|
13
|
+
* → „Der Ordner gibt es schon und ist nicht leer. Gib einen anderen
|
|
14
|
+
* Namen an."
|
|
15
|
+
* Also gibt er einen an: `kimal3 arbeitsplatz neu werkbank-sabine`
|
|
16
|
+
* → ✓ 22 Dateien, darunter `werkbank-sabine/kontext/`
|
|
17
|
+
*
|
|
18
|
+
* Er bekommt **keinen Fehler**, sondern eine ZWEITE Firma im Bauch der ersten:
|
|
19
|
+
* eigene `firma.md`, eigene `marke.md`, eigene `sicherheit.md`. Und der
|
|
20
|
+
* Fehlertext des Befehls **schlägt diesen Weg selbst vor**.
|
|
21
|
+
*
|
|
22
|
+
* 🔑 **Die Vorlage wusste längst, dass es anders sein sollte.** Die
|
|
23
|
+
* Firmen-`.gitignore` erklärt in ihrem eigenen Kommentar die Ordner
|
|
24
|
+
* `werkbank-<name>` als Geschwister neben `kontext/`. *Der Entwurf sah eine
|
|
25
|
+
* Form vor, die die CLI nicht herstellen konnte.*
|
|
26
|
+
*
|
|
27
|
+
* arbeitsplatz neu → einmal pro FIRMA (die ganze Familie)
|
|
28
|
+
* werkbank neu → einmal pro MENSCH (nur der eigene Teil)
|
|
29
|
+
*
|
|
30
|
+
* 🔴 **GRENZE, die dieser Befehl heute NICHT aufhebt** (Review-Runde 4 zu
|
|
31
|
+
* PR #25, nachgemessen an `src/lib/pro/zugang.ts`): Er holt die Vorlage über
|
|
32
|
+
* `/api/cli/arbeitsplatz`, und die Tür dort fragt `istPro(user_id)` — eine
|
|
33
|
+
* Zeile in `konto_plan`, **pro Person**. Es gibt keinen Weg über
|
|
34
|
+
* `memberships`/`organizations`.
|
|
35
|
+
*
|
|
36
|
+
* Der zweite Mensch einer Kundenfirma — also genau der, für den dieser Befehl
|
|
37
|
+
* gebaut wurde — braucht damit ein **eigenes Pro-Konto**. Hat er keins,
|
|
38
|
+
* bekommt er `402` und Rückgabewert `4`, und `SKILL.md` weist Claude dort
|
|
39
|
+
* ausdrücklich an, NICHT zu wiederholen.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ Das ist **keine Panne, sondern eine offene Produktfrage**: Erbt ein
|
|
42
|
+
* Firmenmitglied den Zugang, oder kauft jeder einzeln? Sie gehört Dennis, nicht
|
|
43
|
+
* diesem Befehl — deshalb steht sie in `WEITER-HIER.md` unter „Entscheidungen"
|
|
44
|
+
* und nicht als stille Erweiterung von `istPro` hier drin.
|
|
45
|
+
* *Eine Grenze, die man benennt, ist etwas anderes als eine, die man
|
|
46
|
+
* übersieht* — und sie reiht sich neben die schon bekannte: Der Angestellte hat
|
|
47
|
+
* ohnehin noch keinen gebauten Weg an den Firmen-Ordner (Stufe 2).
|
|
48
|
+
*
|
|
49
|
+
* 🔴 **Die wichtigste Eigenschaft: Er fasst die Firmenebene NICHT an.** Kein
|
|
50
|
+
* einziges Byte in `kontext/`, `CLAUDE.md`, `README.md`, `.gitignore` — auch
|
|
51
|
+
* nicht schreibend „identisch drüber". Das ist keine Höflichkeit, sondern
|
|
52
|
+
* Vorbereitung: Perspektivisch soll die Firma ihre `kontext/`-Ebene zentral
|
|
53
|
+
* verwalten (s. `WEITER-HIER.md`, Entscheidungen). Was dann von außen kommt,
|
|
54
|
+
* darf ein lokaler Befehl nicht überschreiben.
|
|
55
|
+
*/
|
|
56
|
+
const HILFE = `
|
|
57
|
+
kimal3 werkbank neu [dein-vorname]
|
|
58
|
+
|
|
59
|
+
Legt eine eigene Werkbank in einem bestehenden Firmen-Ordner an.
|
|
60
|
+
Die Firmenebene (kontext/, CLAUDE.md, README.md) wird NICHT angefasst.
|
|
61
|
+
|
|
62
|
+
dein-vorname Optional. Der Ordner traegt deinen Namen -- aus "sabine"
|
|
63
|
+
wird "werkbank-sabine". Ohne Angabe steht dort der Name,
|
|
64
|
+
den der Server aus deiner Anmelde-Adresse gebildet hat.
|
|
65
|
+
|
|
66
|
+
🔴 Bricht ab, wenn der Zielordner schon existiert und NICHT leer ist.
|
|
67
|
+
|
|
68
|
+
Du hast noch gar keinen Firmen-Ordner?
|
|
69
|
+
kimal3 arbeitsplatz neu <dein-vorname>
|
|
70
|
+
`;
|
|
71
|
+
/**
|
|
72
|
+
* 🔴 **Review-Runde 2 zu PR #25 — und es ist DERSELBE Fehler wie in Runde 1,
|
|
73
|
+
* nur eine Datei weiter.** Hier stand:
|
|
74
|
+
*
|
|
75
|
+
* Gib einen anderen Namen an:
|
|
76
|
+
* kimal3 werkbank neu <ein-anderer-vorname>
|
|
77
|
+
*
|
|
78
|
+
* Der haeufigste Grund fuer diese Absage ist, dass der Mensch seine Werkbank
|
|
79
|
+
* **schon angelegt hat**. Befolgt er den Rat, bekommt er eine ZWEITE
|
|
80
|
+
* persoenliche Werkbank neben der ersten (`werkbank-sabine-2`) — dieselbe
|
|
81
|
+
* stille Verdopplung, die dieser PR bei `arbeitsplatz neu` gerade beseitigt
|
|
82
|
+
* hat.
|
|
83
|
+
*
|
|
84
|
+
* *Ich habe den Fehler bei einem Befehl behoben und ihn beim Zwilling stehen
|
|
85
|
+
* lassen — obwohl die Behebung des einen der Anlass fuer den anderen war.*
|
|
86
|
+
*
|
|
87
|
+
* Der Rat nennt jetzt zuerst den wahrscheinlichen Fall (du bist es selbst) und
|
|
88
|
+
* dann den seltenen (du bist es nicht).
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ Und er ist eine FUNKTION, keine Konstante -- aus demselben Grund wie beim
|
|
91
|
+
* Zwilling in `arbeitsplatz.ts`: Der Befehl kennt den Ordner, in den er schickt.
|
|
92
|
+
* Ein Platzhalter wie `cd <der-ordner-oben>` waere eine Luecke, die der Mensch
|
|
93
|
+
* fuellen muss, obwohl die Antwort danebensteht. *Ein Platzhalter ist ein
|
|
94
|
+
* Eingestaendnis, dass man es nicht weiss -- hier weiss man es.*
|
|
95
|
+
*/
|
|
96
|
+
function hinweisFuer(ordnerName) {
|
|
97
|
+
const ziel = ordnerName.includes(" ") ? `"${ordnerName}"` : ordnerName;
|
|
98
|
+
return (" Ist das DEINE Werkbank? Dann steht sie schon -- geh einfach hinein:\n" +
|
|
99
|
+
` cd ${ziel}\n` +
|
|
100
|
+
" Bist du es nicht, nimm deinen eigenen Vornamen:\n" +
|
|
101
|
+
" kimal3 werkbank neu <dein-vorname>");
|
|
102
|
+
}
|
|
103
|
+
export async function werkbankNeu(argv) {
|
|
104
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
105
|
+
console.log(HILFE);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
/*
|
|
109
|
+
Dieselben drei Absagen wie bei `arbeitsplatz neu` -- bewusst wortgleich: Wer
|
|
110
|
+
den einen Befehl kennt, soll beim anderen nichts Neues lernen muessen.
|
|
111
|
+
|
|
112
|
+
⚠️ Sie sprechen seit dem 2026-08-20 von einem NAMEN statt von einem
|
|
113
|
+
Ordnernamen. Die alte Hilfe hier hat ausdruecklich
|
|
114
|
+
`kimal3 werkbank neu werkbank-sabine` beigebracht -- deshalb schneidet
|
|
115
|
+
`werkbankOrdnerAus` ein bereits mitgetipptes `werkbank-` wieder ab, statt
|
|
116
|
+
`werkbank-werkbank-sabine` zu bauen. *Alte Anleitungen ueberleben laenger
|
|
117
|
+
als der Code, den sie beschreiben.*
|
|
118
|
+
*/
|
|
119
|
+
const nameArg = argv[0];
|
|
120
|
+
if (nameArg !== undefined && nameArg.trim() === "") {
|
|
121
|
+
console.error(`Ein leerer Name ist kein gueltiger Name.\n${HILFE}`);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
if (nameArg !== undefined && nameArg.startsWith("--")) {
|
|
125
|
+
console.error(`Die Option „${nameArg}" kenne ich nicht.\n${HILFE}`);
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
if (argv.length > 1) {
|
|
129
|
+
/*
|
|
130
|
+
🔴 **Der Vorschlag darf keine Optionen einsammeln** (Review-Runde 5 zu
|
|
131
|
+
PR #25). Hier stand `argv.join("-")` -- aus `werkbank neu sabine --force`
|
|
132
|
+
wurde damit der Rat `werkbank neu sabine---force`. Die `--`-Wache ein paar
|
|
133
|
+
Zeilen hoeher sieht nur `argv[0]`.
|
|
134
|
+
*Ein Rat, der ein falsches Kommando erzeugt, ist schlimmer als keiner* --
|
|
135
|
+
dieselbe Klasse wie die Absagen aus Runde 1 und 2, nur kleiner.
|
|
136
|
+
|
|
137
|
+
Deshalb: Sieht irgendein Wort wie eine Option aus, gibt es gar keinen
|
|
138
|
+
Vorschlag. Ein Mensch, der `--force` tippt, sucht keine Schreibhilfe fuer
|
|
139
|
+
seinen Vornamen.
|
|
140
|
+
*/
|
|
141
|
+
const woerter = argv.filter((w) => !w.startsWith("-"));
|
|
142
|
+
const vorschlag = woerter.length === argv.length && woerter.length > 1
|
|
143
|
+
? `Besteht dein Name aus mehreren Woertern? Dann schreib ihn zusammen:\n` +
|
|
144
|
+
` kimal3 werkbank neu ${woerter.join("-")}\n`
|
|
145
|
+
: "";
|
|
146
|
+
console.error(`Mit „${argv[1]}" kann ich hier nichts anfangen -- nur EIN Name ist erlaubt.\n` +
|
|
147
|
+
`${vorschlag}${HILFE}`);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
// `null` heisst: nach dem Umschreiben blieb kein Zeichen uebrig, das in einem
|
|
151
|
+
// Ordnernamen stehen darf. Ein Mensch hat hier getippt und verdient eine
|
|
152
|
+
// Antwort -- der Server schweigt im selben Fall und bleibt beim Vordruck.
|
|
153
|
+
let gewuenschteWerkbank = null;
|
|
154
|
+
if (nameArg !== undefined) {
|
|
155
|
+
gewuenschteWerkbank = werkbankOrdnerAus(nameArg);
|
|
156
|
+
if (gewuenschteWerkbank === null) {
|
|
157
|
+
console.error(`Aus „${nameArg}" kann ich keinen Ordnernamen machen -- es bleiben nur`);
|
|
158
|
+
console.error("Zeichen uebrig, die in keinem Ordnernamen stehen duerfen.");
|
|
159
|
+
console.error("");
|
|
160
|
+
console.error(" Nimm deinen Vornamen: kimal3 werkbank neu sabine");
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const holen = await vorlageHolen();
|
|
165
|
+
if (!holen.ok)
|
|
166
|
+
return holen.wert;
|
|
167
|
+
const daten = holen.daten;
|
|
168
|
+
const werkbank = werkbankOrdner(daten.dateien);
|
|
169
|
+
/*
|
|
170
|
+
🔴 **Review-Runde 1 zu PR #25: eine Asymmetrie zur Schwesterdatei.**
|
|
171
|
+
`arbeitsplatz.ts` prueft `daten.wurzel` ausdruecklich mit `istUnsichererPfad`
|
|
172
|
+
(„kommt genauso ROH vom Server wie jeder `datei.pfad` -- dieselbe Bremse").
|
|
173
|
+
Hier wurde `werkbank` genauso roh benutzt, nur ungeprueft.
|
|
174
|
+
|
|
175
|
+
Der Fall: Eine manipulierte Antwort (z. B. ueber ein umgebogenes
|
|
176
|
+
`KIMAL3_URL`) enthaelt den Pfad `../CLAUDE.md`. `werkbankAusDateien` zerlegt
|
|
177
|
+
ihn in zwei Teile und liefert `".."` -- und `dateienSchreiben` macht daraus
|
|
178
|
+
`resolve("..")`, also den ELTERNORDNER.
|
|
179
|
+
|
|
180
|
+
⚠️ Und die Firmen-Wache faengt das nicht: `firmenEbeneAus` gibt fuer so eine
|
|
181
|
+
Antwort eine leere Liste zurueck, `fehlend.length > 0` ist damit falsch, und
|
|
182
|
+
die Pruefung geht still durch.
|
|
183
|
+
|
|
184
|
+
🔬 **Ehrlich nachgemessen: fuer `..` haette er trotzdem nichts geschrieben.**
|
|
185
|
+
Der Angriff wurde mit einem eigenen Server nachgestellt (Antwort mit
|
|
186
|
+
`../CLAUDE.md`), einmal mit und einmal ohne diese Bremse. Ohne sie faengt ihn
|
|
187
|
+
der Nicht-leer-Riegel in `dateienSchreiben`: Ein Elternordner enthaelt immer
|
|
188
|
+
mindestens den Ordner, in dem man steht, ist also nie leer.
|
|
189
|
+
|
|
190
|
+
🔴 **Fuer `""` stimmte das NICHT -- und genau das stand hier als Begruendung**
|
|
191
|
+
(Review-Runde 2). Der Satz lautete, `""` fuehre auf das aktuelle Verzeichnis,
|
|
192
|
+
„das `kontext/` enthaelt, sonst waere die Wache oben schon rot". Ist sie
|
|
193
|
+
aber nicht: Bei einer Antwort aus `/CLAUDE.md` liefert `werkbankAusDateien`
|
|
194
|
+
den leeren Teil `""`, und `firmenEbeneAus` UEBERSPRINGT genau diese Pfade
|
|
195
|
+
(`teile[0] === werkbank`) -- die Firmen-Wache sieht `[]` und laesst durch.
|
|
196
|
+
In einem leeren Ordner waeren die Dateien dann dorthin gelandet, wo der
|
|
197
|
+
Mensch steht.
|
|
198
|
+
*Ein Kommentar, der eine Luecke mit einer anderen Wache begruendet, muss
|
|
199
|
+
diese Wache im FEHLERFALL gelesen haben, nicht im Normalfall.*
|
|
200
|
+
Deshalb prueft hier jetzt `istUnsichererOrdnername` statt
|
|
201
|
+
`istUnsichererPfad`: Ein Ordnername ist EIN Pfadteil, und der ist weder leer
|
|
202
|
+
noch `.` noch `..`.
|
|
203
|
+
|
|
204
|
+
🔑 **Der Gewinn ist deshalb die MELDUNG, nicht die Abwehr.** Ohne diese
|
|
205
|
+
Zeile las der Mensch *„Der Ordner `..` gibt es schon und ist nicht leer. Gib
|
|
206
|
+
einen anderen Namen an"* -- und sucht den Fehler bei sich, obwohl der Server
|
|
207
|
+
luegt. Jetzt steht dort `Bitte melden`, und der Rueckgabewert ist `3` statt
|
|
208
|
+
`1`, also „Stoerung" statt „deine Eingabe passt nicht". *Kein Fehlerfall
|
|
209
|
+
traegt den Namen eines anderen* -- und dass die Abwehr schon anderswo steht,
|
|
210
|
+
ist kein Grund, sie hier falsch zu benennen.
|
|
211
|
+
*/
|
|
212
|
+
if (werkbank !== null && istUnsichererOrdnername(werkbank)) {
|
|
213
|
+
console.error(`Die Antwort enthaelt einen unsicheren Ordnernamen: „${werkbank}". Bitte melden.`);
|
|
214
|
+
return 3;
|
|
215
|
+
}
|
|
216
|
+
if (werkbank === null) {
|
|
217
|
+
console.error("In der Vorlage steckt kein Werkbank-Ordner -- damit kann ich hier nichts anlegen.");
|
|
218
|
+
console.error("Bitte melden.");
|
|
219
|
+
return 3;
|
|
220
|
+
}
|
|
221
|
+
/*
|
|
222
|
+
🔑 **Die Prüfung, ob wir überhaupt in einer Firma stehen** -- und sie ist
|
|
223
|
+
keine Förmlichkeit: In `<werkbank>/.claude/settings.json` steht
|
|
224
|
+
`"additionalDirectories": ["../kontext"]`. Eine Werkbank ohne Firmenebene
|
|
225
|
+
daneben zeigt damit ins Leere, und Claude fände den Firmenkontext nie.
|
|
226
|
+
Das merkt man erst Tage später -- genau die Sorte Fehler, gegen die dieser
|
|
227
|
+
Befehl gebaut wurde.
|
|
228
|
+
|
|
229
|
+
⚠️ Welche Ordner das sind, wird ABGELEITET und nicht getippt (dieselbe Regel
|
|
230
|
+
wie bei `werkbankOrdner`): Firmenebene ist alles, was in der Lieferung
|
|
231
|
+
NEBEN dem Werkbank-Ordner in einem eigenen Unterordner liegt. Heute ist das
|
|
232
|
+
`kontext`. Wird die Vorlage umgebaut, wandert diese Prüfung mit -- statt
|
|
233
|
+
auf einen Ordnernamen zu zeigen, den es nicht mehr gibt.
|
|
234
|
+
*/
|
|
235
|
+
const firmenOrdner = firmenEbeneAus(daten.dateien, werkbank);
|
|
236
|
+
const fehlend = firmenOrdner.filter((o) => !istOrdner(o));
|
|
237
|
+
/*
|
|
238
|
+
🔴 **Die leere Liste ist ein eigener Fall** (Review-Runde 3 zu PR #25).
|
|
239
|
+
Vorher stand hier nur `fehlend.length > 0` -- und eine LEERE Liste hat
|
|
240
|
+
nichts, was fehlen koennte. Liefert die Antwort keine Firmenebene, ging die
|
|
241
|
+
Wache still durch und legte die Werkbank in irgendein Verzeichnis, in dem
|
|
242
|
+
der Mensch gerade steht. Ihr `../kontext` zeigte dann ins Leere.
|
|
243
|
+
*Eine Wache, die aus einer leeren Liste „alles in Ordnung" liest, hat die
|
|
244
|
+
Frage nicht gestellt, fuer die sie gebaut wurde.*
|
|
245
|
+
|
|
246
|
+
Heute nicht ausloesbar (`check:arbeitsplatz` Pruefung 2 erzwingt `kontext/*`)
|
|
247
|
+
-- erreichbar also nur ueber eine kaputte oder manipulierte Antwort.
|
|
248
|
+
*/
|
|
249
|
+
/*
|
|
250
|
+
🔴 **ZWEI Wirklichkeiten, zwei Ausgaenge** (Review-Runde 4 zu PR #25, und der
|
|
251
|
+
Fehler stammt aus dem Fix von Runde 3).
|
|
252
|
+
|
|
253
|
+
Runde 3 hat `firmenOrdner.length === 0` mit in dieselbe Bedingung gehaengt.
|
|
254
|
+
Dieser Zweig hat per Definition NICHTS in `fehlend` -- `join` lieferte `""`,
|
|
255
|
+
und der Mensch las woertlich *„Hier steht kein Firmen-Ordner -- ich sehe
|
|
256
|
+
kein ."* Beim Reparieren fiel der zweite Teil auf: Danach folgten
|
|
257
|
+
unveraendert die Raete fuer jemanden, der am FALSCHEN ORT steht („Geh
|
|
258
|
+
dorthin", „Bist du der ERSTE in deiner Firma?"). Die helfen nicht, wenn der
|
|
259
|
+
SERVER kaputt ist.
|
|
260
|
+
|
|
261
|
+
*Ein Fehlerfall darf nie denselben Namen tragen wie ein anderer* -- und der
|
|
262
|
+
Rueckgabewert ist Teil des Namens: `3` heisst laut `cli/skill/SKILL.md`
|
|
263
|
+
„Stoerung, gleich noch einmal", `1` heisst „deine Eingabe passt nicht".
|
|
264
|
+
Eine kaputte Lieferung ist keine falsche Eingabe.
|
|
265
|
+
*/
|
|
266
|
+
if (firmenOrdner.length === 0) {
|
|
267
|
+
console.error("Die Vorlage vom Server nennt gar keine Firmenebene -- damit kann ich");
|
|
268
|
+
console.error("nicht pruefen, ob du an der richtigen Stelle stehst.");
|
|
269
|
+
console.error("Das liegt nicht an dir. Bitte melden.");
|
|
270
|
+
return 3;
|
|
271
|
+
}
|
|
272
|
+
if (fehlend.length > 0) {
|
|
273
|
+
console.error("Hier steht kein Firmen-Ordner -- ich sehe kein " + fehlend.join(", ") + ".");
|
|
274
|
+
console.error("Eine Werkbank braucht die Firmenebene daneben; ohne sie findet Claude");
|
|
275
|
+
console.error("euren Firmenkontext nicht.");
|
|
276
|
+
console.error("");
|
|
277
|
+
console.error(" Steht er woanders? Geh dorthin und versuch es dort noch einmal.");
|
|
278
|
+
console.error("");
|
|
279
|
+
/*
|
|
280
|
+
🔴 **Hier stand „Du hast noch keinen? kimal3 arbeitsplatz neu"**
|
|
281
|
+
(Befund Dennis, 2026-08-20, wenige Minuten nach dem Bauen). Das ist
|
|
282
|
+
derselbe falsche Rat wie der aus `SKILL.md`, nur eine Ebene hoeher:
|
|
283
|
+
|
|
284
|
+
Fuer den ERSTEN Menschen einer Firma stimmt er. Fuer den ZWEITEN --
|
|
285
|
+
also genau den, fuer den dieser Befehl gebaut wurde -- legt er eine
|
|
286
|
+
leere Zweitfirma an, statt ihn an die echte zu bringen. Und diese
|
|
287
|
+
zweite Firma ist schlimmer als die verschachtelte von vorhin: Sie
|
|
288
|
+
sieht voellig richtig aus.
|
|
289
|
+
|
|
290
|
+
*Ich habe den Fehler aus SKILL.md entfernt und ihn in meiner eigenen
|
|
291
|
+
Absage wieder eingebaut. Ein Rat, der fuer die Haelfte der Leser
|
|
292
|
+
falsch ist, muss die Haelften benennen.*
|
|
293
|
+
|
|
294
|
+
⚠️ Der zweite Fall hat heute KEINE gute Antwort -- der Angestellte hat
|
|
295
|
+
keinen gebauten Weg an den Firmen-Ordner seiner Firma (s. WEITER-HIER,
|
|
296
|
+
"Verwaltet die Firma ihre kontext/-Ebene kuenftig in der App?"). Solange
|
|
297
|
+
das so ist, ist die ehrliche Auskunft „frag, wo er liegt" -- und nicht
|
|
298
|
+
ein Befehl, der etwas Falsches anlegt.
|
|
299
|
+
*/
|
|
300
|
+
console.error(" Bist du der ERSTE in deiner Firma?");
|
|
301
|
+
console.error(" kimal3 arbeitsplatz neu <dein-vorname>");
|
|
302
|
+
console.error("");
|
|
303
|
+
console.error(" Hat deine Firma schon einen? Dann hol dir DEN -- z. B. als Kopie");
|
|
304
|
+
console.error(" oder ueber euer Repo. Leg keinen neuen an, sonst habt ihr zwei");
|
|
305
|
+
console.error(" verschiedene Firmenordner mit verschiedenen Regeln darin.");
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
/*
|
|
309
|
+
🔑 Der Zielordner ist der Werkbank-Name -- entweder der, den der Server
|
|
310
|
+
schon gesetzt hat (`werkbank`, aus der Anmelde-Adresse gebildet), oder der
|
|
311
|
+
mitgegebene. Der Ordner TRAEGT damit immer einen Namen, und genau darum ging
|
|
312
|
+
es bei der Umbenennung: *Wo ein Name im Ordner steht, gehoert er einer
|
|
313
|
+
Person; wo keiner steht, gehoert er allen.*
|
|
314
|
+
*/
|
|
315
|
+
const ordnerName = gewuenschteWerkbank ?? werkbank;
|
|
316
|
+
/*
|
|
317
|
+
Erst UMBENENNEN, dann ausschneiden -- und zwar auch im Inhalt.
|
|
318
|
+
|
|
319
|
+
⚠️ **Hier stand: „Die Dateien muessen dafuer nicht umgeschrieben werden."**
|
|
320
|
+
Der Satz war wahr, als ich ihn schrieb: Heute nennt keine Datei unterhalb der
|
|
321
|
+
Werkbank ihren eigenen Ordner (nachgemessen), und die Texte, die ihn nennen,
|
|
322
|
+
gehoeren zur Firmenebene, die dieser Befehl nicht anfasst.
|
|
323
|
+
|
|
324
|
+
Nur HAELT das nichts. Wer morgen in die Werkbank-`CLAUDE.md` schreibt „deine
|
|
325
|
+
Projekte liegen in `werkbank-sabine/projekte/`", baut damit einen Satz ein,
|
|
326
|
+
der beim naechsten Menschen auf den falschen Ordner zeigt -- ohne dass etwas
|
|
327
|
+
fehlschlaegt. *Die Prueffrage aus CLAUDE.md: Beschreibt der Kommentar ein
|
|
328
|
+
Verhalten, das jemand WILL -- oder nur eines, das PASSIERT?* Hier passierte
|
|
329
|
+
es. Also nehmen wir die Annahme heraus, statt eine Wache um sie zu bauen.
|
|
330
|
+
|
|
331
|
+
Die Reihenfolge ist der ganze Trick: Nach dem Umbenennen heisst der Praefix
|
|
332
|
+
schon wie das Ziel, und der Ausschnitt schneidet ihn mit derselben
|
|
333
|
+
Zeichenkette weg, die vorher eingesetzt wurde. Aus `werkbank-jonas/CLAUDE.md`
|
|
334
|
+
wird `CLAUDE.md`, das Ziel ist der neue Ordner selbst.
|
|
335
|
+
|
|
336
|
+
🔑 Das ist die einzige Stelle, an der sich dieser Befehl vom grossen
|
|
337
|
+
unterscheidet -- alles andere (Ueberschreib-Schutz, Pfad-Pruefung, Fehler-
|
|
338
|
+
Trennung) ist dieselbe Maschinerie aus `arbeitsplatz-dateien.ts`.
|
|
339
|
+
*/
|
|
340
|
+
const umbenannt = werkbankUmbenennen(daten.dateien, werkbank, ordnerName);
|
|
341
|
+
const praefix = `${ordnerName}/`;
|
|
342
|
+
const meine = umbenannt
|
|
343
|
+
.filter((d) => d.pfad.startsWith(praefix))
|
|
344
|
+
.map((d) => ({ pfad: d.pfad.slice(praefix.length), inhalt: d.inhalt }));
|
|
345
|
+
const wert = dateienSchreiben(meine, ordnerName, hinweisFuer(ordnerName));
|
|
346
|
+
if (wert !== 0)
|
|
347
|
+
return wert;
|
|
348
|
+
console.log(`✓ ${meine.length} ${meine.length === 1 ? "Datei" : "Dateien"} angelegt.`);
|
|
349
|
+
console.log(` ${resolve(ordnerName)}`);
|
|
350
|
+
console.log("");
|
|
351
|
+
console.log(" Die Firmenebene ist unberuehrt geblieben.");
|
|
352
|
+
console.log("");
|
|
353
|
+
console.log(" Weiter geht's:");
|
|
354
|
+
console.log(` cd ${ordnerName.includes(" ") ? `"${ordnerName}"` : ordnerName}`);
|
|
355
|
+
console.log(" claude");
|
|
356
|
+
console.log(' dann "Lass uns einrichten" tippen');
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Die Ordner der Firmenebene, aus der Lieferung abgeleitet: jeder erste
|
|
361
|
+
* Pfad-Teil, der einen Unterordner benennt und nicht die Werkbank ist.
|
|
362
|
+
*
|
|
363
|
+
* Dateien direkt an der Wurzel (`CLAUDE.md`, `README.md`, `.gitignore`) zaehlen
|
|
364
|
+
* bewusst NICHT mit: Sie sagen weniger verlaesslich, dass hier eine Firma steht
|
|
365
|
+
* -- eine `CLAUDE.md` liegt in fast jedem Projektordner.
|
|
366
|
+
*/
|
|
367
|
+
function firmenEbeneAus(dateien, werkbank) {
|
|
368
|
+
const ordner = new Set();
|
|
369
|
+
for (const datei of dateien) {
|
|
370
|
+
const teile = datei.pfad.split("/");
|
|
371
|
+
if (teile.length < 2)
|
|
372
|
+
continue;
|
|
373
|
+
if (teile[0] === werkbank)
|
|
374
|
+
continue;
|
|
375
|
+
ordner.add(teile[0]);
|
|
376
|
+
}
|
|
377
|
+
return [...ordner].sort();
|
|
378
|
+
}
|
|
379
|
+
function istOrdner(name) {
|
|
380
|
+
try {
|
|
381
|
+
return existsSync(name) && statSync(name).isDirectory();
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Nicht lesbar heisst "kann ich nicht bestaetigen" -- und was ich nicht
|
|
385
|
+
// bestaetigen kann, behandle ich als fehlend. Lieber eine Absage zu viel
|
|
386
|
+
// als eine Werkbank, die ins Leere zeigt.
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
package/dist/befehle-katalog.js
CHANGED
|
@@ -49,15 +49,39 @@ export const BEFEHLE = [
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
befehl: "kimal3 arbeitsplatz neu",
|
|
52
|
-
beschreibung: "
|
|
52
|
+
beschreibung: "Den ganzen KI-Arbeitsplatz anlegen — Firmenebene und Werkbank",
|
|
53
53
|
wer: "du",
|
|
54
|
-
rhythmus: "einmal pro
|
|
54
|
+
rhythmus: "einmal pro Firma",
|
|
55
55
|
tippen: "! kimal3 arbeitsplatz neu",
|
|
56
56
|
terminal: "kimal3 arbeitsplatz neu",
|
|
57
57
|
details: [
|
|
58
58
|
"Holt die Vorlage von deinem Pro-Konto und legt sie hier als echten Ordner an — CLAUDE.md, Takt-Befehle, die Werkbank für deine Projekte. Dasselbe Paket, das auch als Download auf /pro liegt.",
|
|
59
|
-
"
|
|
59
|
+
"Der Ordner heißt immer KI-Arbeitsplatz — mehrere Firmen trennst du über den Ordner darüber, z. B. Kunden/ARNO/KI-Arbeitsplatz. Gib deinen Vornamen mit, dann heißt deine Werkbank darin werkbank-sabine: kimal3 arbeitsplatz neu sabine.",
|
|
60
60
|
"Gibt es den Zielordner schon und ist er nicht leer, bricht der Befehl ab, statt etwas zu überschreiben — deine Arbeit ist sicher. Geklappt hat es, wenn er die Zahl der angelegten Dateien nennt.",
|
|
61
|
+
"⚠️ Er legt auch die Firmenebene an (kontext/ mit Marke, Systemen, Sicherheit). Steht die schon, brauchst du kimal3 werkbank neu — sonst entsteht eine zweite Firma in der ersten.",
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
/*
|
|
65
|
+
🔴 Der Eintrag steht direkt NEBEN `arbeitsplatz neu` und nicht am Ende der
|
|
66
|
+
Liste (die Reihenfolge ist laut Kopf dieser Datei „der Weg, den ein Mensch
|
|
67
|
+
geht"). Genau hier steht der Mensch vor der Verwechslung: Beide legen
|
|
68
|
+
Ordner aus derselben Vorlage an, und die falsche Wahl merkt man erst Tage
|
|
69
|
+
spaeter. Nebeneinander sind die verschiedenen `rhythmus`-Angaben --
|
|
70
|
+
„einmal pro Firma" gegen „einmal pro Mensch" -- die kuerzeste Erklaerung
|
|
71
|
+
des Unterschieds, die es gibt.
|
|
72
|
+
*/
|
|
73
|
+
{
|
|
74
|
+
befehl: "kimal3 werkbank neu",
|
|
75
|
+
beschreibung: "Nur eine eigene Werkbank in einer bestehenden Firma anlegen",
|
|
76
|
+
wer: "du",
|
|
77
|
+
rhythmus: "einmal pro Mensch",
|
|
78
|
+
tippen: "! kimal3 werkbank neu",
|
|
79
|
+
terminal: "kimal3 werkbank neu",
|
|
80
|
+
details: [
|
|
81
|
+
"Für alle außer der ersten Person: Der Firmen-Ordner steht schon (kontext/ mit Marke, Systemen, Sicherheit) — du brauchst nur deinen eigenen Arbeitsplatz darin.",
|
|
82
|
+
"🔴 Er fasst die Firmenebene nicht an. Kein Byte in kontext/, CLAUDE.md oder README.md — auch nicht schreibend „identisch drüber“.",
|
|
83
|
+
"Er läuft nur, wenn du wirklich in einem Firmen-Ordner stehst. Fehlt die Firmenebene daneben, bricht er ab und sagt es — eine Werkbank ohne sie zeigt ins Leere, weil ihre Einstellungen auf ../kontext verweisen.",
|
|
84
|
+
"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.",
|
|
61
85
|
],
|
|
62
86
|
},
|
|
63
87
|
{
|
|
@@ -131,3 +155,25 @@ ${zeilen}
|
|
|
131
155
|
Mehr zu einem Befehl: kimal3 projekt neu --help
|
|
132
156
|
`;
|
|
133
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Wie ein Befehl **getippt** wird — aus dem Katalog geholt, nicht abgeschrieben.
|
|
160
|
+
*
|
|
161
|
+
* 🔑 **Warum das eine eigene Funktion ist** (2026-08-20): Auf `/pro` stand
|
|
162
|
+
* `const ANMELDEN = "! kimal3 login"` als Zeichenkette im Seitencode — eine
|
|
163
|
+
* zweite Wahrheit ueber einen Befehl, den der Katalog schon fuehrt. Wer im
|
|
164
|
+
* Katalog umbenennt, laesst sie stehen, und die Seite zeigt weiter den alten
|
|
165
|
+
* Aufruf. Dieselbe Klasse Fehler wie der fest getippte `cd`-Pfad in Runde 3.
|
|
166
|
+
*
|
|
167
|
+
* ⚠️ Sie **wirft**, statt einen leeren Text zu liefern. Die Aufrufer sind
|
|
168
|
+
* Modul-Konstanten — ein falscher Name fliegt damit beim `npm run build` auf,
|
|
169
|
+
* nicht erst beim Kunden vor einem Kopier-Kasten, in dem nichts steht.
|
|
170
|
+
* *Ein stiller Fehler auf einer bezahlten Seite ist teurer als ein lauter im
|
|
171
|
+
* Build.*
|
|
172
|
+
*/
|
|
173
|
+
export function tippBefehl(befehl) {
|
|
174
|
+
const eintrag = BEFEHLE.find((b) => b.befehl === befehl);
|
|
175
|
+
if (!eintrag) {
|
|
176
|
+
throw new Error(`Kein Katalog-Eintrag fuer „${befehl}". Steht der Befehl noch in BEFEHLE?`);
|
|
177
|
+
}
|
|
178
|
+
return eintrag.tippen;
|
|
179
|
+
}
|
package/dist/gesehen.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Welche Nachricht an welchem Tag schon im Terminal stand.
|
|
6
|
+
*
|
|
7
|
+
* 🔑 WARUM EINE EIGENE DATEI UND NICHT IN anmeldung.json: `kimal3 logout`
|
|
8
|
+
* loescht die Anmeldedatei. Laege der Merkzettel darin, kaeme nach jedem
|
|
9
|
+
* Abmelden jede Nachricht von vorn -- und ausgerechnet der Mensch, der sich
|
|
10
|
+
* gerade neu anmeldet, bekaeme die Wand.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 BEIDE FUNKTIONEN WERFEN NIE. Ein kaputter oder schreibgeschuetzter
|
|
13
|
+
* Merkzettel darf keinen Befehl scheitern lassen (Spec US-8). Schlimmstenfalls
|
|
14
|
+
* sieht der Kunde denselben Hinweis zweimal -- das ist kein Schaden, ein
|
|
15
|
+
* abgebrochener `projekt neu` schon.
|
|
16
|
+
*/
|
|
17
|
+
const ORDNER = join(homedir(), ".kimal3");
|
|
18
|
+
const DATEI = join(ORDNER, "gesehen.json");
|
|
19
|
+
export function leseGesehen() {
|
|
20
|
+
try {
|
|
21
|
+
const roh = JSON.parse(readFileSync(DATEI, "utf8"));
|
|
22
|
+
if (!roh || typeof roh !== "object" || Array.isArray(roh))
|
|
23
|
+
return {};
|
|
24
|
+
const raus = {};
|
|
25
|
+
for (const [k, v] of Object.entries(roh)) {
|
|
26
|
+
// Nur Zeichenketten uebernehmen. Wer die Datei von Hand bearbeitet hat,
|
|
27
|
+
// soll damit hoechstens seinen eigenen Merkzettel verlieren.
|
|
28
|
+
if (typeof v === "string")
|
|
29
|
+
raus[k] = v;
|
|
30
|
+
}
|
|
31
|
+
return raus;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function merkeGesehen(id, tag) {
|
|
38
|
+
try {
|
|
39
|
+
const alles = leseGesehen();
|
|
40
|
+
/*
|
|
41
|
+
🔑 ERST LOESCHEN, DANN SETZEN -- damit der Eintrag ans ENDE rutscht.
|
|
42
|
+
Ohne die erste Zeile behaelt ein schon vorhandener Schluessel seinen
|
|
43
|
+
alten Platz, und `slice(-20)` unten wirft dann ausgerechnet den weg, den
|
|
44
|
+
wir gerade geschrieben haben (Befund der vierten Review-Runde: der
|
|
45
|
+
Kommentar unten versprach genau das Gegenteil).
|
|
46
|
+
|
|
47
|
+
Die Folge waere klein -- derselbe Hinweis erschiene einmal zu viel --,
|
|
48
|
+
aber sie steht im Widerspruch zu einer Zusage im Code. Und eine Zusage
|
|
49
|
+
im Code ist das, was der naechste Leser glaubt, statt nachzurechnen.
|
|
50
|
+
*/
|
|
51
|
+
delete alles[id];
|
|
52
|
+
alles[id] = tag;
|
|
53
|
+
/*
|
|
54
|
+
⚠️ Nur die letzten 20 behalten. Ohne diese Zeile waechst die Datei mit
|
|
55
|
+
jeder je gezeigten Nachricht -- langsam, aber ohne Ende, und niemand
|
|
56
|
+
raeumt sie je auf.
|
|
57
|
+
|
|
58
|
+
🔑 Warum das gefahrlos ist: `Object.entries` gibt Zeichenketten-Schluessel
|
|
59
|
+
in Einfuegereihenfolge zurueck, und der gerade gesetzte Eintrag ist immer
|
|
60
|
+
dabei -- entweder als letzter (neu) oder an seinem alten Platz
|
|
61
|
+
(aktualisiert). Was hinten abfaellt, ist die aelteste Nachricht; die
|
|
62
|
+
schlimmste Folge waere, dass sie noch einmal erscheint.
|
|
63
|
+
*/
|
|
64
|
+
const paare = Object.entries(alles).slice(-20);
|
|
65
|
+
/*
|
|
66
|
+
⚠️ `mode: 0o700` wie in anmeldung.ts -- und das ist KEINE Kosmetik
|
|
67
|
+
(Befund der ersten Review-Runde): `mkdirSync` mit `recursive: true`
|
|
68
|
+
aendert die Rechte eines Ordners, den es schon gibt, NICHT MEHR. Wer
|
|
69
|
+
diesen Ordner also zuerst anlegt, legt seine Rechte fuer immer fest.
|
|
70
|
+
|
|
71
|
+
Der Fall ist echt: Ein Mensch mit altem Skill, aber ohne Anmeldung,
|
|
72
|
+
tippt `kimal3` -- der Skill-Hinweis feuert, dieser Merkzettel entsteht,
|
|
73
|
+
und `~/.kimal3` bekaeme die Vorgabe-Rechte (auf einem gemeinsam
|
|
74
|
+
genutzten Rechner meist 0755). Ein spaeteres `kimal3 login` koennte sie
|
|
75
|
+
nicht mehr enger ziehen. Der Ausweis selbst bliebe zwar 0600 -- aber die
|
|
76
|
+
Haertung des Ordners waere still verloren, und "still" ist hier das
|
|
77
|
+
Schlimme.
|
|
78
|
+
*/
|
|
79
|
+
mkdirSync(ORDNER, { recursive: true, mode: 0o700 });
|
|
80
|
+
writeFileSync(DATEI, JSON.stringify(Object.fromEntries(paare), null, 2), "utf8");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* Merkzettel nicht schreibbar -- der Befehl laeuft trotzdem durch. */
|
|
84
|
+
}
|
|
85
|
+
}
|