fix: address PR review — shebang, double-escaping, data loss, uninstall scope

- Add shebang banner to NPX CLI esbuild config so npx claude-mem works
- Remove manual backslash pre-escaping in WindsurfHooksInstaller (JSON.stringify handles it)
- Scope cache deletion to claude-mem only, not entire vendor namespace
- Use getWorkerPort() in OpenCodeInstaller instead of hard-coded 37777
- Throw on corrupt JSON in readJsonSafe/readGeminiSettings/Windsurf to prevent data loss
- Fix Cursor install stub to warn instead of silently succeeding
- Fix Gemini uninstall to remove individual hooks within groups, not whole groups
- Update tests for new corrupt-file-throws behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-04 13:49:14 -07:00
parent cdffdba97a
commit ae6915b88e
9 changed files with 53 additions and 61 deletions
@@ -147,19 +147,19 @@ function createHookGroup(hookCommand: string): GeminiHookGroup {
// ============================================================================
/**
* Read ~/.gemini/settings.json, returning empty object if missing/corrupt.
* Read ~/.gemini/settings.json, returning empty object if missing.
* Throws on corrupt JSON to prevent silent data loss.
*/
function readGeminiSettings(): GeminiSettingsJson {
if (!existsSync(GEMINI_SETTINGS_PATH)) {
return {};
}
const content = readFileSync(GEMINI_SETTINGS_PATH, 'utf-8');
try {
const content = readFileSync(GEMINI_SETTINGS_PATH, 'utf-8');
return JSON.parse(content) as GeminiSettingsJson;
} catch (error) {
logger.warn('GEMINI', `Failed to parse ${GEMINI_SETTINGS_PATH}, treating as empty`, {});
return {};
throw new Error(`Corrupt JSON in ${GEMINI_SETTINGS_PATH}, refusing to overwrite user settings`);
}
}
@@ -358,15 +358,15 @@ export function uninstallGeminiCliHooks(): number {
let removedCount = 0;
// Remove claude-mem hooks from each event
// Remove claude-mem hooks from within each group, preserving other hooks
for (const [eventName, groups] of Object.entries(settings.hooks)) {
const filteredGroups = groups.filter(group =>
!group.hooks.some(hook => hook.name === HOOK_NAME)
);
if (filteredGroups.length < groups.length) {
removedCount += groups.length - filteredGroups.length;
}
const filteredGroups = groups
.map(group => {
const remainingHooks = group.hooks.filter(hook => hook.name !== HOOK_NAME);
removedCount += group.hooks.length - remainingHooks.length;
return { ...group, hooks: remainingHooks };
})
.filter(group => group.hooks.length > 0);
if (filteredGroups.length > 0) {
settings.hooks[eventName] = filteredGroups;
@@ -381,7 +381,7 @@ export function uninstallGeminiCliHooks(): number {
}
writeGeminiSettings(settings);
console.log(` Removed ${removedCount} claude-mem hook group(s) from ${GEMINI_SETTINGS_PATH}`);
console.log(` Removed ${removedCount} claude-mem hook(s) from ${GEMINI_SETTINGS_PATH}`);
// Remove claude-mem context section from GEMINI.md
if (existsSync(GEMINI_MD_PATH)) {
@@ -19,6 +19,7 @@ import { homedir } from 'os';
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from 'fs';
import { logger } from '../../utils/logger.js';
import { CONTEXT_TAG_OPEN, CONTEXT_TAG_CLOSE, injectContextIntoMarkdownFile } from '../../utils/context-injection.js';
import { getWorkerPort } from '../../shared/worker-utils.js';
// ============================================================================
// Path Resolution
@@ -301,10 +302,11 @@ Use claude-mem search tools for manual memory queries.`;
// Try to fetch real context from worker first
try {
const healthResponse = await fetch('http://127.0.0.1:37777/api/readiness');
const workerPort = getWorkerPort();
const healthResponse = await fetch(`http://127.0.0.1:${workerPort}/api/readiness`);
if (healthResponse.ok) {
const contextResponse = await fetch(
`http://127.0.0.1:37777/api/context/inject?project=opencode`,
`http://127.0.0.1:${workerPort}/api/context/inject?project=opencode`,
);
if (contextResponse.ok) {
const realContext = await contextResponse.text();
@@ -213,11 +213,7 @@ function buildHookCommand(bunPath: string, workerServicePath: string, eventName:
const hookCommand = eventToCommand[eventName] ?? 'observation';
// Escape backslashes for JSON on Windows
const escapedBunPath = bunPath.replace(/\\/g, '\\\\');
const escapedWorkerPath = workerServicePath.replace(/\\/g, '\\\\');
return `"${escapedBunPath}" "${escapedWorkerPath}" hook windsurf ${hookCommand}`;
return `"${bunPath}" "${workerServicePath}" hook windsurf ${hookCommand}`;
}
/**
@@ -240,10 +236,7 @@ function mergeAndWriteHooksJson(
existingConfig.hooks = {};
}
} catch (error) {
logger.error('WINDSURF', 'Corrupt hooks.json, starting fresh', {
path: WINDSURF_HOOKS_JSON_PATH,
}, error as Error);
existingConfig = { hooks: {} };
throw new Error(`Corrupt hooks.json at ${WINDSURF_HOOKS_JSON_PATH}, refusing to overwrite`);
}
}
@@ -410,9 +403,7 @@ export function uninstallWindsurfHooks(): number {
console.log(` Removed claude-mem entries from hooks.json (other hooks preserved)`);
}
} catch (error) {
// Corrupt file — just remove it
unlinkSync(WINDSURF_HOOKS_JSON_PATH);
console.log(` Removed corrupt hooks.json`);
console.log(` Warning: could not parse hooks.json — leaving file intact to preserve other hooks`);
}
} else {
console.log(` No hooks.json found`);