specmem-hardwicksoftware 3.7.0 → 3.7.2

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.
@@ -41,27 +41,72 @@ const HOOKS_DIR = path.join(CLAUDE_CONFIG_DIR, 'hooks');
41
41
  const COMMANDS_DIR = path.join(CLAUDE_CONFIG_DIR, 'commands');
42
42
  // stores per-project MCP configs in ~/.claude.json under "projects" key
43
43
  const CLAUDE_JSON_PATH = path.join(HOME_DIR, '.claude.json');
44
- // SpecMem directory detection - works from both src/ and dist/
44
+ // SpecMem directory detection - dynamically resolves from how specmem was launched
45
+ // Handles bootstrap.cjs AND bootstrap.js, works with ANY install location
46
+ function hasBootstrap(dir) {
47
+ return fs.existsSync(path.join(dir, 'bootstrap.cjs')) ||
48
+ fs.existsSync(path.join(dir, 'bootstrap.js'));
49
+ }
45
50
  function getSpecmemRoot() {
46
- // Check environment variable first
47
- if (process.env.SPECMEM_ROOT) {
51
+ // 1. Check environment variable (explicit override)
52
+ if (process.env.SPECMEM_ROOT && hasBootstrap(process.env.SPECMEM_ROOT)) {
48
53
  return process.env.SPECMEM_ROOT;
49
54
  }
50
- // Try to detect from current file location
51
- // When compiled: dist/init/claudeConfigInjector.js -> need to go up 2 levels
52
- // When in src: src/init/claudeConfigInjector.ts -> need to go up 2 levels
53
- const currentDir = __dirname;
54
- const possibleRoot = path.resolve(currentDir, '..', '..');
55
- // Verify by checking for bootstrap.js
56
- const bootstrapPath = path.join(possibleRoot, 'bootstrap.js');
57
- if (fs.existsSync(bootstrapPath)) {
58
- return possibleRoot;
59
- }
60
- // Fallback to cwd
61
- return process.cwd();
55
+ // 2. Detect from current file location (__dirname is most reliable)
56
+ // dist/init/claudeConfigInjector.js -> go up 2 levels to package root
57
+ const fromThisFile = path.resolve(__dirname, '..', '..');
58
+ if (hasBootstrap(fromThisFile)) {
59
+ return fromThisFile;
60
+ }
61
+ // 3. Detect from process.argv - what script launched us
62
+ // e.g. node /usr/local/lib/.../bootstrap.cjs or /usr/lib/.../bin/specmem-cli.cjs
63
+ for (const arg of process.argv) {
64
+ if (typeof arg === 'string' && arg.includes('specmem')) {
65
+ // Resolve symlinks to get real path
66
+ try {
67
+ const realArg = fs.realpathSync(arg);
68
+ // Walk up from the script to find package root
69
+ let candidate = path.dirname(realArg);
70
+ for (let i = 0; i < 4; i++) {
71
+ if (hasBootstrap(candidate)) return candidate;
72
+ if (fs.existsSync(path.join(candidate, 'package.json'))) {
73
+ try {
74
+ const pkg = JSON.parse(fs.readFileSync(path.join(candidate, 'package.json'), 'utf-8'));
75
+ if (pkg.name === 'specmem-hardwicksoftware') return candidate;
76
+ } catch { /* ignore */ }
77
+ }
78
+ candidate = path.dirname(candidate);
79
+ }
80
+ } catch { /* ignore resolve errors */ }
81
+ }
82
+ }
83
+ // 4. Try resolving the `specmem` command via PATH (execSync imported at top)
84
+ try {
85
+ const whichResult = execSync('which specmem 2>/dev/null', { encoding: 'utf-8' }).trim();
86
+ if (whichResult) {
87
+ const realBin = fs.realpathSync(whichResult);
88
+ // specmem binary is at <root>/bin/specmem-cli.cjs -> go up 2 levels
89
+ const candidate = path.resolve(path.dirname(realBin), '..');
90
+ if (hasBootstrap(candidate)) return candidate;
91
+ }
92
+ } catch { /* which not available or specmem not in PATH */ }
93
+ // 5. Fallback to cwd (dev mode - running from source)
94
+ if (hasBootstrap(process.cwd())) {
95
+ return process.cwd();
96
+ }
97
+ // 6. Last resort - return __dirname-based path even without bootstrap
98
+ return fromThisFile;
99
+ }
100
+ // Find the actual bootstrap file (cjs or js)
101
+ function findBootstrapPath(root) {
102
+ for (const name of ['bootstrap.cjs', 'bootstrap.js']) {
103
+ const p = path.join(root, name);
104
+ if (fs.existsSync(p)) return p;
105
+ }
106
+ return path.join(root, 'bootstrap.cjs'); // default
62
107
  }
63
108
  const SPECMEM_ROOT = getSpecmemRoot();
64
- const BOOTSTRAP_PATH = path.join(SPECMEM_ROOT, 'bootstrap.js');
109
+ const BOOTSTRAP_PATH = findBootstrapPath(SPECMEM_ROOT);
65
110
  const SOURCE_HOOKS_DIR = path.join(SPECMEM_ROOT, 'claude-hooks');
66
111
  const SOURCE_COMMANDS_DIR = path.join(SPECMEM_ROOT, 'commands');
67
112
  // ============================================================================
@@ -72,10 +117,10 @@ const SOURCE_COMMANDS_DIR = path.join(SPECMEM_ROOT, 'commands');
72
117
  const HOOK_ENV = {
73
118
  SPECMEM_HOME: path.join(HOME_DIR, '.specmem'), // Dynamic path using os.homedir()
74
119
  SPECMEM_PKG: SPECMEM_ROOT, // Use detected package root
75
- // Per-project socket paths - ${cwd} is expanded at runtime by Code
120
+ // Per-project socket paths - ${PWD} is expanded at MCP server startup
76
121
  SPECMEM_RUN_DIR: '${cwd}/specmem/sockets',
77
122
  SPECMEM_EMBEDDING_SOCKET: '${cwd}/specmem/sockets/embeddings.sock',
78
- SPECMEM_PROJECT_PATH: '${cwd}', // Dynamically set by Code
123
+ SPECMEM_PROJECT_PATH: '${PWD}', // Dynamically set by Code
79
124
  SPECMEM_SEARCH_LIMIT: '5',
80
125
  SPECMEM_THRESHOLD: '0.30',
81
126
  SPECMEM_MAX_CONTENT: '200'
@@ -155,7 +200,7 @@ export function isSpecmemMcpConfigured(projectPath) {
155
200
  return false;
156
201
  }
157
202
  const specmem = config.mcpServers.specmem;
158
- // Check if it points to a valid bootstrap.js
203
+ // Check if it points to a valid bootstrap file (cjs or js)
159
204
  if (!specmem.args || specmem.args.length < 2) {
160
205
  return false;
161
206
  }
@@ -167,8 +212,8 @@ export function isSpecmemMcpConfigured(projectPath) {
167
212
  // If projectPath specified, check if env has correct project path
168
213
  if (projectPath) {
169
214
  const configuredPath = specmem.env?.SPECMEM_PROJECT_PATH;
170
- // ${PWD} is expanded at runtime by Code, so it's valid
171
- if (configuredPath && configuredPath !== '${PWD}' && configuredPath !== projectPath) {
215
+ // ${PWD} and ${cwd} are expanded at runtime by Claude Code, so they're valid
216
+ if (configuredPath && configuredPath !== '${PWD}' && configuredPath !== '${PWD}' && configuredPath !== projectPath) {
172
217
  return false;
173
218
  }
174
219
  }
@@ -179,11 +224,11 @@ export function isSpecmemMcpConfigured(projectPath) {
179
224
  * Returns true if changes were made
180
225
  */
181
226
  function configureMcpServer() {
182
- // Verify bootstrap.js exists
227
+ // Verify bootstrap file exists (cjs or js)
183
228
  if (!fs.existsSync(BOOTSTRAP_PATH)) {
184
229
  return {
185
230
  configured: false,
186
- error: `bootstrap.js not found at ${BOOTSTRAP_PATH}`
231
+ error: `bootstrap not found at ${BOOTSTRAP_PATH}`
187
232
  };
188
233
  }
189
234
  const config = safeReadJson(CONFIG_PATH, {});
@@ -191,17 +236,16 @@ function configureMcpServer() {
191
236
  config.mcpServers = {};
192
237
  }
193
238
  // Build the SpecMem MCP server config
194
- // CRITICAL: ${cwd} is expanded by Code at runtime to current working directory
195
- // NOTE: ${PWD} resolves at MCP server startup, ${cwd} resolves per-invocation
239
+ // CRITICAL: ${PWD} is expanded at MCP server startup to current working directory
196
240
  const specmemConfig = {
197
241
  command: 'node',
198
242
  args: ['--max-old-space-size=250', BOOTSTRAP_PATH],
199
243
  env: {
200
- // Core paths - ${cwd} gives us project isolation (dynamic per-directory)
244
+ // Core paths - ${PWD} gives us project isolation (dynamic per-directory)
201
245
  HOME: HOME_DIR,
202
- SPECMEM_PROJECT_PATH: '${cwd}',
203
- SPECMEM_WATCHER_ROOT_PATH: '${cwd}',
204
- SPECMEM_CODEBASE_PATH: '${cwd}',
246
+ SPECMEM_PROJECT_PATH: '${PWD}',
247
+ SPECMEM_WATCHER_ROOT_PATH: '${PWD}',
248
+ SPECMEM_CODEBASE_PATH: '${PWD}',
205
249
  // Database (use environment values or defaults)
206
250
  SPECMEM_DB_HOST: process.env.SPECMEM_DB_HOST || 'localhost',
207
251
  SPECMEM_DB_PORT: process.env.SPECMEM_DB_PORT || '5432',
@@ -265,8 +309,9 @@ function fixProjectMcpConfigs() {
265
309
  // Scan all project entries
266
310
  for (const [projectPath, projectConfig] of Object.entries(claudeJson.projects)) {
267
311
  const config = projectConfig;
268
- // Check if this project has a specmem MCP server config
269
- if (config?.mcpServers?.specmem) {
312
+ if (!config) continue;
313
+ // Case 1: Project has specmem MCP config but with outdated path
314
+ if (config.mcpServers?.specmem) {
270
315
  const specmem = config.mcpServers.specmem;
271
316
  const args = specmem.args || [];
272
317
  // Check if the args contain an outdated specmem path
@@ -275,28 +320,49 @@ function fixProjectMcpConfigs() {
275
320
  if (typeof arg === 'string' &&
276
321
  arg.includes('specmem') &&
277
322
  arg !== BOOTSTRAP_PATH &&
278
- (arg.endsWith('index.js') || arg.endsWith('bootstrap.js'))) {
323
+ (arg.endsWith('index.js') || arg.endsWith('bootstrap.js') || arg.endsWith('bootstrap.cjs'))) {
279
324
  needsUpdate = true;
280
325
  return BOOTSTRAP_PATH;
281
326
  }
282
327
  return arg;
283
328
  });
284
329
  if (needsUpdate) {
285
- // Update the args
286
330
  specmem.args = updatedArgs;
287
- // Ensure SPECMEM_PROJECT_PATH is set to actual project path
288
- // CRITICAL: ${PWD} doesn't get expanded by Code, use literal path
289
331
  if (!specmem.env) {
290
332
  specmem.env = {};
291
333
  }
292
- if (!specmem.env.SPECMEM_PROJECT_PATH || specmem.env.SPECMEM_PROJECT_PATH === '${PWD}' || specmem.env.SPECMEM_PROJECT_PATH === '${cwd}') {
293
- specmem.env.SPECMEM_PROJECT_PATH = projectPath; // Use the actual project path key
334
+ if (!specmem.env.SPECMEM_PROJECT_PATH || specmem.env.SPECMEM_PROJECT_PATH === '${PWD}' || specmem.env.SPECMEM_PROJECT_PATH === '${PWD}') {
335
+ specmem.env.SPECMEM_PROJECT_PATH = projectPath;
294
336
  }
295
337
  logger.info({ projectPath, oldArgs: args, newArgs: updatedArgs }, '[ConfigInjector] Fixed outdated specmem path in project config');
296
338
  fixed++;
297
339
  modified = true;
298
340
  }
299
341
  }
342
+ // Case 2: Project has mcpServers but NO specmem entry (empty {} or missing key)
343
+ // This empty override hides the global config.json MCP server, so we inject it
344
+ else if (config.mcpServers && !config.mcpServers.specmem) {
345
+ // Don't clobber other MCP servers - only add specmem
346
+ config.mcpServers.specmem = {
347
+ command: 'node',
348
+ args: ['--max-old-space-size=250', BOOTSTRAP_PATH],
349
+ env: {
350
+ HOME: HOME_DIR,
351
+ SPECMEM_PROJECT_PATH: '${PWD}',
352
+ SPECMEM_WATCHER_ROOT_PATH: '${PWD}',
353
+ SPECMEM_CODEBASE_PATH: '${PWD}',
354
+ SPECMEM_DB_HOST: process.env.SPECMEM_DB_HOST || 'localhost',
355
+ SPECMEM_DB_PORT: process.env.SPECMEM_DB_PORT || '5432',
356
+ SPECMEM_SESSION_WATCHER_ENABLED: 'true',
357
+ SPECMEM_WATCHER_ENABLED: 'true',
358
+ SPECMEM_DASHBOARD_ENABLED: 'true',
359
+ SPECMEM_DASHBOARD_PORT: process.env.SPECMEM_DASHBOARD_PORT || '8595',
360
+ }
361
+ };
362
+ logger.info({ projectPath }, '[ConfigInjector] Injected specmem MCP server into project with empty mcpServers');
363
+ fixed++;
364
+ modified = true;
365
+ }
300
366
  }
301
367
  // Write back if modified
302
368
  if (modified) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specmem-hardwicksoftware",
3
- "version": "3.7.0",
3
+ "version": "3.7.2",
4
4
  "type": "module",
5
5
  "description": "Persistent memory system for coding sessions - semantic search with pgvector, token compression, team coordination, file watching. Needs root: installs system-wide hooks, manages docker/PostgreSQL, writes global configs, handles screen sessions. justcalljon.pro",
6
6
  "main": "dist/index.js",