@monoes/monomindcli 1.6.8 → 1.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.
@@ -1,118 +1,33 @@
1
1
  'use strict';
2
- // Runs at SessionStart — rebuilds the knowledge graph for the current project in the background.
2
+ // Runs at SessionStart — rebuilds the knowledge graph using graphify (Python) in the background.
3
3
  // Fire-and-forget: spawns detached child, logs start, exits immediately without blocking session.
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
+ const { spawn, execSync } = require('child_process');
6
7
 
7
8
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
8
9
  const graphDir = path.join(projectDir, '.monomind', 'graph');
9
- const statsFile = path.join(graphDir, 'stats.json');
10
10
 
11
- // Locate @monomind/graph check local, global npm, bundled-graph, pnpm
12
- function findGraphPkg(base) {
13
- const candidates = [
14
- path.join(base, 'node_modules', '@monomind', 'graph', 'dist', 'src', 'index.js'),
15
- path.join(base, 'packages', '@monomind', 'cli', 'node_modules', '@monomind', 'graph', 'dist', 'src', 'index.js'),
16
- path.join(base, 'packages', '@monomind', 'graph', 'dist', 'src', 'index.js'),
17
- ];
18
- // Check global npm install paths (monomind umbrella ships bundled-graph)
19
- try {
20
- const globalRoot = require('child_process').execSync('npm root -g', { encoding: 'utf-8' }).trim();
21
- candidates.push(
22
- path.join(globalRoot, 'monomind', 'packages', '@monomind', 'cli', 'bundled-graph', 'dist', 'src', 'index.js'),
23
- path.join(globalRoot, 'monomind', 'node_modules', '@monoes', 'monomindcli', 'bundled-graph', 'dist', 'src', 'index.js'),
24
- );
25
- } catch {}
26
- for (const c of candidates) {
27
- if (fs.existsSync(c)) return c;
28
- }
29
- // pnpm: glob for @monomind+graph in .pnpm
30
- const pnpmDir = path.join(base, 'node_modules', '.pnpm');
31
- if (fs.existsSync(pnpmDir)) {
32
- for (const entry of fs.readdirSync(pnpmDir)) {
33
- if (entry.startsWith('@monomind+graph')) {
34
- const p = path.join(pnpmDir, entry, 'node_modules', '@monomind', 'graph', 'dist', 'src', 'index.js');
35
- if (fs.existsSync(p)) return p;
36
- }
37
- }
38
- }
39
- return null;
40
- }
41
-
42
- const graphPkg = findGraphPkg(projectDir);
43
-
44
- if (!graphPkg) {
45
- console.log('[graph] skip: @monomind/graph not found');
11
+ // Check if graphify CLI is available
12
+ try {
13
+ execSync('graphify --help', { encoding: 'utf-8', stdio: 'ignore' });
14
+ } catch {
15
+ console.log('[graph] skip: graphify not installed (run: uv tool install graphifyy)');
46
16
  process.exit(0);
47
17
  }
48
18
 
49
19
  fs.mkdirSync(graphDir, { recursive: true });
50
20
 
51
- // Locate the enricher script — works for both monorepo layout and npm-install layout
52
- const enricherCandidates = [
53
- path.join(projectDir, 'packages', '@monomind', 'cli', 'dist', 'src', 'graph', 'enrich.mjs'),
54
- path.join(projectDir, 'node_modules', '@monomind', 'cli', 'dist', 'src', 'graph', 'enrich.mjs'),
55
- ];
56
- const enricherPath = enricherCandidates.find(p => fs.existsSync(p)) ?? null;
57
- const hasEnricher = enricherPath !== null;
58
-
59
- const { spawn } = require('child_process');
60
- const script = [
61
- `import { buildGraph } from ${JSON.stringify('file://' + graphPkg)};`,
62
- `import fs from 'fs';`,
63
- `import path from 'path';`,
64
- `const projectDir = ${JSON.stringify(projectDir)};`,
65
- `const graphDir = ${JSON.stringify(graphDir)};`,
66
- `const statsFile = ${JSON.stringify(statsFile)};`,
67
- `buildGraph(projectDir, { codeOnly: true, outputDir: graphDir })`,
68
- `.then(async r => {`,
69
- ` fs.writeFileSync(statsFile, JSON.stringify({ nodes: r.analysis?.stats?.nodes, edges: r.analysis?.stats?.edges, files: r.filesProcessed, builtAt: Date.now() }));`,
70
- ` console.log('[graph] built: ' + r.filesProcessed + ' files, ' + (r.analysis?.stats?.nodes ?? '?') + ' nodes');`,
71
- hasEnricher ? [
72
- ` try {`,
73
- ` const { enrichGraph } = await import(${JSON.stringify('file://' + enricherPath)});`,
74
- ` const er = await enrichGraph(projectDir, { graphDir });`,
75
- ` console.log('[graph] enriched: ' + er.metrics.enrichedNodes + '/' + er.metrics.totalNodes + ' nodes');`,
76
- ` } catch (ee) { console.error('[graph] enrichment failed:', ee.message); }`,
77
- ].join('\n') : '',
78
- // Normalize graph.json: add snake_case field aliases expected by the MCP tools
79
- ` try {`,
80
- ` const graphPath = path.join(graphDir, 'graph.json');`,
81
- ` const raw = JSON.parse(fs.readFileSync(graphPath, 'utf-8'));`,
82
- ` if (Array.isArray(raw.nodes)) {`,
83
- ` for (const n of raw.nodes) {`,
84
- ` n.source_file = n.sourceFile || '';`,
85
- ` n.source_location = n.sourceLocation || '';`,
86
- ` n.file_type = n.fileType || '';`,
87
- ` // Zero out degree for external symbols so they don't dominate god_nodes results`,
88
- ` if (!n.source_file) n.degree = 0;`,
89
- ` }`,
90
- ` fs.writeFileSync(graphPath, JSON.stringify(raw));`,
91
- ` console.log('[graph] normalized: added MCP field aliases to ' + raw.nodes.length + ' nodes');`,
92
- ` }`,
93
- ` } catch (ne) { console.error('[graph] normalize failed:', ne.message); }`,
94
- // Ensure .monomind/graph symlink exists so the MCP server can locate the graph
95
- ` try {`,
96
- ` const monomindDir = path.join(projectDir, '.monomind');`,
97
- ` fs.mkdirSync(monomindDir, { recursive: true });`,
98
- ` const symlinkTarget = path.join(monomindDir, 'graph');`,
99
- ` let exists = false;`,
100
- ` try { fs.lstatSync(symlinkTarget); exists = true; } catch {}`,
101
- ` if (!exists) { fs.symlinkSync(graphDir, symlinkTarget); console.log('[graph] created .monomind/graph symlink'); }`,
102
- ` } catch (se) { console.error('[graph] symlink setup failed:', se.message); }`,
103
- `})`,
104
- `.catch(e => console.error('[graph] build failed:', e.message));`,
105
- ].join('\n');
106
-
107
21
  const logPath = path.join(graphDir, 'build.log');
108
22
  let logFd;
109
23
  try { logFd = fs.openSync(logPath, 'a'); } catch { logFd = 'ignore'; }
110
- const child = spawn(process.execPath, ['--input-type=module'], {
24
+
25
+ // graphify update <path> — re-extracts code files and rebuilds graph.json
26
+ const child = spawn('graphify', ['update', projectDir], {
111
27
  detached: true,
112
- stdio: ['pipe', logFd, logFd],
28
+ stdio: ['ignore', logFd, logFd],
29
+ cwd: projectDir,
113
30
  });
114
- child.stdin.write(script);
115
- child.stdin.end();
116
31
  child.unref();
117
32
 
118
33
  console.log('[graph] background build started for ' + projectDir);
@@ -961,6 +961,22 @@ const handlers = {
961
961
  }
962
962
  } catch (e) { /* non-fatal */ }
963
963
 
964
+ // ── Monomind Control UI Status ────────────────────────────────────────
965
+ try {
966
+ var http = require('http');
967
+ var controlPort = 4242;
968
+ var req = http.get('http://localhost:' + controlPort + '/', function(res) {
969
+ if (res.statusCode === 200) {
970
+ console.log('[CONTROL_UI] UP — http://localhost:' + controlPort);
971
+ }
972
+ res.resume();
973
+ });
974
+ req.on('error', function() {
975
+ console.log('[CONTROL_UI] offline — run: npx monomind mcp start');
976
+ });
977
+ req.setTimeout(800, function() { req.destroy(); });
978
+ } catch (e) { /* non-fatal */ }
979
+
964
980
  // ── Worker Queue Resume (SR-003) ────────────────────────────────────
965
981
  try {
966
982
  var dispatchDir = path.join(CWD, '.monomind', 'worker-dispatch');
@@ -104,6 +104,18 @@ function safeStat(filePath) {
104
104
  return null;
105
105
  }
106
106
 
107
+ // Project identifier — github owner/repo from git remote, else folder name
108
+ function getProjectName() {
109
+ try {
110
+ const remote = safeExec('git remote get-url origin 2>/dev/null', 2000).trim();
111
+ if (remote) {
112
+ const m = remote.match(/[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
113
+ if (m) return `${m[1]}/${m[2]}`;
114
+ }
115
+ } catch { /* ignore */ }
116
+ return path.basename(CWD);
117
+ }
118
+
107
119
  // Shared settings cache — read once, used by multiple functions
108
120
  let _settingsCache = undefined;
109
121
  function getSettings() {
@@ -565,37 +577,56 @@ function getAgentDBStats() {
565
577
  let namespaces = 0;
566
578
  let hasHnsw = false;
567
579
 
568
- // 0. PRIMARY: Count drawers from Memory Palace (this is where memories actually live)
580
+ // Count all memory entries across sources (sum, not max)
581
+ // 0. Palace drawers
569
582
  const drawersPath = path.join(CWD, '.monomind', 'palace', 'drawers.jsonl');
570
583
  const drawersStat = safeStat(drawersPath);
571
584
  if (drawersStat) {
572
585
  dbSizeKB += drawersStat.size / 1024;
573
586
  try {
574
- const lines = fs.readFileSync(drawersPath, 'utf-8').split('\n').filter(Boolean);
575
- if (lines.length > vectorCount) vectorCount = lines.length;
587
+ vectorCount += fs.readFileSync(drawersPath, 'utf-8').split('\n').filter(Boolean).length;
588
+ } catch { /* ignore */ }
589
+ }
590
+
591
+ // 1. Palace closets
592
+ const closetsPath = path.join(CWD, '.monomind', 'palace', 'closets.jsonl');
593
+ const closetsStat = safeStat(closetsPath);
594
+ if (closetsStat) {
595
+ dbSizeKB += closetsStat.size / 1024;
596
+ try {
597
+ vectorCount += fs.readFileSync(closetsPath, 'utf-8').split('\n').filter(Boolean).length;
576
598
  } catch { /* ignore */ }
577
599
  }
578
600
 
579
- // 1. Count real entries from auto-memory-store.json (intelligence layer)
601
+ // 2. Knowledge chunks
602
+ const chunksPath = path.join(CWD, '.monomind', 'knowledge', 'chunks.jsonl');
603
+ const chunksStat = safeStat(chunksPath);
604
+ if (chunksStat) {
605
+ dbSizeKB += chunksStat.size / 1024;
606
+ try {
607
+ vectorCount += fs.readFileSync(chunksPath, 'utf-8').split('\n').filter(Boolean).length;
608
+ } catch { /* ignore */ }
609
+ }
610
+
611
+ // 3. Auto-memory store (intelligence layer)
580
612
  const storePath = path.join(CWD, '.monomind', 'data', 'auto-memory-store.json');
581
613
  const storeStat = safeStat(storePath);
582
614
  if (storeStat) {
583
615
  dbSizeKB += storeStat.size / 1024;
584
616
  try {
585
617
  const store = JSON.parse(fs.readFileSync(storePath, 'utf-8'));
586
- const storeCount = Array.isArray(store) ? store.length : (store?.entries?.length || 0);
587
- if (storeCount > vectorCount) vectorCount = storeCount;
588
- } catch { /* fall back to size estimate */ }
618
+ vectorCount += Array.isArray(store) ? store.length : (store?.entries?.length || 0);
619
+ } catch { /* ignore */ }
589
620
  }
590
621
 
591
- // 2. Count entries from ranked-context.json
622
+ // 4. Ranked context
592
623
  const rankedPath = path.join(CWD, '.monomind', 'data', 'ranked-context.json');
593
624
  try {
594
625
  const ranked = readJSON(rankedPath);
595
- if (ranked?.entries?.length > vectorCount) vectorCount = ranked.entries.length;
626
+ if (ranked?.entries?.length) vectorCount += ranked.entries.length;
596
627
  } catch { /* ignore */ }
597
628
 
598
- // 3. Add DB file sizes
629
+ // 5. DB file sizes
599
630
  const dbFiles = [
600
631
  path.join(CWD, 'data', 'memory.db'),
601
632
  path.join(CWD, '.monomind', 'memory.db'),
@@ -603,31 +634,17 @@ function getAgentDBStats() {
603
634
  ];
604
635
  for (const f of dbFiles) {
605
636
  const stat = safeStat(f);
606
- if (stat) {
607
- dbSizeKB += stat.size / 1024;
608
- namespaces++;
609
- }
637
+ if (stat) { dbSizeKB += stat.size / 1024; namespaces++; }
610
638
  }
611
639
 
612
- // 4. Check for graph data
613
- const graphPath = path.join(CWD, 'data', 'memory.graph');
614
- const graphStat = safeStat(graphPath);
615
- if (graphStat) dbSizeKB += graphStat.size / 1024;
616
-
617
- // 5. HNSW index
640
+ // 6. HNSW index
618
641
  const hnswPaths = [
619
642
  path.join(CWD, '.swarm', 'hnsw.index'),
620
643
  path.join(CWD, '.monomind', 'hnsw.index'),
621
644
  ];
622
645
  for (const p of hnswPaths) {
623
- const stat = safeStat(p);
624
- if (stat) {
625
- hasHnsw = true;
626
- break;
627
- }
646
+ if (safeStat(p)) { hasHnsw = true; break; }
628
647
  }
629
-
630
- // HNSW is available if memory package is present
631
648
  if (!hasHnsw) {
632
649
  const memPkgPaths = [
633
650
  path.join(CWD, 'packages', '@monomind', 'memory', 'dist'),
@@ -641,6 +658,26 @@ function getAgentDBStats() {
641
658
  return { vectorCount, dbSizeKB: Math.floor(dbSizeKB), namespaces, hasHnsw };
642
659
  }
643
660
 
661
+ // Graphify knowledge graph stats
662
+ function getGraphifyStats() {
663
+ const statsPath = path.join(CWD, '.monomind', 'graph', 'stats.json');
664
+ const graphPath = path.join(CWD, '.monomind', 'graph', 'graph.json');
665
+ try {
666
+ const s = readJSON(statsPath);
667
+ if (s && s.nodes !== undefined) return { nodes: s.nodes, edges: s.edges || 0, exists: true };
668
+ } catch { /* ignore */ }
669
+ try {
670
+ const stat = safeStat(graphPath);
671
+ if (stat && stat.size < 10 * 1024 * 1024) {
672
+ const g = JSON.parse(fs.readFileSync(graphPath, 'utf-8'));
673
+ const nodes = Array.isArray(g.nodes) ? g.nodes.length : 0;
674
+ const edges = (Array.isArray(g.edges) ? g.edges : (Array.isArray(g.links) ? g.links : [])).length;
675
+ return { nodes, edges, exists: true };
676
+ }
677
+ } catch { /* ignore */ }
678
+ return { nodes: 0, edges: 0, exists: false };
679
+ }
680
+
644
681
  // Memory Palace stats — drawers.jsonl + kg.json (the real persistent memory)
645
682
  function getMemoryPalaceStats() {
646
683
  const palaceDir = path.join(CWD, '.monomind', 'palace');
@@ -907,9 +944,10 @@ function generateStatusline() {
907
944
  const tokens = getTokenStats();
908
945
  const parts = [];
909
946
 
910
- // Brand + swarm dot
947
+ // Brand + project + swarm dot
911
948
  const swarmDot = swarm.coordinationActive ? `${x.green}●${x.reset}` : `${x.slate}○${x.reset}`;
912
- parts.push(`${x.bold}${x.purple}▊ Monomind${x.reset} ${swarmDot}`);
949
+ const projName = getProjectName();
950
+ parts.push(`${x.bold}${x.purple}▊ MonoMind${x.reset} ${x.teal}${projName}${x.reset} ${swarmDot}`);
913
951
 
914
952
  // Git branch + changes (compact)
915
953
  if (git.gitBranch) {
@@ -958,12 +996,18 @@ function generateStatusline() {
958
996
  parts.push(`${x.purple}🧠 ${autoMemCompact.count}m${x.reset}${typeSuffix}`);
959
997
  }
960
998
 
961
- // Knowledge chunks (Task 28) — show when populated
999
+ // Knowledge chunks — show when populated
962
1000
  if (knowledge.chunks > 0) {
963
1001
  parts.push(`${x.teal}📚 ${knowledge.chunks}k${x.reset}`);
964
1002
  }
965
1003
 
966
- // Triggers (Task 32) — show when populated
1004
+ // Graphify code graph
1005
+ const gfCompact = getGraphifyStats();
1006
+ if (gfCompact.exists) {
1007
+ parts.push(`${x.sky}🔗 ${gfCompact.nodes}n ${gfCompact.edges}e${x.reset}`);
1008
+ }
1009
+
1010
+ // Triggers — show when populated
967
1011
  if (triggers.triggers > 0) {
968
1012
  parts.push(`${x.mint}🎯 ${triggers.triggers}t${x.reset}`);
969
1013
  }
@@ -997,7 +1041,7 @@ function generateDashboard() {
997
1041
  const security = getSecurityStatus();
998
1042
  const swarm = getSwarmStatus();
999
1043
  const system = getSystemMetrics();
1000
- const adrs = getADRStatus();
1044
+ // adrs removed — internal dev metric
1001
1045
  const hooks = getHooksStatus();
1002
1046
  const agentdb = getAgentDBStats();
1003
1047
  const tests = getTestStats();
@@ -1015,7 +1059,8 @@ function generateDashboard() {
1015
1059
 
1016
1060
  // ── Header: brand + git + model + session ────────────────────
1017
1061
  const swarmDot = swarm.coordinationActive ? `${x.green}● LIVE${x.reset}` : `${x.slate}○ IDLE${x.reset}`;
1018
- let hdr = `${x.bold}${x.purple}▊ Monomind ${VERSION}${x.reset} ${swarmDot} ${x.teal}${x.bold}${git.name}${x.reset}`;
1062
+ const projName = getProjectName();
1063
+ let hdr = `${x.bold}${x.purple}▊ MonoMind${x.reset} ${x.dim}${VERSION}${x.reset} ${swarmDot} ${x.teal}${x.bold}${projName}${x.reset}`;
1019
1064
 
1020
1065
  if (git.gitBranch) {
1021
1066
  hdr += ` ${DIV} ${x.sky}⎇ ${x.bold}${git.gitBranch}${x.reset}`;
@@ -1036,30 +1081,29 @@ function generateDashboard() {
1036
1081
  lines.push(hdr);
1037
1082
  lines.push(SEP);
1038
1083
 
1039
- // ── Row 1: Intelligence & Learning ───────────────────────────
1040
- const intellCol = pctColor(system.intelligencePct);
1041
- const intellBar = blockBar(system.intelligencePct, 100, 6);
1042
-
1043
- // Knowledge (Task 28)
1084
+ // ── Row 1: Knowledge & Graphify ──────────────────────────────
1044
1085
  const knowStr = knowledge.chunks > 0
1045
1086
  ? `${x.teal}📚 ${x.bold}${knowledge.chunks}${x.reset}${x.slate} chunks${x.reset}`
1046
1087
  : `${x.slate}📚 no chunks${x.reset}`;
1047
1088
 
1048
- // Skills (Task 45)
1049
1089
  const skillStr = knowledge.skills > 0
1050
1090
  ? ` ${x.mint}✦ ${knowledge.skills} skills${x.reset}`
1051
1091
  : '';
1052
1092
 
1053
- // Patterns
1054
1093
  const patStr = progress.patternsLearned > 0
1055
1094
  ? `${x.gold}${progress.patternsLearned >= 1000 ? (progress.patternsLearned / 1000).toFixed(1) + 'k' : progress.patternsLearned} patterns${x.reset}`
1056
1095
  : `${x.slate}0 patterns${x.reset}`;
1057
1096
 
1097
+ const gf = getGraphifyStats();
1098
+ const gfStr = gf.exists
1099
+ ? `${x.sky}🔗 ${x.bold}${gf.nodes}${x.reset}${x.slate} nodes · ${x.reset}${x.sky}${x.bold}${gf.edges}${x.reset}${x.slate} edges${x.reset}`
1100
+ : `${x.slate}🔗 no graph${x.reset}`;
1101
+
1058
1102
  lines.push(
1059
1103
  `${x.purple}💡 INTEL${x.reset} ` +
1060
- `${intellCol}${intellBar} ${x.bold}${system.intelligencePct}%${x.reset} ${DIV} ` +
1061
1104
  `${knowStr}${skillStr} ${DIV} ` +
1062
- patStr
1105
+ `${patStr} ${DIV} ` +
1106
+ gfStr
1063
1107
  );
1064
1108
  lines.push(SEP);
1065
1109
 
@@ -1112,26 +1156,14 @@ function generateDashboard() {
1112
1156
  );
1113
1157
  lines.push(SEP);
1114
1158
 
1115
- // ── Row 3: Architecture & Security ───────────────────────────
1116
- const adrCol = adrs.count > 0
1117
- ? (adrs.implemented >= adrs.count ? x.green : x.gold)
1118
- : x.slate;
1119
- const adrStr = adrs.count > 0
1120
- ? `${adrCol}${x.bold}${adrs.implemented}${x.reset}${x.slate}/${x.reset}${x.white}${adrs.count}${x.reset} ADRs`
1121
- : `${x.slate}no ADRs${x.reset}`;
1122
-
1123
- const dddCol = pctColor(progress.dddProgress);
1124
- const dddBar = blockBar(progress.dddProgress, 100, 5);
1125
-
1159
+ // ── Row 3: Security ──────────────────────────────────────────
1126
1160
  const cveStatus = security.totalCves === 0
1127
1161
  ? (security.status === 'NONE' ? `${x.slate}not scanned${x.reset}` : `${x.green}✔ clean${x.reset}`)
1128
1162
  : `${x.coral}${security.cvesFixed}/${security.totalCves} fixed${x.reset}`;
1129
1163
 
1130
1164
  lines.push(
1131
- `${x.purple}🧩 ARCH${x.reset} ` +
1132
- `${adrStr} ${DIV} ` +
1133
- `DDD ${dddBar} ${dddCol}${x.bold}${progress.dddProgress}%${x.reset} ${DIV} ` +
1134
- `🛡️ ${sec.col}${sec.label}${x.reset} ${DIV} ` +
1165
+ `${x.purple}🛡️ SECURITY${x.reset} ` +
1166
+ `${sec.col}${sec.label}${x.reset} ${DIV} ` +
1135
1167
  `CVE ${cveStatus}`
1136
1168
  );
1137
1169
  lines.push(SEP);
@@ -1190,13 +1222,6 @@ function generateDashboard() {
1190
1222
  siStr = `${x.slate}📄 no shared instructions${x.reset}`;
1191
1223
  }
1192
1224
 
1193
- // Domains
1194
- const domCol = progress.domainsCompleted >= 4 ? x.green
1195
- : progress.domainsCompleted >= 2 ? x.gold
1196
- : progress.domainsCompleted >= 1 ? x.orange
1197
- : x.slate;
1198
- const domBar = blockBar(progress.domainsCompleted, progress.totalDomains);
1199
-
1200
1225
  let monthStr = '';
1201
1226
  if (tokens) {
1202
1227
  const mFmt = tokens.monthCost >= 100 ? `$${tokens.monthCost.toFixed(2)}` : tokens.monthCost >= 1 ? `$${tokens.monthCost.toFixed(3)}` : `$${tokens.monthCost.toFixed(4)}`;
@@ -1205,7 +1230,6 @@ function generateDashboard() {
1205
1230
  lines.push(
1206
1231
  `${x.slate}📋 CONTEXT${x.reset} ` +
1207
1232
  `${siStr} ${DIV} ` +
1208
- `${x.teal}🏗 ${domBar} ${domCol}${x.bold}${progress.domainsCompleted}${x.reset}${x.slate}/${x.reset}${x.white}${progress.totalDomains}${x.reset} domains ${DIV} ` +
1209
1233
  `${x.dim}💾 ${system.memoryMB} MB RAM${x.reset}` +
1210
1234
  monthStr
1211
1235
  );