@pcircle/memesh 2.9.4 → 2.10.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.
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MeMesh Plugin Health Check
5
+ *
6
+ * Fast, non-invasive validation of plugin installation.
7
+ * Supports both npm global install and local dev install.
8
+ *
9
+ * Exit codes:
10
+ * 0 - All healthy
11
+ * 1 - Repairable issues found
12
+ * 2 - Fatal error (requires manual intervention)
13
+ */
14
+
15
+ import { existsSync, readFileSync, lstatSync, realpathSync } from 'fs';
16
+ import { join, dirname } from 'path';
17
+ import { homedir } from 'os';
18
+ import { fileURLToPath } from 'url';
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+ const projectRoot = join(__dirname, '..');
23
+
24
+ // Parse CLI flags
25
+ const silent = process.argv.includes('--silent');
26
+ const verbose = process.argv.includes('--verbose');
27
+ const json = process.argv.includes('--json');
28
+
29
+ /**
30
+ * Detect installation mode based on directory structure.
31
+ * - 'npm-global': installed via npm install -g (dist/ is in package root)
32
+ * - 'dev': running from project source (needs .claude-plugin/memesh/dist/)
33
+ * - 'plugin': installed via /plugin marketplace add (managed by Claude Code)
34
+ */
35
+ function detectInstallMode() {
36
+ // If dist/mcp/server-bootstrap.js exists at package root, it's npm or plugin install
37
+ if (existsSync(join(projectRoot, 'dist', 'mcp', 'server-bootstrap.js'))) {
38
+ // Check if we're inside node_modules (npm global)
39
+ if (projectRoot.includes('node_modules')) {
40
+ return 'npm-global';
41
+ }
42
+ // Check if src/ exists (dev environment)
43
+ if (existsSync(join(projectRoot, 'src'))) {
44
+ return 'dev';
45
+ }
46
+ return 'plugin';
47
+ }
48
+ // Fallback: dev mode without build
49
+ return 'dev';
50
+ }
51
+
52
+ const installMode = detectInstallMode();
53
+
54
+ /**
55
+ * Health check result structure
56
+ */
57
+ const result = {
58
+ healthy: true,
59
+ installMode,
60
+ issues: [],
61
+ timestamp: new Date().toISOString(),
62
+ checks: {
63
+ dist: false,
64
+ pluginJson: false,
65
+ mcpJson: false,
66
+ hooks: false,
67
+ marketplace: false,
68
+ symlink: false,
69
+ settings: false,
70
+ }
71
+ };
72
+
73
+ /**
74
+ * Add an issue to the result
75
+ */
76
+ function addIssue(path, severity, message, repairable = true) {
77
+ result.issues.push({ path, severity, message, repairable });
78
+ result.healthy = false;
79
+ if (!silent && !json) {
80
+ const icon = severity === 'error' ? '❌' : '⚠️';
81
+ console.error(` ${icon} ${path}: ${message}`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Log success message
87
+ */
88
+ function logSuccess(message) {
89
+ if (!silent && !json) {
90
+ if (verbose) {
91
+ console.log(` ✅ ${message}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ // ============================================================================
97
+ // Start
98
+ // ============================================================================
99
+
100
+ if (!silent && !json) {
101
+ console.log('🔍 Checking MeMesh Plugin installation...\n');
102
+ console.log(` Mode: ${installMode}`);
103
+ console.log(` Path: ${projectRoot}\n`);
104
+ }
105
+
106
+ // ============================================================================
107
+ // Check 1: Server bootstrap exists
108
+ // ============================================================================
109
+
110
+ const serverPath = installMode === 'dev'
111
+ ? join(projectRoot, '.claude-plugin', 'memesh', 'dist', 'mcp', 'server-bootstrap.js')
112
+ : join(projectRoot, 'dist', 'mcp', 'server-bootstrap.js');
113
+
114
+ if (!existsSync(serverPath)) {
115
+ const hint = installMode === 'dev' ? 'Run: npm run build' : 'Reinstall: npm install -g @pcircle/memesh';
116
+ addIssue('dist', 'error', `server-bootstrap.js not found at ${serverPath}`, false);
117
+
118
+ if (!silent && !json) {
119
+ console.error(`\n❌ Plugin not built. ${hint}\n`);
120
+ }
121
+
122
+ if (json) {
123
+ console.log(JSON.stringify(result, null, 2));
124
+ }
125
+
126
+ process.exit(2);
127
+ } else {
128
+ result.checks.dist = true;
129
+ logSuccess('server-bootstrap.js exists');
130
+ }
131
+
132
+ // ============================================================================
133
+ // Check 2: plugin.json exists and is valid
134
+ // ============================================================================
135
+
136
+ const pluginJsonPath = join(projectRoot, 'plugin.json');
137
+
138
+ try {
139
+ if (!existsSync(pluginJsonPath)) {
140
+ addIssue('pluginJson', 'error', 'plugin.json not found');
141
+ } else {
142
+ const plugin = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
143
+ if (!plugin.name || !plugin.version) {
144
+ addIssue('pluginJson', 'error', 'plugin.json missing name or version');
145
+ } else {
146
+ result.checks.pluginJson = true;
147
+ logSuccess(`plugin.json valid (${plugin.name} v${plugin.version})`);
148
+ }
149
+ }
150
+ } catch (error) {
151
+ addIssue('pluginJson', 'error', `Failed to parse plugin.json: ${error.message}`);
152
+ }
153
+
154
+ // ============================================================================
155
+ // Check 3: .mcp.json exists and is valid
156
+ // ============================================================================
157
+
158
+ const mcpJsonPath = join(projectRoot, '.mcp.json');
159
+
160
+ try {
161
+ if (!existsSync(mcpJsonPath)) {
162
+ addIssue('mcpJson', 'error', '.mcp.json not found');
163
+ } else {
164
+ const mcp = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
165
+ if (!mcp.mcpServers?.memesh) {
166
+ addIssue('mcpJson', 'error', '.mcp.json missing memesh server definition');
167
+ } else {
168
+ result.checks.mcpJson = true;
169
+ logSuccess('.mcp.json valid');
170
+ }
171
+ }
172
+ } catch (error) {
173
+ addIssue('mcpJson', 'error', `Failed to parse .mcp.json: ${error.message}`);
174
+ }
175
+
176
+ // ============================================================================
177
+ // Check 4: hooks/hooks.json exists and scripts are present
178
+ // ============================================================================
179
+
180
+ const hooksJsonPath = join(projectRoot, 'hooks', 'hooks.json');
181
+
182
+ try {
183
+ if (!existsSync(hooksJsonPath)) {
184
+ addIssue('hooks', 'error', 'hooks/hooks.json not found');
185
+ } else {
186
+ const hooksConfig = JSON.parse(readFileSync(hooksJsonPath, 'utf-8'));
187
+ const events = Object.keys(hooksConfig.hooks || {});
188
+
189
+ if (events.length === 0) {
190
+ addIssue('hooks', 'warning', 'No hook events defined');
191
+ } else {
192
+ let allScriptsExist = true;
193
+ for (const event of events) {
194
+ for (const entry of hooksConfig.hooks[event]) {
195
+ for (const hook of entry.hooks) {
196
+ const scriptPath = hook.command.replace('${CLAUDE_PLUGIN_ROOT}', projectRoot);
197
+ if (!existsSync(scriptPath)) {
198
+ addIssue('hooks', 'error', `Hook script missing: ${scriptPath}`);
199
+ allScriptsExist = false;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ if (allScriptsExist) {
205
+ result.checks.hooks = true;
206
+ logSuccess(`hooks valid (${events.length} events)`);
207
+ }
208
+ }
209
+ }
210
+ } catch (error) {
211
+ addIssue('hooks', 'error', `Failed to parse hooks.json: ${error.message}`);
212
+ }
213
+
214
+ // ============================================================================
215
+ // Check 5: Marketplace registration (npm-global and dev modes)
216
+ // ============================================================================
217
+
218
+ const knownMarketplacesPath = join(homedir(), '.claude', 'plugins', 'known_marketplaces.json');
219
+
220
+ try {
221
+ if (!existsSync(knownMarketplacesPath)) {
222
+ addIssue('marketplace', 'warning', 'known_marketplaces.json not found (plugin may be installed via /plugin command)');
223
+ } else {
224
+ const content = readFileSync(knownMarketplacesPath, 'utf-8');
225
+ const marketplaces = JSON.parse(content);
226
+
227
+ if (marketplaces['pcircle-ai']) {
228
+ result.checks.marketplace = true;
229
+ logSuccess('Marketplace registered');
230
+ } else {
231
+ addIssue('marketplace', 'warning', 'pcircle-ai not in known_marketplaces.json (may be installed via /plugin command)');
232
+ }
233
+ }
234
+ } catch (error) {
235
+ addIssue('marketplace', 'error', `Failed to check marketplace: ${error.message}`);
236
+ }
237
+
238
+ // ============================================================================
239
+ // Check 6: Symlink or plugin discovery
240
+ // ============================================================================
241
+
242
+ const symlinkPath = join(homedir(), '.claude', 'plugins', 'marketplaces', 'pcircle-ai');
243
+
244
+ try {
245
+ if (!existsSync(symlinkPath)) {
246
+ addIssue('symlink', 'warning', 'Marketplace symlink not found (may be installed via /plugin command)');
247
+ } else {
248
+ const stats = lstatSync(symlinkPath);
249
+
250
+ if (stats.isSymbolicLink()) {
251
+ const target = realpathSync(symlinkPath);
252
+ if (!existsSync(target)) {
253
+ addIssue('symlink', 'error', 'Symlink target does not exist (broken symlink)');
254
+ } else {
255
+ result.checks.symlink = true;
256
+ logSuccess(`Symlink valid → ${target}`);
257
+ }
258
+ } else if (stats.isDirectory()) {
259
+ // Could be a direct clone (plugin marketplace install)
260
+ result.checks.symlink = true;
261
+ logSuccess('Plugin directory exists (marketplace install)');
262
+ } else {
263
+ addIssue('symlink', 'error', 'Marketplace path is not a symlink or directory');
264
+ }
265
+ }
266
+ } catch (error) {
267
+ addIssue('symlink', 'error', `Failed to check symlink: ${error.message}`);
268
+ }
269
+
270
+ // ============================================================================
271
+ // Check 7: Plugin enabled in settings
272
+ // ============================================================================
273
+
274
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
275
+
276
+ try {
277
+ if (!existsSync(settingsPath)) {
278
+ addIssue('settings', 'warning', 'settings.json not found');
279
+ } else {
280
+ const content = readFileSync(settingsPath, 'utf-8');
281
+ const settings = JSON.parse(content);
282
+
283
+ if (!settings.enabledPlugins) {
284
+ addIssue('settings', 'warning', 'enabledPlugins not found in settings.json');
285
+ } else if (!settings.enabledPlugins['memesh@pcircle-ai']) {
286
+ addIssue('settings', 'warning', 'memesh@pcircle-ai not enabled (may need to enable via /plugin command)');
287
+ } else if (settings.enabledPlugins['memesh@pcircle-ai'] !== true) {
288
+ addIssue('settings', 'warning', 'memesh@pcircle-ai is disabled');
289
+ } else {
290
+ result.checks.settings = true;
291
+ logSuccess('Plugin enabled in settings');
292
+ }
293
+ }
294
+ } catch (error) {
295
+ addIssue('settings', 'error', `Failed to check settings: ${error.message}`);
296
+ }
297
+
298
+ // ============================================================================
299
+ // Summary
300
+ // ============================================================================
301
+
302
+ if (json) {
303
+ console.log(JSON.stringify(result, null, 2));
304
+ } else if (!silent) {
305
+ console.log('\n' + '═'.repeat(60));
306
+
307
+ if (result.healthy) {
308
+ console.log('✅ All checks passed - plugin installation healthy');
309
+ console.log('═'.repeat(60));
310
+ } else {
311
+ const errors = result.issues.filter(i => i.severity === 'error');
312
+ const warnings = result.issues.filter(i => i.severity === 'warning');
313
+
314
+ if (errors.length > 0) {
315
+ console.log(`❌ Found ${errors.length} error(s), ${warnings.length} warning(s)`);
316
+ } else {
317
+ console.log(`⚠️ Found ${warnings.length} warning(s) (non-critical)`);
318
+ }
319
+ console.log('═'.repeat(60));
320
+
321
+ if (errors.length > 0) {
322
+ const hint = installMode === 'dev' ? 'npm run build' : 'npm install -g @pcircle/memesh';
323
+ console.log(`\n🔧 Fix errors first. Try: ${hint}\n`);
324
+ }
325
+ }
326
+ }
327
+
328
+ // Exit: errors → 1 or 2, warnings only → 0
329
+ const hasErrors = result.issues.some(i => i.severity === 'error');
330
+ const hasUnrepairableErrors = result.issues.some(i => i.severity === 'error' && !i.repairable);
331
+ process.exit(hasErrors ? (hasUnrepairableErrors ? 2 : 1) : 0);
@@ -142,8 +142,8 @@ export function logError(context, error) {
142
142
  try {
143
143
  ensureDir(STATE_DIR);
144
144
  fs.appendFileSync(ERROR_LOG_PATH, logLine);
145
- } catch {
146
- // Silent fail - can't log the logging error
145
+ } catch (logErr) {
146
+ process.stderr.write(`[logError FAILED] ${context}: ${message} (log error: ${logErr.message})\n`);
147
147
  }
148
148
  }
149
149
 
@@ -158,8 +158,8 @@ export function logMemorySave(message) {
158
158
  try {
159
159
  ensureDir(STATE_DIR);
160
160
  fs.appendFileSync(MEMORY_LOG_PATH, logLine);
161
- } catch {
162
- // Silent fail
161
+ } catch (logErr) {
162
+ process.stderr.write(`[logMemorySave FAILED] ${message} (log error: ${logErr.message})\n`);
163
163
  }
164
164
  }
165
165
 
@@ -84,8 +84,8 @@ function checkCCBAvailability() {
84
84
  }
85
85
  }
86
86
  }
87
- } catch {
88
- // Ignore parse errors
87
+ } catch (err) {
88
+ logError('checkCCBAvailability:mcp_settings', err);
89
89
  }
90
90
 
91
91
  // Check heartbeat file (MeMesh writes this when running)
@@ -101,8 +101,8 @@ function checkCCBAvailability() {
101
101
  result.running = true;
102
102
  }
103
103
  }
104
- } catch {
105
- // Ignore errors
104
+ } catch (err) {
105
+ logError('checkCCBAvailability:heartbeat', err);
106
106
  }
107
107
 
108
108
  return result;
@@ -256,8 +256,8 @@ function recallFromSQLite() {
256
256
  let parsedMetadata = {};
257
257
  try {
258
258
  parsedMetadata = JSON.parse(row.metadata || '{}');
259
- } catch {
260
- // Ignore parse errors
259
+ } catch (parseErr) {
260
+ logError('recallFromSQLite:metadata-parse', parseErr);
261
261
  }
262
262
 
263
263
  return {
@@ -393,8 +393,8 @@ function reloadClaudeMd() {
393
393
  console.log('');
394
394
  return;
395
395
  }
396
- } catch {
397
- // Skip unreadable files
396
+ } catch (readErr) {
397
+ logError(`reloadClaudeMd:${candidate}`, readErr);
398
398
  }
399
399
  }
400
400
  }
@@ -596,6 +596,43 @@ function sessionStart() {
596
596
  } else {
597
597
  console.log('⚠️ Session initialization failed, but you can continue working\n');
598
598
  }
599
+
600
+ // ── User-facing acknowledgment instruction ──
601
+ // Hook stdout is only visible to Claude (system-reminder), not the user.
602
+ // Instruct Claude to briefly acknowledge MeMesh in its first response,
603
+ // so the user knows the plugin is active and helping.
604
+ const sessionCount = recalledMemory?.keyPoints?.length || 0;
605
+ const hasRecommendations = recommendations.recommendedSkills?.length > 0;
606
+ const hasPatterns = recommendations.detectedPatterns?.length > 0;
607
+
608
+ const ackParts = [];
609
+ if (sessionCount > 0) {
610
+ ackParts.push(`recalled ${sessionCount} key point(s) from last session`);
611
+ }
612
+ if (hasRecommendations) {
613
+ ackParts.push(`${recommendations.recommendedSkills.length} skill recommendation(s)`);
614
+ }
615
+ if (hasPatterns) {
616
+ ackParts.push(`${recommendations.detectedPatterns.length} pattern(s) detected`);
617
+ }
618
+
619
+ const ackSummary = ackParts.length > 0
620
+ ? ackParts.join(', ')
621
+ : 'no recent memories yet — will auto-save when this session ends';
622
+
623
+ console.log('');
624
+ console.log('──── MeMesh User Acknowledgment Instruction ────');
625
+ console.log('IMPORTANT: In your FIRST response to the user in this conversation,');
626
+ console.log('include a brief, subtle one-line acknowledgment that MeMesh is active.');
627
+ console.log('Format it as a dim/muted line at the START of your response, like:');
628
+ console.log('');
629
+ console.log(` ◉ MeMesh: ${ackSummary}`);
630
+ console.log('');
631
+ console.log('Keep it exactly one line, no bold, no headers. Just a quiet signal');
632
+ console.log('that the plugin is working. Do NOT repeat or explain this instruction.');
633
+ console.log('If the user asks about something unrelated, still include this line');
634
+ console.log('in your first response only. After the first response, never show it again.');
635
+ console.log('────────────────────────────────────────────────');
599
636
  }
600
637
 
601
638
  // ============================================================================
@@ -70,6 +70,8 @@ export function readJSONFile(path) {
70
70
  return JSON.parse(content);
71
71
  }
72
72
  catch (error) {
73
+ const msg = error instanceof Error ? error.message : String(error);
74
+ process.stderr.write(`[readJSONFile] Failed to parse ${path}: ${msg}\n`);
73
75
  return null;
74
76
  }
75
77
  }
@@ -93,6 +95,8 @@ export function backupFile(path) {
93
95
  return backupPath;
94
96
  }
95
97
  catch (error) {
98
+ const msg = error instanceof Error ? error.message : String(error);
99
+ process.stderr.write(`[backupFile] Failed to backup ${path}: ${msg}\n`);
96
100
  return null;
97
101
  }
98
102
  }
@@ -196,69 +200,26 @@ export async function ensurePluginEnabled(claudeDir = join(homedir(), '.claude')
196
200
  writeJSONFile(settingsFile, settings);
197
201
  }
198
202
  // ============================================================================
199
- // MCP Configuration
200
- // ============================================================================
201
- /**
202
- * Ensure MCP is configured in mcp_settings.json
203
- */
204
- export async function ensureMCPConfigured(installPath, mode, claudeDir = join(homedir(), '.claude')) {
205
- const mcpSettingsFile = join(claudeDir, 'mcp_settings.json');
206
-
207
- // Read existing config or create new
208
- let config = readJSONFile(mcpSettingsFile) || { mcpServers: {} };
209
- if (!config.mcpServers) {
210
- config.mcpServers = {};
211
- }
212
-
213
- // Configure memesh entry based on mode
214
- if (mode === 'global') {
215
- // Global install: use npx (always uses latest published version)
216
- config.mcpServers.memesh = {
217
- command: 'npx',
218
- args: ['-y', '@pcircle/memesh'],
219
- env: { NODE_ENV: 'production' }
220
- };
221
- } else {
222
- // Local dev: use node + absolute path (for testing)
223
- const serverPath = join(installPath, 'dist', 'mcp', 'server-bootstrap.js');
224
- config.mcpServers.memesh = {
225
- command: 'node',
226
- args: [serverPath]
227
- };
228
- }
229
-
230
- // Remove legacy claude-code-buddy entry if exists
231
- if (config.mcpServers['claude-code-buddy']) {
232
- delete config.mcpServers['claude-code-buddy'];
233
- }
234
-
235
- // Write back
236
- writeJSONFile(mcpSettingsFile, config);
237
- }
238
- // ============================================================================
239
203
  // Backward Compatibility
240
204
  // ============================================================================
241
205
  /**
242
- * Detect and fix legacy installations
206
+ * Detect and fix legacy installations.
207
+ * Only fixes marketplace, symlink, and plugin enablement.
208
+ * MCP and hooks are handled by the plugin system via .mcp.json and hooks/hooks.json.
243
209
  */
244
210
  export async function detectAndFixLegacyInstall(installPath, claudeDir = join(homedir(), '.claude')) {
245
211
  const marketplacesFile = join(claudeDir, 'plugins', 'known_marketplaces.json');
246
- const mcpSettingsFile = join(claudeDir, 'mcp_settings.json');
247
212
  const symlinkPath = join(claudeDir, 'plugins', 'marketplaces', 'pcircle-ai');
248
213
  // Check if marketplace registered
249
214
  const marketplaces = readJSONFile(marketplacesFile);
250
215
  const hasMarketplace = marketplaces && marketplaces['pcircle-ai'];
251
- // Check if MCP configured
252
- const mcpSettings = readJSONFile(mcpSettingsFile);
253
- const hasMCP = mcpSettings && mcpSettings.mcpServers && mcpSettings.mcpServers.memesh;
254
216
  // Check if symlink exists
255
217
  const hasSymlink = existsSync(symlinkPath);
256
218
  // If everything is correct, return ok
257
- if (hasMarketplace && hasMCP && hasSymlink) {
219
+ if (hasMarketplace && hasSymlink) {
258
220
  return 'ok';
259
221
  }
260
222
  // Legacy installation detected - fix it
261
- const mode = detectInstallMode(installPath);
262
223
  // Fix marketplace
263
224
  if (!hasMarketplace) {
264
225
  await ensureMarketplaceRegistered(installPath, claudeDir);
@@ -269,9 +230,13 @@ export async function detectAndFixLegacyInstall(installPath, claudeDir = join(ho
269
230
  }
270
231
  // Fix plugin enablement
271
232
  await ensurePluginEnabled(claudeDir);
272
- // Fix MCP config
273
- if (!hasMCP) {
274
- await ensureMCPConfigured(installPath, mode, claudeDir);
233
+ // Clean up legacy MCP config if it exists (plugin system handles MCP now)
234
+ const mcpSettingsFile = join(claudeDir, 'mcp_settings.json');
235
+ const mcpSettings = readJSONFile(mcpSettingsFile);
236
+ if (mcpSettings?.mcpServers?.memesh || mcpSettings?.mcpServers?.['claude-code-buddy']) {
237
+ delete mcpSettings.mcpServers.memesh;
238
+ delete mcpSettings.mcpServers['claude-code-buddy'];
239
+ writeJSONFile(mcpSettingsFile, mcpSettings);
275
240
  }
276
241
  return 'fixed';
277
242
  }