kimal3 0.4.0 → 0.5.0
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 +4 -0
- package/coach/de/kimal3-coach.md +2 -2
- package/coach/en/kimal3-coach.md +2 -2
- package/dist/befehle/einrichten.js +1 -1
- package/dist/befehle/login.js +6 -2
- package/dist/befehle/skill-install.js +4 -1
- package/dist/befehle/zeigen-einrichten.js +104 -0
- package/dist/befehle/zeigen-start.js +143 -0
- package/dist/befehle/zeigen.js +288 -0
- package/dist/befehle-katalog-en.js +27 -0
- package/dist/befehle-katalog.js +27 -0
- package/dist/befehlswoerter.js +11 -0
- package/dist/kimal3.js +10 -0
- package/dist/logo-ablegen.js +26 -6
- package/dist/slash-befehle.js +1 -0
- package/dist/texte/einrichten.js +10 -3
- package/dist/texte/zeigen.js +189 -0
- package/dist/zeigen-rechnung.js +226 -0
- package/dist/zeigen-umgebung.js +190 -0
- package/package.json +2 -1
- package/skill/SKILL.md +3 -2
- package/skill/commands/k3.md +4 -3
- package/skill/commands/k3kurs.md +41 -41
- package/skill/commands/k3start.md +1 -1
- package/skill/commands/k3zeigen.md +303 -0
- package/skill/en/SKILL.md +3 -2
- package/skill/en/commands/k3.md +4 -3
- package/skill/en/commands/k3course.md +41 -41
- package/skill/en/commands/k3show.md +302 -0
- package/skill/en/commands/k3start.md +1 -1
- package/zeigen/gemeinsam.py +82 -0
- package/zeigen/starter.py +163 -0
- package/zeigen/werkzeug.py +250 -0
- package/zeigen/zeitleiste.py +393 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Reine Rechnungen hinter `kimal3 zeigen` (Aufnahme von Bildschirm und Ton fuer Claude).
|
|
2
|
+
// Kein Dateisystem, kein Prozess, keine Anzeige-Texte: Die Befehle holen die Fakten und fragen hier.
|
|
3
|
+
// Gedeckt von `npm run check:zeigen` (Spec 2026-10-18-k3zeigen-design.md).
|
|
4
|
+
import { win32 } from "node:path";
|
|
5
|
+
const MAX_PFAD_WINDOWS = 170;
|
|
6
|
+
/** "2026-10-19-0912" in Ortszeit. */
|
|
7
|
+
export function ordnerName(jetzt) {
|
|
8
|
+
const z = (n) => String(n).padStart(2, "0");
|
|
9
|
+
return `${jetzt.getFullYear()}-${z(jetzt.getMonth() + 1)}-${z(jetzt.getDate())}-${z(jetzt.getHours())}${z(jetzt.getMinutes())}`;
|
|
10
|
+
}
|
|
11
|
+
/** Index des Projektordners im Segment-Array, -1 wenn keiner, -2 wenn der Pfad bei der Werkbank endet. */
|
|
12
|
+
function projektEnde(segmente) {
|
|
13
|
+
const i = segmente.findIndex((s) => /^(werkbank|workbench)-/i.test(s));
|
|
14
|
+
if (i < 0)
|
|
15
|
+
return -1;
|
|
16
|
+
const projekte = /^werkbank-/i.test(segmente[i]) ? /^projekte$/i : /^projects$/i;
|
|
17
|
+
if (i === segmente.length - 1)
|
|
18
|
+
return -2;
|
|
19
|
+
if (!projekte.test(segmente[i + 1]))
|
|
20
|
+
return -1;
|
|
21
|
+
return i + 2 < segmente.length ? i + 2 : -2;
|
|
22
|
+
}
|
|
23
|
+
/** Wohin die Aufnahme faellt: ins Projekt, in dem man steht -- nie in die Werkbank selbst. */
|
|
24
|
+
export function aufnahmeOrt(start, jetzt, plattform, gibtEs) {
|
|
25
|
+
const trenner = start.includes("\\") ? "\\" : "/";
|
|
26
|
+
const segmente = start.split(/[\\/]/);
|
|
27
|
+
while (segmente.length > 1 && segmente[segmente.length - 1] === "")
|
|
28
|
+
segmente.pop();
|
|
29
|
+
const ende = projektEnde(segmente);
|
|
30
|
+
if (ende === -2)
|
|
31
|
+
return { art: "werkbank" };
|
|
32
|
+
const projekt = (ende >= 0 ? segmente.slice(0, ende + 1) : segmente).join(trenner);
|
|
33
|
+
const aufnahmen = `${projekt}${trenner}material${trenner}aufnahmen`; // kein-anzeigetext: Ordnerpfad
|
|
34
|
+
const basis = `${aufnahmen}${trenner}${ordnerName(jetzt)}`;
|
|
35
|
+
let ordner = basis;
|
|
36
|
+
for (let n = 2; gibtEs(ordner); n++)
|
|
37
|
+
ordner = `${basis}-${n}`;
|
|
38
|
+
if (plattform === "win32" && ordner.length > MAX_PFAD_WINDOWS) {
|
|
39
|
+
return { art: "zu-lang", laenge: ordner.length, hoechstens: MAX_PFAD_WINDOWS };
|
|
40
|
+
}
|
|
41
|
+
return { art: "ort", ordner, aufnahmen };
|
|
42
|
+
}
|
|
43
|
+
/** 93 -> "1:33", 3725 -> "1:02:05". */
|
|
44
|
+
export function dauerText(sekunden) {
|
|
45
|
+
const s = Math.floor(sekunden);
|
|
46
|
+
const h = Math.floor(s / 3600);
|
|
47
|
+
const m = Math.floor((s % 3600) / 60);
|
|
48
|
+
const rest = String(s % 60).padStart(2, "0");
|
|
49
|
+
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${rest}` : `${m}:${rest}`;
|
|
50
|
+
}
|
|
51
|
+
/** "0:26" -> 26, "1:02:05" -> 3725, "26" -> 26; alles andere null. */
|
|
52
|
+
export function zeitAusText(text) {
|
|
53
|
+
const teile = text.trim().split(":");
|
|
54
|
+
if (teile.length > 3 || !teile.every((t) => /^\d+$/.test(t)))
|
|
55
|
+
return null;
|
|
56
|
+
const zahlen = teile.map(Number);
|
|
57
|
+
if (zahlen.slice(1).some((n) => n >= 60))
|
|
58
|
+
return null;
|
|
59
|
+
return zahlen.reduce((summe, n) => summe * 60 + n, 0);
|
|
60
|
+
}
|
|
61
|
+
/** 26 -> "0-26.png", 3725 -> "1-02-05.png" (Doppelpunkte duerfen nicht in Dateinamen). */
|
|
62
|
+
export function bildName(sekunden) {
|
|
63
|
+
return `${dauerText(sekunden).replaceAll(":", "-")}.png`; // kein-anzeigetext: Dateiname
|
|
64
|
+
}
|
|
65
|
+
// Reihenfolge zaehlt: das erste Muster, das trifft, gewinnt. Das Protokoll enthaelt Fenstertitel -- es wird nie
|
|
66
|
+
// zurueckgegeben oder angezeigt, nur nach diesen festen Mustern durchsucht.
|
|
67
|
+
const GRUENDE = [
|
|
68
|
+
[/FFmpegUnavailableError|ffmpeg.*not found/i, "ffmpeg-fehlt"],
|
|
69
|
+
[/returned success without a non-empty output/, "pfad-zu-lang"],
|
|
70
|
+
[/PortAudio|No Default Input Device|sounddevice/, "kein-mikrofon"],
|
|
71
|
+
[/DesktopCaptureError|screen layout/, "bildschirm-gesperrt"],
|
|
72
|
+
[/did not become ready|did not start/, "startet-nicht"],
|
|
73
|
+
];
|
|
74
|
+
export function fehlerGrund(protokoll) {
|
|
75
|
+
return GRUENDE.find(([muster]) => muster.test(protokoll))?.[1] ?? "unbekannt";
|
|
76
|
+
}
|
|
77
|
+
/** Der Aufnehmer antwortet mit einer JSON-Zeile ganz am Ende; alles andere (auch ein Traceback) ist ein Fehlschlag. */
|
|
78
|
+
export function liesWerkzeugAntwort(rueckgabe, stdout) {
|
|
79
|
+
if (rueckgabe !== 0)
|
|
80
|
+
return { ok: false };
|
|
81
|
+
const zeilen = stdout.split(/\r?\n/).filter((z) => z.trim() !== "");
|
|
82
|
+
const letzte = zeilen[zeilen.length - 1];
|
|
83
|
+
if (letzte === undefined)
|
|
84
|
+
return { ok: false };
|
|
85
|
+
try {
|
|
86
|
+
const daten = JSON.parse(letzte);
|
|
87
|
+
if (typeof daten !== "object" || daten === null || Array.isArray(daten))
|
|
88
|
+
return { ok: false };
|
|
89
|
+
return { ok: true, daten: daten };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { ok: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** So alt darf das Lebenszeichen des Starters hoechstens sein, damit er als lebend gilt (er schreibt alle 2 s). */
|
|
96
|
+
const LEBENSZEICHEN_SEK = 10;
|
|
97
|
+
/** Lebt der Starter nachweislich -- meldet `startet`/`laeuft` UND sein Lebenszeichen ist hoechstens 10 s alt? (R-3) */
|
|
98
|
+
export function starterLebt(s, jetzt) {
|
|
99
|
+
if (s === null || (s.zustand !== "startet" && s.zustand !== "laeuft"))
|
|
100
|
+
return false;
|
|
101
|
+
return s.zuletzt !== undefined && jetzt - s.zuletzt <= LEBENSZEICHEN_SEK;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Die Wahrheit ueber "laeuft" kommt vom Aufnehmer; Notiz und Starter erzaehlen nur dazu.
|
|
105
|
+
* `jetzt` in Epochensekunden. Ausnahme Uebergangsfenster (R-3): Der Aufnehmer meldet noch/schon keine Sitzung, der
|
|
106
|
+
* Starter lebt aber nachweislich (frisches Lebenszeichen) -- dann laeuft etwas, sonst waere "abgebrochen" falsch.
|
|
107
|
+
*/
|
|
108
|
+
export function aufnahmeZustand(f, jetzt) {
|
|
109
|
+
if (f.aktiveSitzungen === null)
|
|
110
|
+
return { art: "unbekannt" };
|
|
111
|
+
// Der Starter konnte den Aufnehmer nicht beenden: Solange er noch lebt (und es weiter versucht) oder der Aufnehmer
|
|
112
|
+
// eine Sitzung meldet, weiss niemand, ob aufgenommen wird. Ein laengst stiller Starter ohne aktive Sitzung blockiert
|
|
113
|
+
// dagegen nicht jeden weiteren Start im Projekt -- er zaehlt unten als abgebrochen (Endpruefung, Punkt 3).
|
|
114
|
+
const frisch = f.starter?.zuletzt !== undefined && jetzt - f.starter.zuletzt <= LEBENSZEICHEN_SEK;
|
|
115
|
+
if (f.starter?.grund === "nicht-beendet" && (frisch || f.aktiveSitzungen.length > 0))
|
|
116
|
+
return { art: "unbekannt" };
|
|
117
|
+
const n = f.notiz;
|
|
118
|
+
const ausNotiz = (sitzung, eigenerOrdner) => n !== null && ((sitzung !== null && n.sitzung === sitzung) || eigenerOrdner)
|
|
119
|
+
? { art: "laeuft", sitzung, ordner: n.ordner, gestartet: n.gestartet, frist: n.frist, fremd: false }
|
|
120
|
+
: { art: "laeuft", sitzung, ordner: null, gestartet: null, frist: null, fremd: true };
|
|
121
|
+
if (f.aktiveSitzungen.length > 0) {
|
|
122
|
+
const eigene = n !== null && f.aktiveSitzungen.includes(n.sitzung) ? n.sitzung : null;
|
|
123
|
+
return ausNotiz(eigene ?? f.aktiveSitzungen[0], false);
|
|
124
|
+
}
|
|
125
|
+
const s = f.starter;
|
|
126
|
+
if (s === null)
|
|
127
|
+
return { art: "keine", letzte: null };
|
|
128
|
+
if (starterLebt(s, jetzt))
|
|
129
|
+
return ausNotiz(s.sitzung ?? null, f.starterImNotizOrdner);
|
|
130
|
+
if (s.zustand === "selbst-gestoppt" || s.zustand === "gestoppt")
|
|
131
|
+
return { art: "keine", letzte: s.zustand };
|
|
132
|
+
return { art: "keine", letzte: "abgebrochen" };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Das Python des Aufnehmers ist nicht zu finden (Ruling R-13). Ohne Notiz und ohne offenen Starter kann nichts laufen,
|
|
136
|
+
* was kimal3 gestartet hat -> wie "keine aktive Sitzung". Gibt es aber eine Notiz oder einen Starter in
|
|
137
|
+
* `startet`/`laeuft`, entscheidet allein sein Lebenszeichen: frisch -> laeuft, sonst weiss es niemand.
|
|
138
|
+
* Steht der Starter in einem ENDZUSTAND, ist die Aufnahme zu, auch mit Notiz (Ruling R-17): Er schreibt ihn erst, wenn
|
|
139
|
+
* sein Kind weg ist. Ausnahme `grund: nicht-beendet` mit frischem Lebenszeichen -- die faengt `aufnahmeZustand` als
|
|
140
|
+
* "weiss nicht" ab.
|
|
141
|
+
*/
|
|
142
|
+
export function zustandOhneAufnehmer(f, jetzt) {
|
|
143
|
+
const mitListe = { ...f, aktiveSitzungen: [] };
|
|
144
|
+
const offen = f.starter?.zustand === "startet" || f.starter?.zustand === "laeuft";
|
|
145
|
+
const zu = f.starter !== null && !offen;
|
|
146
|
+
if (zu || f.notiz === null && !offen)
|
|
147
|
+
return aufnahmeZustand(mitListe, jetzt);
|
|
148
|
+
return starterLebt(f.starter, jetzt) ? aufnahmeZustand(mitListe, jetzt) : { art: "unbekannt" };
|
|
149
|
+
}
|
|
150
|
+
export const AUFNEHMER_FASSUNG = "1.3.0";
|
|
151
|
+
/** Die Fassung des Aufnehmers, wie `teile` sie meldet -- nur in der Form `1.3.0…`, alles andere ist keine Angabe (R-14, Punkt 5). */
|
|
152
|
+
export const gueltigeFassung = (w) => (typeof w === "string" && /^\d+\.\d+\.\d+\S*$/.test(w) ? w : null);
|
|
153
|
+
/**
|
|
154
|
+
* Die Antwort von `werkzeug.py teile` -> `Teile`. Ohne Antwort (kein Aufnehmer-Python) oder mit einer unlesbaren gilt
|
|
155
|
+
* alles ausser uv als fehlend: Einrichten holt es dann (wieder), statt einem stummen Aufnehmer zu glauben.
|
|
156
|
+
*/
|
|
157
|
+
export function teileAus(uv, antwort) {
|
|
158
|
+
if (antwort === null || !antwort.ok)
|
|
159
|
+
return { uv, aufnehmer: null, ffmpeg: false, modell: false };
|
|
160
|
+
const d = antwort.daten;
|
|
161
|
+
return { uv, aufnehmer: gueltigeFassung(d.aufnehmer), ffmpeg: d.ffmpeg === true, modell: d.modell === true };
|
|
162
|
+
}
|
|
163
|
+
export const AUFNEHMER_PAKET = `openadapt-capture[transcribe-fast]==${AUFNEHMER_FASSUNG}`; // kein-anzeigetext: Paketname
|
|
164
|
+
/** `uv tool install …` -- `ersetzen`, sobald schon eine Umgebung des Aufnehmers liegt (andere Fassung, kaputt). */
|
|
165
|
+
export function aufnehmerArgumente(ersetzen) {
|
|
166
|
+
// kein-anzeigetext: Befehlszeile
|
|
167
|
+
return ["tool", "install", "--python", "3.12", ...(ersetzen ? ["--force"] : []), AUFNEHMER_PAKET];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Wie uv auf den Rechner kommt: winget zuerst (Coach-Regel: Paketmanager), sonst der offizielle Installer pro Benutzer.
|
|
171
|
+
* Ohne die beiden Zustimmungsschalter haengt winget auf einem frischen Rechner an einer Frage (Spec US-2).
|
|
172
|
+
*/
|
|
173
|
+
export function uvEinrichtung(winget) {
|
|
174
|
+
// kein-anzeigetext: Befehlszeile
|
|
175
|
+
if (winget)
|
|
176
|
+
return { befehl: "winget", argumente: ["install", "--id", "astral-sh.uv", "-e", "--scope", "user", "--accept-source-agreements", "--accept-package-agreements"] };
|
|
177
|
+
// kein-anzeigetext: Befehlszeile
|
|
178
|
+
return { befehl: "powershell", argumente: ["-ExecutionPolicy", "ByPass", "-c", "irm https://astral.sh/uv/install.ps1 | iex"] };
|
|
179
|
+
}
|
|
180
|
+
/** Was beim Einrichten noch fehlt, in der Reihenfolge, in der es gebraucht wird. */
|
|
181
|
+
export function einrichtPlan(t) {
|
|
182
|
+
const fehlt = [
|
|
183
|
+
[t.uv === null, "uv"],
|
|
184
|
+
[t.aufnehmer !== AUFNEHMER_FASSUNG, "aufnehmer"],
|
|
185
|
+
[!t.ffmpeg, "ffmpeg"],
|
|
186
|
+
[!t.modell, "modell"],
|
|
187
|
+
];
|
|
188
|
+
return fehlt.filter(([f]) => f).map(([, schritt]) => schritt);
|
|
189
|
+
}
|
|
190
|
+
/** Wo uv unter Windows liegen kann, wenn es nicht im PATH steht (frisch installiert, Terminal noch alt). */
|
|
191
|
+
export function uvKandidaten(env, home) {
|
|
192
|
+
const lokal = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local"); // kein-anzeigetext: Ordnerpfad
|
|
193
|
+
return [
|
|
194
|
+
// kein-anzeigetext: Ordnerpfad
|
|
195
|
+
win32.join(lokal, "Microsoft", "WinGet", "Packages", "astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe", "uv.exe"),
|
|
196
|
+
win32.join(home, ".local", "bin", "uv.exe"),
|
|
197
|
+
win32.join(home, ".cargo", "bin", "uv.exe"),
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Welcher Ordner in `material/aufnahmen/` ist die juengste Aufnahme? Nur Namen von `ordnerName` (mit Endung `-2` …)
|
|
202
|
+
* zaehlen; `-10` kommt nach `-9` (als Text stuende es davor). `istAufnahme` sortiert Ordner aus, in denen keine
|
|
203
|
+
* Aufnahme liegt (ein gescheiterter Start hinterlaesst nur Protokoll und Starter-Bericht) -- sonst verdeckte er die
|
|
204
|
+
* letzte echte fuer auswerten und stopp (Endpruefung, Punkt 4). `null`, wenn keiner passt.
|
|
205
|
+
*/
|
|
206
|
+
export function juengsteAufnahme(namen, istAufnahme) {
|
|
207
|
+
const schluessel = (n) => {
|
|
208
|
+
const m = /^(\d{4}-\d{2}-\d{2}-\d{4})(?:-(\d+))?$/.exec(n);
|
|
209
|
+
return m ? { name: n, basis: m[1], nr: m[2] === undefined ? 1 : Number(m[2]) } : null;
|
|
210
|
+
};
|
|
211
|
+
const passend = namen.map(schluessel).filter((k) => k !== null);
|
|
212
|
+
passend.sort((a, b) => (a.basis === b.basis ? b.nr - a.nr : a.basis < b.basis ? 1 : -1));
|
|
213
|
+
return passend.find((k) => istAufnahme(k.name))?.name ?? null;
|
|
214
|
+
}
|
|
215
|
+
/** `zahlenDa`: Dauer, Klicks, Tasten und Ton kamen in der erwarteten Form. Ohne sie ist auch "gestoppt" nicht bestaetigt. */
|
|
216
|
+
export function stoppAusgang(ergebnis, zahlenDa) {
|
|
217
|
+
if (ergebnis === "keine")
|
|
218
|
+
return { satz: "keine", code: 0, notizWeg: true };
|
|
219
|
+
if ((ergebnis === "gestoppt" || ergebnis === "selbst-gestoppt") && zahlenDa)
|
|
220
|
+
return { satz: ergebnis, code: 0, notizWeg: true };
|
|
221
|
+
if (ergebnis === "nicht-sauber" || ergebnis === "abgebrochen")
|
|
222
|
+
return { satz: ergebnis, code: 1, notizWeg: true };
|
|
223
|
+
if (ergebnis === "wird-abgeschlossen")
|
|
224
|
+
return { satz: ergebnis, code: 1, notizWeg: false };
|
|
225
|
+
return { satz: "unklar", code: 1, notizWeg: false };
|
|
226
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join, win32 } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { juengsteAufnahme, liesWerkzeugAntwort, starterLebt, uvKandidaten } from "./zeigen-rechnung.js";
|
|
7
|
+
/**
|
|
8
|
+
* Die Umgebung hinter `kimal3 zeigen`: wo der Aufnehmer und sein Python liegen, wie das Werkzeug gerufen wird, und
|
|
9
|
+
* die Notiz `~/.kimal3/zeigen.json` (welcher Ordner zu welcher Sitzung gehoert). Entscheidungen trifft
|
|
10
|
+
* `zeigen-rechnung.ts`; hier wird nur gesucht, gestartet, gelesen und geschrieben.
|
|
11
|
+
* 🔴 Ausgaben fremder Programme werden nie weitergereicht: vom Werkzeug zaehlt nur die eine JSON-Zeile (gelesen von
|
|
12
|
+
* `liesWerkzeugAntwort`), stderr wird verworfen.
|
|
13
|
+
*/
|
|
14
|
+
/** `cli/dist/zeigen-umgebung.js` -> `cli/zeigen/werkzeug.py` (im Paket unter `files`, Task 7). */
|
|
15
|
+
const WERKZEUG = fileURLToPath(new URL("../zeigen/werkzeug.py", import.meta.url)); // kein-anzeigetext: Paketpfad
|
|
16
|
+
function laeuft(befehl, argumente) {
|
|
17
|
+
try {
|
|
18
|
+
const r = spawnSync(befehl, argumente, { encoding: "utf8", windowsHide: true, timeout: 20_000 });
|
|
19
|
+
return r.status === 0 ? r.stdout : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Das erste uv, das antwortet: Suchpfad, dann die bekannten Orte (frisch installiert, Terminal noch alt). */
|
|
26
|
+
export function findeUv() {
|
|
27
|
+
for (const uv of ["uv", ...uvKandidaten(process.env, homedir())]) {
|
|
28
|
+
const version = laeuft(uv, ["--version"]);
|
|
29
|
+
if (version !== null)
|
|
30
|
+
return { uv, version };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** "uv 0.12.10 (…)" -> "0.12.10"; `null`, wenn es kein uv gibt. */
|
|
35
|
+
export function uvFassung() {
|
|
36
|
+
return /^uv (\d+\.\d+\.\d+\S*)/.exec(findeUv()?.version ?? "")?.[1] ?? null;
|
|
37
|
+
}
|
|
38
|
+
/** Python und pythonw des Aufnehmers: `<uv tool dir>/openadapt-capture/Scripts/`, sonst `%APPDATA%\uv\tools\…`. */
|
|
39
|
+
export function aufnehmerPython() {
|
|
40
|
+
const uv = findeUv();
|
|
41
|
+
const toolDir = uv === null ? null : laeuft(uv.uv, ["tool", "dir"])?.trim() || null;
|
|
42
|
+
const appdata = process.env.APPDATA ?? win32.join(homedir(), "AppData", "Roaming"); // kein-anzeigetext: Ordnerpfad
|
|
43
|
+
// Antwortet uv, gilt sein Ordner allein (so greift `UV_TOOL_DIR`); der Standardort nur, wenn uv selbst fehlt (Task 5:
|
|
44
|
+
// sonst faende ein nachgestellter frischer Rechner den echten Aufnehmer unter %APPDATA% doch).
|
|
45
|
+
const orte = toolDir !== null ? [toolDir] : [win32.join(appdata, "uv", "tools")]; // kein-anzeigetext: Ordnerpfad
|
|
46
|
+
for (const ort of orte) {
|
|
47
|
+
const scripts = win32.join(ort, "openadapt-capture", "Scripts"); // kein-anzeigetext: Ordnerpfad
|
|
48
|
+
const python = win32.join(scripts, "python.exe");
|
|
49
|
+
const pythonw = win32.join(scripts, "pythonw.exe");
|
|
50
|
+
if (existsSync(python) && existsSync(pythonw))
|
|
51
|
+
return { python, pythonw };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Wohin uv die Programme seiner Werkzeuge legt (`uv tool dir --bin`, beachtet `UV_TOOL_BIN_DIR`). */
|
|
56
|
+
export function uvBinOrdner(uv) {
|
|
57
|
+
return laeuft(uv, ["tool", "dir", "--bin"])?.trim() || null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ein fremdes Programm (winget, uv, capture) ohne Fenster und ohne dass eine Zeile davon irgendwo erscheint -- nicht
|
|
61
|
+
* einmal eingesammelt. `true` nur bei Rueckgabewert 0; ob die Sache danach WIRKLICH da ist, prueft der Aufrufer neu.
|
|
62
|
+
* Arbeitsordner ist der Temp-Ordner: nie das Projekt (eine `uv.toml`/`.env` dort gehoert nicht zum Einrichten).
|
|
63
|
+
*/
|
|
64
|
+
export function stillAusfuehren(befehl, argumente, zeitgrenze) {
|
|
65
|
+
try {
|
|
66
|
+
const r = spawnSync(befehl, argumente, { stdio: "ignore", windowsHide: true, cwd: tmpdir(), env: UMGEBUNG(), timeout: zeitgrenze });
|
|
67
|
+
return r.status === 0;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Umgebung jedes Werkzeug-Aufrufs, auch des Starters (R-14, Punkt 6); den Rest setzt das Werkzeug selbst. */
|
|
74
|
+
const UMGEBUNG = () => ({ ...process.env, PYTHONUTF8: "1", PYTHONUNBUFFERED: "1" });
|
|
75
|
+
/** Lesende Aufrufe (`teile`, `zustand`) duerfen nicht haengen: nach 20 s gilt die Antwort als "weiss nicht" (R-14, Punkt 7). */
|
|
76
|
+
export const LESEN_MS = 20_000;
|
|
77
|
+
/**
|
|
78
|
+
* `stoppen` antwortet nach spaetestens 495 s selbst (werkzeug.py, STOPP_SEK + WARTEN_SEK). Diese Grenze ist nur das
|
|
79
|
+
* Netz, falls es haengt: Claude Code bricht einen Werkzeugaufruf nach 10 Minuten ab, und dann kaeme gar kein Satz.
|
|
80
|
+
*/
|
|
81
|
+
export const STOPPEN_MS = 540_000;
|
|
82
|
+
/**
|
|
83
|
+
* Ein Unterbefehl des Werkzeugs, synchron. Arbeitsordner = Aufnahmeordner (nie das Projekt: `.env`-Falle).
|
|
84
|
+
* `zeitgrenze` in ms oder `null` (stoppen und auswerten duerfen dauern). Abgebrochen = `status` null = `{ ok: false }`.
|
|
85
|
+
*/
|
|
86
|
+
export function rufeWerkzeug(p, argumente, arbeitsordner, zeitgrenze) {
|
|
87
|
+
try {
|
|
88
|
+
// stderr wird gar nicht erst eingesammelt: Er wird nie gelesen, und ein Fortschrittsbalken (Modell laden, 1,6 GB)
|
|
89
|
+
// koennte sonst `maxBuffer` sprengen -- dann bricht Node den Aufruf ab (Task 5).
|
|
90
|
+
const r = spawnSync(p.python, [WERKZEUG, ...argumente], {
|
|
91
|
+
encoding: "utf8", windowsHide: true, cwd: arbeitsordner ?? tmpdir(), maxBuffer: 1024 * 1024, env: UMGEBUNG(),
|
|
92
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
93
|
+
...(zeitgrenze === null ? {} : { timeout: zeitgrenze }),
|
|
94
|
+
});
|
|
95
|
+
return liesWerkzeugAntwort(r.status, r.stdout ?? "");
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { ok: false };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Startet den Starter abgehaengt mit pythonw (kein Fenster, keine Ausgabe). `false`, wenn schon das Starten scheitert. */
|
|
102
|
+
export function starteStarter(p, konfig, ordner) {
|
|
103
|
+
try {
|
|
104
|
+
const kind = spawn(p.pythonw, [WERKZEUG, "starten", konfig], { cwd: ordner, detached: true, stdio: "ignore", windowsHide: true, env: UMGEBUNG() });
|
|
105
|
+
kind.on("error", () => { });
|
|
106
|
+
kind.unref();
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ── Die Notiz ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
const ORDNER = join(homedir(), ".kimal3");
|
|
115
|
+
const NOTIZ = join(ORDNER, "zeigen.json"); // kein-anzeigetext: Dateiname
|
|
116
|
+
export function leseNotiz() {
|
|
117
|
+
try {
|
|
118
|
+
const n = JSON.parse(readFileSync(NOTIZ, "utf8"));
|
|
119
|
+
const zahl = (w) => typeof w === "number" && Number.isFinite(w);
|
|
120
|
+
if (typeof n.sitzung !== "string" || typeof n.ordner !== "string" || !zahl(n.gestartet) || !zahl(n.frist))
|
|
121
|
+
return null;
|
|
122
|
+
return { sitzung: n.sitzung, ordner: n.ordner, gestartet: n.gestartet, frist: n.frist };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** `false`, wenn die Notiz nicht geschrieben werden konnte -- die Aufnahme laeuft trotzdem (status findet sie ueber den Ordner). */
|
|
129
|
+
export function schreibeNotiz(n) {
|
|
130
|
+
try {
|
|
131
|
+
mkdirSync(ORDNER, { recursive: true, mode: 0o700 }); // wie wache-schalter.ts: wer den Ordner anlegt, legt die Rechte fest
|
|
132
|
+
writeFileSync(NOTIZ, JSON.stringify(n, null, 2), "utf8");
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function loescheNotiz() {
|
|
140
|
+
try {
|
|
141
|
+
rmSync(NOTIZ, { force: true });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
/* bleibt liegen -- status vergleicht sie ohnehin mit den Sitzungen des Aufnehmers */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ── Ordner und Bericht des Starters ───────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Der juengste Aufnahmeordner unter `aufnahmen`, oder `null`. Zaehlt nur, wo eine Aufnahme liegt (`roh/recording.db`)
|
|
150
|
+
* oder ein Starter noch lebt (er startet gerade, die Datei kommt erst noch). `roh/` gehoert allein dem Aufnehmer
|
|
151
|
+
* (gemeinsam.py, ROH: sein Siegel bricht an jeder Datei, die wir dort schreiben).
|
|
152
|
+
*/
|
|
153
|
+
export function juengsterOrdner(aufnahmen) {
|
|
154
|
+
try {
|
|
155
|
+
const namen = readdirSync(aufnahmen, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
156
|
+
const istAufnahme = (name) => existsSync(join(aufnahmen, name, "roh", "recording.db")) || // kein-anzeigetext: Dateiname
|
|
157
|
+
starterLebt(starterFakt(leseStarter(join(aufnahmen, name))), Date.now() / 1000);
|
|
158
|
+
const name = juengsteAufnahme(namen, istAufnahme);
|
|
159
|
+
return name === null ? null : join(aufnahmen, name);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Der Bericht des Starters (`starter.json`), roh; `null`, wenn er fehlt oder kaputt ist. */
|
|
166
|
+
export function leseStarter(ordner) {
|
|
167
|
+
try {
|
|
168
|
+
const d = JSON.parse(readFileSync(join(ordner, "starter.json"), "utf8")); // kein-anzeigetext: Dateiname
|
|
169
|
+
return d && typeof d === "object" && !Array.isArray(d) ? d : null;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const STARTER_ZUSTAENDE = ["startet", "laeuft", "gestoppt", "selbst-gestoppt", "abgebrochen", "fehlgeschlagen"];
|
|
176
|
+
/** Bericht des Starters -> `Fakten.starter`; alles, was nicht die erwartete Form hat, faellt weg. */
|
|
177
|
+
export function starterFakt(roh) {
|
|
178
|
+
if (!roh || typeof roh !== "object")
|
|
179
|
+
return null;
|
|
180
|
+
const r = roh;
|
|
181
|
+
const zustand = STARTER_ZUSTAENDE.find((z) => z === r.zustand);
|
|
182
|
+
if (zustand === undefined)
|
|
183
|
+
return null;
|
|
184
|
+
return {
|
|
185
|
+
zustand,
|
|
186
|
+
...(typeof r.sitzung === "string" ? { sitzung: r.sitzung } : {}),
|
|
187
|
+
...(typeof r.zuletzt === "number" && Number.isFinite(r.zuletzt) ? { zuletzt: r.zuletzt } : {}),
|
|
188
|
+
...(typeof r.grund === "string" ? { grund: r.grund } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimal3",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal. · Log your AI projects where they happen: in the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"skill",
|
|
12
12
|
"claude-plugins",
|
|
13
13
|
"coach",
|
|
14
|
+
"zeigen",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
package/skill/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ description: Projekte ins kimal3-Erfolgsprofil eintragen oder auflisten, Firmend
|
|
|
13
13
|
Sobald der Mensch sagt, ein Projekt sei fertig, oder ausdrücklich bittet, es einzutragen.
|
|
14
14
|
**Oder** wenn er seinen KI-Arbeitsplatz anlegen will — dann s. „Den Arbeitsplatz anlegen".
|
|
15
15
|
|
|
16
|
-
## Die
|
|
16
|
+
## Die neun Slash-Befehle, die der Mensch hat
|
|
17
17
|
|
|
18
18
|
`kimal3 skill install` legt sie global ab (`~/.claude/commands/`) — er kann sie also in
|
|
19
19
|
**jedem** Ordner tippen. Sie sind der Weg, den er selbst auslöst; dieses Skill ist der Weg,
|
|
@@ -29,6 +29,7 @@ den du erkennst.
|
|
|
29
29
|
| `/k3liste` | Seine eingetragenen Projekte zeigen |
|
|
30
30
|
| `/k3analyse` | Firmendaten holen und seine Frage dazu beantworten |
|
|
31
31
|
| `/k3kontext` | Die offenen Fragen über seine Firma im Gespräch beantworten |
|
|
32
|
+
| `/k3zeigen` | Eine Aufgabe vormachen statt beschreiben — nimmt mit `kimal3 zeigen …` auf, wertet auf dem Rechner aus und schreibt den Plan; gelesen wird erst nach seinem Ja |
|
|
32
33
|
|
|
33
34
|
🔴 **Nenne einen dieser Befehle nur, wenn du weißt, dass er ihn hat.** Sie kommen mit
|
|
34
35
|
`kimal3 skill install`; wer das nie ausgeführt hat, sieht beim Tippen nichts, und ein Rat ins
|
|
@@ -37,7 +38,7 @@ einen Befehl zu zeigen.
|
|
|
37
38
|
|
|
38
39
|
⚠️ **`/k3start`, `/k3feierabend` und `/k3kurs` kamen bis 0.2.4 NUR im Kursordner mit** — Pro-
|
|
39
40
|
Kunden ohne Mini-Kurs hatten sie nie, und aktualisieren ließen sie sich gar nicht. Seit 0.2.5
|
|
40
|
-
liefert das Programm alle
|
|
41
|
+
liefert das Programm sie alle aus. *Triffst du jemanden mit einer älteren Fassung, ist ein
|
|
41
42
|
fehlender Befehl dort kein Fehler, sondern der alte Stand.*
|
|
42
43
|
|
|
43
44
|
## 🔴 Mehrere Firmen: DU WÄHLST NICHT
|
package/skill/commands/k3.md
CHANGED
|
@@ -83,17 +83,18 @@ Stell die Liste in zwei Gruppen dar:
|
|
|
83
83
|
| `/k3projekt <was du gebaut hast>` | Ein fertiges Projekt ins Erfolgsprofil eintragen |
|
|
84
84
|
| `/k3liste` | Deine eingetragenen Projekte zeigen |
|
|
85
85
|
| `/k3analyse <deine Frage>` | Firmendaten holen und deine Frage dazu beantworten |
|
|
86
|
+
| `/k3zeigen` | Eine Aufgabe vormachen statt beschreiben — ich nehme auf und mache daraus einen Plan |
|
|
86
87
|
|
|
87
88
|
Sag dazu: Die Slash-Befehle sind der **sichere** Weg — du musst dir keinen Satz merken. Ohne
|
|
88
89
|
sie geht es auch: „das Projekt ist fertig" genügt, dann greife ich über den Skill.
|
|
89
90
|
|
|
90
|
-
## 6. Der
|
|
91
|
+
## 6. Der zehnte Eintrag im Menü: `/kimal3` ist KEIN Befehl
|
|
91
92
|
|
|
92
93
|
In derselben `/`-Liste steht auch **`/kimal3`**. Erklär den Unterschied **ungefragt** — das Menü
|
|
93
94
|
wirft zwei verschiedene Sorten in eine Liste, und genau daran hat sich der erste Mensch gestoßen,
|
|
94
95
|
der die Familie gesehen hat (Dennis, 2026-08-19):
|
|
95
96
|
|
|
96
|
-
- Die
|
|
97
|
+
- Die neun **`/k3…`** sind **Slash-Befehle** (Dateien in `~/.claude/commands/`) — **der Mensch**
|
|
97
98
|
tippt sie bewusst.
|
|
98
99
|
- **`/kimal3`** ist der **Skill** (`~/.claude/skills/kimal3/SKILL.md`) — meine
|
|
99
100
|
Bedienungsanleitung. Sie ist der Grund, warum „das Projekt ist fertig" genügt: Ich schlage sie
|
|
@@ -101,5 +102,5 @@ der die Familie gesehen hat (Dennis, 2026-08-19):
|
|
|
101
102
|
sie mehrere Dinge auf einmal und liest sich nicht wie ein Menüpunkt.
|
|
102
103
|
|
|
103
104
|
Sag auch, was `/kimal3` **nicht** ist: **nicht die Installation.** Die ist `kimal3 skill install`
|
|
104
|
-
im Terminal — der eine Befehl, der beides ablegt, den Skill **und** die
|
|
105
|
+
im Terminal — der eine Befehl, der beides ablegt, den Skill **und** die neun `/k3…`. Was im Menü
|
|
105
106
|
steht, ist sein Ergebnis.
|
package/skill/commands/k3kurs.md
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Zurück in den Mini-Kurs — da weiter, wo du aufgehört hast
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
<!-- kimal3:erzeugt — diese Datei wird von `kimal3 skill install` gepflegt und bei
|
|
6
|
-
jedem Update ueberschrieben. Eigene Aenderungen gehen dabei verloren; wer sie
|
|
7
|
-
behalten will, kopiert die Datei unter einem anderen Namen. -->
|
|
8
|
-
# Kurs
|
|
9
|
-
|
|
10
|
-
> Zurück in den Mini-Kurs: „Wo war ich nochmal?" — macht da weiter, wo du aufgehört hast.
|
|
11
|
-
> Den Stand holt `kimal3 kurs` frisch von kimal3 — dieselben Haken, die der Mensch im Browser unter
|
|
12
|
-
> https://app.kimal3.de/kurs sieht.
|
|
13
|
-
|
|
14
|
-
Führe die Schritte der Reihe nach aus. Bleib im Coaching-Ton: Deutsch, freundlich, ein Schritt
|
|
15
|
-
nach dem anderen, das Warum vor dem Wie, null Vorwissen vorausgesetzt.
|
|
16
|
-
|
|
17
|
-
## 1. Den Stand holen
|
|
18
|
-
- Führ `kimal3 kurs` aus (unter Windows so, wie es `SKILL.md` beschreibt).
|
|
19
|
-
- Rückgabewert **2**: Der Mensch ist nicht angemeldet. Zeig ihm `kimal3 login`, lass ihn selbst im Browser bestätigen
|
|
20
|
-
und nenn **keinen** Kursstand.
|
|
21
|
-
- Rückgabewert **3**: Der Stand ist gerade nicht abrufbar. Sag es in einem Satz und **erzähl den Kurs nicht aus dem
|
|
22
|
-
Gedächtnis**.
|
|
23
|
-
- Kennt das Programm den Befehl nicht, ist es zu alt: Nenn `npm install -g kimal3@latest`, danach `kimal3 skill install`.
|
|
24
|
-
|
|
25
|
-
## 2. Stand in einem Satz sagen
|
|
26
|
-
- Sag in **einem** Satz, wo ihr im Kurs steht — aus der Ausgabe (Zeile `Kurs:`), nichts dazuerfinden. Kein Vortrag.
|
|
27
|
-
|
|
28
|
-
## 3. Als Nächstes führen
|
|
29
|
-
- Die Zeile `Als Nächstes:` nennt den nächsten Schritt. Führ **einen** Schritt nach dem anderen: zeigen, machen lassen,
|
|
30
|
-
verstehen. Nie die ganze Liste auf einmal.
|
|
31
|
-
- Steht dahinter **„(du hakst ab)"** und hat der Mensch den Schritt offensichtlich schon getan (zum Beispiel „Claude ganz
|
|
32
|
-
beendet und neu geöffnet" — er sitzt ja gerade in Claude), führ ihn nicht noch einmal: Frag kurz nach und bitte ihn,
|
|
33
|
-
den Schritt unter https://app.kimal3.de/kurs abzuhaken.
|
|
34
|
-
- Steht dahinter **„(setzt Claude)"** und ist er offen, lass die Einrichtung noch einmal laufen — über die Anleitung
|
|
35
|
-
`https://app.kimal3.de/einrichten` (lade sie wie der Kurs-Satz und folge ihr), **nicht** mit einem frei aufgerufenen
|
|
36
|
-
`kimal3 einrichten`. Bleibt der Schritt danach offen, nenn den Grund aus der Ausgabe von `einrichten` und schlag
|
|
37
|
-
keinen dritten Lauf vor.
|
|
38
|
-
|
|
39
|
-
## 4. Abgeschlossen
|
|
40
|
-
- Steht in der Ausgabe `Kurs: abgeschlossen`, gratuliere kurz und frag mit einem Vorschlag, woran er hier arbeiten
|
|
41
|
-
möchte. Kein Kurs-Gerede mehr.
|
|
1
|
+
---
|
|
2
|
+
description: Zurück in den Mini-Kurs — da weiter, wo du aufgehört hast
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<!-- kimal3:erzeugt — diese Datei wird von `kimal3 skill install` gepflegt und bei
|
|
6
|
+
jedem Update ueberschrieben. Eigene Aenderungen gehen dabei verloren; wer sie
|
|
7
|
+
behalten will, kopiert die Datei unter einem anderen Namen. -->
|
|
8
|
+
# Kurs
|
|
9
|
+
|
|
10
|
+
> Zurück in den Mini-Kurs: „Wo war ich nochmal?" — macht da weiter, wo du aufgehört hast.
|
|
11
|
+
> Den Stand holt `kimal3 kurs` frisch von kimal3 — dieselben Haken, die der Mensch im Browser unter
|
|
12
|
+
> https://app.kimal3.de/kurs sieht.
|
|
13
|
+
|
|
14
|
+
Führe die Schritte der Reihe nach aus. Bleib im Coaching-Ton: Deutsch, freundlich, ein Schritt
|
|
15
|
+
nach dem anderen, das Warum vor dem Wie, null Vorwissen vorausgesetzt.
|
|
16
|
+
|
|
17
|
+
## 1. Den Stand holen
|
|
18
|
+
- Führ `kimal3 kurs` aus (unter Windows so, wie es `SKILL.md` beschreibt).
|
|
19
|
+
- Rückgabewert **2**: Der Mensch ist nicht angemeldet. Zeig ihm `kimal3 login`, lass ihn selbst im Browser bestätigen
|
|
20
|
+
und nenn **keinen** Kursstand.
|
|
21
|
+
- Rückgabewert **3**: Der Stand ist gerade nicht abrufbar. Sag es in einem Satz und **erzähl den Kurs nicht aus dem
|
|
22
|
+
Gedächtnis**.
|
|
23
|
+
- Kennt das Programm den Befehl nicht, ist es zu alt: Nenn `npm install -g kimal3@latest`, danach `kimal3 skill install`.
|
|
24
|
+
|
|
25
|
+
## 2. Stand in einem Satz sagen
|
|
26
|
+
- Sag in **einem** Satz, wo ihr im Kurs steht — aus der Ausgabe (Zeile `Kurs:`), nichts dazuerfinden. Kein Vortrag.
|
|
27
|
+
|
|
28
|
+
## 3. Als Nächstes führen
|
|
29
|
+
- Die Zeile `Als Nächstes:` nennt den nächsten Schritt. Führ **einen** Schritt nach dem anderen: zeigen, machen lassen,
|
|
30
|
+
verstehen. Nie die ganze Liste auf einmal.
|
|
31
|
+
- Steht dahinter **„(du hakst ab)"** und hat der Mensch den Schritt offensichtlich schon getan (zum Beispiel „Claude ganz
|
|
32
|
+
beendet und neu geöffnet" — er sitzt ja gerade in Claude), führ ihn nicht noch einmal: Frag kurz nach und bitte ihn,
|
|
33
|
+
den Schritt unter https://app.kimal3.de/kurs abzuhaken.
|
|
34
|
+
- Steht dahinter **„(setzt Claude)"** und ist er offen, lass die Einrichtung noch einmal laufen — über die Anleitung
|
|
35
|
+
`https://app.kimal3.de/einrichten` (lade sie wie der Kurs-Satz und folge ihr), **nicht** mit einem frei aufgerufenen
|
|
36
|
+
`kimal3 einrichten`. Bleibt der Schritt danach offen, nenn den Grund aus der Ausgabe von `einrichten` und schlag
|
|
37
|
+
keinen dritten Lauf vor.
|
|
38
|
+
|
|
39
|
+
## 4. Abgeschlossen
|
|
40
|
+
- Steht in der Ausgabe `Kurs: abgeschlossen`, gratuliere kurz und frag mit einem Vorschlag, woran er hier arbeiten
|
|
41
|
+
möchte. Kein Kurs-Gerede mehr.
|
|
@@ -24,7 +24,7 @@ nicht relevant, sag das kurz und überspring ihn — kein Leerlauf.
|
|
|
24
24
|
- Gibt es ein Status-Doc (`WEITER-HIER.md` oder `ROADMAP.md`) — eine kleine Datei, in der der
|
|
25
25
|
letzte Stand und der nächste Schritt festgehalten sind? Dann lies es.
|
|
26
26
|
- **Wo der Statuszettel liegt und wie er heißt.** Die Projektregel gilt **nur innerhalb eines Arbeitsplatzes**: Liegt ein Ordner `projekte/` oder `projects/` direkt in einem Ordner `werkbank-…` oder `workbench-…` (beide Schreibweisen kommen vor, auch gemischt) und steht die Sitzung in oder unter einem Ordner darin, ist dieser **Projektordner** der Ort. Name dort: unter `projekte/` `WEITER-HIER.md`, unter `projects/` `NEXT-STEPS.md`. **Überall sonst** (Starter-Paket, eigene Repos, auch ein Ordner `~/projects/mein-repo`) bleibt es wie bisher: Der Zettel liegt im Startordner, liegt einer da, nimm ihn; muss einer neu entstehen, heißt er `WEITER-HIER.md` — oder `NEXT-STEPS.md`, wenn der Ordner englisch angelegt wurde. Liegen beide da, frag, welcher gilt, und schreib in keinen, bevor die Antwort da ist. **Welcher gilt (im Arbeitsplatz):** Hat das Projekt einen, gilt der des Projekts. Sonst gilt ein vorhandener Werkbank-Zettel (`werkbank-…/WEITER-HIER.md` oder `workbench-…/NEXT-STEPS.md` aus der alten Vorlage) — den liest und pflegst du weiter. Gibt es keinen von beiden, entsteht er **im Projekt**. **Leg nie einen neuen Zettel in der Werkbank an.**
|
|
27
|
-
- **Stehst du in der Werkbank selbst (`werkbank-…` oder `workbench-…`), nicht in einem Projekt?** Steht in ihrer `CLAUDE.md` der Abschnitt „Erste Sitzung“ (englisch „First session“), folge **ihm** — er hat Vorrang vor allem Folgenden. Steht er nicht (mehr) da, zeig die Projekte in `projekte/` bzw. `projects/` (ohne `beispiel-rechnungen-erfassen` und `example-capture-invoices`) mit je der **ersten Stand-Zeile** ihres Zettels, oder „noch kein Zettel“, wenn keiner da ist; ein vorhandener Werkbank-Zettel (`WEITER-HIER.md` oder `NEXT-STEPS.md`) steht darüber. Empfiehl daraus **eines** und stell **eine** Frage dazu.
|
|
27
|
+
- **Stehst du in der Werkbank selbst (`werkbank-…` oder `workbench-…`), nicht in einem Projekt?** Steht in ihrer `CLAUDE.md` der Abschnitt „Erste Sitzung“ (englisch „First session“), folge **ihm** — er hat Vorrang vor allem Folgenden. Steht er nicht (mehr) da, zeig die Projekte in `projekte/` bzw. `projects/` (ohne `beispiel-rechnungen-erfassen` und `example-capture-invoices`) mit je der **ersten Stand-Zeile** ihres Zettels, oder „noch kein Zettel“, wenn keiner da ist; ein vorhandener Werkbank-Zettel (`WEITER-HIER.md` oder `NEXT-STEPS.md`) steht darüber. Empfiehl daraus **eines** und stell **eine** Frage dazu. Wählt der Mensch ein Projekt aus der Liste oder nennt eine neue Aufgabe (dann legst du erst Ordner und Statuszettel im Projekt an, nie in der Werkbank), bring die Sitzung in den Projektordner: mit `change_directory` (steht es nur als Name in deiner Werkzeugliste, lade es über die Werkzeugsuche) — und sag vorher: „Gleich fragt dich die Claude-App, ob ich den Ordner wechseln darf, und danach, ob du ihm vertraust — beides ist richtig so, klick jeweils auf Erlauben. Landest du danach in einem leeren Gespräch, tipp dort `/k3start`; dieses Gespräch findest du links in der Liste wieder.“ Ohne das Werkzeug oder wenn der Mensch ablehnt: nenne den genauen Projektordner (nicht die Werkbank darüber) — in der App „Ordner wählen“ und diesen Ordner nehmen, im Terminal `/exit`, `cd '‹Pfad›'`, `claude` — und dann `/k3start`. Bei einem neuen Projekt lies vorher `projekte/CLAUDE.md` (englisch `projects/CLAUDE.md`): ab jetzt gilt sie.
|
|
28
28
|
- Kein Status-Doc? **Sieh erst nach, ob ein `.git`-Ordner im Zettel-Ort liegt** (im Arbeitsplatz der Projektordner, sonst der Startordner). Wenn ja, führt dieses
|
|
29
29
|
Projekt eine Versionsgeschichte, und `git log --oneline -10` plus `git status` zeigen dir den
|
|
30
30
|
letzten Stand.
|