fix: address 10 unresolved PR review threads
- README: add language specifier to fenced code block - paths.ts: guard npmPackageRootDirectory() against bundle structure drift - OpenCodeInstaller: resolve bundle from import.meta.url, not process.cwd() - OpenCodeInstaller: log warnings on AGENTS.md injection failures - WindsurfHooksInstaller: key registry by full workspace path, not basename - uninstall.ts: poll health endpoint to wait for worker exit before file deletion - uninstall.ts: call IDE-specific uninstallers (Gemini, Windsurf, OpenCode, OpenClaw, Codex) - opencode-plugin: cap session tracking Map at 1000 entries with LRU eviction - GeminiCliHooksInstaller: document intentional JSON double-escaping Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,7 +141,7 @@ npx claude-mem install --ide gemini-cli
|
|||||||
|
|
||||||
Or install from the plugin marketplace inside Claude Code:
|
Or install from the plugin marketplace inside Claude Code:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
/plugin marketplace add thedotmack/claude-mem
|
/plugin marketplace add thedotmack/claude-mem
|
||||||
|
|
||||||
/plugin install claude-mem
|
/plugin install claude-mem
|
||||||
|
|||||||
@@ -165,8 +165,19 @@ async function workerGetText(path: string): Promise<string | null> {
|
|||||||
|
|
||||||
const contentSessionIdsByOpenCodeSessionId = new Map<string, string>();
|
const contentSessionIdsByOpenCodeSessionId = new Map<string, string>();
|
||||||
|
|
||||||
|
const MAX_SESSION_MAP_ENTRIES = 1000;
|
||||||
|
|
||||||
function getOrCreateContentSessionId(openCodeSessionId: string): string {
|
function getOrCreateContentSessionId(openCodeSessionId: string): string {
|
||||||
if (!contentSessionIdsByOpenCodeSessionId.has(openCodeSessionId)) {
|
if (!contentSessionIdsByOpenCodeSessionId.has(openCodeSessionId)) {
|
||||||
|
// Evict oldest entries when the map exceeds the cap (Map preserves insertion order)
|
||||||
|
while (contentSessionIdsByOpenCodeSessionId.size >= MAX_SESSION_MAP_ENTRIES) {
|
||||||
|
const oldestKey = contentSessionIdsByOpenCodeSessionId.keys().next().value;
|
||||||
|
if (oldestKey !== undefined) {
|
||||||
|
contentSessionIdsByOpenCodeSessionId.delete(oldestKey);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
contentSessionIdsByOpenCodeSessionId.set(
|
contentSessionIdsByOpenCodeSessionId.set(
|
||||||
openCodeSessionId,
|
openCodeSessionId,
|
||||||
`opencode-${openCodeSessionId}-${Date.now()}`,
|
`opencode-${openCodeSessionId}-${Date.now()}`,
|
||||||
|
|||||||
@@ -105,13 +105,25 @@ export async function runUninstallCommand(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the worker first (best-effort)
|
// Stop the worker and wait for it to exit before deleting files
|
||||||
try {
|
|
||||||
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777';
|
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777';
|
||||||
|
try {
|
||||||
await fetch(`http://127.0.0.1:${workerPort}/api/admin/shutdown`, {
|
await fetch(`http://127.0.0.1:${workerPort}/api/admin/shutdown`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
|
// Poll health endpoint until worker is gone (max 10s)
|
||||||
|
for (let attempt = 0; attempt < 20; attempt++) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
try {
|
||||||
|
await fetch(`http://127.0.0.1:${workerPort}/api/health`, {
|
||||||
|
signal: AbortSignal.timeout(1000),
|
||||||
|
});
|
||||||
|
// Still alive — keep waiting
|
||||||
|
} catch {
|
||||||
|
break; // Connection refused = worker is gone
|
||||||
|
}
|
||||||
|
}
|
||||||
p.log.info('Worker service stopped.');
|
p.log.info('Worker service stopped.');
|
||||||
} catch {
|
} catch {
|
||||||
// Worker may not be running — that is fine
|
// Worker may not be running — that is fine
|
||||||
@@ -159,6 +171,41 @@ export async function runUninstallCommand(): Promise<void> {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Remove IDE-specific hooks and config (best-effort, each is independent)
|
||||||
|
const ideCleanups: Array<{ label: string; fn: () => Promise<number> | number }> = [
|
||||||
|
{ label: 'Gemini CLI hooks', fn: async () => {
|
||||||
|
const { uninstallGeminiCliHooks } = await import('../../services/integrations/GeminiCliHooksInstaller.js');
|
||||||
|
return uninstallGeminiCliHooks();
|
||||||
|
}},
|
||||||
|
{ label: 'Windsurf hooks', fn: async () => {
|
||||||
|
const { uninstallWindsurfHooks } = await import('../../services/integrations/WindsurfHooksInstaller.js');
|
||||||
|
return uninstallWindsurfHooks();
|
||||||
|
}},
|
||||||
|
{ label: 'OpenCode plugin', fn: async () => {
|
||||||
|
const { uninstallOpenCodePlugin } = await import('../../services/integrations/OpenCodeInstaller.js');
|
||||||
|
return uninstallOpenCodePlugin();
|
||||||
|
}},
|
||||||
|
{ label: 'OpenClaw plugin', fn: async () => {
|
||||||
|
const { uninstallOpenClawPlugin } = await import('../../services/integrations/OpenClawInstaller.js');
|
||||||
|
return uninstallOpenClawPlugin();
|
||||||
|
}},
|
||||||
|
{ label: 'Codex CLI', fn: async () => {
|
||||||
|
const { uninstallCodexCli } = await import('../../services/integrations/CodexCliInstaller.js');
|
||||||
|
return uninstallCodexCli();
|
||||||
|
}},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { label, fn } of ideCleanups) {
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
if (result === 0) {
|
||||||
|
p.log.info(`${label}: removed.`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// IDE not configured or uninstaller errored — skip silently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.note(
|
p.note(
|
||||||
[
|
[
|
||||||
`Your data directory at ${pc.cyan('~/.claude-mem')} was preserved.`,
|
`Your data directory at ${pc.cyan('~/.claude-mem')} was preserved.`,
|
||||||
|
|||||||
@@ -73,7 +73,14 @@ export function claudeMemDataDirectory(): string {
|
|||||||
export function npmPackageRootDirectory(): string {
|
export function npmPackageRootDirectory(): string {
|
||||||
const currentFilePath = fileURLToPath(import.meta.url);
|
const currentFilePath = fileURLToPath(import.meta.url);
|
||||||
// <pkg>/dist/npx-cli/index.js -> up 2 levels -> <pkg>
|
// <pkg>/dist/npx-cli/index.js -> up 2 levels -> <pkg>
|
||||||
return join(dirname(currentFilePath), '..', '..');
|
const root = join(dirname(currentFilePath), '..', '..');
|
||||||
|
if (!existsSync(join(root, 'package.json'))) {
|
||||||
|
throw new Error(
|
||||||
|
`npmPackageRootDirectory: expected package.json at ${root}. ` +
|
||||||
|
`Bundle structure may have changed — update the path walk.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -119,7 +119,10 @@ function buildHookCommand(
|
|||||||
throw new Error(`Unknown Gemini CLI event: ${geminiEventName}`);
|
throw new Error(`Unknown Gemini CLI event: ${geminiEventName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape backslashes for JSON compatibility on Windows
|
// Double-escape backslashes intentionally: this command string is embedded inside
|
||||||
|
// a JSON value, so `\\` in the source becomes `\` when the JSON is parsed by the
|
||||||
|
// IDE. Without double-escaping, Windows paths like C:\Users would lose their
|
||||||
|
// backslashes and break when the IDE deserializes the hook configuration.
|
||||||
const escapedBunPath = bunPath.replace(/\\/g, '\\\\');
|
const escapedBunPath = bunPath.replace(/\\/g, '\\\\');
|
||||||
const escapedWorkerPath = workerServicePath.replace(/\\/g, '\\\\');
|
const escapedWorkerPath = workerServicePath.replace(/\\/g, '\\\\');
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from 'fs';
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from 'fs';
|
||||||
import { logger } from '../../utils/logger.js';
|
import { logger } from '../../utils/logger.js';
|
||||||
import { CONTEXT_TAG_OPEN, CONTEXT_TAG_CLOSE, injectContextIntoMarkdownFile } from '../../utils/context-injection.js';
|
import { CONTEXT_TAG_OPEN, CONTEXT_TAG_CLOSE, injectContextIntoMarkdownFile } from '../../utils/context-injection.js';
|
||||||
@@ -74,8 +75,8 @@ export function findBuiltPluginPath(): string | null {
|
|||||||
'plugins', 'marketplaces', 'thedotmack',
|
'plugins', 'marketplaces', 'thedotmack',
|
||||||
'dist', 'opencode-plugin', 'index.js',
|
'dist', 'opencode-plugin', 'index.js',
|
||||||
),
|
),
|
||||||
// Development location (relative to project root)
|
// Development location (relative to this module's package root)
|
||||||
path.join(process.cwd(), 'dist', 'opencode-plugin', 'index.js'),
|
path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'dist', 'opencode-plugin', 'index.js'),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const candidatePath of possiblePaths) {
|
for (const candidatePath of possiblePaths) {
|
||||||
@@ -171,7 +172,10 @@ export async function syncContextToAgentsMd(
|
|||||||
|
|
||||||
const contextText = await response.text();
|
const contextText = await response.text();
|
||||||
if (contextText && contextText.trim()) {
|
if (contextText && contextText.trim()) {
|
||||||
injectContextIntoAgentsMd(contextText);
|
const injectResult = injectContextIntoAgentsMd(contextText);
|
||||||
|
if (injectResult !== 0) {
|
||||||
|
logger.warn('OPENCODE', 'Failed to inject context into AGENTS.md during sync');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Worker not available — non-critical
|
// Worker not available — non-critical
|
||||||
@@ -315,23 +319,42 @@ Use claude-mem search tools for manual memory queries.`;
|
|||||||
if (contextResponse.ok) {
|
if (contextResponse.ok) {
|
||||||
const realContext = await contextResponse.text();
|
const realContext = await contextResponse.text();
|
||||||
if (realContext && realContext.trim()) {
|
if (realContext && realContext.trim()) {
|
||||||
injectContextIntoAgentsMd(realContext);
|
const injectResult = injectContextIntoAgentsMd(realContext);
|
||||||
console.log(' Context injected from existing memory');
|
if (injectResult !== 0) {
|
||||||
|
logger.warn('OPENCODE', 'Failed to inject real context into AGENTS.md during install');
|
||||||
|
} else {
|
||||||
|
console.log(' Context injected from existing memory');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const injectResult = injectContextIntoAgentsMd(placeholderContext);
|
||||||
|
if (injectResult !== 0) {
|
||||||
|
logger.warn('OPENCODE', 'Failed to inject placeholder context into AGENTS.md during install');
|
||||||
} else {
|
} else {
|
||||||
injectContextIntoAgentsMd(placeholderContext);
|
|
||||||
console.log(' Placeholder context created (will populate after first session)');
|
console.log(' Placeholder context created (will populate after first session)');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
injectContextIntoAgentsMd(placeholderContext);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
injectContextIntoAgentsMd(placeholderContext);
|
const injectResult = injectContextIntoAgentsMd(placeholderContext);
|
||||||
|
if (injectResult !== 0) {
|
||||||
|
logger.warn('OPENCODE', 'Failed to inject placeholder context into AGENTS.md during install');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const injectResult = injectContextIntoAgentsMd(placeholderContext);
|
||||||
|
if (injectResult !== 0) {
|
||||||
|
logger.warn('OPENCODE', 'Failed to inject placeholder context into AGENTS.md during install');
|
||||||
|
} else {
|
||||||
console.log(' Placeholder context created (worker not running)');
|
console.log(' Placeholder context created (worker not running)');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
injectContextIntoAgentsMd(placeholderContext);
|
const injectResult = injectContextIntoAgentsMd(placeholderContext);
|
||||||
|
if (injectResult !== 0) {
|
||||||
|
logger.warn('OPENCODE', 'Failed to inject placeholder context into AGENTS.md during install');
|
||||||
|
} else {
|
||||||
console.log(' Placeholder context created (worker not running)');
|
console.log(' Placeholder context created (worker not running)');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`
|
console.log(`
|
||||||
Installation complete!
|
Installation complete!
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ interface WindsurfHooksJson {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface WindsurfProjectRegistry {
|
interface WindsurfProjectRegistry {
|
||||||
[projectName: string]: {
|
[workspacePath: string]: {
|
||||||
workspacePath: string;
|
|
||||||
installedAt: string;
|
installedAt: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -104,37 +103,37 @@ export function writeWindsurfRegistry(registry: WindsurfProjectRegistry): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a project for auto-context updates
|
* Register a project for auto-context updates.
|
||||||
|
* Keys by full workspacePath to avoid collisions between directories with the same basename.
|
||||||
*/
|
*/
|
||||||
export function registerWindsurfProject(projectName: string, workspacePath: string): void {
|
export function registerWindsurfProject(workspacePath: string): void {
|
||||||
const registry = readWindsurfRegistry();
|
const registry = readWindsurfRegistry();
|
||||||
registry[projectName] = {
|
registry[workspacePath] = {
|
||||||
workspacePath,
|
|
||||||
installedAt: new Date().toISOString(),
|
installedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
writeWindsurfRegistry(registry);
|
writeWindsurfRegistry(registry);
|
||||||
logger.info('WINDSURF', 'Registered project for auto-context updates', { projectName, workspacePath });
|
logger.info('WINDSURF', 'Registered project for auto-context updates', { workspacePath });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister a project from auto-context updates
|
* Unregister a project from auto-context updates
|
||||||
*/
|
*/
|
||||||
export function unregisterWindsurfProject(projectName: string): void {
|
export function unregisterWindsurfProject(workspacePath: string): void {
|
||||||
const registry = readWindsurfRegistry();
|
const registry = readWindsurfRegistry();
|
||||||
if (registry[projectName]) {
|
if (registry[workspacePath]) {
|
||||||
delete registry[projectName];
|
delete registry[workspacePath];
|
||||||
writeWindsurfRegistry(registry);
|
writeWindsurfRegistry(registry);
|
||||||
logger.info('WINDSURF', 'Unregistered project', { projectName });
|
logger.info('WINDSURF', 'Unregistered project', { workspacePath });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update Windsurf context files for all registered projects matching this project name.
|
* Update Windsurf context files for a registered project.
|
||||||
* Called by SDK agents after saving a summary.
|
* Called by SDK agents after saving a summary.
|
||||||
*/
|
*/
|
||||||
export async function updateWindsurfContextForProject(projectName: string, port: number): Promise<void> {
|
export async function updateWindsurfContextForProject(projectName: string, workspacePath: string, port: number): Promise<void> {
|
||||||
const registry = readWindsurfRegistry();
|
const registry = readWindsurfRegistry();
|
||||||
const entry = registry[projectName];
|
const entry = registry[workspacePath];
|
||||||
|
|
||||||
if (!entry) return; // Project doesn't have Windsurf hooks installed
|
if (!entry) return; // Project doesn't have Windsurf hooks installed
|
||||||
|
|
||||||
@@ -148,11 +147,11 @@ export async function updateWindsurfContextForProject(projectName: string, port:
|
|||||||
const context = await response.text();
|
const context = await response.text();
|
||||||
if (!context || !context.trim()) return;
|
if (!context || !context.trim()) return;
|
||||||
|
|
||||||
writeWindsurfContextFile(entry.workspacePath, context);
|
writeWindsurfContextFile(workspacePath, context);
|
||||||
logger.debug('WINDSURF', 'Updated context file', { projectName, workspacePath: entry.workspacePath });
|
logger.debug('WINDSURF', 'Updated context file', { projectName, workspacePath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Background context update — failure is non-critical
|
// Background context update — failure is non-critical
|
||||||
logger.error('WINDSURF', 'Failed to update context file', { projectName }, error as Error);
|
logger.error('WINDSURF', 'Failed to update context file', { projectName, workspacePath }, error as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +370,7 @@ Use claude-mem's MCP search tools for manual memory queries.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register project for automatic context updates after summaries
|
// Register project for automatic context updates after summaries
|
||||||
registerWindsurfProject(projectName, workspaceRoot);
|
registerWindsurfProject(workspaceRoot);
|
||||||
console.log(` Registered for auto-context updates`);
|
console.log(` Registered for auto-context updates`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,8 +422,7 @@ export function uninstallWindsurfHooks(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unregister project
|
// Unregister project
|
||||||
const projectName = path.basename(workspaceRoot);
|
unregisterWindsurfProject(workspaceRoot);
|
||||||
unregisterWindsurfProject(projectName);
|
|
||||||
console.log(` Unregistered from auto-context updates`);
|
console.log(` Unregistered from auto-context updates`);
|
||||||
|
|
||||||
console.log(`\nUninstallation complete!\n`);
|
console.log(`\nUninstallation complete!\n`);
|
||||||
|
|||||||
Reference in New Issue
Block a user