refactor: consolidate MCP factory, add non-TTY support, auto-detect transcript watchers

- Phase 1: Replace 5 duplicate MCP installers with config-driven factory, extract
  shared context-injection and json-utils utilities, fix process.execPath usage
- Phase 2: Add non-TTY fallback for @clack/prompts to prevent ENOENT in CI/Docker
- Phase 3: Wire GeminiCliHooksInstaller through hook command framework with adapter
- Phase 4: Auto-start transcript watchers on worker boot when config exists

Net -107 lines via DRY consolidation of duplicated installer logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-04 00:35:55 -07:00
parent a2ac116aac
commit 2495f98496
10 changed files with 959 additions and 972 deletions
+68
View File
@@ -0,0 +1,68 @@
/**
* Shared context injection utilities for claude-mem.
*
* Provides tag constants and a function to inject or update a
* <claude-mem-context> section in any markdown file. Used by
* MCP integrations and OpenCode installer.
*/
import path from 'path';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
// ============================================================================
// Tag Constants
// ============================================================================
export const CONTEXT_TAG_OPEN = '<claude-mem-context>';
export const CONTEXT_TAG_CLOSE = '</claude-mem-context>';
// ============================================================================
// Context Injection
// ============================================================================
/**
* Inject or update a <claude-mem-context> section in a markdown file.
* Creates the file if it doesn't exist. Preserves content outside the tags.
*
* @param filePath - Absolute path to the target markdown file.
* @param contextContent - The content to place between the context tags.
* @param headerLine - Optional first line written when creating a new file
* (e.g. `"# Claude-Mem Memory Context"` for AGENTS.md).
*/
export function injectContextIntoMarkdownFile(
filePath: string,
contextContent: string,
headerLine?: string,
): void {
const parentDirectory = path.dirname(filePath);
mkdirSync(parentDirectory, { recursive: true });
const wrappedContent = `${CONTEXT_TAG_OPEN}\n${contextContent}\n${CONTEXT_TAG_CLOSE}`;
if (existsSync(filePath)) {
let existingContent = readFileSync(filePath, 'utf-8');
const tagStartIndex = existingContent.indexOf(CONTEXT_TAG_OPEN);
const tagEndIndex = existingContent.indexOf(CONTEXT_TAG_CLOSE);
if (tagStartIndex !== -1 && tagEndIndex !== -1) {
// Replace existing section
existingContent =
existingContent.slice(0, tagStartIndex) +
wrappedContent +
existingContent.slice(tagEndIndex + CONTEXT_TAG_CLOSE.length);
} else {
// Append section
existingContent = existingContent.trimEnd() + '\n\n' + wrappedContent + '\n';
}
writeFileSync(filePath, existingContent, 'utf-8');
} else {
// Create new file
if (headerLine) {
writeFileSync(filePath, `${headerLine}\n\n${wrappedContent}\n`, 'utf-8');
} else {
writeFileSync(filePath, wrappedContent + '\n', 'utf-8');
}
}
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Shared JSON file utilities for claude-mem.
*
* Provides safe read/write helpers used across the CLI and services.
*/
import { existsSync, readFileSync } from 'fs';
import { logger } from './logger.js';
/**
* Read a JSON file safely, returning a default value if the file
* does not exist or contains corrupt JSON.
*
* @param filePath - Absolute path to the JSON file.
* @param defaultValue - Value returned when the file is missing or unreadable.
* @returns The parsed JSON content, or `defaultValue` on failure.
*/
export function readJsonSafe<T>(filePath: string, defaultValue: T): T {
if (!existsSync(filePath)) return defaultValue;
try {
return JSON.parse(readFileSync(filePath, 'utf-8'));
} catch (error) {
logger.error('JSON', `Corrupt JSON file, using default`, { path: filePath }, error as Error);
return defaultValue;
}
}