kimal3 0.6.4 → 0.7.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.
Files changed (51) hide show
  1. package/README.md +10 -4
  2. package/claude-plugins/.claude-plugin/marketplace.json +2 -1
  3. package/claude-plugins/README.md +2 -0
  4. package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +1 -1
  5. package/claude-plugins/kimal3-logo/hooks/ablage.ts +8 -0
  6. package/claude-plugins/kimal3-logo/hooks/app-oeffnen.ts +37 -9
  7. package/claude-plugins/kimal3-logo/hooks/register.tsx +42 -18
  8. package/claude-plugins/kimal3-logo/hooks/update-rechnung.ts +175 -0
  9. package/claude-plugins/kimal3-logo/hooks/update.ts +244 -0
  10. package/claude-plugins/kimal3-logo/types/index.d.ts +4 -0
  11. package/claude-plugins/kimal3-plan/.claude-plugin/plugin.json +7 -0
  12. package/claude-plugins/kimal3-plan/hooks/erinnerung.ts +79 -0
  13. package/claude-plugins/kimal3-plan/hooks/fehlerzeile.ts +41 -0
  14. package/claude-plugins/kimal3-plan/hooks/hooks.json +1 -0
  15. package/claude-plugins/kimal3-plan/hooks/lauf.ts +143 -0
  16. package/claude-plugins/kimal3-plan/hooks/lebenszeichen.ts +197 -0
  17. package/claude-plugins/kimal3-plan/hooks/ordner.ts +141 -0
  18. package/claude-plugins/kimal3-plan/hooks/register.tsx +22 -0
  19. package/claude-plugins/kimal3-plan/hooks/spec-text.ts +45 -0
  20. package/claude-plugins/kimal3-plan/types/index.d.ts +10 -0
  21. package/coach/de/kimal3-coach.md +33 -0
  22. package/coach/en/kimal3-coach.md +33 -0
  23. package/dist/argumente.js +12 -0
  24. package/dist/atomar.js +22 -0
  25. package/dist/befehle/einrichten.js +4 -4
  26. package/dist/befehle/login.js +2 -2
  27. package/dist/befehle/plan-hoch.js +8 -0
  28. package/dist/befehle/plan-melde.js +105 -0
  29. package/dist/befehle/plan-senden.js +149 -0
  30. package/dist/befehle/plan.js +45 -10
  31. package/dist/befehle/projekt-verbinden.js +141 -0
  32. package/dist/befehle/skill-install.js +2 -2
  33. package/dist/befehle-katalog-en.js +6 -6
  34. package/dist/befehle-katalog.js +8 -7
  35. package/dist/befehlswoerter.js +9 -0
  36. package/dist/einstellungen-schreiben.js +1 -1
  37. package/dist/kimal3.js +22 -0
  38. package/dist/meldungen-speicher.js +189 -0
  39. package/dist/{logo-ablegen.js → mods-ablegen.js} +87 -20
  40. package/dist/plugins-mischen.js +7 -2
  41. package/dist/projekt-ordner.js +189 -0
  42. package/dist/texte/einrichten.js +3 -1
  43. package/dist/texte/meldungen.js +127 -0
  44. package/dist/texte/plan.js +42 -22
  45. package/dist/texte/wache.js +4 -4
  46. package/dist/wache-rechnung.js +3 -1
  47. package/package.json +1 -1
  48. package/skill/commands/k3feierabend.md +20 -0
  49. package/skill/commands/k3plan.md +151 -211
  50. package/skill/en/commands/k3plan.md +157 -219
  51. package/skill/en/commands/k3wrapup.md +19 -0
@@ -0,0 +1,105 @@
1
+ import { schalterOhneWert } from "../argumente.js";
2
+ import { sprache } from "../sprache.js";
3
+ import { MELDUNGEN_TEXTE } from "../texte/meldungen.js";
4
+ import { merke, meldungFehler, sitzungFuerMeldung } from "../meldungen-speicher.js";
5
+ import { ladeSpecText, sendeUmschlaege } from "./plan-senden.js";
6
+ import { projektDesOrdners } from "./projekt-verbinden.js";
7
+ /*
8
+ `kimal3 plan melde` -- Claude meldet der Projektseite EINE Aenderung (oder eine Liste), statt die ganze Datei
9
+ hochzuladen (Board in Echtzeit, Spec 1.1, 1.2, E-6, S-10, US-1, US-4).
10
+
11
+ Das Projekt kommt vom Ordnerzettel, nie aus einer Wahl (S-12). `entstanden` ist JETZT, `sitzung` die Nummer der
12
+ Claude-Sitzung: Beides setzt das Programm, nicht Claude -- eine von Hand gesetzte Zeit koennte eine fremde,
13
+ neuere Meldung verdraengen (E-2, E-6).
14
+
15
+ Nicht erreichbar -> gemerkt, Rueckgabewert 3; beim naechsten Befehl geht sie mit. Erfolg ist still (US-4).
16
+ */
17
+ async function leseEingabe() {
18
+ if (process.stdin.isTTY)
19
+ return null;
20
+ const teile = [];
21
+ for await (const teil of process.stdin)
22
+ teile.push(Buffer.from(teil));
23
+ const text = Buffer.concat(teile).toString("utf8").replace(/^/, "");
24
+ return text.trim() === "" ? null : text;
25
+ }
26
+ export async function planMelde(argv) {
27
+ const t = MELDUNGEN_TEXTE[sprache()];
28
+ if (argv.includes("--help") || argv.includes("-h")) {
29
+ console.log(t.reportHelp);
30
+ return 0;
31
+ }
32
+ const projekt = projektDesOrdners();
33
+ if (projekt === null)
34
+ return 1;
35
+ const text = await leseEingabe();
36
+ if (text === null) {
37
+ console.error(t.noInput);
38
+ return 1;
39
+ }
40
+ let roh;
41
+ try {
42
+ roh = JSON.parse(text);
43
+ }
44
+ catch {
45
+ console.error(t.inputNoJson);
46
+ return 1;
47
+ }
48
+ const liste = Array.isArray(roh) ? roh : [roh];
49
+ if (liste.length === 0) {
50
+ console.error(t.noInput);
51
+ return 1;
52
+ }
53
+ for (const [i, m] of liste.entries()) {
54
+ const feld = meldungFehler(m);
55
+ if (feld !== null) {
56
+ console.error(t.reportUnreadable(i + 1, feld));
57
+ return 1;
58
+ }
59
+ }
60
+ const entstanden = new Date().toISOString();
61
+ // Prüfen ist nicht Arbeiten (Spec 2026-10-24 E-8): `--ohne-sitzung` schickt keine Sitzung mit, also leuchtet kein Punkt.
62
+ const sitzung = schalterOhneWert(argv, "ohne-sitzung").gesetzt ? null : sitzungFuerMeldung(process.env);
63
+ const umschlaege = liste.map((m) => ({ meldung: m, entstanden, sitzung }));
64
+ const sendung = await sendeUmschlaege(projekt.projektId, umschlaege);
65
+ const u = MELDUNGEN_TEXTE[sprache()];
66
+ // Erst die Absagen (je Satz einmal), dann was mit dem Rest geschieht.
67
+ for (const satz of new Set(sendung.je.flatMap((e) => (e.art === "abgesagt" ? [e.meldung] : []))))
68
+ console.error(satz);
69
+ const offen = umschlaege.filter((_, i) => sendung.je[i].art === "offen");
70
+ const p = sendung.problem;
71
+ if (offen.length > 0 && p !== null && p.art !== "absage") {
72
+ // Jede einzeln merken und zaehlen: Scheitert das Merken mittendrin, muss der Satz sagen, wie viele gemerkt sind.
73
+ const gemerkt = offen.filter((x) => merke(projekt.projektId, projekt.ordner, x)).length;
74
+ const satz = gemerkt === offen.length ? (p.art === "anmelden" ? u.storedSignIn : u.stored)
75
+ : gemerkt > 0 ? u.storedPartly(gemerkt, offen.length)
76
+ : p.art === "anmelden" ? p.meldung : u.notStored;
77
+ if (p.art === "anmelden" && gemerkt < offen.length && gemerkt > 0)
78
+ console.error(p.meldung);
79
+ console.error(satz);
80
+ return p.art === "anmelden" ? 2 : 3;
81
+ }
82
+ // Ruling Task 7: Eine Spec-Datei, die VOR ihrer Meldung geschrieben wurde, hat die App noch nicht -- jetzt mitnehmen.
83
+ // Fuer jede ANGENOMMENE Spec-Meldung, auch wenn eine andere der Liste abgesagt wurde.
84
+ let code = sendung.je.some((e) => e.art === "abgesagt") ? 1 : 0;
85
+ for (const [i, x] of umschlaege.entries()) {
86
+ const m = x.meldung;
87
+ if (sendung.je[i].art !== "angenommen" || m.art !== "spec" || m.datei === null)
88
+ continue;
89
+ const text = await ladeSpecText(projekt.projektId, projekt.ordner, m.schluessel, m.datei);
90
+ if (text === "anmelden") {
91
+ console.error(u.specTextSignIn(m.datei));
92
+ return 2;
93
+ }
94
+ if (text === "fehler") {
95
+ console.error(u.specTextFailed(m.datei));
96
+ code = 3;
97
+ }
98
+ else if (text === "zu-gross") {
99
+ console.error(u.specTextTooBig(m.datei));
100
+ if (code === 0)
101
+ code = 1;
102
+ }
103
+ }
104
+ return code;
105
+ }
@@ -0,0 +1,149 @@
1
+ import { NichtAngemeldet, ruf } from "../api.js";
2
+ import { leseAnmeldung } from "../anmeldung.js";
3
+ import { sprache } from "../sprache.js";
4
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
5
+ import { MELDUNGEN_TEXTE } from "../texte/meldungen.js";
6
+ import { leseSpecTexte } from "../plan-datei.js";
7
+ import { gemerkteMitUnlesbaren, gemerktOrdner, sendeInPaketen, vergiss, } from "../meldungen-speicher.js";
8
+ /*
9
+ `kimal3 plan senden` und das Senden, das vor jedem Befehl laeuft, der die App erreicht (Board in Echtzeit, S-10,
10
+ US-4; Bauplan R-4). Dazu der gemeinsame Weg, auf dem auch `plan melde` schickt.
11
+
12
+ 🔴 EINE Anfrage fuer bis zu 50 Meldungen, nie eine je Meldung (Ruling Task 7): Jeder `kimal3`-Aufruf frischt die
13
+ Anmeldung auf, und rund 100 Aufrufe in zwei Minuten loesen bei Supabase `429` aus (gemessen in Task 4, D2). Die App
14
+ wendet sie in der Reihenfolge an, in der sie entstanden, und antwortet je Meldung. Die Ablaufregel (welche Absage
15
+ welche Meldungen trifft) steht rein in `sendeInPaketen` und wird in `check:cli` geprueft.
16
+ */
17
+ const PFAD = "/api/cli/projekt/meldungen"; // kein-anzeigetext: Pfad der Schnittstelle
18
+ /** Schickt die Umschlaege eines Projekts ueber das Netz. */
19
+ export async function sendeUmschlaege(projektId, umschlaege) {
20
+ const schicke = async (paket) => {
21
+ try {
22
+ return await ruf(PFAD, { mitAusweis: true, koerper: { projektId, umschlaege: paket } });
23
+ }
24
+ catch (f) {
25
+ return f instanceof NichtAngemeldet ? "nicht-angemeldet" : "netz";
26
+ }
27
+ };
28
+ const sendung = await sendeInPaketen(umschlaege, schicke);
29
+ const p = sendung.problem;
30
+ if (p?.art !== "anmelden")
31
+ return sendung;
32
+ // Ohne Satz der App (keine Anmeldung auf dieser Platte) der eigene; sonst der der App mit dem Rat dahinter.
33
+ const t = ALLGEMEIN_TEXTE[sprache()];
34
+ return { ...sendung, problem: { art: "anmelden", meldung: p.meldung === "" ? t.signInFirst : `${p.meldung}${t.pleaseSignIn}` } };
35
+ }
36
+ /**
37
+ * Laedt den Text der Datei hoch, die eine Spec-Meldung nennt (Ruling Task 7): Hat Claude die Datei geschrieben, BEVOR
38
+ * die Spec gemeldet war, hat die App den Text des Mods verworfen (`204`, unbekannte Spec). `ok` auch, wenn es nichts
39
+ * hochzuladen gibt (keine Datei, ausserhalb des Projekts, allein zu gross -- das sagt `/k3plan`).
40
+ * Derselbe Ausweis wie der Mod (`~/.kimal3/anmeldung.json`), dieselbe Route.
41
+ */
42
+ export async function ladeSpecText(projektId, ordner, schluessel, datei) {
43
+ const gelesen = leseSpecTexte(ordner, [{ schluessel, titel: schluessel, datei }], (e) => e).eintraege[0];
44
+ if (!("text" in gelesen) || datei === null)
45
+ return "ok";
46
+ try {
47
+ const antwort = await ruf("/api/mod/spec-text", { mitAusweis: true, koerper: { projektId, pfad: datei, text: gelesen.text } }); // kein-anzeigetext: Pfad der Schnittstelle
48
+ if (antwort.status === 200 || antwort.status === 204)
49
+ return "ok";
50
+ if (antwort.status === 401)
51
+ return "anmelden";
52
+ return antwort.status === 413 ? "zu-gross" : "fehler";
53
+ }
54
+ catch (f) {
55
+ return f instanceof NichtAngemeldet ? "anmelden" : "fehler";
56
+ }
57
+ }
58
+ /** Schickt alle gemerkten, je Projekt eine Sendung; angenommene und abgesagte werden vergessen, offene bleiben. */
59
+ export async function sendeGemerkte() {
60
+ const { liste: alle } = gemerkteMitUnlesbaren();
61
+ const projekte = [...new Set(alle.map((g) => g.projektId))];
62
+ const ergebnis = { angenommen: 0, abgesagt: 0, saetze: [], offen: 0, unlesbar: [], problem: null };
63
+ for (const projektId of projekte) {
64
+ const liste = alle.filter((g) => g.projektId === projektId);
65
+ const sendung = await sendeUmschlaege(projektId, liste.map((g) => g.umschlag));
66
+ const saetze = new Set();
67
+ for (const [i, g] of liste.entries()) {
68
+ const e = sendung.je[i];
69
+ if (e.art === "offen")
70
+ continue;
71
+ vergiss(g.datei);
72
+ if (e.art === "abgesagt") {
73
+ ergebnis.abgesagt++;
74
+ saetze.add(e.meldung);
75
+ continue;
76
+ }
77
+ ergebnis.angenommen++;
78
+ // Nebenbei und ohne Laerm: Scheitert der Text, bringt ihn der Mod beim naechsten Schreiben oder `/k3plan`.
79
+ const m = g.umschlag.meldung;
80
+ if (m.art === "spec" && g.ordner !== null)
81
+ await ladeSpecText(projektId, g.ordner, m.schluessel, m.datei);
82
+ }
83
+ ergebnis.saetze.push(...saetze);
84
+ if (sendung.problem !== null && sendung.problem.art !== "absage") {
85
+ ergebnis.problem = sendung.problem;
86
+ break;
87
+ }
88
+ }
89
+ const danach = gemerkteMitUnlesbaren();
90
+ ergebnis.offen = danach.liste.length;
91
+ ergebnis.unlesbar = danach.unlesbar;
92
+ return ergebnis;
93
+ }
94
+ /** Die Zeilen ueber das Nachschicken: angenommen, abgesagt (mit Gruenden), unlesbare Dateien. */
95
+ function berichte(e, ausgabe) {
96
+ const t = MELDUNGEN_TEXTE[sprache()];
97
+ if (e.angenommen > 0)
98
+ ausgabe(t.sentStored(e.angenommen));
99
+ if (e.abgesagt > 0)
100
+ console.error(t.refusedStored(e.abgesagt));
101
+ for (const satz of e.saetze)
102
+ console.error(satz);
103
+ if (e.unlesbar.length > 0)
104
+ console.error(t.unreadableStored(e.unlesbar, gemerktOrdner()));
105
+ }
106
+ /**
107
+ * Vor jedem Befehl, der die App erreicht (kimal3.ts): gemerkte Meldungen zuerst. Wirft nie und haelt den Befehl nie
108
+ * auf -- ist die App nicht erreichbar, sagt es gleich der Befehl selbst. Gesagt wird nur, was Claude wissen muss:
109
+ * wie viele angekommen sind, wie viele abgesagt (mit Grund, E-6: „kam zu spaet an“).
110
+ */
111
+ export async function vorabSenden() {
112
+ try {
113
+ if (gemerkteMitUnlesbaren().liste.length === 0 || leseAnmeldung() === null)
114
+ return;
115
+ berichte(await sendeGemerkte(), (z) => console.error(z));
116
+ }
117
+ catch {
118
+ /* Ein Nebenschauplatz darf den Befehl nicht umwerfen. */
119
+ }
120
+ }
121
+ export async function planSenden(argv) {
122
+ const t = MELDUNGEN_TEXTE[sprache()];
123
+ if (argv.includes("--help") || argv.includes("-h")) {
124
+ console.log(t.sendHelp);
125
+ return 0;
126
+ }
127
+ const vorher = gemerkteMitUnlesbaren();
128
+ if (vorher.liste.length === 0) {
129
+ if (vorher.unlesbar.length === 0) {
130
+ console.log(t.nothingStored);
131
+ return 0;
132
+ }
133
+ // Nur unlesbare: nicht „nichts gemerkt“ sagen -- es liegt etwas da, das jemand ansehen muss.
134
+ console.error(t.unreadableStored(vorher.unlesbar, gemerktOrdner()));
135
+ return 1;
136
+ }
137
+ const e = await sendeGemerkte();
138
+ const u = MELDUNGEN_TEXTE[sprache()];
139
+ berichte(e, (z) => console.log(z));
140
+ if (e.problem?.art === "anmelden") {
141
+ console.error(e.problem.meldung);
142
+ return 2;
143
+ }
144
+ if (e.problem?.art === "nicht-erreichbar") {
145
+ console.error(u.stillStored(e.offen));
146
+ return 3;
147
+ }
148
+ return e.abgesagt > 0 || e.unlesbar.length > 0 ? 1 : 0;
149
+ }
@@ -4,8 +4,46 @@ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
4
4
  import { PLAN_TEXTE } from "../texte/plan.js";
5
5
  import { schlussFehler } from "../fehler.js";
6
6
  import { lesePlanDatei } from "../plan-datei.js";
7
+ import { projektDesOrdners } from "./projekt-verbinden.js";
7
8
  const istZahl = (w) => typeof w === "number" && Number.isInteger(w);
8
9
  const textOderNull = (w) => typeof w === "string" || w === null;
10
+ const alsObjekt = (w) => (w && typeof w === "object" && !Array.isArray(w) ? w : null);
11
+ /** Der Stand aus der Antwort; `undefined`, wenn er da ist, aber nicht die erwartete Form hat. */
12
+ function standAus(w) {
13
+ if (w === null)
14
+ return null;
15
+ const o = alsObjekt(w);
16
+ if (o === null || !Array.isArray(o.specs) || !Array.isArray(o.themen))
17
+ return undefined;
18
+ const specs = [];
19
+ for (const sp of o.specs) {
20
+ const s = alsObjekt(sp);
21
+ if (s === null || typeof s.schluessel !== "string" || typeof s.titel !== "string" || !textOderNull(s.datei) || !Array.isArray(s.stories))
22
+ return undefined;
23
+ const stories = s.stories.map(alsObjekt);
24
+ if (stories.some((st) => st === null || typeof st.schluessel !== "string" || typeof st.titel !== "string" || typeof st.stand !== "string" || !istZahl(st.runde)))
25
+ return undefined;
26
+ specs.push({
27
+ schluessel: s.schluessel, titel: s.titel, datei: s.datei,
28
+ stories: stories.map((st) => ({ schluessel: st.schluessel, titel: st.titel, stand: st.stand, runde: st.runde })),
29
+ });
30
+ }
31
+ const themen = o.themen.flatMap((th) => {
32
+ const x = alsObjekt(th);
33
+ return x !== null && typeof x.schluessel === "string" && typeof x.text === "string" ? [{ schluessel: x.schluessel, text: x.text }] : [];
34
+ });
35
+ return { specs, themen };
36
+ }
37
+ /** Reine Rechnung: je Spec eine Zeile, je Story „US-2 · in Arbeit · Runde 1 · Titel“, danach Claudes notierte Themen. */
38
+ export function standZeilen(stand, s) {
39
+ const t = PLAN_TEXTE[s];
40
+ if (stand === null)
41
+ return [t.noPlanning];
42
+ return [
43
+ ...stand.specs.flatMap((sp) => [t.specLine(sp.schluessel, sp.titel, sp.datei), ...sp.stories.map((st) => t.storyLine(st.schluessel, t.storyStand(st.stand), st.runde, st.titel))]),
44
+ ...stand.themen.map((th) => t.topicLine(th.schluessel, th.text)),
45
+ ];
46
+ }
9
47
  function offenAus(w) {
10
48
  if (!w || typeof w !== "object")
11
49
  return null;
@@ -34,9 +72,10 @@ export function planAntwortAus(koerper) {
34
72
  return null;
35
73
  const offen = k.offen.map((o) => offenAus(o));
36
74
  const bestaetigt = k.bestaetigt.map((b) => bestaetigtAus(b));
37
- if (offen.includes(null) || bestaetigt.includes(null))
75
+ const stand = standAus(k.stand);
76
+ if (offen.includes(null) || bestaetigt.includes(null) || stand === undefined)
38
77
  return null;
39
- return { adresse: k.adresse, offen: offen, bestaetigt: bestaetigt };
78
+ return { adresse: k.adresse, offen: offen, bestaetigt: bestaetigt, stand };
40
79
  }
41
80
  /** Reine Rechnung: die Zeilen, die Claude zu sehen bekommt. */
42
81
  export function planZeilen(a, s) {
@@ -87,14 +126,10 @@ export async function plan(argv) {
87
126
  console.log(PLAN_TEXTE[s].listHelp);
88
127
  return 0;
89
128
  }
90
- const datei = planDateiOderAbsage(s);
91
- if (datei === null)
129
+ const projekt = projektDesOrdners();
130
+ if (projekt === null)
92
131
  return 1;
93
- const suche = new URLSearchParams();
94
- if (typeof datei.json.projekt_id === "string")
95
- suche.set("projekt-id", datei.json.projekt_id);
96
- if (typeof datei.json.projekt === "string")
97
- suche.set("projekt", datei.json.projekt);
132
+ const suche = new URLSearchParams({ "projekt-id": projekt.projektId, projekt: projekt.titel });
98
133
  const antwort = await ruf(`/api/cli/projekt/plan?${suche.toString()}`, { methode: "GET", mitAusweis: true }); // kein-anzeigetext: Pfad und HTTP-Methode
99
134
  // 🌐 Die Antwort kann die Kontosprache umgestellt haben: Was ab hier gedruckt wird, in der neuen.
100
135
  s = sprache();
@@ -105,6 +140,6 @@ export async function plan(argv) {
105
140
  console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
106
141
  return 3;
107
142
  }
108
- planZeilen(a, s).forEach((z) => console.log(z));
143
+ [...standZeilen(a.stand, s), ...planZeilen(a, s)].forEach((z) => console.log(z));
109
144
  return 0;
110
145
  }
@@ -0,0 +1,141 @@
1
+ import { feld, ruf } from "../api.js";
2
+ import { schalterOhneWert, zerlege } from "../argumente.js";
3
+ import { schalterIn } from "../befehlswoerter.js";
4
+ import { schlussFehler } from "../fehler.js";
5
+ import { firmenKoerperAus, ohneFirmaAusArgv } from "../projekt-neu-firma.js";
6
+ import { ordnerlageJetzt, schreibeZettel, verbindenErgebnis } from "../projekt-ordner.js";
7
+ import { sprache } from "../sprache.js";
8
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
9
+ import { MELDUNGEN_TEXTE } from "../texte/meldungen.js";
10
+ import { absage } from "./kontext-gemeinsam.js";
11
+ /*
12
+ `kimal3 projekt verbinden` -- jeder Projektordner hat sein Projekt in der App (Board in Echtzeit, Spec 1.7, E-7,
13
+ S-27 bis S-32, US-8). Der Mod stoesst es an, Claude ruft es auf.
14
+
15
+ - Zettel liegt: die App fragen, ob es das Projekt noch gibt. Ja -> nichts sagen, `0` (S-27, Gegenprobe). Nein ->
16
+ „Soll ich ein neues anlegen?“, `1`; erst `--neu` legt an (S-31: ohne Ja passiert nichts).
17
+ - Kein Zettel, aber `plan/stand.json` mit `projekt_id` (1.4): dasselbe Nachfragen, dann den Zettel daraus schreiben.
18
+ - Sonst: Titel = Name des Projektordners; die App verbindet mit dem einen gleichnamigen oder legt an (wie `projekt neu`).
19
+ Zwei gleichnamige -> Liste mit Nummern, `--projekt <n>` waehlt (S-28: es waehlt nie still).
20
+ */
21
+ /**
22
+ * Fuer `plan` und `plan melde`: das Projekt dieses Ordners -- oder `null`, nachdem der Grund gesagt ist. Kommt die
23
+ * Nummer noch aus `plan/stand.json`, wird der Zettel daraus jetzt angelegt (1.4: „beim ersten Lesen“) und das gesagt.
24
+ */
25
+ export function projektDesOrdners() {
26
+ const t = MELDUNGEN_TEXTE[sprache()];
27
+ const lage = ordnerlageJetzt();
28
+ if (lage.art === "kein-projektordner") {
29
+ console.error(t.notProjectFolder);
30
+ return null;
31
+ }
32
+ if (lage.zettel === null) {
33
+ console.error(t.notConnected);
34
+ return null;
35
+ }
36
+ if (lage.ausStandJson && schreibeZettel(lage.ordner, lage.zettel))
37
+ console.error(t.connected(lage.zettel.titel));
38
+ return { projektId: lage.zettel.projekt_id, titel: lage.zettel.titel, ordner: lage.ordner };
39
+ }
40
+ /**
41
+ * Was hinter `kimal3 projekt verbinden` in den Firmen-Vorschlaegen steht (Fixrunde 1): Mit `--neu` muss es mitreisen --
42
+ * sonst traefe der kopierte Vorschlag wieder den Zettel des geloeschten Projekts, und Claude fragte im Kreis (S-31).
43
+ */
44
+ export function verbindenOptionen(neu, s) {
45
+ return neu ? ` --${schalterIn("neu", s)}` : "";
46
+ }
47
+ export async function projektVerbinden(argv) {
48
+ let s = sprache();
49
+ if (argv.includes("--help") || argv.includes("-h")) {
50
+ console.log(MELDUNGEN_TEXTE[s].connectHelp);
51
+ return 0;
52
+ }
53
+ const neu = schalterOhneWert(argv, "neu");
54
+ const { rest, ohneFirma } = ohneFirmaAusArgv(neu.rest);
55
+ const z = zerlege(rest, ["projekt", "firma", "firma-id"], s);
56
+ if (!z.ok) {
57
+ console.error(z.meldung);
58
+ return 1;
59
+ }
60
+ const o = z.werte.optionen;
61
+ const wunsch = firmenKoerperAus(o.firma, o["firma-id"], ohneFirma, s);
62
+ if (!wunsch.ok) {
63
+ console.error(wunsch.meldung);
64
+ return 1;
65
+ }
66
+ const lage = ordnerlageJetzt();
67
+ if (lage.art === "kein-projektordner") {
68
+ console.error(MELDUNGEN_TEXTE[s].notProjectFolder);
69
+ return 1;
70
+ }
71
+ const vorhanden = neu.gesetzt ? null : lage.zettel;
72
+ const antwort = await ruf("/api/cli/projekt/verbinden", {
73
+ mitAusweis: true,
74
+ koerper: { titel: vorhanden?.titel ?? lage.name, projektId: vorhanden?.projekt_id ?? null, ...wunsch.koerper },
75
+ });
76
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben: Was ab hier gedruckt wird, in der neuen.
77
+ s = sprache();
78
+ const t = MELDUNGEN_TEXTE[s];
79
+ const e = verbindenErgebnis(antwort.status, antwort.koerper);
80
+ const merkeZettel = (z, satz) => {
81
+ if (!schreibeZettel(lage.ordner, z)) {
82
+ console.error(t.noteNotWritten(z.titel, z.projekt_id));
83
+ return 1;
84
+ }
85
+ console.log(satz);
86
+ return 0;
87
+ };
88
+ switch (e.art) {
89
+ case "verbunden": {
90
+ const z = { format: 1, projekt_id: e.projektId, titel: e.titel };
91
+ if (e.angelegt)
92
+ return merkeZettel(z, t.created(e.titel));
93
+ // Der Zettel lag schon und stimmt: still (S-27, Gegenprobe). Aus `plan/stand.json` oder per Titel: Zettel schreiben.
94
+ if (vorhanden !== null && !lage.ausStandJson)
95
+ return 0;
96
+ return merkeZettel(z, t.connected(e.titel));
97
+ }
98
+ case "geloescht":
99
+ console.error(t.deleted);
100
+ console.error(t.deletedHow);
101
+ return 1;
102
+ case "mehrdeutig": {
103
+ const wahl = o.projekt;
104
+ if (wahl === undefined) {
105
+ console.error(e.meldung);
106
+ e.kandidaten.forEach((k, i) => console.error(t.candidate(i + 1, k.titel, k.projektId)));
107
+ console.error(t.chooseHow);
108
+ return 1;
109
+ }
110
+ const nummer = Number(wahl);
111
+ const k = Number.isInteger(nummer) ? e.kandidaten[nummer - 1] : undefined;
112
+ if (k === undefined) {
113
+ console.error(t.noSuchCandidate(wahl, e.kandidaten.length));
114
+ return 1;
115
+ }
116
+ return merkeZettel({ format: 1, projekt_id: k.projektId, titel: k.titel }, t.connected(k.titel));
117
+ }
118
+ case "kein-profil":
119
+ console.error(t.noProfile);
120
+ return 1;
121
+ case "firma":
122
+ return absage(antwort, "projekt verbinden", verbindenOptionen(neu.gesetzt, s)); // kein-anzeigetext: Befehlsschluessel
123
+ case "unlesbar":
124
+ console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
125
+ return 3;
126
+ case "sonst": {
127
+ const meldung = feld(antwort.koerper, "meldung") ?? ALLGEMEIN_TEXTE[s].genericFailure;
128
+ if (antwort.status === 401) {
129
+ console.error(`${meldung}${ALLGEMEIN_TEXTE[s].pleaseSignIn}`);
130
+ return 2;
131
+ }
132
+ if (antwort.status === 400 || antwort.status === 409) {
133
+ console.error(meldung);
134
+ return 1;
135
+ }
136
+ const schluss = schlussFehler(antwort.status, meldung, s);
137
+ console.error(schluss.text);
138
+ return schluss.wert;
139
+ }
140
+ }
141
+ }
@@ -10,7 +10,7 @@ import { sprache } from "../sprache.js";
10
10
  import { andereSpracheZuRaeumen, skillQuelle } from "../slash-befehle.js";
11
11
  import { SKILL_TEXTE } from "../texte/skill.js";
12
12
  import { frischeCoachAuf } from "../coach-ablegen.js";
13
- import { frischeLogoAuf } from "../logo-ablegen.js";
13
+ import { frischeModsAuf } from "../mods-ablegen.js";
14
14
  export const VERALTETE_BEFEHLE = [
15
15
  /*
16
16
  🔴 **Der ERSTE Eintrag ueberhaupt** (2026-08-28). `/k3firma` heisst jetzt
@@ -332,7 +332,7 @@ export async function skillInstall() {
332
332
  export async function skillInstallBefehl() {
333
333
  const code = await skillInstall();
334
334
  frischeCoachAuf(sprache());
335
- frischeLogoAuf(sprache());
335
+ frischeModsAuf(sprache());
336
336
  return code;
337
337
  }
338
338
  /**
@@ -119,17 +119,17 @@ export const BEFEHLE_EN = {
119
119
  ],
120
120
  },
121
121
  "kimal3 plan": {
122
- stempel: "5a366449",
122
+ stempel: "6a0d2fec",
123
123
  befehl: "kimal3 plan",
124
124
  tippen: "/k3plan",
125
125
  terminal: "kimal3 plan",
126
- beschreibung: "Upload planning or fetch responses",
126
+ beschreibung: "Fetch the planning state and report changes",
127
127
  rhythmusOderSatz: "check my planning",
128
128
  details: [
129
- "kimal3 plan fetches what you've said in the app since the last upload: every “doesn't work” and every noted topic with its number, plus the confirmed stories. Claude sorts them and enters them in plan/stand.json.",
130
- "kimal3 plan upload sends plan/stand.json to your project and then enters projekt_id and basis in the file itself. With --dry-run it only checks and saves nothing.",
131
- "If the plan can't be read, a round goes down or the app already holds a newer plan, it refuses and saves nothing at all — the message says what to do. Use --overwrite only if you really mean it.",
132
- "You'll know it worked when it reports with ✓ how many stories were uploaded and how many are waiting for your test, and gives the link to the planning.",
129
+ "kimal3 plan fetches the state of your planning from the app: every spec with its stories, plus every “doesn't work” and every noted topic with its number. The project is named in the folder note plan/projekt.json.",
130
+ "When Claude changes something — spec created, story started or built, topic noted — it reports each change on its own with kimal3 plan report. Every session enters only its own change; nothing is overwritten.",
131
+ "If the app can't be reached right now, kimal3 keeps the report and sends it along with the next command (or with kimal3 plan send). If the folder doesn't belong to a project yet, kimal3 project connect creates it.",
132
+ "You'll know it worked when the report goes through silently — the command tells you about an error. kimal3 plan upload is no longer needed; only with --overwrite does it replace the whole state with plan/stand.json.",
133
133
  ],
134
134
  },
135
135
  "kimal3 kurs": {
@@ -168,21 +168,22 @@ export const BEFEHLE = [
168
168
  ],
169
169
  },
170
170
  /*
171
- `kimal3 plan` und `kimal3 plan hoch` (Spec 2026-10-21-projektseite-planung) -- EIN Eintrag fuer beide: Claude
172
- ruft sie nacheinander auf, und ein Mensch sucht hier nach der Planung, nicht nach zwei Befehlen.
171
+ `kimal3 plan` und seine Geschwister (Spec 2026-10-21-projektseite-planung; seit 0.7.0 Board in Echtzeit: `plan melde`,
172
+ `plan senden`, `projekt verbinden`) -- EIN Eintrag fuer alle: Claude ruft sie auf, und ein Mensch sucht hier nach
173
+ der Planung, nicht nach vier Befehlen.
173
174
  */
174
175
  {
175
176
  befehl: "kimal3 plan",
176
- beschreibung: "Planung hochladen oder Rückmeldungen holen",
177
+ beschreibung: "Den Stand der Planung holen und Änderungen melden",
177
178
  wer: "claude",
178
179
  ausloeser: "prüf meine Planung",
179
180
  tippen: "/k3plan",
180
181
  terminal: "kimal3 plan",
181
182
  details: [
182
- "kimal3 plan holt aus der App, was du seit dem letzten Hochladen gesagt hast: jedes „passt nicht“ und jedes notierte Thema mit seiner Nummer, dazu die bestätigten Stories. Claude ordnet sie ein und trägt sie in plan/stand.json ein.",
183
- "kimal3 plan hoch schickt plan/stand.json an dein Projekt und trägt danach projekt_id und basis selbst in die Datei ein. Mit --probe prüft er nur und speichert nichts.",
184
- "Ist der Plan nicht lesbar, wird eine Runde kleiner oder liegt in der App schon ein neuerer Plan, lehnt er ab und speichert gar nichts — die Meldung sagt, was zu tun ist. --ueberschreiben nur, wenn du es wirklich willst.",
185
- "Geklappt hat es, wenn er mit ✓ meldet, wie viele Stories hochgeladen sind und wie viele auf deinen Test warten, und den Link zur Planung nennt.",
183
+ "kimal3 plan holt den Stand deiner Planung aus der App: jede Spec mit ihren Stories, dazu jedes „passt nicht“ und jedes notierte Thema mit seiner Nummer. Das Projekt steht im Ordnerzettel plan/projekt.json.",
184
+ "Ändert Claude etwas — Spec angelegt, Story begonnen oder gebaut, Thema notiert —, meldet es das einzeln mit kimal3 plan melde. Jede Sitzung trägt nur ihre eigene Änderung ein; nichts wird überschrieben.",
185
+ "Ist die App gerade nicht erreichbar, merkt sich kimal3 die Meldung und schickt sie beim nächsten Befehl mit (oder mit kimal3 plan senden). Gehört der Ordner noch zu keinem Projekt, legt kimal3 projekt verbinden es an.",
186
+ "Geklappt hat es, wenn die Meldung still durchgeht — einen Fehler sagt der Befehl. kimal3 plan hoch braucht es nicht mehr; nur mit --ueberschreiben ersetzt es den ganzen Stand durch plan/stand.json.",
186
187
  ],
187
188
  },
188
189
  {
@@ -13,6 +13,10 @@ export const BEFEHLSWOERTER = {
13
13
  // `kimal3 plan` (Spec 2026-10-21-projektseite-planung, US-9): `plan` heisst in beiden Sprachen gleich.
14
14
  plan: { de: "plan", en: "plan" },
15
15
  "plan hoch": { de: "plan hoch", en: "plan upload" }, // kein-anzeigetext: Befehlswörter (Spec projektseite-planung)
16
+ // Board in Echtzeit (Spec 2026-10-23-brett-in-echtzeit-design.md, 1.2, 1.7; Bauplan Task 7)
17
+ "projekt verbinden": { de: "projekt verbinden", en: "project connect" }, // kein-anzeigetext: Befehlswörter (Spec brett-in-echtzeit)
18
+ "plan melde": { de: "plan melde", en: "plan report" }, // kein-anzeigetext: Befehlswörter (Spec brett-in-echtzeit)
19
+ "plan senden": { de: "plan senden", en: "plan send" }, // kein-anzeigetext: Befehlswörter (Spec brett-in-echtzeit)
16
20
  "firma daten": { de: "firma daten", en: "company data" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
17
21
  kurs: { de: "kurs", en: "course" }, // kein-anzeigetext: Befehlswörter (Spec Abschnitt 6)
18
22
  wache: { de: "wache", en: "watch" },
@@ -76,6 +80,11 @@ export const SCHALTER_EN = {
76
80
  "without-typed": "ohne-getipptes",
77
81
  // `kimal3 plan upload` (Spec 2026-10-21-projektseite-planung, US-11); `--overwrite` steht schon oben.
78
82
  "dry-run": "probe",
83
+ // `kimal3 project connect` (Board in Echtzeit, Task 7)
84
+ new: "neu",
85
+ project: "projekt",
86
+ // `kimal3 plan report --no-session` (Board auf einen Blick, Spec 2026-10-24 E-8)
87
+ "no-session": "ohne-sitzung",
79
88
  };
80
89
  /** `company` -> `firma`; ein deutscher (oder unbekannter) Name bleibt, wie er ist. */
81
90
  export function deutscherSchalter(getippt) {
@@ -36,7 +36,7 @@ export function tragePluginsEin(katalogPfad, mitSuperpowers) {
36
36
  const stand = pluginStand(text);
37
37
  if (mitSuperpowers && stand.superpowers === "fehlt")
38
38
  return false;
39
- return katalogPfad === null || (stand.logo !== "fehlt" && text.includes(`"${KATALOG}"`) && text.includes(JSON.stringify(katalogPfad)));
39
+ return katalogPfad === null || (stand.logo !== "fehlt" && stand.plan !== "fehlt" && text.includes(`"${KATALOG}"`) && text.includes(JSON.stringify(katalogPfad)));
40
40
  }, pluginSicherungsDatei());
41
41
  }
42
42
  /**
package/dist/kimal3.js CHANGED
@@ -38,6 +38,10 @@ import { projektListe } from "./befehle/projekt-liste.js";
38
38
  import { projektNeu } from "./befehle/projekt-neu.js";
39
39
  import { plan } from "./befehle/plan.js";
40
40
  import { planHoch } from "./befehle/plan-hoch.js";
41
+ import { planMeldeAngenommen } from "./argumente.js";
42
+ import { planMelde } from "./befehle/plan-melde.js";
43
+ import { planSenden, vorabSenden } from "./befehle/plan-senden.js";
44
+ import { projektVerbinden } from "./befehle/projekt-verbinden.js";
41
45
  import { skillDatei, skillInstallBefehl } from "./befehle/skill-install.js";
42
46
  import { wache, wacheSchalten } from "./befehle/wache.js";
43
47
  import { zeigenAuswerten, zeigenBild, zeigenStatus, zeigenStopp } from "./befehle/zeigen.js";
@@ -480,9 +484,19 @@ function zustandsZeile() {
480
484
  const a = leseAnmeldung();
481
485
  return a ? t.credentialsStored(a.email) : t.notSignedIn;
482
486
  }
487
+ /**
488
+ * Befehle, die die App erreichen und vor denen gemerkte Meldungen nachgeschickt werden (Board in Echtzeit, Task 7).
489
+ * Bewusst NICHT: `wache` (laeuft beim Start jeder Sitzung und soll dort nichts verzoegern), `plan senden` (tut es
490
+ * selbst), Anmelden/Abmelden, `einrichten` und die Befehle, die nur auf dieser Platte arbeiten.
491
+ */
492
+ const ERREICHT_APP = [
493
+ "projekt neu", "projekt liste", "projekt verbinden", "plan", "plan hoch", "plan melde", "firma daten", "kurs", // kein-anzeigetext: Befehlsnamen
494
+ "kontext runter", "kontext hoch", "kontext fragen", // kein-anzeigetext: Befehlsnamen
495
+ ];
483
496
  /** `null` = der Rest passt nicht zu diesem Befehl -> "kenne ich nicht", wie bisher. */
484
497
  async function fuehreAus(name, rest) {
485
498
  const allein = rest.length === 0;
499
+ const hilfe = rest.length === 1 && (rest[0] === "--help" || rest[0] === "-h");
486
500
  switch (name) {
487
501
  case "login": return allein ? login() : null;
488
502
  case "logout": return allein ? logout() : null;
@@ -492,6 +506,11 @@ async function fuehreAus(name, rest) {
492
506
  // `plan` nimmt nur `--help`; ein Wort dahinter (`plan muell`) bleibt unbekannt wie bei `login extra`.
493
507
  case "plan": return allein || (rest.length === 1 && (rest[0] === "--help" || rest[0] === "-h")) ? plan(rest) : null;
494
508
  case "plan hoch": return planHoch(rest);
509
+ // Board in Echtzeit (Task 7): `plan melde` liest die Meldung von der Eingabe, nicht aus Argumenten.
510
+ // Seit Board auf einen Blick (Spec 2026-10-24 E-8) auch `--ohne-sitzung`: `/k3plan` prueft nur, es arbeitet nicht.
511
+ case "plan melde": return planMeldeAngenommen(rest) ? planMelde(rest) : null;
512
+ case "plan senden": return allein || hilfe ? planSenden(rest) : null;
513
+ case "projekt verbinden": return projektVerbinden(rest);
495
514
  case "kurs": return allein ? kurs() : null;
496
515
  // ⚠️ Rest wird durchgereicht, nicht auf Gleichheit geprueft: `firma daten --firma X` gehoert dazu.
497
516
  case "firma daten": return firmaDaten(rest);
@@ -562,6 +581,9 @@ async function haupt() {
562
581
  if (erkannt?.name === "einrichten")
563
582
  merkeBefehlsSprache(argv[0] === BEFEHLSWOERTER.einrichten.en ? "en" : "de");
564
583
  if (erkannt) {
584
+ // 🔑 Gemerkte Meldungen zuerst (Board in Echtzeit, S-10, US-4): vor jedem Befehl, der die App erreicht.
585
+ if (ERREICHT_APP.includes(erkannt.name) && !erkannt.rest.includes("--help") && !erkannt.rest.includes("-h"))
586
+ await vorabSenden();
565
587
  const code = await fuehreAus(erkannt.name, erkannt.rest);
566
588
  if (code !== null)
567
589
  return code;