@psiclawops/hypermem 0.8.5 → 0.9.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 (87) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/INSTALL.md +132 -9
  3. package/README.md +119 -272
  4. package/bench/README.md +42 -0
  5. package/bench/data-access-bench.mjs +380 -0
  6. package/bin/hypermem-bench.mjs +2 -0
  7. package/bin/hypermem-doctor.mjs +412 -0
  8. package/bin/hypermem-model-audit.mjs +339 -0
  9. package/bin/hypermem-status.mjs +491 -70
  10. package/dist/adaptive-lifecycle.d.ts +81 -0
  11. package/dist/adaptive-lifecycle.d.ts.map +1 -0
  12. package/dist/adaptive-lifecycle.js +190 -0
  13. package/dist/budget-policy.d.ts +1 -1
  14. package/dist/budget-policy.d.ts.map +1 -1
  15. package/dist/budget-policy.js +10 -5
  16. package/dist/cache.d.ts +1 -0
  17. package/dist/cache.d.ts.map +1 -1
  18. package/dist/cache.js +2 -0
  19. package/dist/composition-snapshot-integrity.d.ts +36 -0
  20. package/dist/composition-snapshot-integrity.d.ts.map +1 -0
  21. package/dist/composition-snapshot-integrity.js +131 -0
  22. package/dist/composition-snapshot-runtime.d.ts +59 -0
  23. package/dist/composition-snapshot-runtime.d.ts.map +1 -0
  24. package/dist/composition-snapshot-runtime.js +250 -0
  25. package/dist/composition-snapshot-store.d.ts +44 -0
  26. package/dist/composition-snapshot-store.d.ts.map +1 -0
  27. package/dist/composition-snapshot-store.js +117 -0
  28. package/dist/compositor.d.ts +125 -1
  29. package/dist/compositor.d.ts.map +1 -1
  30. package/dist/compositor.js +692 -44
  31. package/dist/doc-chunk-store.d.ts +19 -0
  32. package/dist/doc-chunk-store.d.ts.map +1 -1
  33. package/dist/doc-chunk-store.js +56 -6
  34. package/dist/hybrid-retrieval.d.ts +38 -0
  35. package/dist/hybrid-retrieval.d.ts.map +1 -1
  36. package/dist/hybrid-retrieval.js +86 -1
  37. package/dist/index.d.ts +12 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +28 -2
  40. package/dist/knowledge-store.d.ts +4 -1
  41. package/dist/knowledge-store.d.ts.map +1 -1
  42. package/dist/knowledge-store.js +27 -4
  43. package/dist/library-schema.d.ts +12 -8
  44. package/dist/library-schema.d.ts.map +1 -1
  45. package/dist/library-schema.js +22 -8
  46. package/dist/message-store.d.ts.map +1 -1
  47. package/dist/message-store.js +7 -3
  48. package/dist/metrics-dashboard.d.ts +18 -1
  49. package/dist/metrics-dashboard.d.ts.map +1 -1
  50. package/dist/metrics-dashboard.js +52 -14
  51. package/dist/reranker.d.ts +1 -1
  52. package/dist/reranker.js +2 -2
  53. package/dist/schema.d.ts +1 -1
  54. package/dist/schema.d.ts.map +1 -1
  55. package/dist/schema.js +28 -1
  56. package/dist/seed.d.ts.map +1 -1
  57. package/dist/seed.js +2 -0
  58. package/dist/topic-synthesizer.d.ts +20 -0
  59. package/dist/topic-synthesizer.d.ts.map +1 -1
  60. package/dist/topic-synthesizer.js +113 -3
  61. package/dist/trigger-registry.d.ts.map +1 -1
  62. package/dist/trigger-registry.js +10 -2
  63. package/dist/types.d.ts +271 -1
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/version.d.ts +7 -7
  66. package/dist/version.d.ts.map +1 -1
  67. package/dist/version.js +17 -7
  68. package/docs/DIAGNOSTICS.md +205 -0
  69. package/docs/INTEGRATION_VALIDATION.md +186 -0
  70. package/docs/MIGRATION.md +9 -6
  71. package/docs/MIGRATION_GUIDE.md +125 -101
  72. package/docs/ROADMAP.md +238 -20
  73. package/docs/TUNING.md +19 -5
  74. package/install.sh +152 -401
  75. package/memory-plugin/LICENSE +190 -0
  76. package/memory-plugin/README.md +20 -0
  77. package/memory-plugin/dist/index.js +50 -0
  78. package/memory-plugin/package.json +2 -2
  79. package/package.json +18 -4
  80. package/plugin/LICENSE +190 -0
  81. package/plugin/README.md +20 -0
  82. package/plugin/dist/index.d.ts +29 -0
  83. package/plugin/dist/index.d.ts.map +1 -1
  84. package/plugin/dist/index.js +288 -23
  85. package/plugin/dist/index.js.map +1 -1
  86. package/plugin/package.json +2 -2
  87. package/scripts/install-runtime.mjs +12 -1
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hypermem-doctor — installed-system validator for HyperMem + OpenClaw.
4
+ *
5
+ * This tool is intentionally read-only. It reports required failures,
6
+ * recommended settings, and exact config commands operators can review.
7
+ */
8
+
9
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { spawnSync } from 'node:child_process';
13
+
14
+ const args = process.argv.slice(2);
15
+
16
+ function usage() {
17
+ console.log(`
18
+ hypermem doctor — validate a HyperMem/OpenClaw installation
19
+
20
+ Usage:
21
+ hypermem-doctor [options]
22
+
23
+ Options:
24
+ --openclaw-config <path> OpenClaw config to inspect
25
+ default: ~/.openclaw/openclaw.json
26
+ --hypermem-config <path> HyperMem config to inspect
27
+ default: ~/.openclaw/hypermem/config.json
28
+ --data-dir <path> HyperMem data dir
29
+ default: config value or ~/.openclaw/hypermem
30
+ --json Output machine-readable JSON
31
+ --fix-plan Print exact read-only remediation commands
32
+ --strict Treat recommendation warnings as failures
33
+ --skip-runtime Do not call openclaw plugins list
34
+ -h, --help Show this help
35
+
36
+ Examples:
37
+ hypermem-doctor
38
+ hypermem-doctor --fix-plan
39
+ hypermem-doctor --json --strict
40
+ `);
41
+ }
42
+
43
+ if (args.includes('--help') || args.includes('-h')) {
44
+ usage();
45
+ process.exit(0);
46
+ }
47
+
48
+ function getArg(flag) {
49
+ const idx = args.indexOf(flag);
50
+ return idx !== -1 ? args[idx + 1] : undefined;
51
+ }
52
+
53
+ const home = os.homedir();
54
+ const flags = {
55
+ json: args.includes('--json'),
56
+ fixPlan: args.includes('--fix-plan'),
57
+ strict: args.includes('--strict'),
58
+ skipRuntime: args.includes('--skip-runtime'),
59
+ openclawConfig: path.resolve(getArg('--openclaw-config') || process.env.OPENCLAW_CONFIG || path.join(home, '.openclaw', 'openclaw.json')),
60
+ hypermemConfig: path.resolve(getArg('--hypermem-config') || path.join(home, '.openclaw', 'hypermem', 'config.json')),
61
+ dataDir: getArg('--data-dir') ? path.resolve(getArg('--data-dir')) : null,
62
+ };
63
+
64
+ function readJson(filePath) {
65
+ if (!existsSync(filePath)) return { exists: false, value: null, error: null };
66
+ try {
67
+ return { exists: true, value: JSON.parse(readFileSync(filePath, 'utf8')), error: null };
68
+ } catch (err) {
69
+ return { exists: true, value: null, error: err.message };
70
+ }
71
+ }
72
+
73
+ const openclawRead = readJson(flags.openclawConfig);
74
+ const hypermemRead = readJson(flags.hypermemConfig);
75
+ const openclaw = openclawRead.value ?? {};
76
+ const hypermem = hypermemRead.value ?? {};
77
+
78
+ const pluginConfig = openclaw?.plugins?.entries?.hypercompositor?.config
79
+ ?? openclaw?.plugins?.entries?.hypermem?.config
80
+ ?? {};
81
+ const dataDir = flags.dataDir
82
+ || process.env.HYPERMEM_DATA_DIR
83
+ || pluginConfig.dataDir
84
+ || hypermem.dataDir
85
+ || path.join(home, '.openclaw', 'hypermem');
86
+
87
+ const checks = [];
88
+ const recommendations = [];
89
+ const commands = [];
90
+
91
+ function add(kind, status, id, message, details = {}) {
92
+ const item = { kind, status, id, message, ...details };
93
+ checks.push(item);
94
+ if (details.command) commands.push(details.command);
95
+ }
96
+
97
+ function required(status, id, message, details = {}) {
98
+ add('required', status, id, message, details);
99
+ }
100
+
101
+ function recommended(status, id, message, details = {}) {
102
+ add('recommended', status, id, message, details);
103
+ if (status !== 'ok') recommendations.push({ id, message, ...details });
104
+ }
105
+
106
+ function get(obj, dotted) {
107
+ return dotted.split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), obj);
108
+ }
109
+
110
+ function isPlainObject(value) {
111
+ return value && typeof value === 'object' && !Array.isArray(value);
112
+ }
113
+
114
+ function arrayIncludesLike(values, needle) {
115
+ if (!Array.isArray(values)) return false;
116
+ return values.some(value => String(value).includes(needle));
117
+ }
118
+
119
+ function setCommand(pathKey, value, strictJson = false) {
120
+ const rendered = typeof value === 'string' && !strictJson
121
+ ? value
122
+ : JSON.stringify(value);
123
+ return `openclaw config set ${pathKey} ${shellQuote(rendered)}${strictJson ? ' --strict-json' : ''}`;
124
+ }
125
+
126
+ function shellQuote(value) {
127
+ if (/^[A-Za-z0-9_./:@=-]+$/.test(value)) return value;
128
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
129
+ }
130
+
131
+ function statExists(filePath) {
132
+ try {
133
+ return existsSync(filePath) ? statSync(filePath) : null;
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ function findMessageDb(dir) {
140
+ const agentsDir = path.join(dir, 'agents');
141
+ if (!existsSync(agentsDir)) return null;
142
+ try {
143
+ for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
144
+ if (!entry.isDirectory()) continue;
145
+ const candidate = path.join(agentsDir, entry.name, 'messages.db');
146
+ if (existsSync(candidate)) return candidate;
147
+ }
148
+ } catch {
149
+ return null;
150
+ }
151
+ return null;
152
+ }
153
+
154
+ function checkConfigReadable() {
155
+ if (!openclawRead.exists) {
156
+ required('fail', 'openclaw-config-present', `OpenClaw config not found: ${flags.openclawConfig}`);
157
+ } else if (openclawRead.error) {
158
+ required('fail', 'openclaw-config-json', `OpenClaw config is not valid JSON: ${openclawRead.error}`);
159
+ } else {
160
+ required('ok', 'openclaw-config-json', `OpenClaw config readable: ${flags.openclawConfig}`);
161
+ }
162
+
163
+ if (hypermemRead.exists && hypermemRead.error) {
164
+ required('fail', 'hypermem-config-json', `HyperMem config is not valid JSON: ${hypermemRead.error}`);
165
+ } else if (hypermemRead.exists) {
166
+ required('ok', 'hypermem-config-json', `HyperMem config readable: ${flags.hypermemConfig}`);
167
+ } else {
168
+ recommended('warn', 'hypermem-config-present', `No legacy HyperMem config found at ${flags.hypermemConfig}; ok if config lives in openclaw.json`);
169
+ }
170
+ }
171
+
172
+ function checkPluginWiring() {
173
+ const loadPaths = get(openclaw, 'plugins.load.paths') ?? get(openclaw, 'plugins.paths') ?? [];
174
+ const contextEngine = get(openclaw, 'plugins.slots.contextEngine');
175
+ const memorySlot = get(openclaw, 'plugins.slots.memory');
176
+ const allow = get(openclaw, 'plugins.allow');
177
+
178
+ required(contextEngine === 'hypercompositor' ? 'ok' : 'fail',
179
+ 'context-engine-slot',
180
+ contextEngine === 'hypercompositor'
181
+ ? 'Context engine slot points to hypercompositor'
182
+ : `Context engine slot is ${JSON.stringify(contextEngine)}; expected hypercompositor`,
183
+ { command: contextEngine === 'hypercompositor' ? undefined : setCommand('plugins.slots.contextEngine', 'hypercompositor') });
184
+
185
+ required(memorySlot === 'hypermem' ? 'ok' : 'fail',
186
+ 'memory-slot',
187
+ memorySlot === 'hypermem'
188
+ ? 'Memory slot points to hypermem'
189
+ : `Memory slot is ${JSON.stringify(memorySlot)}; expected hypermem`,
190
+ { command: memorySlot === 'hypermem' ? undefined : setCommand('plugins.slots.memory', 'hypermem') });
191
+
192
+ required(arrayIncludesLike(loadPaths, 'hypermem/plugin') || arrayIncludesLike(loadPaths, 'hypercompositor') ? 'ok' : 'fail',
193
+ 'hypercompositor-path',
194
+ 'Plugin load paths include the hypercompositor package path');
195
+ required(arrayIncludesLike(loadPaths, 'hypermem/memory-plugin') || arrayIncludesLike(loadPaths, 'hypermem-memory') ? 'ok' : 'fail',
196
+ 'hypermem-memory-path',
197
+ 'Plugin load paths include the HyperMem memory package path');
198
+
199
+ if (Array.isArray(allow) && allow.length > 0) {
200
+ required(allow.includes('hypercompositor') && allow.includes('hypermem') ? 'ok' : 'fail',
201
+ 'plugins-allow-merged',
202
+ allow.includes('hypercompositor') && allow.includes('hypermem')
203
+ ? 'Plugin allowlist includes hypercompositor and hypermem'
204
+ : 'Plugin allowlist exists but does not include both hypercompositor and hypermem; merge them without deleting existing entries');
205
+ } else {
206
+ required('ok', 'plugins-allow-merged', 'Plugin allowlist is unset/empty, so HyperMem is not blocked by allowlist');
207
+ }
208
+ }
209
+
210
+ function checkRuntimePlugins() {
211
+ if (flags.skipRuntime) {
212
+ recommended('ok', 'runtime-plugin-list', 'Runtime plugin load check skipped');
213
+ return;
214
+ }
215
+ const result = spawnSync('openclaw', ['plugins', 'list'], { encoding: 'utf8', timeout: 8000 });
216
+ if (result.error) {
217
+ recommended('warn', 'runtime-plugin-list', `Could not run openclaw plugins list: ${result.error.message}`);
218
+ return;
219
+ }
220
+ const output = `${result.stdout}\n${result.stderr}`;
221
+ const hasComposer = /hypercompositor/.test(output);
222
+ const hasMemory = /\bhypermem\b/.test(output);
223
+ recommended(hasComposer && hasMemory ? 'ok' : 'warn',
224
+ 'runtime-plugin-list',
225
+ hasComposer && hasMemory
226
+ ? 'Runtime plugin list mentions hypercompositor and hypermem'
227
+ : 'Runtime plugin list did not clearly show both hypercompositor and hypermem; restart gateway after config changes');
228
+ }
229
+
230
+ function checkOpenClawRecommendations() {
231
+ const expected = [
232
+ ['agents.defaults.contextPruning.mode', 'off', 'required', false],
233
+ ['agents.defaults.promptOverlays.gpt5.personality', 'off', 'recommended', false],
234
+ ['agents.defaults.startupContext.dailyMemoryDays', 4, 'recommended', true],
235
+ ['agents.defaults.startupContext.maxFileChars', 4000, 'recommended', true],
236
+ ['agents.defaults.startupContext.maxTotalChars', 12000, 'recommended', true],
237
+ ['agents.defaults.startupContext.maxFileBytes', 32768, 'recommended', true],
238
+ ['agents.defaults.bootstrapMaxChars', 20000, 'recommended', true],
239
+ ['agents.defaults.compaction.mode', 'safeguard', 'recommended', false],
240
+ ['agents.defaults.compaction.reserveTokens', 16384, 'recommended', true],
241
+ ['agents.defaults.compaction.keepRecentTokens', 6000, 'recommended', true],
242
+ ['agents.defaults.compaction.reserveTokensFloor', 15000, 'recommended', true],
243
+ ['agents.defaults.compaction.maxHistoryShare', 0.65, 'recommended', true],
244
+ ];
245
+
246
+ for (const [pathKey, value, level, strictJson] of expected) {
247
+ const actual = get(openclaw, pathKey);
248
+ const ok = actual === value;
249
+ const message = ok
250
+ ? `${pathKey} is recommended value ${JSON.stringify(value)}`
251
+ : `${pathKey} is ${JSON.stringify(actual)}; recommended ${JSON.stringify(value)}`;
252
+ const details = ok ? {} : { command: setCommand(pathKey, value, strictJson) };
253
+ if (level === 'required') required(ok ? 'ok' : 'fail', pathKey, message, details);
254
+ else recommended(ok ? 'ok' : 'warn', pathKey, message, details);
255
+ }
256
+
257
+ const injection = get(openclaw, 'agents.defaults.contextInjection');
258
+ if (injection == null || injection === 'always' || injection === 'continuation-skip') {
259
+ recommended('ok', 'agents.defaults.contextInjection', `Context injection mode is ${JSON.stringify(injection ?? 'default')}`);
260
+ } else {
261
+ recommended('warn', 'agents.defaults.contextInjection', `Unknown context injection mode ${JSON.stringify(injection)}; verify bootstrap file injection still matches OpenClaw defaults`);
262
+ }
263
+ }
264
+
265
+ function checkDataDir() {
266
+ const dirStat = statExists(dataDir);
267
+ required(dirStat?.isDirectory() ? 'ok' : 'fail', 'data-dir', dirStat?.isDirectory() ? `HyperMem data dir exists: ${dataDir}` : `HyperMem data dir missing: ${dataDir}`);
268
+
269
+ for (const [id, file] of [
270
+ ['library-db', path.join(dataDir, 'library.db')],
271
+ ['vectors-db', path.join(dataDir, 'vectors.db')],
272
+ ]) {
273
+ const st = statExists(file);
274
+ required(st?.isFile() ? 'ok' : 'fail', id, st?.isFile() ? `${path.basename(file)} exists` : `${path.basename(file)} missing in ${dataDir}`);
275
+ }
276
+
277
+ const messageDb = findMessageDb(dataDir);
278
+ recommended(messageDb ? 'ok' : 'warn', 'messages-db', messageDb ? `Found agent messages DB: ${messageDb}` : 'No agent messages.db found yet; ok before first real agent turn');
279
+ }
280
+
281
+ const MODEL_CONTEXT_WINDOWS = [
282
+ ['claude-opus-4', 200000], ['claude-sonnet-4', 200000], ['claude', 200000],
283
+ ['gpt-5', 128000], ['gpt-4o', 128000], ['gpt-4', 128000], ['o3', 128000], ['o4', 128000],
284
+ ['gemini-3.1', 1000000], ['gemini-2.5', 1000000], ['gemini', 1000000],
285
+ ['glm-5', 131072], ['glm-4', 131072], ['qwen3', 262144], ['qwen', 131072], ['deepseek', 131072],
286
+ ];
287
+ const HIGH_RISK_PROVIDERS = ['openai/', 'openai-codex/', 'openrouter/', 'lmstudio/', 'vllm/', 'ollama/', 'litellm/', 'copilot-local/'];
288
+
289
+ function normalizeModel(value) {
290
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
291
+ }
292
+
293
+ function addModel(out, value) {
294
+ if (typeof value === 'string') {
295
+ const normalized = normalizeModel(value);
296
+ if (normalized.includes('/')) out.add(normalized);
297
+ return;
298
+ }
299
+ if (Array.isArray(value)) {
300
+ for (const item of value) addModel(out, item);
301
+ return;
302
+ }
303
+ if (!isPlainObject(value)) return;
304
+ addModel(out, value.primary);
305
+ addModel(out, value.model);
306
+ addModel(out, value.id);
307
+ addModel(out, value.name);
308
+ if (Array.isArray(value.fallbacks)) for (const fb of value.fallbacks) addModel(out, fb);
309
+ }
310
+
311
+ function collectModels() {
312
+ const out = new Set();
313
+ addModel(out, get(openclaw, 'agents.defaults.model'));
314
+ addModel(out, get(openclaw, 'agents.defaults.fallbacks'));
315
+ addModel(out, get(openclaw, 'agents.defaults.heartbeat.model'));
316
+ addModel(out, get(openclaw, 'agents.defaults.subagents.model'));
317
+ if (Array.isArray(openclaw?.agents?.list)) {
318
+ for (const agent of openclaw.agents.list) {
319
+ addModel(out, agent?.model);
320
+ addModel(out, agent?.fallbacks);
321
+ }
322
+ }
323
+ return [...out].sort();
324
+ }
325
+
326
+ function contextOverrides() {
327
+ return pluginConfig?.compositor?.contextWindowOverrides
328
+ ?? pluginConfig?.contextWindowOverrides
329
+ ?? hypermem?.compositor?.contextWindowOverrides
330
+ ?? hypermem?.contextWindowOverrides
331
+ ?? {};
332
+ }
333
+
334
+ function checkModels() {
335
+ const models = collectModels();
336
+ const overrides = contextOverrides();
337
+ if (models.length === 0) {
338
+ recommended('warn', 'model-audit', 'No configured provider/model ids found to audit');
339
+ return;
340
+ }
341
+
342
+ for (const model of models) {
343
+ const detected = MODEL_CONTEXT_WINDOWS.find(([pattern]) => model.includes(pattern));
344
+ const override = overrides[model];
345
+ const hasCompleteOverride = Number.isInteger(override?.contextTokens) && Number.isInteger(override?.contextWindow);
346
+ const highRisk = HIGH_RISK_PROVIDERS.some(prefix => model.startsWith(prefix));
347
+
348
+ if (!detected && !hasCompleteOverride) {
349
+ recommended('warn', `model-window:${model}`, `${model} has no known context-window pattern and no complete contextWindowOverrides entry`, {
350
+ command: `# Add plugins.entries.hypercompositor.config.contextWindowOverrides[${JSON.stringify(model)}] with contextTokens and contextWindow after provider validation`,
351
+ });
352
+ } else if (highRisk && !hasCompleteOverride) {
353
+ recommended('warn', `model-window:${model}`, `${model} is on an OpenAI-compatible or local gateway path; add explicit contextWindowOverrides unless logs prove runtime tokenBudget is correct`, {
354
+ command: `# Verify logs show: budget source: runtime tokenBudget=... model=${model}`,
355
+ });
356
+ } else if (override && !hasCompleteOverride) {
357
+ recommended('warn', `model-window:${model}`, `${model} has an incomplete contextWindowOverrides entry; set both contextTokens and contextWindow`);
358
+ } else {
359
+ recommended('ok', `model-window:${model}`, `${model} has ${hasCompleteOverride ? 'explicit context-window override' : `known context-window pattern (${detected?.[1]} tokens)`}`);
360
+ }
361
+ }
362
+ }
363
+
364
+ checkConfigReadable();
365
+ if (openclawRead.value) {
366
+ checkPluginWiring();
367
+ checkOpenClawRecommendations();
368
+ checkModels();
369
+ }
370
+ checkDataDir();
371
+ checkRuntimePlugins();
372
+
373
+ const failedRequired = checks.filter(c => c.kind === 'required' && c.status === 'fail');
374
+ const warnings = checks.filter(c => c.status === 'warn');
375
+ const summary = {
376
+ status: failedRequired.length > 0 ? 'fail' : (warnings.length > 0 ? 'warn' : 'ok'),
377
+ strictStatus: failedRequired.length > 0 || (flags.strict && warnings.length > 0) ? 'fail' : 'ok',
378
+ openclawConfig: flags.openclawConfig,
379
+ hypermemConfig: flags.hypermemConfig,
380
+ dataDir,
381
+ counts: {
382
+ ok: checks.filter(c => c.status === 'ok').length,
383
+ warn: warnings.length,
384
+ fail: failedRequired.length,
385
+ },
386
+ checks,
387
+ fixPlan: [...new Set(commands.filter(Boolean))],
388
+ };
389
+
390
+ if (flags.json) {
391
+ console.log(JSON.stringify(summary, null, 2));
392
+ } else {
393
+ const icon = summary.status === 'ok' ? '✅' : summary.status === 'warn' ? '⚠️' : '❌';
394
+ console.log(`${icon} hypermem doctor: ${summary.status.toUpperCase()} (${summary.counts.ok} ok, ${summary.counts.warn} warn, ${summary.counts.fail} fail)`);
395
+ console.log(`OpenClaw config: ${summary.openclawConfig}`);
396
+ console.log(`HyperMem config: ${summary.hypermemConfig}`);
397
+ console.log(`Data dir: ${summary.dataDir}`);
398
+ console.log('');
399
+
400
+ for (const check of checks) {
401
+ const mark = check.status === 'ok' ? '✅' : check.status === 'warn' ? '⚠️' : '❌';
402
+ console.log(`${mark} [${check.kind}] ${check.id}: ${check.message}`);
403
+ }
404
+
405
+ if (flags.fixPlan && summary.fixPlan.length > 0) {
406
+ console.log('\nFix plan:');
407
+ for (const command of summary.fixPlan) console.log(` ${command}`);
408
+ console.log(' openclaw gateway restart');
409
+ }
410
+ }
411
+
412
+ process.exit(summary.strictStatus === 'fail' ? 1 : 0);