kimal3 0.4.0-durchstich.1 → 0.4.0-durchstich.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/claude-plugins/.claude-plugin/marketplace.json +8 -0
- package/claude-plugins/README.md +7 -0
- package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +6 -0
- package/claude-plugins/kimal3-logo/hooks/hooks.json +1 -0
- package/claude-plugins/kimal3-logo/hooks/register.tsx +80 -0
- package/dist/befehle/einrichten.js +68 -7
- package/dist/befehle/skill-install.js +4 -2
- package/dist/einstellungen-mischen.js +68 -28
- package/dist/einstellungen-schreiben.js +48 -12
- package/dist/kimal3.js +2 -1
- package/dist/logo-ablegen.js +117 -0
- package/dist/plugins-mischen.js +78 -0
- package/dist/texte/einrichten-marken.js +2 -2
- package/dist/texte/einrichten.js +25 -0
- package/dist/wache-schalter.js +30 -5
- package/package.json +28 -27
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kimal3",
|
|
3
|
+
"owner": { "name": "KImal3", "url": "https://kimal3.de" },
|
|
4
|
+
"description": "Eingetragen vom Programm kimal3 · Added by the kimal3 program",
|
|
5
|
+
"plugins": [
|
|
6
|
+
{ "name": "kimal3-logo", "source": "./kimal3-logo", "description": "KImal3-Logo über dem Eingabefeld · KImal3 logo above the prompt" }
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# kimal3 — Plugins für Claude Code / Plugins for Claude Code
|
|
2
|
+
|
|
3
|
+
Diesen Ordner legt `kimal3 einrichten` an und ersetzt ihn bei jedem Lauf. Eigene Änderungen hier gehen dabei verloren.
|
|
4
|
+
Das Logo abschalten: in Claude unter **Plugins** `kimal3-logo` ausschalten — `kimal3` schaltet es danach nicht wieder ein.
|
|
5
|
+
|
|
6
|
+
`kimal3 setup` creates this folder and replaces it on every run; your own changes here are lost.
|
|
7
|
+
To turn the logo off: switch off `kimal3-logo` under **Plugins** in Claude — `kimal3` won't switch it back on.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Register } from 'claude-code'
|
|
2
|
+
|
|
3
|
+
// Die Sprache des Kontos. `kimal3 einrichten` setzt diese Zeile beim Ablegen in `~/.kimal3/claude-plugins` und prüft
|
|
4
|
+
// danach, dass sie gesetzt ist (`cli/src/logo-ablegen.ts`) — der Mod selbst kann keine Datei lesen.
|
|
5
|
+
const SPRACHE: 'de' | 'en' = 'de'
|
|
6
|
+
|
|
7
|
+
// Icon: docs/design/kimal3-icon-animated.svg, unverändert bis auf die viewBox:
|
|
8
|
+
// Sie schneidet den leeren Rand um die drei Dreiecke weg (Original: 0 0 80 80).
|
|
9
|
+
const ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="20 19 40 36" width="24" height="22">
|
|
10
|
+
<style>
|
|
11
|
+
/* color-scheme: sonst malt der Browser hinter die Grafik eine eigene Fläche, wenn hell/dunkel nicht zur App passt */
|
|
12
|
+
:root { background: transparent; color-scheme: light dark; }
|
|
13
|
+
.icon { opacity: 0.8; }
|
|
14
|
+
.tri { fill: none; stroke: hsl(194 100% 51%); stroke-width: 2.8; stroke-linejoin: round; opacity: 0.7; }
|
|
15
|
+
.t1 { animation: trianglePulse 3s ease-in-out infinite; }
|
|
16
|
+
.t2 { animation: trianglePulse 3.5s ease-in-out infinite; animation-delay: .4s; }
|
|
17
|
+
.t3 { animation: trianglePulse 4s ease-in-out infinite; animation-delay: .8s; }
|
|
18
|
+
@keyframes trianglePulse { 0%,100% { opacity: .5; } 50% { opacity: 1; } }
|
|
19
|
+
</style>
|
|
20
|
+
<g class="icon">
|
|
21
|
+
<polygon class="tri t1" points="33.28,21.6 22.08,41.0 44.48,41.0" />
|
|
22
|
+
<polygon class="tri t2" points="46.72,21.6 35.52,41.0 57.92,41.0" />
|
|
23
|
+
<polygon class="tri t3" points="40.00,33.36 28.80,52.76 51.20,52.76" />
|
|
24
|
+
</g>
|
|
25
|
+
</svg>`
|
|
26
|
+
|
|
27
|
+
// Das Cyan der Marke (kimal3.de, --brand in der App)
|
|
28
|
+
const MARKEN_CYAN = '#00C3FF'
|
|
29
|
+
// Der Kurs, für alle (Dennis, 15.10.: „mach alle auf den Kurs“). Abgemeldete schickt die App von dort zum Login.
|
|
30
|
+
// Fest auf `/firma` zeigte der Link Kostenlosen ohne Firma eine leere Seite (Spec L-51). Im Terminal: Strg+Klick; Claude Code hängt dort die Adresse
|
|
31
|
+
// grau dahinter, wenn es dem Terminal keine Links zutraut — FORCE_HYPERLINK hilft nicht.
|
|
32
|
+
const APP = 'https://app.kimal3.de/kurs'
|
|
33
|
+
|
|
34
|
+
// Vor und nach der Wortmarke. Englisch: „AI Workplace“, nie „workspace“ (Spec, Global Constraints).
|
|
35
|
+
const TEXT = {
|
|
36
|
+
de: { vor: 'Dein ', nach: '-Arbeitsplatz', alt: 'KImal3-Logo' },
|
|
37
|
+
en: { vor: 'Your ', nach: ' AI Workplace', alt: 'KImal3 logo' },
|
|
38
|
+
}[SPRACHE]
|
|
39
|
+
|
|
40
|
+
export const register: Register = on => {
|
|
41
|
+
on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
|
|
42
|
+
if (e.props.hasSurvey) {
|
|
43
|
+
return next(e)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Im Terminal gibt es kein Svg: drei Dreiecke als Zeichen, die Wortmarke wie auf kimal3.de
|
|
47
|
+
// („KI" in Schriftfarbe, „mal3" im Marken-Cyan). Das Cyan steht als Farbwert, nicht als „cyan" —
|
|
48
|
+
// das Terminal-Cyan ist je nach Farbschema ein anderes, oft ein blasses Türkis.
|
|
49
|
+
if (e.surface === 'terminal') {
|
|
50
|
+
const { Box, Link, Text } = $.ui.resolve(e)
|
|
51
|
+
return (
|
|
52
|
+
<Box flexDirection="row" gap={1}>
|
|
53
|
+
<Text color={MARKEN_CYAN} bold>{'▲▲▲'}</Text>
|
|
54
|
+
<Text bold>
|
|
55
|
+
{TEXT.vor}
|
|
56
|
+
<Link href={APP}>
|
|
57
|
+
{'KI'}
|
|
58
|
+
<Text color={MARKEN_CYAN}>{'mal3'}</Text>
|
|
59
|
+
{TEXT.nach}
|
|
60
|
+
</Link>
|
|
61
|
+
{'.'}
|
|
62
|
+
</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// isInteractive: erst damit spielt die Leiste die CSS-Animation ab, sonst steht das Icon still.
|
|
68
|
+
const { Box, Link, Svg, Text } = $.ui.resolve(e)
|
|
69
|
+
return (
|
|
70
|
+
<Box flexDirection="row" alignItems="center" gap={1}>
|
|
71
|
+
<Svg source={ICON} alt={TEXT.alt} width={24} height={22} isInteractive />
|
|
72
|
+
<Text bold>
|
|
73
|
+
{TEXT.vor}
|
|
74
|
+
<Link href={APP}>{`KImal3${TEXT.nach}`}</Link>
|
|
75
|
+
{'.'}
|
|
76
|
+
</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { leseAnmeldung } from "../anmeldung.js";
|
|
6
|
+
import { tragePluginsEin } from "../einstellungen-schreiben.js";
|
|
7
|
+
import { logoAblegen } from "../logo-ablegen.js";
|
|
8
|
+
import { pluginStand } from "../plugins-mischen.js";
|
|
5
9
|
import { ruf } from "../api.js";
|
|
6
10
|
import { FIRMEN_OPTIONEN, fehlendeDateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
|
|
7
11
|
import { zerlege } from "../argumente.js";
|
|
@@ -9,7 +13,8 @@ import { sprache } from "../sprache.js";
|
|
|
9
13
|
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
10
14
|
import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
|
|
11
15
|
import { anmeldenMitBrowser } from "./login.js";
|
|
12
|
-
import {
|
|
16
|
+
import { SKILL_TEXTE } from "../texte/skill.js";
|
|
17
|
+
import { skillInstall, ursacheDes } from "./skill-install.js";
|
|
13
18
|
/**
|
|
14
19
|
* `kimal3 einrichten` / `kimal3 setup` -- der eine Befehl, den Claude beim Einstieg mit Leitung
|
|
15
20
|
* aufruft (Spec 2026-10-11, Abschnitt 0, S-1, S-2, S-7). Er verbindet, was es schon gibt:
|
|
@@ -20,8 +25,10 @@ import { skillInstall } from "./skill-install.js";
|
|
|
20
25
|
* steht -- die Sitzung ohne Ordner ist ein Wegwerf-Ordner der App (M-L2).
|
|
21
26
|
* 🔑 Nichts wird überschrieben: Ein zweiter Lauf ergänzt nur Fehlendes (`fehlendeDateienSchreiben`).
|
|
22
27
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
28
|
+
* 🔑 superpowers und das Logo trägt er seit Bauplan 2026-10-15 selbst in Claudes Einstellungen ein
|
|
29
|
+
* (`pluginsEinrichten`) -- Claude installiert dafür nichts mehr per Befehlszeile oder Menü.
|
|
30
|
+
*
|
|
31
|
+
* 📌 Noch ohne Coach und ohne Suche nach einem vorhandenen Arbeitsplatz an anderer Stelle (S-8).
|
|
25
32
|
*/
|
|
26
33
|
export async function einrichten(argv) {
|
|
27
34
|
const s = sprache();
|
|
@@ -73,16 +80,70 @@ export async function einrichten(argv) {
|
|
|
73
80
|
console.log(t.filesWritten(geschrieben.neu, geschrieben.schonDa));
|
|
74
81
|
console.log(t.workplaceLine(ziel));
|
|
75
82
|
console.log(t.workbenchLine(werkbank));
|
|
83
|
+
// Plugins VOR der Wache: So hält ihre Sicherungskopie Sabines unveränderte Einstellungen (QA-Fund 9).
|
|
84
|
+
const git = gitAntwortet();
|
|
85
|
+
pluginsEinrichten(git);
|
|
76
86
|
const befehle = await skillInstall();
|
|
77
|
-
await kursAbhaken(
|
|
87
|
+
await kursAbhaken(git ? ["beat2-werkzeuge", "beat2-kimal3", "beat2-arbeitsplatz"] : ["beat2-kimal3", "beat2-arbeitsplatz"]);
|
|
78
88
|
if (befehle !== 0)
|
|
79
89
|
return befehle;
|
|
80
90
|
console.log(t.done);
|
|
81
91
|
return 0;
|
|
82
92
|
}
|
|
83
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* superpowers und das Logo: Ordner ablegen, in Claudes Einstellungen eintragen, eine Zeile `Plugins:`.
|
|
95
|
+
* 🔑 Scheitert etwas, bricht die Einrichtung NICHT ab (Lage P-3): Arbeitsplatz, Befehle und Wache
|
|
96
|
+
* stehen ja -- die Zeile sagt, was fehlt, und Claude nimmt dann den Menü-Weg für superpowers.
|
|
97
|
+
* 🔑 Ohne Git trägt er superpowers NICHT ein (Durchsicht 3, Fund 5): Claude Code klont Anthropics Katalog mit Git und
|
|
98
|
+
* scheiterte sonst bei jedem Start. Eine Zeile sagt es; der nächste Lauf mit Git holt es nach (Mac-Weg ohne Adminrechte).
|
|
99
|
+
*/
|
|
100
|
+
function pluginsEinrichten(git) {
|
|
101
|
+
const s = sprache();
|
|
102
|
+
const t = EINRICHTEN_TEXTE[s];
|
|
103
|
+
const ablage = logoAblegen(s);
|
|
104
|
+
// Ohne Logo-Ordner kommt superpowers trotzdem (Durchsicht 2, Fund 1): eine Zeile zum Logo, dann weiter.
|
|
105
|
+
if (!ablage.ok)
|
|
106
|
+
console.log(t.logoNotWritten(ablage.detail));
|
|
107
|
+
if (ablage.ok && ablage.alterStand)
|
|
108
|
+
console.log(t.logoOldVersion);
|
|
109
|
+
// Schrägstriche vorwärts: So steht der Pfad in Claudes Einstellungen auf jedem System gleich (P-13).
|
|
110
|
+
const eintrag = tragePluginsEin(ablage.ok ? ablage.ordner.replace(/\\/g, "/") : null, git);
|
|
111
|
+
if (eintrag.art === "abgelehnt") {
|
|
112
|
+
console.log(t.pluginsNotWritten(eintrag.grund === "kein-gueltiges-json" ? t.settingsInvalid : t.settingsForeign));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (eintrag.art === "gescheitert") {
|
|
116
|
+
// Den Schritt in Sabines Sprache, nie das interne Wort (Durchsicht-Fund 3) -- derselbe Satz wie bei der Wache.
|
|
117
|
+
console.log(t.pluginsNotWritten(t.settingsFailed(ursacheDes(eintrag, SKILL_TEXTE[s]))));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const stand = pluginStand(eintrag.inhalt);
|
|
121
|
+
const namen = { superpowers: "superpowers", logo: t.logoName };
|
|
122
|
+
const mit = (wert) => ["superpowers", "logo"].filter((p) => stand[p] === wert).map((p) => namen[p]);
|
|
123
|
+
console.log(t.pluginsLine(mit("an"), mit("aus"), eintrag.art === "eingetragen"));
|
|
124
|
+
// Auch wenn superpowers schon von früher drinsteht: ohne Git lädt es nicht (Durchsicht 4, Fund 3).
|
|
125
|
+
if (!git && stand.superpowers !== "aus")
|
|
126
|
+
console.log(t.superpowersNeedsGit);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Ist Git da? Node.js ist es, sonst liefe dieses Programm nicht.
|
|
130
|
+
* 🔴 AUF DEM MAC NIE BLIND `git --version` (Durchsicht 3, Fund 1): Ohne Entwickler-Tools ist `/usr/bin/git` nur ein
|
|
131
|
+
* Platzhalter, der das Installationsfenster öffnet -- auf einem Standardkonto samt Frage nach einem Admin-Passwort,
|
|
132
|
+
* mitten in der Einrichtung. Dort zählt Git nur, wenn die Entwickler-Tools da sind (`xcode-select -p`) oder ein Git
|
|
133
|
+
* außerhalb von `/usr/bin` im Pfad liegt (Homebrew) -- und dann wird genau DAS gefragt, nicht das erste im Pfad.
|
|
134
|
+
* Dieselbe Regel steht in der Anleitung, Schritt 1.
|
|
135
|
+
*/
|
|
84
136
|
function gitAntwortet() {
|
|
85
|
-
|
|
137
|
+
const antwortet = (befehl) => spawnSync(befehl, ["--version"], { stdio: "ignore", windowsHide: true }).status === 0;
|
|
138
|
+
if (platform() !== "darwin")
|
|
139
|
+
return antwortet("git");
|
|
140
|
+
if (spawnSync("xcode-select", ["-p"], { stdio: "ignore" }).status === 0)
|
|
141
|
+
return antwortet("git");
|
|
142
|
+
// Nur absolute Ordner: Ein leerer Eintrag oder `.` ergäbe den bloßen Namen `git`, und der fände wieder den
|
|
143
|
+
// Platzhalter in `/usr/bin` (Durchsicht 4, Fund 2).
|
|
144
|
+
const eigenes = (process.env.PATH ?? "").split(":").filter((ordner) => ordner.startsWith("/") && ordner !== "/usr/bin")
|
|
145
|
+
.map((ordner) => join(ordner, "git")).find((datei) => existsSync(datei));
|
|
146
|
+
return eigenes !== undefined && antwortet(eigenes);
|
|
86
147
|
}
|
|
87
148
|
/**
|
|
88
149
|
* Die Haken sind eine Zugabe: Scheitern sie, ist der Arbeitsplatz trotzdem da -- eine Zeile,
|
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { einstellungenDatei, trageWacheEin } from "../einstellungen-schreiben.js";
|
|
6
|
-
import {
|
|
6
|
+
import { merkeEinrichtung, sitzungJetzt } from "../wache-schalter.js";
|
|
7
7
|
import { stempelZeile } from "../nachricht.js";
|
|
8
8
|
import { eigeneVersion } from "../version.js";
|
|
9
9
|
import { sprache } from "../sprache.js";
|
|
@@ -286,8 +286,10 @@ export async function skillInstall() {
|
|
|
286
286
|
taegliche Pruefung laeuft nicht" -- ausgerechnet in dem Augenblick, in
|
|
287
287
|
dem er sie gerade eingerichtet hat. *Der erste Eindruck waere ein
|
|
288
288
|
Fehlalarm.*
|
|
289
|
+
⚠️ Als EINRICHTUNG, nicht als "nachgesehen" -- sonst schweigt die Wache in
|
|
290
|
+
dieser Sitzung auch beim Neustart (Sandbox-Lauf 14.10., S-3; `merkeEinrichtung`).
|
|
289
291
|
*/
|
|
290
|
-
|
|
292
|
+
merkeEinrichtung(sitzungJetzt());
|
|
291
293
|
}
|
|
292
294
|
if (wache.art === "eingetragen") {
|
|
293
295
|
console.log(t.watchSetUp);
|
|
@@ -18,6 +18,8 @@ export const WACHE_MARKE = "kimal3 wache"; // kein-anzeigetext: Startbefehl in C
|
|
|
18
18
|
* Der Befehl, der in die Einstellungen eingetragen wird.
|
|
19
19
|
*
|
|
20
20
|
* 🔴 NUR EIN BEFEHL, keine Kommandozeilen-Zeichen -- kein `||`, kein `2>/dev/null`.
|
|
21
|
+
* 📌 Die eine Ausnahme seit dem 15.10.: Auf Mac und Linux steht `PATH="…" ` davor (`wacheBefehl` unten). Dort
|
|
22
|
+
* startet Claude Code Startbefehle immer über `sh -c`, und `sh` kennt diese Form; unter Windows bleibt er bloß.
|
|
21
23
|
* Das sind Bash-Zeichen, und ob auf dem Kundenrechner eine Bash laeuft, wissen
|
|
22
24
|
* wir nicht. Die Fehlerbehandlung gehoert ins Programm, nicht in die Zeile.
|
|
23
25
|
* *Ein Befehl, der beim Erbauer laeuft, ist keine Aussage ueber den Rechner
|
|
@@ -38,6 +40,31 @@ export const WACHE_MARKE = "kimal3 wache"; // kein-anzeigetext: Startbefehl in C
|
|
|
38
40
|
* AUCH BEI RUECKGABEWERT 127 gelesen (ebenfalls gemessen).
|
|
39
41
|
*/
|
|
40
42
|
export const WACHE_BEFEHL = `${WACHE_MARKE} --hook`; // kein-anzeigetext: Startbefehl in Claudes Einstellungen, bleibt deutsch (T3-5)
|
|
43
|
+
/**
|
|
44
|
+
* Der Startbefehl, wie er auf DIESEM Rechner eingetragen wird. Auf Mac und Linux stehen die Ordner davor, in denen
|
|
45
|
+
* `kimal3` und Node.js liegen (`ordner`, doppelte nur einmal).
|
|
46
|
+
* 🔑 Warum beide: `kimal3` liegt dort, wo npm Programme ablegt -- beim Mac-Weg `~/.kimal3/node/bin`, bei Homebrew
|
|
47
|
+
* `/opt/homebrew/bin`; Node.js selbst liegt bei Homebrew im versionierten Ordner darunter. Ein `kimal3` in einem
|
|
48
|
+
* eigenen npm-Ordner braucht zusätzlich den Ordner von Node.js für seine erste Zeile (`#!/usr/bin/env node`).
|
|
49
|
+
*
|
|
50
|
+
* 🔑 WARUM: Startbefehle laufen mit Claudes eigener Umgebung über `sh -c` und lesen weder `~/.zprofile`
|
|
51
|
+
* noch `~/.zshrc` (code.claude.com/docs/en/hooks, gelesen 15.10.). Liegt Node.js im Benutzerordner
|
|
52
|
+
* (`~/.kimal3/node`, der Mac-Weg ohne Adminrechte) und wurde Claude aus dem Dock gestartet, fände `sh`
|
|
53
|
+
* weder `kimal3` noch das `node` aus dessen erster Zeile -- und die Wache schwiege, ohne dass es jemand merkt.
|
|
54
|
+
* Unter Windows bleibt es beim bloßen Befehl: Dort steht Node.js im Pfad des Systems, und die Wache läuft
|
|
55
|
+
* dort seit Wochen so.
|
|
56
|
+
* 🔴 Nur Zeichen aus einer kurzen Liste kommen in den Befehl (messen-und-pruefen §23); alles andere
|
|
57
|
+
* -- ein Anführungszeichen, ein `$` -- gäbe einen Befehl, den `sh` anders liest. Ein solcher Ordner fällt weg; bleibt
|
|
58
|
+
* keiner übrig, steht der bloße Befehl da.
|
|
59
|
+
*/
|
|
60
|
+
export function wacheBefehl(system, ordner) {
|
|
61
|
+
// Ein Wegwerf-Ort fällt weg (Durchsicht 4, Fund 4): `npx kimal3 …` startet aus einem Zwischenspeicher, der bald
|
|
62
|
+
// verschwindet, ein Start aus einem Projektordner aus dessen `node_modules`.
|
|
63
|
+
const sicher = [...new Set(ordner)].filter((o) => /^\/[A-Za-z0-9 ._@+\/-]*$/.test(o) && !/\/(node_modules|_npx)\//.test(`${o}/`));
|
|
64
|
+
if (system === "win32" || sicher.length === 0)
|
|
65
|
+
return WACHE_BEFEHL;
|
|
66
|
+
return `PATH="${sicher.join(":")}:$PATH" ${WACHE_BEFEHL}`; // kein-anzeigetext: Startbefehl in Claudes Einstellungen
|
|
67
|
+
}
|
|
41
68
|
/**
|
|
42
69
|
* Wie lange Claude auf die Wache wartet, in Sekunden.
|
|
43
70
|
*
|
|
@@ -48,9 +75,45 @@ export const WACHE_BEFEHL = `${WACHE_MARKE} --hook`; // kein-anzeigetext: Startb
|
|
|
48
75
|
* seiner Arbeit ab -- und ausgerechnet unserer soll ihm helfen.*
|
|
49
76
|
*/
|
|
50
77
|
export const WACHE_ZEITGRENZE = 10;
|
|
51
|
-
function istObjekt(wert) {
|
|
78
|
+
export function istObjekt(wert) {
|
|
52
79
|
return typeof wert === "object" && wert !== null && !Array.isArray(wert);
|
|
53
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Die Einstellungsdatei als Objekt -- oder die Absage. Gemeinsam fuer die Wache und die Plugins
|
|
83
|
+
* (`plugins-mischen.ts`), damit beide eine kaputte Datei gleich behandeln: nicht anfassen.
|
|
84
|
+
*/
|
|
85
|
+
export function leseEinstellungen(alt) {
|
|
86
|
+
/*
|
|
87
|
+
Eine fehlende und eine leere Datei sind dasselbe: Es gibt nichts zu
|
|
88
|
+
bewahren, also legen wir an. `null` kommt vom Aufrufer, wenn die Datei gar
|
|
89
|
+
nicht existiert; ein leerer Text entsteht, wenn ein frueherer Lauf sie
|
|
90
|
+
angelegt, aber nicht mehr befuellt hat.
|
|
91
|
+
*/
|
|
92
|
+
const leer = alt === null || alt.trim() === "";
|
|
93
|
+
if (leer)
|
|
94
|
+
return { ok: true, objekt: {}, leer };
|
|
95
|
+
let gelesen;
|
|
96
|
+
try {
|
|
97
|
+
// Eine unsichtbare Kennung am Anfang (UTF-8-BOM, z. B. von PowerShell 5.1 geschrieben) macht die
|
|
98
|
+
// Datei nicht kaputt -- `JSON.parse` stolpert aber darüber (QA-Fund 3 zum Bauplan 2026-10-15).
|
|
99
|
+
gelesen = JSON.parse(alt.replace(/^\uFEFF/, ""));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/*
|
|
103
|
+
🔴 HIER WIRD NICHTS GESCHRIEBEN. Wer eine unlesbare Einstellungsdatei
|
|
104
|
+
ueberschreibt, wirft weg, was der Mensch dort stehen hat -- und
|
|
105
|
+
ausgerechnet der Fall, in dem sie schon beschaedigt ist, ist der, in
|
|
106
|
+
dem er sie am dringendsten braucht. Die Absage geht nach oben, der
|
|
107
|
+
Mensch erfaehrt sie.
|
|
108
|
+
*/
|
|
109
|
+
return { ok: false, grund: "kein-gueltiges-json" };
|
|
110
|
+
}
|
|
111
|
+
// Eine Liste oder eine blosse Zahl ist gueltiges JSON, aber keine
|
|
112
|
+
// Einstellungsdatei. Das ist ein DRITTER Fall, nicht der zweite.
|
|
113
|
+
if (!istObjekt(gelesen))
|
|
114
|
+
return { ok: false, grund: "fremde-form" };
|
|
115
|
+
return { ok: true, objekt: gelesen, leer };
|
|
116
|
+
}
|
|
54
117
|
/**
|
|
55
118
|
* Ist das ein Eintrag, den WIR frueher einmal geschrieben haben?
|
|
56
119
|
*
|
|
@@ -75,33 +138,10 @@ function istUnserer(eintrag) {
|
|
|
75
138
|
return typeof eintrag.command === "string" && eintrag.command.includes(WACHE_MARKE);
|
|
76
139
|
}
|
|
77
140
|
export function mischeEinstellungen(alt, befehl) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
angelegt, aber nicht mehr befuellt hat.
|
|
83
|
-
*/
|
|
84
|
-
const leer = alt === null || alt.trim() === "";
|
|
85
|
-
let gelesen = {};
|
|
86
|
-
if (!leer) {
|
|
87
|
-
try {
|
|
88
|
-
gelesen = JSON.parse(alt);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
/*
|
|
92
|
-
🔴 HIER WIRD NICHTS GESCHRIEBEN. Wer eine unlesbare Einstellungsdatei
|
|
93
|
-
ueberschreibt, wirft weg, was der Mensch dort stehen hat -- und
|
|
94
|
-
ausgerechnet der Fall, in dem sie schon beschaedigt ist, ist der, in
|
|
95
|
-
dem er sie am dringendsten braucht. Die Absage geht nach oben, der
|
|
96
|
-
Mensch erfaehrt sie.
|
|
97
|
-
*/
|
|
98
|
-
return { art: "abgelehnt", grund: "kein-gueltiges-json" };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Eine Liste oder eine blosse Zahl ist gueltiges JSON, aber keine
|
|
102
|
-
// Einstellungsdatei. Das ist ein DRITTER Fall, nicht der zweite.
|
|
103
|
-
if (!istObjekt(gelesen))
|
|
104
|
-
return { art: "abgelehnt", grund: "fremde-form" };
|
|
141
|
+
const lesen = leseEinstellungen(alt);
|
|
142
|
+
if (!lesen.ok)
|
|
143
|
+
return { art: "abgelehnt", grund: lesen.grund };
|
|
144
|
+
const { objekt: gelesen, leer } = lesen;
|
|
105
145
|
const vorher = JSON.stringify(gelesen);
|
|
106
146
|
const hooks = gelesen.hooks === undefined ? {} : gelesen.hooks;
|
|
107
147
|
if (!istObjekt(hooks))
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { mischeEinstellungen, wacheBefehl } from "./einstellungen-mischen.js";
|
|
5
|
+
import { KATALOG, mischePlugins, pluginStand } from "./plugins-mischen.js";
|
|
5
6
|
/** Die Kontrolle nach dem Schreiben fand unseren Eintrag nicht -- eigener Fall, damit `detail` `null` sein kann. */
|
|
6
7
|
class EintragFehlt extends Error {
|
|
7
8
|
}
|
|
@@ -12,6 +13,41 @@ export function sicherungsDatei() {
|
|
|
12
13
|
return einstellungenDatei() + ".kimal3-sicherung";
|
|
13
14
|
}
|
|
14
15
|
export function trageWacheEin() {
|
|
16
|
+
// Vor den Befehl kommen der Ordner, aus dem `kimal3` gerade gestartet wurde (dort legt npm es ab; bei Homebrew liegt
|
|
17
|
+
// dort auch `node`), und der Ordner des laufenden Node.js (für ein `kimal3` in einem eigenen npm-Ordner) -- Durchsicht 3, Fund 2.
|
|
18
|
+
const befehl = wacheBefehl(platform(), [dirname(process.argv[1] ?? ""), dirname(process.execPath)]);
|
|
19
|
+
return trageEin((alt) => mischeEinstellungen(alt, befehl), (text) => text.includes(JSON.stringify(befehl)), sicherungsDatei());
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Die Kopie, die die Plugins vor dem Schreiben anlegen. Eine eigene, weil `kimal3 einrichten` die Plugins
|
|
23
|
+
* VOR der Wache einträgt: So hält diese Kopie Sabines unveränderte Datei (Prüfer-Fund 9 zum Bauplan 2026-10-15).
|
|
24
|
+
*/
|
|
25
|
+
export function pluginSicherungsDatei() {
|
|
26
|
+
return einstellungenDatei() + ".kimal3-sicherung-plugins";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* superpowers und das Logo eintragen (`plugins-mischen.ts`); `katalogPfad` mit Schrägstrichen vorwärts,
|
|
30
|
+
* `null` ohne Logo-Ordner. 🔑 Die Kontrolle liest die Einschalter selbst, nicht nur Katalogname und Pfad:
|
|
31
|
+
* Fehlte ein Einschalter, meldete die Zeile `Plugins:` sonst „eingetragen“ für etwas, das nie lädt
|
|
32
|
+
* (Durchsicht 2, Fund 7). „Von Sabine ausgeschaltet“ zählt als da -- es steht ja drin.
|
|
33
|
+
*/
|
|
34
|
+
export function tragePluginsEin(katalogPfad, mitSuperpowers) {
|
|
35
|
+
return trageEin((alt) => mischePlugins(alt, katalogPfad, mitSuperpowers), (text) => {
|
|
36
|
+
const stand = pluginStand(text);
|
|
37
|
+
if (mitSuperpowers && stand.superpowers === "fehlt")
|
|
38
|
+
return false;
|
|
39
|
+
return katalogPfad === null || (stand.logo !== "fehlt" && text.includes(`"${KATALOG}"`) && text.includes(JSON.stringify(katalogPfad)));
|
|
40
|
+
}, pluginSicherungsDatei());
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Der Vier-Schritte-Weg fuer jeden Eintrag in Claudes Einstellungen -- die Wache und die Plugins
|
|
44
|
+
* (`plugins-mischen.ts`) gehen denselben, damit keiner von beiden ohne Sicherung schreibt.
|
|
45
|
+
* `kontrolle` bekommt den zurueckgelesenen Text und sagt, ob unser Eintrag drinsteht.
|
|
46
|
+
*/
|
|
47
|
+
export function trageEin(mische, kontrolle,
|
|
48
|
+
// 🔑 Je Schreiber eine eigene Kopie: Schrieben Wache und Plugins in dieselbe, überschriebe die zweite
|
|
49
|
+
// Sabines Originaldatei mit dem Stand nach dem ersten Eintrag (QA-Fund 9).
|
|
50
|
+
sicherung) {
|
|
15
51
|
const datei = einstellungenDatei();
|
|
16
52
|
const gabEsSchon = existsSync(datei);
|
|
17
53
|
// --- Schritt 1: lesen ---
|
|
@@ -30,15 +66,15 @@ export function trageWacheEin() {
|
|
|
30
66
|
return { art: "gescheitert", schritt: "lesen", detail: e.message };
|
|
31
67
|
}
|
|
32
68
|
}
|
|
33
|
-
const ergebnis =
|
|
69
|
+
const ergebnis = mische(alt);
|
|
34
70
|
if (ergebnis.art === "abgelehnt")
|
|
35
71
|
return { art: "abgelehnt", grund: ergebnis.grund };
|
|
36
72
|
if (ergebnis.art === "unveraendert")
|
|
37
|
-
return { art: "stand-schon" };
|
|
73
|
+
return { art: "stand-schon", inhalt: alt ?? "" };
|
|
38
74
|
// --- Schritt 2: Sicherungskopie ---
|
|
39
75
|
if (gabEsSchon) {
|
|
40
76
|
try {
|
|
41
|
-
copyFileSync(datei,
|
|
77
|
+
copyFileSync(datei, sicherung);
|
|
42
78
|
}
|
|
43
79
|
catch (e) {
|
|
44
80
|
/*
|
|
@@ -55,22 +91,22 @@ export function trageWacheEin() {
|
|
|
55
91
|
writeFileSync(datei, ergebnis.inhalt, "utf8");
|
|
56
92
|
}
|
|
57
93
|
catch (e) {
|
|
58
|
-
zurueck(gabEsSchon);
|
|
94
|
+
zurueck(gabEsSchon, sicherung);
|
|
59
95
|
return { art: "gescheitert", schritt: "schreiben", detail: e.message };
|
|
60
96
|
}
|
|
61
97
|
// --- Schritt 4: zuruecklesen und erneut pruefen ---
|
|
62
98
|
try {
|
|
63
99
|
const zurueckgelesen = readFileSync(datei, "utf8");
|
|
64
100
|
const geparst = JSON.parse(zurueckgelesen);
|
|
65
|
-
if (!zurueckgelesen
|
|
101
|
+
if (!kontrolle(zurueckgelesen) || geparst === null) {
|
|
66
102
|
throw new EintragFehlt();
|
|
67
103
|
}
|
|
68
104
|
}
|
|
69
105
|
catch (e) {
|
|
70
|
-
zurueck(gabEsSchon);
|
|
106
|
+
zurueck(gabEsSchon, sicherung);
|
|
71
107
|
return { art: "gescheitert", schritt: "kontrolle", detail: e instanceof EintragFehlt ? null : e.message };
|
|
72
108
|
}
|
|
73
|
-
return { art: "eingetragen" };
|
|
109
|
+
return { art: "eingetragen", inhalt: ergebnis.inhalt };
|
|
74
110
|
}
|
|
75
111
|
/**
|
|
76
112
|
* Die Sicherung zurueckspielen.
|
|
@@ -79,11 +115,11 @@ export function trageWacheEin() {
|
|
|
79
115
|
* bleibt die halb geschriebene stehen. Das ist die harmlose Seite: Sie
|
|
80
116
|
* enthielt vorher nichts, was jemandem gehoerte.
|
|
81
117
|
*/
|
|
82
|
-
function zurueck(gabEsSchon) {
|
|
118
|
+
function zurueck(gabEsSchon, sicherung) {
|
|
83
119
|
if (!gabEsSchon)
|
|
84
120
|
return;
|
|
85
121
|
try {
|
|
86
|
-
copyFileSync(
|
|
122
|
+
copyFileSync(sicherung, einstellungenDatei());
|
|
87
123
|
}
|
|
88
124
|
catch {
|
|
89
125
|
/* Mehr koennen wir nicht tun -- der Grund steht in der Meldung oben. */
|
package/dist/kimal3.js
CHANGED
|
@@ -495,7 +495,8 @@ async function fuehreAus(name, rest) {
|
|
|
495
495
|
case "wache aus": return allein ? wacheSchalten(false) : null;
|
|
496
496
|
case "wache":
|
|
497
497
|
// 🔴 Der Startbefehl aus `~/.claude/settings.json`, den `skill install` eintraegt (T3-5:
|
|
498
|
-
// eingetragen wird immer `kimal3 wache --hook
|
|
498
|
+
// eingetragen wird immer `kimal3 wache --hook`, auf Mac/Linux mit `PATH="…"` davor -- `wacheBefehl`;
|
|
499
|
+
// `kimal3 watch --hook` laeuft genauso).
|
|
499
500
|
// Er verpackt die Meldung anders -- Begruendung an `wache()`.
|
|
500
501
|
// ⚠️ Ein Mensch tippt das nie; der Katalog nennt ihn deshalb auch nicht.
|
|
501
502
|
if (rest.length === 1 && rest[0] === "--hook")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
/**
|
|
6
|
+
* Legt den Plugin-Katalog mit dem Logo-Mod in `~/.kimal3/claude-plugins` ab und setzt dort die
|
|
7
|
+
* Sprache des Kontos. Claude Code liest den Mod aus genau diesem Ordner (Katalog `kimal3`,
|
|
8
|
+
* `plugins-mischen.ts`).
|
|
9
|
+
*
|
|
10
|
+
* 🔑 Warum ein fester Ordner im Benutzerordner und nicht der Ordner im installierten npm-Paket:
|
|
11
|
+
* Der hängt am Installationsort von Node.js und wechselt mit ihm -- der Eintrag in Claudes
|
|
12
|
+
* Einstellungen zeigte dann ins Leere.
|
|
13
|
+
* 🔑 Der Ordner gehört `kimal3` (seine README sagt es): Jeder Lauf ersetzt ihn ganz (Lage P-11).
|
|
14
|
+
*/
|
|
15
|
+
export function katalogOrdner() {
|
|
16
|
+
return join(homedir(), ".kimal3", "claude-plugins"); // kein-anzeigetext: Ordnername
|
|
17
|
+
}
|
|
18
|
+
/** Die Zeile im Mod, die die Sprache festlegt -- `check:logo` prüft, dass es sie genau einmal gibt. */
|
|
19
|
+
export const SPRACH_ZEILE = "const SPRACHE: 'de' | 'en' = 'de'"; // kein-anzeigetext: Quelltext des Mods
|
|
20
|
+
/** Reine Rechnung: der Mod-Text in der Sprache des Kontos, oder `null`, wenn die Zeile fehlt. */
|
|
21
|
+
export function mitSprache(quelltext, sprache) {
|
|
22
|
+
if (quelltext.split(SPRACH_ZEILE).length !== 2)
|
|
23
|
+
return null;
|
|
24
|
+
return quelltext.replace(SPRACH_ZEILE, `const SPRACHE: 'de' | 'en' = '${sprache}'`); // kein-anzeigetext: Quelltext des Mods
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 🔴 NIE EIN HALBER ORDNER (Durchsicht-Funde 2 und 9): Claudes Einstellungen zeigen auf `katalogOrdner()`,
|
|
28
|
+
* und eine laufende Claude-Sitzung hat den Mod daraus geladen. Deshalb:
|
|
29
|
+
* 1. Sprache setzen, BEVOR irgendetwas angefasst wird -- fehlt die Zeile, bleibt alles, wie es war.
|
|
30
|
+
* 2. Steht schon genau das da, was entstehen soll: nichts anfassen (Lage P-8). Das ist auch der Normalfall
|
|
31
|
+
* eines zweiten Laufs, und so kommt er gar nicht erst in die Nähe der Windows-Sperren unten.
|
|
32
|
+
* 3. Den neuen Ordner daneben fertig bauen (`….neu`), dann tauschen: alten Ordner wegschieben, neuen an
|
|
33
|
+
* seinen Platz, alten löschen.
|
|
34
|
+
* 🔑 Der weggeschobene Ordner bekommt bei JEDEM Lauf einen frischen Namen (Durchsicht 2, Funde 2 und 4):
|
|
35
|
+
* Ein Rest, den Windows festhält, blockiert so keinen späteren Lauf, und zurückgeholt wird nur, was
|
|
36
|
+
* DIESER Lauf weggeschoben hat -- nie ein veralteter Rest von früher.
|
|
37
|
+
* Lässt sich der alte nicht wegschieben (Claude hält ihn manchmal fest), bleibt er unversehrt stehen und
|
|
38
|
+
* gilt weiter: ein Erfolg mit `alterStand`, den `kimal3 einrichten` in einer Zeile nennt.
|
|
39
|
+
*/
|
|
40
|
+
export function logoAblegen(sprache) {
|
|
41
|
+
// `dist/logo-ablegen.js` -> `../claude-plugins` im installierten Paket (`files` in package.json).
|
|
42
|
+
const quelle = fileURLToPath(new URL("../claude-plugins", import.meta.url));
|
|
43
|
+
const ziel = katalogOrdner();
|
|
44
|
+
const neu = `${ziel}.neu`; // kein-anzeigetext: Ordnername
|
|
45
|
+
const modDatei = (ordner) => join(ordner, "kimal3-logo", "hooks", "register.tsx");
|
|
46
|
+
try {
|
|
47
|
+
const text = mitSprache(readFileSync(modDatei(quelle), "utf8"), sprache);
|
|
48
|
+
if (text === null)
|
|
49
|
+
return { ok: false, detail: modDatei(quelle) };
|
|
50
|
+
if (stehtSchonDa(quelle, ziel, text)) {
|
|
51
|
+
// Auch im Normalfall aufräumen, samt einem halb gebauten `….neu`: sonst blieben Reste für immer (Durchsicht 3/4).
|
|
52
|
+
rmSync(neu, { recursive: true, force: true });
|
|
53
|
+
resteWegraeumen(ziel);
|
|
54
|
+
return { ok: true, ordner: ziel };
|
|
55
|
+
}
|
|
56
|
+
rmSync(neu, { recursive: true, force: true });
|
|
57
|
+
cpSync(quelle, neu, { recursive: true });
|
|
58
|
+
writeFileSync(modDatei(neu), text, "utf8");
|
|
59
|
+
if (!existsSync(ziel)) {
|
|
60
|
+
renameSync(neu, ziel);
|
|
61
|
+
return { ok: true, ordner: ziel };
|
|
62
|
+
}
|
|
63
|
+
const alt = `${ziel}.alt-${Date.now()}`; // kein-anzeigetext: Ordnername
|
|
64
|
+
try {
|
|
65
|
+
renameSync(ziel, alt);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
rmSync(neu, { recursive: true, force: true });
|
|
69
|
+
return { ok: true, ordner: ziel, alterStand: true };
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
renameSync(neu, ziel);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
renameSync(alt, ziel);
|
|
76
|
+
return { ok: false, detail: e.message };
|
|
77
|
+
}
|
|
78
|
+
resteWegraeumen(ziel);
|
|
79
|
+
return { ok: true, ordner: ziel };
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
return { ok: false, detail: e.message };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Gleiche Dateien, gleicher Inhalt -- der Mod mit der Sprache des Kontos, alles andere wie im Paket. */
|
|
86
|
+
function stehtSchonDa(quelle, ziel, modText) {
|
|
87
|
+
if (!existsSync(ziel))
|
|
88
|
+
return false;
|
|
89
|
+
const dateien = (ordner) => alleDateien(ordner).sort();
|
|
90
|
+
const soll = dateien(quelle);
|
|
91
|
+
if (JSON.stringify(soll) !== JSON.stringify(dateien(ziel)))
|
|
92
|
+
return false;
|
|
93
|
+
const mod = join("kimal3-logo", "hooks", "register.tsx");
|
|
94
|
+
return soll.every((d) => (d === mod ? Buffer.from(modText, "utf8") : readFileSync(join(quelle, d))).equals(readFileSync(join(ziel, d))));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Alle Dateien unter `ordner`, als Pfade relativ dazu. 🔑 Von Hand statt `readdirSync(…, { recursive: true })`: Das kann
|
|
98
|
+
* Node.js erst ab 20.1, und unter 20.0 fände der Vergleich gar keine Datei -- „gleich“, und das Logo würde nie erneuert
|
|
99
|
+
* (Durchsicht 3, Fund 7; `engines` erlaubt 20).
|
|
100
|
+
*/
|
|
101
|
+
function alleDateien(ordner, unter = "") {
|
|
102
|
+
return readdirSync(join(ordner, unter), { withFileTypes: true }).flatMap((e) => e.isDirectory() ? alleDateien(ordner, join(unter, e.name)) : e.isFile() ? [join(unter, e.name)] : []);
|
|
103
|
+
}
|
|
104
|
+
/** Weggeschobene Ordner früherer Läufe (`claude-plugins.alt…`); was Windows noch festhält, bleibt bis zum nächsten Lauf. */
|
|
105
|
+
function resteWegraeumen(ziel) {
|
|
106
|
+
const name = `${basename(ziel)}.alt`; // kein-anzeigetext: Ordnername
|
|
107
|
+
for (const eintrag of readdirSync(dirname(ziel))) {
|
|
108
|
+
if (!eintrag.startsWith(name))
|
|
109
|
+
continue;
|
|
110
|
+
try {
|
|
111
|
+
rmSync(join(dirname(ziel), eintrag), { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* festgehalten -- der nächste Lauf versucht es wieder */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { istObjekt, leseEinstellungen } from "./einstellungen-mischen.js";
|
|
2
|
+
/**
|
|
3
|
+
* Traegt superpowers und das KImal3-Logo in Claudes Einstellungen ein -- als Katalog plus
|
|
4
|
+
* Einschalter, so wie Anthropic Firmen-Mods auf jeden Rechner verteilt (Bauplan 2026-10-15,
|
|
5
|
+
* „Warum anders als die Spec"). Beim naechsten Start laedt Claude Code beides selbst.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 DIESE DATEI FASST KEINE DATEI AN -- dieselbe Bauform wie `einstellungen-mischen.ts`, aus
|
|
8
|
+
* demselben Grund: In `~/.claude/settings.json` steht der Arbeitsplatz des Menschen.
|
|
9
|
+
* Geschrieben wird ueber `einstellungen-schreiben.ts` (Sicherung, Zuruecklesen).
|
|
10
|
+
*/
|
|
11
|
+
export const KATALOG = "kimal3"; // kein-anzeigetext: Name des Plugin-Katalogs in Claudes Einstellungen
|
|
12
|
+
export const LOGO = `kimal3-logo@${KATALOG}`; // kein-anzeigetext: Plugin-Kennung
|
|
13
|
+
const OFFIZIELL = "claude-plugins-official"; // kein-anzeigetext: Anthropics Katalog
|
|
14
|
+
export const SUPERPOWERS = `superpowers@${OFFIZIELL}`; // kein-anzeigetext: Plugin-Kennung
|
|
15
|
+
/**
|
|
16
|
+
* 🔑 Jede Entscheidung des Menschen bleibt stehen (Lagen P-5 bis P-7, P-10):
|
|
17
|
+
* - Ein Eintrag fuer das Logo -- auch `false` -- heisst: Er hat schon entschieden. Wir setzen
|
|
18
|
+
* nur, was fehlt.
|
|
19
|
+
* - superpowers aus IRGENDEINEM Katalog (`superpowers@…`, an oder aus) heisst: nicht noch eins.
|
|
20
|
+
* Zwei superpowers gaeben jede Faehigkeit doppelt.
|
|
21
|
+
* - Den offiziellen Katalog tragen wir nur ein, wenn er fehlt und wir superpowers einschalten.
|
|
22
|
+
* Nur der Katalog `kimal3` gehoert uns: Er zeigt immer auf unseren Ordner (P-9, Umzug).
|
|
23
|
+
* 🔑 `katalogPfad` ist `null`, wenn der Logo-Ordner nicht angelegt werden konnte: Dann bleiben Katalog und
|
|
24
|
+
* Logo-Eintrag, wie sie sind, und superpowers kommt trotzdem -- es braucht den Ordner nicht (Durchsicht 2, Fund 1).
|
|
25
|
+
* 🔑 `mitSuperpowers` ist `false`, wenn Git fehlt: Claude Code klont Anthropics Katalog mit Git und scheiterte sonst bei
|
|
26
|
+
* jedem Start (Durchsicht 3, Fund 5). Was schon drinsteht, bleibt -- auch ein früher eingetragenes superpowers.
|
|
27
|
+
*/
|
|
28
|
+
export function mischePlugins(alt, katalogPfad, mitSuperpowers = true) {
|
|
29
|
+
const lesen = leseEinstellungen(alt);
|
|
30
|
+
if (!lesen.ok)
|
|
31
|
+
return { art: "abgelehnt", grund: lesen.grund };
|
|
32
|
+
const { objekt, leer } = lesen;
|
|
33
|
+
const kataloge = objekt.extraKnownMarketplaces ?? {};
|
|
34
|
+
const plugins = objekt.enabledPlugins ?? {};
|
|
35
|
+
if (!istObjekt(kataloge) || !istObjekt(plugins))
|
|
36
|
+
return { art: "abgelehnt", grund: "fremde-form" };
|
|
37
|
+
// Nur die Quelle gehört uns; was Claude Code selbst in den Eintrag schreibt (`autoUpdate`, wenn
|
|
38
|
+
// Sabine es unter /plugin umschaltet), bleibt stehen (QA-Fund 7). Zeigt die Quelle schon auf unseren
|
|
39
|
+
// Ordner, bleibt der Eintrag Zeichen für Zeichen, wie er ist -- auch in anderer Reihenfolge der
|
|
40
|
+
// Schlüssel; sonst schriebe jeder Lauf die Datei neu (Durchsicht-Fund 6, Lage P-8).
|
|
41
|
+
const alterEintrag = istObjekt(kataloge[KATALOG]) ? kataloge[KATALOG] : {};
|
|
42
|
+
const alteQuelle = istObjekt(alterEintrag.source) ? alterEintrag.source : {};
|
|
43
|
+
const zeigtSchonHin = alteQuelle.source === "directory" && alteQuelle.path === katalogPfad;
|
|
44
|
+
const neueKataloge = { ...kataloge };
|
|
45
|
+
const neuePlugins = { ...plugins };
|
|
46
|
+
if (katalogPfad !== null) {
|
|
47
|
+
neueKataloge[KATALOG] = zeigtSchonHin ? alterEintrag : { ...alterEintrag, source: { source: "directory", path: katalogPfad } };
|
|
48
|
+
if (!(LOGO in plugins))
|
|
49
|
+
neuePlugins[LOGO] = true;
|
|
50
|
+
}
|
|
51
|
+
if (mitSuperpowers && !Object.keys(plugins).some((k) => k.startsWith("superpowers@"))) {
|
|
52
|
+
neuePlugins[SUPERPOWERS] = true;
|
|
53
|
+
if (!(OFFIZIELL in kataloge)) {
|
|
54
|
+
neueKataloge[OFFIZIELL] = { source: { source: "github", repo: "anthropics/claude-plugins-official" } };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const neu = { ...objekt, extraKnownMarketplaces: neueKataloge, enabledPlugins: neuePlugins };
|
|
58
|
+
// Wie bei der Wache: Wer nichts zu tun hat, fasst die Datei nicht an (P-8).
|
|
59
|
+
if (!leer && JSON.stringify(neu) === JSON.stringify(objekt))
|
|
60
|
+
return { art: "unveraendert" };
|
|
61
|
+
return { art: "geschrieben", inhalt: JSON.stringify(neu, null, 2) + "\n" };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Was nach dem Eintragen wirklich gilt -- fuer die Zeile `Plugins:`, die Claude der Kundin weitersagt.
|
|
65
|
+
* 🔑 Aus der Datei gelesen, nicht aus dem, was wir eingetragen haben: Hat sie superpowers selbst
|
|
66
|
+
* ausgeschaltet, muss die Zeile das sagen, damit die Anleitung sie nicht ins Menue schickt (Durchsicht-Fund 1).
|
|
67
|
+
*/
|
|
68
|
+
export function pluginStand(inhalt) {
|
|
69
|
+
const lesen = leseEinstellungen(inhalt);
|
|
70
|
+
const plugins = lesen.ok ? lesen.objekt.enabledPlugins : undefined;
|
|
71
|
+
if (!istObjekt(plugins))
|
|
72
|
+
return { superpowers: "fehlt", logo: "fehlt" };
|
|
73
|
+
const superpowers = Object.entries(plugins).filter(([k]) => k.startsWith("superpowers@")).map(([, v]) => v);
|
|
74
|
+
return {
|
|
75
|
+
superpowers: superpowers.includes(true) ? "an" : superpowers.length > 0 ? "aus" : "fehlt",
|
|
76
|
+
logo: plugins[LOGO] === true ? "an" : LOGO in plugins ? "aus" : "fehlt",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* Programm druckt sie, die Anleitung nennt sie, `check:kurs` haelt beide gleich.
|
|
5
5
|
*/
|
|
6
6
|
export const EINRICHTEN_MARKEN = {
|
|
7
|
-
de: { code: "Anmelde-Code:", adresse: "Adresse:", arbeitsplatz: "Arbeitsplatz:", werkbank: "Werkbank:", angemeldet: "Angemeldet als" },
|
|
8
|
-
en: { code: "Sign-in code:", adresse: "Address:", arbeitsplatz: "Workplace:", werkbank: "Workbench:", angemeldet: "Signed in as" },
|
|
7
|
+
de: { code: "Anmelde-Code:", adresse: "Adresse:", arbeitsplatz: "Arbeitsplatz:", werkbank: "Werkbank:", angemeldet: "Angemeldet als", plugins: "Plugins:" },
|
|
8
|
+
en: { code: "Sign-in code:", adresse: "Address:", arbeitsplatz: "Workplace:", werkbank: "Workbench:", angemeldet: "Signed in as", plugins: "Plugins:" },
|
|
9
9
|
};
|
package/dist/texte/einrichten.js
CHANGED
|
@@ -15,6 +15,21 @@ const de = {
|
|
|
15
15
|
done: "\n✓ Fertig eingerichtet.",
|
|
16
16
|
unsafeFolder: (name) => `Der Server hat einen Ordnernamen geschickt, den ich nicht anlege: ${name}. Bitte melde das an kimal3.`,
|
|
17
17
|
noWorkbank: "In der Lieferung steckt keine Werkbank. Bitte melde das an kimal3.",
|
|
18
|
+
// Plugins (Bauplan 2026-10-15): Claude liest die Zeile und sagt der Kundin, was nach dem Neustart erscheint.
|
|
19
|
+
logoName: "KImal3-Logo",
|
|
20
|
+
// `an`: eingeschaltet · `aus`: von ihr selbst ausgeschaltet (bleibt so, die Anleitung schickt sie dann nicht ins Menü)
|
|
21
|
+
// · `neu`: gerade eingetragen (kommt mit dem Neustart) oder stand schon da (Durchsicht-Funde 1 und 7).
|
|
22
|
+
pluginsLine: (an, aus, neu) => ` ${M.de.plugins} ${an.length > 0 ? `${an.join(", ")} — ${neu ? "erscheinen, sobald Claude ganz beendet und neu geöffnet ist" : "sind schon eingetragen"}.` : "nichts eingeschaltet."}` +
|
|
23
|
+
(aus.length > 0 ? ` Selbst ausgeschaltet, bleibt aus: ${aus.join(", ")}.` : ""),
|
|
24
|
+
pluginsNotWritten: (grund) => ` ${M.de.plugins} nicht eingetragen — ${grund}`,
|
|
25
|
+
settingsInvalid: "deine Claude-Einstellungen (~/.claude/settings.json) sind kein gültiges JSON. Ich habe sie nicht angefasst.",
|
|
26
|
+
settingsForeign: "in deinen Claude-Einstellungen (~/.claude/settings.json) steht etwas in einer Form, die ich nicht kenne. Ich habe sie nicht angefasst.",
|
|
27
|
+
settingsFailed: (ursache) => `deine Claude-Einstellungen (~/.claude/settings.json): ${ursache}.`,
|
|
28
|
+
// Logo (Durchsicht 2, Funde 1 und 3): Scheitert der Ordner, kommt superpowers trotzdem; hält Claude ihn fest, gilt die alte Fassung.
|
|
29
|
+
logoNotWritten: (detail) => ` Den Ordner für das KImal3-Logo konnte ich nicht anlegen oder erneuern (${detail}).`,
|
|
30
|
+
// Ohne Git kein superpowers (Durchsicht 3, Fund 5); die Anleitung zitiert den Anfang dieser Zeile.
|
|
31
|
+
superpowersNeedsGit: " superpowers braucht Git — es kommt mit dem nächsten `kimal3 einrichten`, sobald Git da ist.",
|
|
32
|
+
logoOldVersion: " Das KImal3-Logo bleibt vorerst in der bisherigen Fassung, weil Claude den Ordner gerade benutzt. Beende Claude ganz und starte danach `kimal3 einrichten` noch einmal.",
|
|
18
33
|
};
|
|
19
34
|
const en = {
|
|
20
35
|
codeLine: (code) => `\n ${M.en.code} ${code}`,
|
|
@@ -31,5 +46,15 @@ const en = {
|
|
|
31
46
|
done: "\n✓ All set up.",
|
|
32
47
|
unsafeFolder: (name) => `The server sent a folder name I won't create: ${name}. Please report this to kimal3.`,
|
|
33
48
|
noWorkbank: "The delivery contains no workbench. Please report this to kimal3.",
|
|
49
|
+
logoName: "KImal3 logo",
|
|
50
|
+
pluginsLine: (an, aus, neu) => ` ${M.en.plugins} ${an.length > 0 ? `${an.join(", ")} — ${neu ? "will appear once Claude is fully quit and reopened" : "already added"}.` : "nothing switched on."}` +
|
|
51
|
+
(aus.length > 0 ? ` Switched off by you, stays off: ${aus.join(", ")}.` : ""),
|
|
52
|
+
pluginsNotWritten: (grund) => ` ${M.en.plugins} not added — ${grund}`,
|
|
53
|
+
settingsInvalid: "your Claude settings (~/.claude/settings.json) are not valid JSON. I left them untouched.",
|
|
54
|
+
settingsForeign: "your Claude settings (~/.claude/settings.json) contain something in a form I don't know. I left them untouched.",
|
|
55
|
+
settingsFailed: (ursache) => `your Claude settings (~/.claude/settings.json): ${ursache}.`,
|
|
56
|
+
logoNotWritten: (detail) => ` I couldn't create or renew the folder for the KImal3 logo (${detail}).`,
|
|
57
|
+
superpowersNeedsGit: " superpowers needs Git — it comes with the next `kimal3 setup` once Git is there.",
|
|
58
|
+
logoOldVersion: " The KImal3 logo stays on its previous version for now, because Claude is using the folder. Quit Claude completely, then run `kimal3 setup` again.",
|
|
34
59
|
};
|
|
35
60
|
export const EINRICHTEN_TEXTE = { de, en };
|
package/dist/wache-schalter.js
CHANGED
|
@@ -32,6 +32,8 @@ export function leseWacheDatei() {
|
|
|
32
32
|
if (Array.isArray(inhalt.sitzungen)) {
|
|
33
33
|
raus.sitzungen = inhalt.sitzungen.filter((s) => typeof s === "string");
|
|
34
34
|
}
|
|
35
|
+
if (typeof inhalt.eingerichtet === "string")
|
|
36
|
+
raus.eingerichtet = inhalt.eingerichtet;
|
|
35
37
|
return raus;
|
|
36
38
|
}
|
|
37
39
|
catch {
|
|
@@ -116,6 +118,22 @@ export function merkeSitzung(sitzung) {
|
|
|
116
118
|
return;
|
|
117
119
|
schreibeWacheDatei(mitSitzung(leseWacheDatei() ?? {}, sitzung));
|
|
118
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Merken, in welcher Claude-Sitzung die Wache gerade eingetragen wurde. Wirft nie.
|
|
123
|
+
*
|
|
124
|
+
* 🔴 NICHT `merkeSitzung` (Sandbox-Lauf 14.10., Befund S-3): Der Eintrag greift erst beim
|
|
125
|
+
* naechsten Start -- ohne Merkzettel meldete der naechste Befehl in derselben Sitzung sofort
|
|
126
|
+
* "hat nicht nachgesehen". Bis 0.4.0-durchstich.1 stempelte `skill install` deshalb die
|
|
127
|
+
* Sitzung als NACHGESEHEN. Das hielt den Fehlalarm still, aber auch die Wache selbst: Wer im
|
|
128
|
+
* selben Gespraech weitermacht, hoerte sie am ersten Tag nie (gemessen: das Pro-Angebot lag
|
|
129
|
+
* bereit, `cli_nachrichten_gezeigt` blieb leer). Ein eigenes Feld trennt beides --
|
|
130
|
+
* `wacheAusgefallen` liest es, `schonFuerDieseSitzung` nicht.
|
|
131
|
+
*/
|
|
132
|
+
export function merkeEinrichtung(sitzung) {
|
|
133
|
+
if (sitzung === null)
|
|
134
|
+
return;
|
|
135
|
+
schreibeWacheDatei({ ...(leseWacheDatei() ?? {}), eingerichtet: sitzung });
|
|
136
|
+
}
|
|
119
137
|
/** Wie viele Sitzungen der Merkzettel sich merkt -- genug fuer mehrere Fenster ueber Tage. */
|
|
120
138
|
const MERKE_SITZUNGEN = 20;
|
|
121
139
|
/**
|
|
@@ -171,7 +189,10 @@ function gesehen(inhalt, jetzt) {
|
|
|
171
189
|
* Terminalfenster tippt, will die Antwort jetzt -- ein Merkzettel von gestern
|
|
172
190
|
* darf ihn nicht zum Schweigen bringen.
|
|
173
191
|
*/
|
|
174
|
-
export function schonFuerDieseSitzung(
|
|
192
|
+
export function schonFuerDieseSitzung(
|
|
193
|
+
// `eingerichtet` steht im Typ, damit der ganze Merkzettel hereinpasst -- gelesen wird es
|
|
194
|
+
// hier ABSICHTLICH nicht: Eingerichtet heisst nicht nachgesehen (S-3, `merkeEinrichtung`).
|
|
195
|
+
inhalt, jetzt) {
|
|
175
196
|
return jetzt !== null && inhalt !== null && gesehen(inhalt, jetzt);
|
|
176
197
|
}
|
|
177
198
|
/**
|
|
@@ -186,9 +207,13 @@ export function schonFuerDieseSitzung(inhalt, jetzt) {
|
|
|
186
207
|
* dann aus wie an einem guten Tag. *Eine Wache, die still stirbt, ist
|
|
187
208
|
* schlimmer als keine: Sie kauft Vertrauen und deckt nichts mehr.*
|
|
188
209
|
*
|
|
189
|
-
* 🔴
|
|
210
|
+
* 🔴 FUENF MAL SCHWEIGEN, EINMAL SPRECHEN:
|
|
190
211
|
* - Keine Sitzungsnummer -> der Mensch sitzt im Terminalfenster, die Wache
|
|
191
212
|
* hatte nie einen Anlass zu laufen. "Nicht gelaufen" heisst hier nichts.
|
|
213
|
+
* - Kein Merkzettel -> die Wache war nie eingerichtet; `skill install` legt ihn an.
|
|
214
|
+
* Bis 0.4.0-durchstich.1 meldete das hier -- die allererste Ausgabe einer neuen
|
|
215
|
+
* Kundin war ein Fehlalarm (Sandbox-Lauf 14.10., Befund S-2).
|
|
216
|
+
* - In dieser Sitzung eingerichtet -> sie greift erst beim naechsten Start (S-3).
|
|
192
217
|
* - Abgeschaltet -> wer `kimal3 wache aus` getippt hat, will nicht taeglich
|
|
193
218
|
* daran erinnert werden. Sonst waere der Ausschalter keiner.
|
|
194
219
|
* - Dieselbe Nummer -> sie hat fuer diese Sitzung nachgesehen.
|
|
@@ -201,9 +226,9 @@ export function wacheAusgefallen(
|
|
|
201
226
|
// Fassung traegt genau das und sonst nichts, und dieser Fall gehoert
|
|
202
227
|
// geprueft. Ein Typ, der ihn verbietet, verbietet auch seine Pruefung.
|
|
203
228
|
inhalt, jetzt) {
|
|
204
|
-
if (jetzt === null)
|
|
229
|
+
if (jetzt === null || inhalt === null)
|
|
205
230
|
return false;
|
|
206
|
-
if (inhalt
|
|
231
|
+
if (inhalt.aus === true)
|
|
207
232
|
return false;
|
|
208
|
-
return
|
|
233
|
+
return !gesehen(inhalt, jetzt) && inhalt.eingerichtet !== jetzt;
|
|
209
234
|
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kimal3",
|
|
3
|
-
"version": "0.4.0-durchstich.
|
|
4
|
-
"description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal. · Log your AI projects where they happen: in the terminal.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"kimal3": "dist/kimal3.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"skill",
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "kimal3",
|
|
3
|
+
"version": "0.4.0-durchstich.3",
|
|
4
|
+
"description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal. · Log your AI projects where they happen: in the terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kimal3": "dist/kimal3.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skill",
|
|
12
|
+
"claude-plugins",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"kimal3",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"homepage": "https://app.kimal3.de/pro",
|
|
27
|
+
"license": "SEE LICENSE IN README.md"
|
|
28
|
+
}
|