kimal3 0.3.2 → 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/arbeitsplatz-dateien.js +50 -0
- package/dist/befehle/einrichten.js +161 -0
- package/dist/befehle/login.js +34 -6
- package/dist/befehle/skill-install.js +4 -2
- package/dist/befehle/werkbank.js +5 -8
- package/dist/befehle-katalog-en.js +4 -4
- package/dist/befehle-katalog.js +2 -2
- package/dist/befehlswoerter.js +1 -0
- package/dist/browser.js +43 -0
- package/dist/einstellungen-mischen.js +68 -28
- package/dist/einstellungen-schreiben.js +48 -12
- package/dist/kimal3.js +10 -3
- package/dist/logo-ablegen.js +117 -0
- package/dist/nachricht.js +5 -1
- package/dist/plugins-mischen.js +78 -0
- package/dist/sprache.js +28 -4
- package/dist/texte/einrichten-marken.js +9 -0
- package/dist/texte/einrichten.js +60 -0
- package/dist/wache-schalter.js +30 -5
- package/package.json +28 -27
- package/skill/SKILL.md +5 -6
- package/skill/en/SKILL.md +5 -6
|
@@ -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
|
+
}
|
|
@@ -315,6 +315,56 @@ export function dateienSchreiben(dateien, ordnerName, hinweis) {
|
|
|
315
315
|
}
|
|
316
316
|
return 0;
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Welche Dateien der Lieferung noch nicht auf der Platte liegen (Spec 2026-10-11, S-7).
|
|
320
|
+
* Rein, damit `check:cli` es ohne Platte misst; `gibtEs` fragt `existsSync`.
|
|
321
|
+
*/
|
|
322
|
+
export function welcheFehlen(dateien, gibtEs) {
|
|
323
|
+
return dateien.filter((d) => !gibtEs(d.pfad));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Für `kimal3 einrichten`: schreibt NUR, was fehlt, und fasst nichts an, was schon liegt
|
|
327
|
+
* (Spec 2026-10-11, S-7, L-26, L-62) -- anders als `dateienSchreiben`, das bei einem nicht
|
|
328
|
+
* leeren Ordner absagt. Ein zweiter Lauf heilt so einen halb geschriebenen Arbeitsplatz.
|
|
329
|
+
*
|
|
330
|
+
* 🔑 `flag: "wx"`: Legt ein zweiter, gleichzeitiger Lauf (L-70) dieselbe Datei an, scheitert
|
|
331
|
+
* dieser hier, statt sie zu überschreiben. Dieselben Absagen und Rückgabewerte wie
|
|
332
|
+
* `dateienSchreiben` (Begründungen dort): unsicherer Pfad 3, Platte 1.
|
|
333
|
+
*/
|
|
334
|
+
export function fehlendeDateienSchreiben(dateien, ziel) {
|
|
335
|
+
const t = ARBEITSPLATZ_TEXTE[sprache()];
|
|
336
|
+
if (dateien.length === 0) {
|
|
337
|
+
console.error(t.noFiles1);
|
|
338
|
+
console.error(t.noFiles2);
|
|
339
|
+
return { ok: false, wert: 3 };
|
|
340
|
+
}
|
|
341
|
+
for (const datei of dateien) {
|
|
342
|
+
if (istUnsichererPfad(datei.pfad)) {
|
|
343
|
+
console.error(t.unsafePath(datei.pfad));
|
|
344
|
+
return { ok: false, wert: 3 };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const ort = (pfad) => join(ziel, ...pfad.split("/"));
|
|
348
|
+
try {
|
|
349
|
+
if (existsSync(ziel) && !statSync(ziel).isDirectory()) {
|
|
350
|
+
console.error(t.isAFile1(ziel));
|
|
351
|
+
console.error(t.isAFile2);
|
|
352
|
+
return { ok: false, wert: 1 };
|
|
353
|
+
}
|
|
354
|
+
const fehlen = welcheFehlen(dateien, (pfad) => existsSync(ort(pfad)));
|
|
355
|
+
for (const datei of fehlen) {
|
|
356
|
+
mkdirSync(dirname(ort(datei.pfad)), { recursive: true });
|
|
357
|
+
writeFileSync(ort(datei.pfad), datei.inhalt, { encoding: "utf8", flag: "wx" });
|
|
358
|
+
}
|
|
359
|
+
return { ok: true, neu: fehlen.length, schonDa: dateien.length - fehlen.length };
|
|
360
|
+
}
|
|
361
|
+
catch (fehler) {
|
|
362
|
+
console.error(t.writeFailed(fehler instanceof Error ? fehler.message : String(fehler)));
|
|
363
|
+
console.error(t.incomplete(ziel));
|
|
364
|
+
console.error(t.writeFailedBlame);
|
|
365
|
+
return { ok: false, wert: 1 };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
318
368
|
/**
|
|
319
369
|
* Ein `..`-Segment oder ein absoluter Pfad duerften das Ziel verlassen.
|
|
320
370
|
*
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
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";
|
|
9
|
+
import { ruf } from "../api.js";
|
|
10
|
+
import { FIRMEN_OPTIONEN, fehlendeDateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
|
|
11
|
+
import { zerlege } from "../argumente.js";
|
|
12
|
+
import { sprache } from "../sprache.js";
|
|
13
|
+
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
14
|
+
import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
|
|
15
|
+
import { anmeldenMitBrowser } from "./login.js";
|
|
16
|
+
import { SKILL_TEXTE } from "../texte/skill.js";
|
|
17
|
+
import { skillInstall, ursacheDes } from "./skill-install.js";
|
|
18
|
+
/**
|
|
19
|
+
* `kimal3 einrichten` / `kimal3 setup` -- der eine Befehl, den Claude beim Einstieg mit Leitung
|
|
20
|
+
* aufruft (Spec 2026-10-11, Abschnitt 0, S-1, S-2, S-7). Er verbindet, was es schon gibt:
|
|
21
|
+
* Anmeldung (mit Browser), die Vorlage von `/api/cli/arbeitsplatz`, `skill install`, und setzt
|
|
22
|
+
* die Kurs-Haken.
|
|
23
|
+
*
|
|
24
|
+
* 🔑 Der Arbeitsplatz liegt IMMER im Benutzerordner (N-8), nie im Ordner, in dem Claude gerade
|
|
25
|
+
* steht -- die Sitzung ohne Ordner ist ein Wegwerf-Ordner der App (M-L2).
|
|
26
|
+
* 🔑 Nichts wird überschrieben: Ein zweiter Lauf ergänzt nur Fehlendes (`fehlendeDateienSchreiben`).
|
|
27
|
+
*
|
|
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).
|
|
32
|
+
*/
|
|
33
|
+
export async function einrichten(argv) {
|
|
34
|
+
const s = sprache();
|
|
35
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
36
|
+
console.log(EINRICHTEN_TEXTE[s].help);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
const zerlegt = zerlege(argv, FIRMEN_OPTIONEN, s);
|
|
40
|
+
if (!zerlegt.ok) {
|
|
41
|
+
console.error(`${zerlegt.meldung}\n${EINRICHTEN_TEXTE[s].help}`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
const wunsch = { firma: zerlegt.werte.optionen.firma, firmaId: zerlegt.werte.optionen["firma-id"] };
|
|
45
|
+
console.log(EINRICHTEN_TEXTE[s].start);
|
|
46
|
+
const gespeichert = leseAnmeldung();
|
|
47
|
+
if (gespeichert)
|
|
48
|
+
console.log(ALLGEMEIN_TEXTE[s].signedInAs(gespeichert.email));
|
|
49
|
+
else {
|
|
50
|
+
const angemeldet = await anmeldenMitBrowser();
|
|
51
|
+
if (angemeldet !== 0)
|
|
52
|
+
return angemeldet;
|
|
53
|
+
}
|
|
54
|
+
let holen = await vorlageHolen("einrichten", "", wunsch, null);
|
|
55
|
+
// Die gespeicherte Anmeldung galt nicht mehr (401 -> 2): einmal neu anmelden, dann noch einmal holen.
|
|
56
|
+
if (!holen.ok && holen.wert === 2 && gespeichert) {
|
|
57
|
+
const angemeldet = await anmeldenMitBrowser();
|
|
58
|
+
if (angemeldet !== 0)
|
|
59
|
+
return angemeldet;
|
|
60
|
+
holen = await vorlageHolen("einrichten", "", wunsch, null);
|
|
61
|
+
}
|
|
62
|
+
if (!holen.ok)
|
|
63
|
+
return holen.wert;
|
|
64
|
+
// 🌐 Die Antwort kann die Kontosprache umgestellt haben -- ab hier in der neuen.
|
|
65
|
+
const t = EINRICHTEN_TEXTE[sprache()];
|
|
66
|
+
const { wurzel, dateien } = holen.daten;
|
|
67
|
+
if (istUnsichererOrdnername(wurzel)) {
|
|
68
|
+
console.error(t.unsafeFolder(wurzel));
|
|
69
|
+
return 3;
|
|
70
|
+
}
|
|
71
|
+
const werkbank = werkbankOrdner(dateien);
|
|
72
|
+
if (werkbank === null) {
|
|
73
|
+
console.error(t.noWorkbank);
|
|
74
|
+
return 3;
|
|
75
|
+
}
|
|
76
|
+
const ziel = join(homedir(), wurzel);
|
|
77
|
+
const geschrieben = fehlendeDateienSchreiben(dateien, ziel);
|
|
78
|
+
if (!geschrieben.ok)
|
|
79
|
+
return geschrieben.wert;
|
|
80
|
+
console.log(t.filesWritten(geschrieben.neu, geschrieben.schonDa));
|
|
81
|
+
console.log(t.workplaceLine(ziel));
|
|
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);
|
|
86
|
+
const befehle = await skillInstall();
|
|
87
|
+
await kursAbhaken(git ? ["beat2-werkzeuge", "beat2-kimal3", "beat2-arbeitsplatz"] : ["beat2-kimal3", "beat2-arbeitsplatz"]);
|
|
88
|
+
if (befehle !== 0)
|
|
89
|
+
return befehle;
|
|
90
|
+
console.log(t.done);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
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
|
+
*/
|
|
136
|
+
function gitAntwortet() {
|
|
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);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Die Haken sind eine Zugabe: Scheitern sie, ist der Arbeitsplatz trotzdem da -- eine Zeile,
|
|
150
|
+
* kein Abbruch, und der nächste Lauf setzt sie (die Route ist dafür wiederholbar).
|
|
151
|
+
*/
|
|
152
|
+
async function kursAbhaken(schritte) {
|
|
153
|
+
const t = EINRICHTEN_TEXTE[sprache()];
|
|
154
|
+
try {
|
|
155
|
+
const a = await ruf("/api/cli/kurs", { koerper: { schritte }, mitAusweis: true }); // kein-anzeigetext: Pfad der Schnittstelle
|
|
156
|
+
console.log(a.status === 200 ? t.courseMarked : t.courseNotMarked);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
console.log(t.courseNotMarked);
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/befehle/login.js
CHANGED
|
@@ -2,23 +2,51 @@ import { hostname } from "node:os";
|
|
|
2
2
|
import { feld, ruf } from "../api.js";
|
|
3
3
|
import { BASIS_URL, schreibeAnmeldung } from "../anmeldung.js";
|
|
4
4
|
import { sprache } from "../sprache.js";
|
|
5
|
+
import { anmeldeAdresse, oeffneImBrowser } from "../browser.js";
|
|
5
6
|
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
7
|
+
import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
|
|
6
8
|
import { stelleSkillUm } from "./skill-tausch.js";
|
|
7
9
|
const warte = (ms) => new Promise((f) => setTimeout(f, ms));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// 🌐 Die Sprache wird bei JEDER Ausgabe neu gelesen: Die Abhol-Antwort nennt die Kontosprache
|
|
11
|
+
// (Task 5) und kann sie mitten im Lauf aendern -- die Adresse vorher kam noch in der alten.
|
|
12
|
+
const t = () => ALLGEMEIN_TEXTE[sprache()];
|
|
13
|
+
async function starteAnmeldung() {
|
|
12
14
|
const start = await ruf("/api/cli/login/start", { koerper: { bezeichnung: hostname() } });
|
|
13
15
|
const nutzerCode = feld(start.koerper, "nutzer_code");
|
|
14
16
|
const geraeteCode = feld(start.koerper, "geraete_code");
|
|
15
17
|
if (start.status !== 200 || !nutzerCode || !geraeteCode) {
|
|
16
18
|
console.error(t().unreachable);
|
|
17
|
-
return
|
|
19
|
+
return null;
|
|
18
20
|
}
|
|
21
|
+
return { nutzerCode, geraeteCode };
|
|
22
|
+
}
|
|
23
|
+
export async function login() {
|
|
24
|
+
const start = await starteAnmeldung();
|
|
25
|
+
if (!start)
|
|
26
|
+
return 3;
|
|
19
27
|
console.log(t().loginOpen(BASIS_URL));
|
|
20
|
-
console.log(t().loginEnterCode(nutzerCode));
|
|
28
|
+
console.log(t().loginEnterCode(start.nutzerCode));
|
|
21
29
|
console.log(t().loginWaiting);
|
|
30
|
+
return warteAufBestaetigung(start.geraeteCode);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Die Anmeldung für `kimal3 einrichten` (Spec 2026-10-11, S-2): Code und Adresse auf eigenen
|
|
34
|
+
* Zeilen, die Claude liest (Marken in `texte/einrichten-marken.ts`), und der Browser öffnet
|
|
35
|
+
* die Seite mit dem Code -- die Seite fragt dort, ob er im Claude-Fenster steht.
|
|
36
|
+
*/
|
|
37
|
+
export async function anmeldenMitBrowser() {
|
|
38
|
+
const start = await starteAnmeldung();
|
|
39
|
+
if (!start)
|
|
40
|
+
return 3;
|
|
41
|
+
const e = EINRICHTEN_TEXTE[sprache()];
|
|
42
|
+
const adresse = anmeldeAdresse(BASIS_URL, start.nutzerCode);
|
|
43
|
+
console.log(e.codeLine(start.nutzerCode));
|
|
44
|
+
console.log(e.addressLine(adresse));
|
|
45
|
+
console.log(oeffneImBrowser(adresse) ? e.browserOpened : e.browserNotOpened);
|
|
46
|
+
console.log(t().loginWaiting);
|
|
47
|
+
return warteAufBestaetigung(start.geraeteCode);
|
|
48
|
+
}
|
|
49
|
+
async function warteAufBestaetigung(geraeteCode) {
|
|
22
50
|
const endeUm = Date.now() + 10 * 60 * 1000;
|
|
23
51
|
while (Date.now() < endeUm) {
|
|
24
52
|
await warte(2000);
|
|
@@ -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);
|
package/dist/befehle/werkbank.js
CHANGED
|
@@ -34,14 +34,11 @@ import { leseEineDatei, leseMerkzettelDatei } from "../kontext-dateien.js";
|
|
|
34
34
|
* arbeitsplatz neu → einmal pro FIRMA (die ganze Familie)
|
|
35
35
|
* werkbank neu → einmal pro MENSCH (nur der eigene Teil)
|
|
36
36
|
*
|
|
37
|
-
* 🔑 **Pro (seit 2026-10-11, Spec „
|
|
38
|
-
* Vorlage über `/api/cli/arbeitsplatz`, und
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* Pro-Konto mehr. Hat er beides nicht, bekommt er `402` und Rückgabewert `4`,
|
|
43
|
-
* und `SKILL.md` weist Claude dort ausdrücklich an, NICHT zu wiederholen.
|
|
44
|
-
* Hier im Befehl ändert sich dafür nichts: Die Entscheidung fällt am Server.
|
|
37
|
+
* 🔑 **Kein Pro nötig (seit 2026-10-11, Spec „Einstieg mit Leitung", S-6)**: Der Befehl
|
|
38
|
+
* holt die Vorlage über `/api/cli/arbeitsplatz`, und diese Route hat keine Pro-Tür mehr —
|
|
39
|
+
* jedes angemeldete Konto bekommt den Ordner. Nur die gemeinsamen Firmenantworten gehen
|
|
40
|
+
* ausschließlich an Pro (`kontextFuerPlan` am Server). Hier im Befehl ändert sich dafür
|
|
41
|
+
* nichts: Die Entscheidung fällt am Server.
|
|
45
42
|
*
|
|
46
43
|
* ⚠️ Eine Grenze bleibt, unabhängig davon: Der Angestellte hat noch keinen
|
|
47
44
|
* gebauten Weg an den Firmen-Ordner (Stufe 2).
|
|
@@ -25,14 +25,14 @@ export const BEFEHLE_EN = {
|
|
|
25
25
|
],
|
|
26
26
|
},
|
|
27
27
|
"kimal3 arbeitsplatz neu": {
|
|
28
|
-
stempel: "
|
|
28
|
+
stempel: "6fd35d27",
|
|
29
29
|
befehl: "kimal3 workplace new",
|
|
30
30
|
tippen: "! kimal3 workplace new",
|
|
31
31
|
terminal: "kimal3 workplace new",
|
|
32
32
|
beschreibung: "Create the whole AI Workplace — company level and workbench",
|
|
33
33
|
rhythmusOderSatz: "once per company",
|
|
34
34
|
details: [
|
|
35
|
-
"Fetches the template from your
|
|
35
|
+
"Fetches the template from your kimal3 account and creates it here as a real folder — CLAUDE.md, routine commands, the workbench for your projects. The same package is available to download on /pro.",
|
|
36
36
|
"The folder is called AI-Workplace (KI-Arbeitsplatz for a German-speaking company). To keep several companies apart, use the folder above it, e.g. Clients/ARNO/AI-Workplace. Add your first name and your workbench inside it is called workbench-sabine: kimal3 workplace new sabine.",
|
|
37
37
|
"If the target folder already exists and isn't empty, the command stops instead of overwriting anything — your work is safe. You'll know it worked when it tells you how many files it created.",
|
|
38
38
|
"⚠️ It also creates the company level (context/ with brand, systems, security — kontext/ in a German workplace). If that already exists, you need kimal3 workbench new — otherwise you end up with a second company inside the first.",
|
|
@@ -53,7 +53,7 @@ export const BEFEHLE_EN = {
|
|
|
53
53
|
],
|
|
54
54
|
},
|
|
55
55
|
"kimal3 kontext runter": {
|
|
56
|
-
stempel: "
|
|
56
|
+
stempel: "55d21a3c",
|
|
57
57
|
befehl: "kimal3 context download",
|
|
58
58
|
tippen: "! kimal3 context download",
|
|
59
59
|
terminal: "kimal3 context download",
|
|
@@ -63,7 +63,7 @@ export const BEFEHLE_EN = {
|
|
|
63
63
|
"Fetches CLAUDE.md and the five files in context/ (kontext/ in a German workplace) from your company and puts them in your AI Workplace. Anything you've changed locally is left alone — it tells you how to send it up.",
|
|
64
64
|
"If you changed a file locally AND the company has changed it too in the meantime, it saves your version in context/before-<date>/ (kontext/vorher-<datum>/ in a German workplace) first, then replaces it — nothing gets lost.",
|
|
65
65
|
"You'll know it worked when it names the updated files or says “Your company context is up to date.”",
|
|
66
|
-
"⚠️
|
|
66
|
+
"⚠️ It needs Pro — your own or through your company. Without it you get exit code 4 — then don't retry.",
|
|
67
67
|
],
|
|
68
68
|
},
|
|
69
69
|
"kimal3 kontext fragen": {
|
package/dist/befehle-katalog.js
CHANGED
|
@@ -64,7 +64,7 @@ export const BEFEHLE = [
|
|
|
64
64
|
tippen: "! kimal3 arbeitsplatz neu",
|
|
65
65
|
terminal: "kimal3 arbeitsplatz neu",
|
|
66
66
|
details: [
|
|
67
|
-
"Holt die Vorlage
|
|
67
|
+
"Holt die Vorlage aus deinem kimal3-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.",
|
|
68
68
|
"Der Ordner heißt KI-Arbeitsplatz (bei einer englischsprachigen Firma AI-Workplace) — 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 (bei einem englischen Konto workbench-sabine): kimal3 arbeitsplatz neu sabine.",
|
|
69
69
|
"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.",
|
|
70
70
|
"⚠️ 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.",
|
|
@@ -112,7 +112,7 @@ export const BEFEHLE = [
|
|
|
112
112
|
"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.",
|
|
113
113
|
"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.",
|
|
114
114
|
"Geklappt hat es, wenn er die aktualisierten Dateien nennt oder „Dein Firmenkontext ist aktuell“ sagt.",
|
|
115
|
-
"⚠️
|
|
115
|
+
"⚠️ Er braucht Pro — eigenes oder über deine Firma. Ohne das kommt Rückgabewert 4 — dann nicht wiederholen.",
|
|
116
116
|
],
|
|
117
117
|
},
|
|
118
118
|
{
|
package/dist/befehlswoerter.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const BEFEHLSWOERTER = {
|
|
2
|
+
einrichten: { de: "einrichten", en: "setup" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
|
|
2
3
|
login: { de: "login", en: "login" },
|
|
3
4
|
logout: { de: "logout", en: "logout" },
|
|
4
5
|
"skill install": { de: "skill install", en: "skill install" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Öffnet eine Adresse im Standard-Browser (Spec 2026-10-11, S-2: Sabine soll keinen Code abtippen).
|
|
5
|
+
*
|
|
6
|
+
* 🔑 Ohne Shell: `rundll32 url.dll,FileProtocolHandler` statt `cmd /c start` -- `cmd` deutet
|
|
7
|
+
* `&` und `^` in der Adresse als Befehl. Dazu die Positivliste (messen-und-pruefen.md, §23):
|
|
8
|
+
* Nur http(s) und nur Zeichen, die in unseren Adressen vorkommen.
|
|
9
|
+
* ⚠️ `platform()` aus `node:os`, nicht `process.platform` -- `check:beenden` lässt hinter
|
|
10
|
+
* `process` nur eine feste Liste zu.
|
|
11
|
+
*/
|
|
12
|
+
const SICHERE_ADRESSE = /^https?:\/\/[A-Za-z0-9.-]+(:[0-9]+)?(\/[A-Za-z0-9\-._~/?=&%]*)?$/;
|
|
13
|
+
/** Die Adresse der Bestätigungsseite mit mitgebrachtem Code (eine Adresse, kein Anzeigetext). */
|
|
14
|
+
export const anmeldeAdresse = (basis, code) => `${basis}/cli?code=${encodeURIComponent(code)}`; // kein-anzeigetext: Adresse der Bestätigungsseite
|
|
15
|
+
export function browserBefehl(plattform, url) {
|
|
16
|
+
if (!SICHERE_ADRESSE.test(url))
|
|
17
|
+
return null;
|
|
18
|
+
if (plattform === "win32")
|
|
19
|
+
return { befehl: "rundll32", argumente: ["url.dll,FileProtocolHandler", url] }; // kein-anzeigetext: Windows-Aufruf
|
|
20
|
+
if (plattform === "darwin")
|
|
21
|
+
return { befehl: "open", argumente: [url] };
|
|
22
|
+
return { befehl: "xdg-open", argumente: [url] };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `true` heißt „versucht", nicht „sichtbar" -- ob ein Fenster aufging, weiß nur der Mensch.
|
|
26
|
+
* Deshalb druckt `einrichten` die Adresse IMMER mit. Der Kindprozess wird abgehängt
|
|
27
|
+
* (`unref`), damit er das Programm nicht am Leben hält; ein Fehler beim Starten (kein
|
|
28
|
+
* `xdg-open`) wird geschluckt, statt das Programm abzubrechen.
|
|
29
|
+
*/
|
|
30
|
+
export function oeffneImBrowser(url) {
|
|
31
|
+
const b = browserBefehl(platform(), url);
|
|
32
|
+
if (b === null)
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const kind = spawn(b.befehl, b.argumente, { detached: true, stdio: "ignore", windowsHide: true });
|
|
36
|
+
kind.on("error", () => { });
|
|
37
|
+
kind.unref();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|