blun-king-cli 9.0.0 → 9.0.1

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 (55) hide show
  1. package/LIESMICH.txt +1 -7
  2. package/README.md +4 -16
  3. package/bin/blun.js +248 -160
  4. package/bin/core-bootstrap.js +47 -0
  5. package/bin/king.js +277 -1
  6. package/bin/launcher-mode.js +2 -1
  7. package/bin/launcher-runtime.js +221 -0
  8. package/bin/plugin-bootstrap.js +0 -0
  9. package/bin/private-paths.js +0 -0
  10. package/bin/update-lease.js +399 -0
  11. package/bin/update-notice.js +1094 -0
  12. package/blun.mjs +4060 -6667
  13. package/package.json +3 -10
  14. package/skills/screenshot-lesen/SKILL.md +0 -1
  15. package/skills/web-lesen/SKILL.md +0 -1
  16. package/telegram-plugin/dist/bridge.mjs +1 -21
  17. package/mnemo/access_routes.js +0 -692
  18. package/mnemo/agent_governance.js +0 -4242
  19. package/mnemo/agent_mail.js +0 -901
  20. package/mnemo/bootstrap_auto.js +0 -137
  21. package/mnemo/brief_coordination.js +0 -226
  22. package/mnemo/code_read_tools.js +0 -375
  23. package/mnemo/context_preview_tools.js +0 -603
  24. package/mnemo/embeddings.js +0 -66
  25. package/mnemo/external_repo_ops.js +0 -575
  26. package/mnemo/facts/example-project-rules.json +0 -90
  27. package/mnemo/facts/example.json +0 -34
  28. package/mnemo/identity_schema.sql +0 -139
  29. package/mnemo/journal_schema.js +0 -561
  30. package/mnemo/loop_doctor_tools.js +0 -661
  31. package/mnemo/mail_secret_refs.js +0 -150
  32. package/mnemo/mcp.js +0 -9309
  33. package/mnemo/memory_consolidation.js +0 -1914
  34. package/mnemo/memory_health_tools.js +0 -165
  35. package/mnemo/package.json +0 -79
  36. package/mnemo/protected_scope_gate.js +0 -627
  37. package/mnemo/resource_access_control.js +0 -684
  38. package/mnemo/runtime_governance.js +0 -1256
  39. package/mnemo/runtime_turn_gate.js +0 -862
  40. package/mnemo/sandbox.js +0 -143
  41. package/mnemo/schema.sql +0 -389
  42. package/mnemo/shared_utils.js +0 -763
  43. package/mnemo/skills/agent-auto-resume/SKILL.md +0 -56
  44. package/mnemo/skills/agent_hand/SKILL.md +0 -43
  45. package/mnemo/skills/agent_hand/run.js +0 -63
  46. package/mnemo/skills/book_flight/SKILL.md +0 -34
  47. package/mnemo/skills/external_repo_review/SKILL.md +0 -43
  48. package/mnemo/skills/external_repo_review/run.js +0 -73
  49. package/mnemo/skills/pay_invoice/SKILL.md +0 -34
  50. package/mnemo/team_quality_ops.js +0 -944
  51. package/mnemo/timeline_report_tools.js +0 -810
  52. package/mnemo/write_gate_risk.js +0 -80
  53. package/mnemo/writer_health.js +0 -152
  54. package/skills/doku-ingestion/SKILL.md +0 -48
  55. package/skills/doku-ingestion/ingest_docs.py +0 -133
@@ -1,80 +0,0 @@
1
- "use strict";
2
-
3
- const PRODUCTION_RE = /\b(production|prod|live|deploy|pm2|nginx|dns|ssl|cert|rollback)\b/;
4
- const BILLING_RE = /\b(stripe|billing|invoice|pricing|checkout|refund|vat|vies|oss|subscription|plan)\b/;
5
- const AUTH_RE = /\b(auth|login|signup|signin|sign-in|sso|session|cookie|oauth|password|reset|forgot|verify|onboarding|account)\b/;
6
- const WRITE_RE = /\b(edit|change|fix|implement|update|deploy|restart|migrate|patch|write|remove|delete|rename|refactor|create|build|rollout)\b/;
7
-
8
- const RISK_TERMS =
9
- "(?:production|prod|live|deploy|deployment|pm2|nginx|dns|ssl|cert|rollback|" +
10
- "auth|login|signup|signin|sign-in|sso|session|cookie|oauth|password|reset|forgot|verify|onboarding|account|" +
11
- "stripe|billing|invoice|pricing|checkout|refund|vat|vies|oss|subscription|plan|" +
12
- "server|servers|env|secret|secrets|source|app|apps)";
13
-
14
- const NEGATED_LIST_RE = new RegExp(
15
- "\\b(?:no|without|never|keine|kein|keinen|ohne)\\s+" +
16
- "(?:" + RISK_TERMS + "(?:\\s*(?:/|,|;|\\+|&|\\band\\b|\\bor\\b|\\bund\\b|\\boder\\b|\\s)\\s*)?){1,16}" +
17
- "(?:\\s+(?:work|edits?|writes?|changes?|scope|paths?|files?|activity|actions?))?",
18
- "gi"
19
- );
20
-
21
- const DO_NOT_TOUCH_RE = new RegExp(
22
- "\\b(?:do\\s+not|don't|dont|never|nicht)\\s+" +
23
- "(?:touch|edit|change|write|deploy|modify|manage|alter)\\s+" +
24
- "[^.;\\n]{0,100}\\b" + RISK_TERMS + "\\b[^.;\\n]{0,80}",
25
- "gi"
26
- );
27
-
28
- const ORPHAN_NEGATED_TAIL_RE = new RegExp(
29
- "\\b(?:or|and|oder|und)\\s+" + RISK_TERMS +
30
- "(?:\\s+(?:work|edits?|writes?|changes?|scope|paths?|files?|activity|actions?))?",
31
- "gi"
32
- );
33
-
34
- function riskScanText(text) {
35
- let removedNegatedRisk = false;
36
- let out = String(text || "")
37
- .replace(NEGATED_LIST_RE, () => {
38
- removedNegatedRisk = true;
39
- return " ";
40
- })
41
- .replace(DO_NOT_TOUCH_RE, () => {
42
- removedNegatedRisk = true;
43
- return " ";
44
- });
45
- if (removedNegatedRisk) out = out.replace(ORPHAN_NEGATED_TAIL_RE, " ");
46
- return out;
47
- }
48
-
49
- function classifyActionRisk(input = {}) {
50
- const text = [
51
- input.project,
52
- input.task,
53
- input.summary,
54
- input.action_type,
55
- Array.isArray(input.topics) ? input.topics.join(" ") : "",
56
- Array.isArray(input.files) ? input.files.join(" ") : "",
57
- Array.isArray(input.system_names) ? input.system_names.join(" ") : ""
58
- ].filter(Boolean).join(" ");
59
- const normalized = riskScanText(text).toLowerCase();
60
- const rawActionType = String(input.action_type || "").toLowerCase();
61
- const touchesProduction = PRODUCTION_RE.test(normalized) || /\bdeploy\b/.test(rawActionType);
62
- const touchesBilling = BILLING_RE.test(normalized);
63
- const touchesAuth = AUTH_RE.test(normalized);
64
- const writeIntent = WRITE_RE.test(normalized) || WRITE_RE.test(rawActionType);
65
- const environment = input.environment || (touchesProduction ? "production" : "staging");
66
- return {
67
- text,
68
- scan_text: normalized,
69
- write_intent: writeIntent,
70
- touches_production: touchesProduction,
71
- touches_billing: touchesBilling,
72
- touches_auth: touchesAuth,
73
- environment,
74
- };
75
- }
76
-
77
- module.exports = {
78
- classifyActionRisk,
79
- riskScanText,
80
- };
@@ -1,152 +0,0 @@
1
- "use strict";
2
-
3
- const DEFAULT_REQUIRED_FRESH_WRITERS = [];
4
- const DEFAULT_STALE_MS = 2 * 60 * 60 * 1000;
5
- const DEFAULT_DEAD_MS = 24 * 60 * 60 * 1000;
6
-
7
- const HEALTHY_STATUSES = new Set([
8
- "alive",
9
- "alive_no_new",
10
- "ok",
11
- "healthy",
12
- "success",
13
- "idle",
14
- ]);
15
-
16
- function normalizeStatus(status) {
17
- return String(status || "").trim().toLowerCase();
18
- }
19
-
20
- function parseList(value, fallback) {
21
- const raw = value == null || value === "" ? fallback : value;
22
- const list = Array.isArray(raw) ? raw : String(raw || "").split(",");
23
- return list.map((item) => String(item || "").trim()).filter(Boolean);
24
- }
25
-
26
- function requiredFreshWriterSet(options = {}) {
27
- return new Set(parseList(
28
- options.requiredFreshWriters ||
29
- process.env.MNEMO_WRITER_HEALTH_REQUIRED ||
30
- process.env.MNEMO_HOOK_WATCHDOG_CRITICAL_WRITERS,
31
- DEFAULT_REQUIRED_FRESH_WRITERS
32
- ));
33
- }
34
-
35
- function writerStatusClass(status) {
36
- const s = normalizeStatus(status);
37
- if (!s) return "unknown";
38
- if (s === "stale") return "stale";
39
- if (s === "dead") return "dead";
40
- if (s.startsWith("disabled")) return "disabled";
41
- if (s.startsWith("error") || s.includes("database is locked") || s.includes("sqlite_corrupt")) return "error";
42
- if (s.startsWith("blocked")) return "blocked";
43
- if (s.startsWith("partial")) return "degraded";
44
- if (HEALTHY_STATUSES.has(s)) return "healthy";
45
- return "unknown";
46
- }
47
-
48
- function ageMs(iso, nowMs) {
49
- const t = iso ? Date.parse(iso) : 0;
50
- if (!t) return null;
51
- return Math.max(0, nowMs - t);
52
- }
53
-
54
- function freshnessFromAge(age, staleMs, deadMs, required) {
55
- if (age == null) return required ? "missing" : "not_required";
56
- if (age > deadMs) return required ? "critical" : "idle";
57
- if (age > staleMs) return required ? "stale" : "idle";
58
- return "fresh";
59
- }
60
-
61
- function assessWriterHealth(row = {}, options = {}) {
62
- const writer = String(row.writer || "");
63
- const status = normalizeStatus(row.status);
64
- const statusClass = writerStatusClass(status);
65
- const nowMs = options.nowMs || Date.now();
66
- const staleMs = Number(options.staleMs || DEFAULT_STALE_MS);
67
- const deadMs = Number(options.deadMs || DEFAULT_DEAD_MS);
68
- const lastWriteAgeMs = ageMs(row.last_write_at, nowMs);
69
- const lastCheckAgeMs = ageMs(row.last_check_at, nowMs);
70
- const freshnessRequired = requiredFreshWriterSet(options).has(writer);
71
- const freshness = freshnessFromAge(lastWriteAgeMs, staleMs, deadMs, freshnessRequired);
72
-
73
- let healthy = true;
74
- let reason = "writer is informational or event-driven";
75
- let nextStatus = status || "idle";
76
- let drift = false;
77
- let driftSeverity = "M";
78
-
79
- if (statusClass === "disabled") {
80
- reason = `writer disabled (${status})`;
81
- nextStatus = status;
82
- } else if (["error", "blocked", "degraded"].includes(statusClass)) {
83
- healthy = false;
84
- drift = true;
85
- driftSeverity = statusClass === "degraded" ? "M" : "H";
86
- reason = `writer status is ${status}`;
87
- nextStatus = status;
88
- } else if (freshnessRequired) {
89
- if (lastWriteAgeMs == null) {
90
- healthy = false;
91
- drift = true;
92
- driftSeverity = "H";
93
- reason = "required writer has never written";
94
- nextStatus = "missing";
95
- } else if (lastWriteAgeMs > deadMs) {
96
- healthy = false;
97
- drift = true;
98
- driftSeverity = "H";
99
- reason = "required writer has not written within dead threshold";
100
- nextStatus = "dead";
101
- } else if (lastWriteAgeMs > staleMs) {
102
- healthy = false;
103
- drift = true;
104
- driftSeverity = "M";
105
- reason = "required writer has not written within stale threshold";
106
- nextStatus = "stale";
107
- } else {
108
- reason = "required writer is fresh";
109
- nextStatus = "alive";
110
- }
111
- } else if (lastWriteAgeMs != null && lastWriteAgeMs <= staleMs && statusClass !== "unknown") {
112
- reason = "writer wrote recently";
113
- nextStatus = HEALTHY_STATUSES.has(status) ? status : "alive";
114
- } else {
115
- reason = "writer is not freshness-gated";
116
- nextStatus = statusClass === "stale" || statusClass === "dead" || !status ? "idle" : status;
117
- }
118
-
119
- return {
120
- writer,
121
- status,
122
- status_class: statusClass,
123
- freshness_required: freshnessRequired,
124
- freshness,
125
- healthy,
126
- drift,
127
- drift_severity: driftSeverity,
128
- health_reason: reason,
129
- next_status: nextStatus,
130
- last_write_age_ms: lastWriteAgeMs,
131
- last_write_age_min: lastWriteAgeMs == null ? null : Math.round(lastWriteAgeMs / 60000),
132
- last_check_age_ms: lastCheckAgeMs,
133
- last_check_age_min: lastCheckAgeMs == null ? null : Math.round(lastCheckAgeMs / 60000),
134
- };
135
- }
136
-
137
- function enrichWriterHealth(row, options = {}) {
138
- return Object.assign({}, row, assessWriterHealth(row, options));
139
- }
140
-
141
- function enrichWriterHealthRows(rows, options = {}) {
142
- return (rows || []).map((row) => enrichWriterHealth(row, options));
143
- }
144
-
145
- module.exports = {
146
- DEFAULT_REQUIRED_FRESH_WRITERS,
147
- assessWriterHealth,
148
- enrichWriterHealth,
149
- enrichWriterHealthRows,
150
- requiredFreshWriterSet,
151
- writerStatusClass,
152
- };
@@ -1,48 +0,0 @@
1
- ---
2
- name: doku-ingestion
3
- description: "Eine API-/Software-Dokumentation dauerhaft ins eigene Gedaechtnis aufnehmen: crawlen, in sinnvolle Abschnitte zerlegen und in Mnemo speichern, damit Endpunkte, Parameter und Code-Beispiele spaeter zuverlaessig zitierbar sind. Mandatory triggers: 'lern die API-Doku von X', 'nimm die Doku auf', 'merk dir die Dokumentation', 'ingest docs', 'speichere die Doku dauerhaft', eine Doku-URL mit der Bitte, sie zu behalten. Fuer einmaliges Lesen ohne Speichern reicht der web-lesen-Skill."
4
- ---
5
-
6
- # Doku-Ingestion
7
-
8
- Der Unterschied zu web-lesen: hier wird die Doku nicht nur EINMAL gelesen,
9
- sondern DAUERHAFT ins Gedaechtnis aufgenommen — in zitierbaren Abschnitten,
10
- sodass du spaeter „welche Parameter hat Endpunkt X?" beantworten kannst, ohne
11
- die Seite erneut zu laden.
12
-
13
- ## Ablauf
14
-
15
- ```bash
16
- python ingest_docs.py "<DOKU-URL>" --source <kurzname>
17
- ```
18
-
19
- Das Skript (liegt neben dieser Datei):
20
- 1. Crawlt die Seite mit Crawl4AI zu sauberem Markdown.
21
- 2. Zerlegt sie an Ueberschriften in Abschnitte (grosse weiter an Absaetzen,
22
- ~1800 Zeichen pro Stueck — ein Endpunkt/Thema pro Abschnitt).
23
- 3. Speichert jeden Abschnitt als Capture in deine Mnemo-Lane, mit URL, Quelle
24
- und Abschnittsnummer in den Metadaten.
25
-
26
- Voraussetzungen (Umgebung setzt der Starter):
27
- `BLUN_MNEMO_MCP` (Pfad mcp.js), `BLUN_MNEMO_AGENT`, ggf. `BLUN_MNEMO_NODE`.
28
- Crawl4AI einmalig: `pip install crawl4ai && crawl4ai-setup`.
29
-
30
- ## Spaeter abrufen
31
-
32
- Nach dem Ingest findest du Doku-Wissen ueber die normale Erinnerung:
33
- `mem_recall` mit der Frage (z.B. „glm chat completions parameter"). Die
34
- Abschnitte kommen mit Quelle+URL zurueck — zitiere sie mit Herkunft.
35
-
36
- ## Mehrseitige Dokus
37
-
38
- Erst die Uebersichtsseite lesen (web-lesen), die Unterseiten-Links sammeln,
39
- dann pro relevanter Unterseite `ingest_docs.py` aufrufen. Nicht die ganze
40
- Domain blind crawlen — nur die fuer die Aufgabe noetigen Seiten.
41
-
42
- ## Regeln
43
-
44
- - Nur oeffentliche/offizielle Dokus. Keine bezahlten/geschuetzten Inhalte
45
- ohne Auftrag.
46
- - Bei bekannten Libraries zuerst den Context7-MCP versuchen (immer aktuell,
47
- kein Crawlen). Crawl4AI fuer alles ohne Context7-Eintrag.
48
- - Immer sagen, WAS du aufgenommen hast (Quelle + wie viele Abschnitte).
@@ -1,133 +0,0 @@
1
- #!/usr/bin/env python
2
- """BLUN Doku-Ingestion: eine (API-)Doku crawlen -> in Abschnitte zerlegen ->
3
- in die eigene Mnemo-Lane speichern, damit sie spaeter zitierbar wiederfindbar
4
- ist. Kein Cloud-Dienst, alles lokal.
5
-
6
- Nutzung:
7
- python ingest_docs.py <URL> [--source doku-name] [--max-chunks N]
8
-
9
- Voraussetzung: crawl4ai (pip install crawl4ai) und ein laufender Mnemo-MCP
10
- (mcp.js) — Pfad via BLUN_MNEMO_MCP, Agent via BLUN_MNEMO_AGENT.
11
- """
12
- import asyncio
13
- import json
14
- import os
15
- import re
16
- import subprocess
17
- import sys
18
-
19
- MCP = os.environ.get("BLUN_MNEMO_MCP")
20
- NODE = os.environ.get("BLUN_MNEMO_NODE") or "node"
21
- AGENT = os.environ.get("BLUN_MNEMO_AGENT", "agent")
22
- CHUNK_CHARS = 1800 # ~ ein sinnvoller Doku-Abschnitt
23
-
24
-
25
- def chunk_markdown(md: str):
26
- """Zerteilt Markdown an Ueberschriften; grosse Bloecke weiter an Absaetzen."""
27
- parts = re.split(r"(?=^#{1,4}\s)", md, flags=re.MULTILINE)
28
- chunks = []
29
- for part in parts:
30
- part = part.strip()
31
- if not part:
32
- continue
33
- if len(part) <= CHUNK_CHARS:
34
- chunks.append(part)
35
- else:
36
- buf = ""
37
- for para in part.split("\n\n"):
38
- if len(buf) + len(para) + 2 > CHUNK_CHARS and buf:
39
- chunks.append(buf.strip())
40
- buf = ""
41
- buf += para + "\n\n"
42
- if buf.strip():
43
- chunks.append(buf.strip())
44
- return [c for c in chunks if len(c) > 40]
45
-
46
-
47
- async def crawl(url: str) -> str:
48
- from crawl4ai import AsyncWebCrawler
49
- async with AsyncWebCrawler() as c:
50
- res = await c.arun(url=url)
51
- return res.markdown or ""
52
-
53
-
54
- def ingest(items):
55
- """Spricht mcp.js per JSON-RPC/stdio, capture-batch in Chunks."""
56
- if not MCP or not os.path.exists(MCP):
57
- print("WARN: BLUN_MNEMO_MCP nicht gesetzt/gefunden — speichere nicht, gebe nur aus.")
58
- for it in items[:3]:
59
- print(" -", it["content"][:80].replace("\n", " "))
60
- print(f" ... {len(items)} Abschnitte insgesamt (nicht gespeichert).")
61
- return
62
- proc = subprocess.Popen(
63
- [NODE, MCP], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
64
- stderr=subprocess.DEVNULL, text=True, env={**os.environ, "MNEMO_AGENT": AGENT},
65
- )
66
- nid = 0
67
-
68
- def rpc(method, params):
69
- nonlocal nid
70
- nid += 1
71
- proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": nid, "method": method, "params": params}) + "\n")
72
- proc.stdin.flush()
73
- for line in proc.stdout:
74
- line = line.strip()
75
- if not line:
76
- continue
77
- try:
78
- msg = json.loads(line)
79
- except Exception:
80
- continue
81
- if msg.get("id") == nid:
82
- return msg
83
- return None
84
-
85
- rpc("initialize", {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "doku-ingest", "version": "1"}})
86
- proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
87
- proc.stdin.flush()
88
- captured = 0
89
- for i in range(0, len(items), 20):
90
- batch = items[i:i + 20]
91
- res = rpc("tools/call", {"name": "mem_capture_ingest_batch", "arguments": {"items": batch}})
92
- text = ((res or {}).get("result", {}).get("content") or [{}])[0].get("text", "{}")
93
- try:
94
- captured += json.loads(text).get("captured", 0)
95
- except Exception:
96
- pass
97
- proc.terminate()
98
- print(f"Gespeichert: {captured}/{len(items)} Doku-Abschnitte in Mnemo (Agent {AGENT}).")
99
-
100
-
101
- def main():
102
- if len(sys.argv) < 2:
103
- print("Nutzung: python ingest_docs.py <URL> [--source name] [--max-chunks N]")
104
- sys.exit(1)
105
- url = sys.argv[1]
106
- source = "doku"
107
- max_chunks = 200
108
- if "--source" in sys.argv:
109
- source = sys.argv[sys.argv.index("--source") + 1]
110
- if "--max-chunks" in sys.argv:
111
- max_chunks = int(sys.argv[sys.argv.index("--max-chunks") + 1])
112
-
113
- md = asyncio.run(crawl(url))
114
- if not md.strip():
115
- print("Keine Inhalte gecrawlt — URL erreichbar?")
116
- sys.exit(1)
117
- chunks = chunk_markdown(md)[:max_chunks]
118
- thread = f"doku-{re.sub(r'[^a-z0-9]+', '-', source.lower()).strip('-') or 'x'}"
119
- items = [{
120
- "source": "doku", "channel": "doku-ingest", "direction": "inbound",
121
- "actor": AGENT, "event_kind": "doc_chunk",
122
- "occurred_at": "1970-01-01T00:00:00Z", # Aufrufer/Hub stempelt real
123
- "ref_id": f"doku-{source}-{n}",
124
- "content": f"[{source}] {url}\n\n{c}"[:4000],
125
- "meta": {"for_agent": AGENT, "doc_url": url, "doc_source": source, "chunk": n},
126
- "thread_id": thread,
127
- } for n, c in enumerate(chunks)]
128
- print(f"Gecrawlt: {len(md)} Zeichen -> {len(items)} Abschnitte aus {url}")
129
- ingest(items)
130
-
131
-
132
- if __name__ == "__main__":
133
- main()