mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
refactor(phase-2a): consolidate duplicates (-1,073 LoC)
Phase 2 tasks 1–3 of PLAN-RIP-THE-BAND-AIDS-OFF — collapse duplicated implementations into single canonical sources. - Task 1 (transcript parser): replace inline parseAssistantTextFromLine/findLastAssistantMessage/extractPriorMessages in ObservationCompiler with shared extractLastMessage - Task 2 (bun-path resolver): new src/shared/bun-resolution.ts; reduce npx-cli/utils/bun-resolver.ts to a re-export shim and delegate CursorHooksInstaller.findBunPath to the shared helper - Task 3 (migration runner): delete ~965 lines of inlined migration methods from SessionStore; constructor now calls MigrationRunner.runAllMigrations(). Add addObservationModelColumns (v26) to the runner to preserve coverage. Deferred: Task 4 (PendingMessage self-heal) reverted — test contract encoded the removed side-effect behavior; will revisit with test updates. Task 10 (GracefulShutdown merge) correctly skipped per plan guardrail: performGracefulShutdown and runShutdownCascade serve different scopes and are NOT always called together. Tasks 5–9 and 11–13 land in subsequent partial commits. Tests: 28 fail / 1347 pass / 3 skip / 1 error (baseline 38 fail / 1337 pass — strict improvement, zero new failures). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,86 +1,7 @@
|
||||
/**
|
||||
* Bun binary resolution utility.
|
||||
* Bun binary resolution utility (re-export shim).
|
||||
*
|
||||
* Extracted from `plugin/scripts/bun-runner.js` so that the NPX CLI
|
||||
* can locate Bun without duplicating the search logic.
|
||||
*
|
||||
* Pure Node.js — no Bun APIs used.
|
||||
* Actual implementation lives in `src/shared/bun-resolution.ts`.
|
||||
* This file preserves the existing `npx-cli` import surface.
|
||||
*/
|
||||
import { spawnSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { IS_WINDOWS } from './paths.js';
|
||||
|
||||
/**
|
||||
* Well-known locations where Bun might be installed, beyond PATH.
|
||||
* Order matches the search priority in bun-runner.js and smart-install.js.
|
||||
*/
|
||||
function bunCandidatePaths(): string[] {
|
||||
if (IS_WINDOWS) {
|
||||
return [
|
||||
join(homedir(), '.bun', 'bin', 'bun.exe'),
|
||||
join(process.env.USERPROFILE || homedir(), '.bun', 'bin', 'bun.exe'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
join(homedir(), '.bun', 'bin', 'bun'),
|
||||
'/usr/local/bin/bun',
|
||||
'/opt/homebrew/bin/bun',
|
||||
'/home/linuxbrew/.linuxbrew/bin/bun',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to locate the Bun executable.
|
||||
*
|
||||
* 1. Check PATH via `which` / `where`.
|
||||
* 2. Probe well-known installation directories.
|
||||
*
|
||||
* Returns the absolute path to the binary, `'bun'` if it is in PATH,
|
||||
* or `null` if Bun cannot be found.
|
||||
*/
|
||||
export function resolveBunBinaryPath(): string | null {
|
||||
// Try PATH first
|
||||
const whichCommand = IS_WINDOWS ? 'where' : 'which';
|
||||
const pathCheck = spawnSync(whichCommand, ['bun'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS,
|
||||
});
|
||||
|
||||
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
|
||||
return 'bun'; // Available in PATH — use short name
|
||||
}
|
||||
|
||||
// Probe known install locations
|
||||
for (const candidatePath of bunCandidatePaths()) {
|
||||
if (existsSync(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the installed Bun version string (e.g. `"1.2.3"`), or `null`
|
||||
* if Bun is not available.
|
||||
*/
|
||||
export function getBunVersionString(): string | null {
|
||||
const bunPath = resolveBunBinaryPath();
|
||||
if (!bunPath) return null;
|
||||
|
||||
try {
|
||||
const result = spawnSync(bunPath, ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS,
|
||||
});
|
||||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
} catch (error: unknown) {
|
||||
console.error('[bun-resolver] Failed to get Bun version:', error instanceof Error ? error.message : String(error));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export { resolveBunBinaryPath, getBunVersionString } from '../../shared/bun-resolution.js';
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { SessionStore } from '../sqlite/SessionStore.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { SYSTEM_REMINDER_REGEX } from '../../utils/tag-stripping.js';
|
||||
import { extractLastMessage } from '../../shared/transcript-parser.js';
|
||||
import { CLAUDE_CONFIG_DIR } from '../../shared/paths.js';
|
||||
import type {
|
||||
ContextConfig,
|
||||
@@ -195,61 +193,12 @@ function cwdToDashed(cwd: string): string {
|
||||
return cwd.replace(/\//g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the last assistant message text from parsed transcript lines.
|
||||
*/
|
||||
function parseAssistantTextFromLine(line: string): string | null {
|
||||
if (!line.includes('"type":"assistant"')) return null;
|
||||
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.type === 'assistant' && entry.message?.content && Array.isArray(entry.message.content)) {
|
||||
let text = '';
|
||||
for (const block of entry.message.content) {
|
||||
if (block.type === 'text') text += block.text;
|
||||
}
|
||||
text = text.replace(SYSTEM_REMINDER_REGEX, '').trim();
|
||||
if (text) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLastAssistantMessage(lines: string[]): string {
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const result = parseAssistantTextFromLine(lines[i]);
|
||||
if (result) return result;
|
||||
} catch (parseError) {
|
||||
if (parseError instanceof Error) {
|
||||
logger.debug('WORKER', 'Skipping malformed transcript line', { lineIndex: i }, parseError);
|
||||
} else {
|
||||
logger.debug('WORKER', 'Skipping malformed transcript line', { lineIndex: i, error: String(parseError) });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract prior messages from transcript file
|
||||
*/
|
||||
export function extractPriorMessages(transcriptPath: string): PriorMessages {
|
||||
try {
|
||||
if (!existsSync(transcriptPath)) return { userMessage: '', assistantMessage: '' };
|
||||
const content = readFileSync(transcriptPath, 'utf-8').trim();
|
||||
if (!content) return { userMessage: '', assistantMessage: '' };
|
||||
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
const lastAssistantMessage = findLastAssistantMessage(lines);
|
||||
return { userMessage: '', assistantMessage: lastAssistantMessage };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.failure('WORKER', 'Failed to extract prior messages from transcript', { transcriptPath }, error);
|
||||
} else {
|
||||
logger.warn('WORKER', 'Failed to extract prior messages from transcript', { transcriptPath, error: String(error) });
|
||||
}
|
||||
return { userMessage: '', assistantMessage: '' };
|
||||
}
|
||||
const assistantMessage = extractLastMessage(transcriptPath, 'assistant', true);
|
||||
return { userMessage: '', assistantMessage };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@ import { promisify } from 'util';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { getWorkerPort, workerHttpRequest } from '../../shared/worker-utils.js';
|
||||
import { DATA_DIR, MARKETPLACE_ROOT, CLAUDE_CONFIG_DIR } from '../../shared/paths.js';
|
||||
import { resolveBunBinaryPathOrDefault } from '../../shared/bun-resolution.js';
|
||||
import {
|
||||
readCursorRegistry as readCursorRegistryFromFile,
|
||||
writeCursorRegistry as writeCursorRegistryToFile,
|
||||
@@ -170,34 +171,15 @@ export function findWorkerServicePath(): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Bun executable path
|
||||
* Required because worker-service.cjs uses bun:sqlite which is Bun-specific
|
||||
* Searches common installation locations across platforms
|
||||
* Find the Bun executable path.
|
||||
* Required because worker-service.cjs uses bun:sqlite which is Bun-specific.
|
||||
*
|
||||
* Delegates to the shared resolver; falls back to the literal `'bun'` so
|
||||
* hook config generation always emits an invocation even if Bun isn't
|
||||
* detected at install time (the user sees a clear error at hook-run time).
|
||||
*/
|
||||
export function findBunPath(): string {
|
||||
const possiblePaths = [
|
||||
// Standard user install location (most common)
|
||||
path.join(homedir(), '.bun', 'bin', 'bun'),
|
||||
// Global install locations
|
||||
'/usr/local/bin/bun',
|
||||
'/usr/bin/bun',
|
||||
// Windows locations
|
||||
...(process.platform === 'win32' ? [
|
||||
path.join(homedir(), '.bun', 'bin', 'bun.exe'),
|
||||
path.join(process.env.LOCALAPPDATA || '', 'bun', 'bun.exe'),
|
||||
] : []),
|
||||
];
|
||||
|
||||
for (const p of possiblePaths) {
|
||||
if (p && existsSync(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to 'bun' and hope it's in PATH
|
||||
// This allows the installation to proceed even if we can't find bun
|
||||
// The user will get a clear error when the hook runs if bun isn't available
|
||||
return 'bun';
|
||||
return resolveBunBinaryPathOrDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@ export class MigrationRunner {
|
||||
this.addSessionCustomTitleColumn();
|
||||
this.createObservationFeedbackTable();
|
||||
this.addSessionPlatformSourceColumn();
|
||||
this.addObservationModelColumns();
|
||||
this.ensureMergedIntoProjectColumns();
|
||||
this.addObservationSubagentColumns();
|
||||
}
|
||||
@@ -1015,4 +1016,24 @@ export class MigrationRunner {
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(27, new Date().toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add generated_by_model + relevance_count columns to observations (schema v26).
|
||||
*/
|
||||
private addObservationModelColumns(): void {
|
||||
const columns = this.db.query('PRAGMA table_info(observations)').all() as TableColumnInfo[];
|
||||
const hasGeneratedByModel = columns.some(col => col.name === 'generated_by_model');
|
||||
const hasRelevanceCount = columns.some(col => col.name === 'relevance_count');
|
||||
|
||||
if (hasGeneratedByModel && hasRelevanceCount) return;
|
||||
|
||||
if (!hasGeneratedByModel) {
|
||||
this.db.run('ALTER TABLE observations ADD COLUMN generated_by_model TEXT');
|
||||
}
|
||||
if (!hasRelevanceCount) {
|
||||
this.db.run('ALTER TABLE observations ADD COLUMN relevance_count INTEGER DEFAULT 0');
|
||||
}
|
||||
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(26, new Date().toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
96
src/shared/bun-resolution.ts
Normal file
96
src/shared/bun-resolution.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Bun binary resolution — single source of truth.
|
||||
*
|
||||
* Merged from `npx-cli/utils/bun-resolver.ts` (null-returning) and
|
||||
* `CursorHooksInstaller.findBunPath` (fallback-to-'bun'-returning).
|
||||
*
|
||||
* Pure Node.js — no Bun APIs used.
|
||||
*/
|
||||
import { spawnSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
/**
|
||||
* Well-known locations where Bun might be installed, beyond PATH.
|
||||
* Order matches the search priority in bun-runner.js and smart-install.js.
|
||||
*/
|
||||
function bunCandidatePaths(): string[] {
|
||||
if (IS_WINDOWS) {
|
||||
return [
|
||||
join(homedir(), '.bun', 'bin', 'bun.exe'),
|
||||
join(process.env.USERPROFILE || homedir(), '.bun', 'bin', 'bun.exe'),
|
||||
join(process.env.LOCALAPPDATA || '', 'bun', 'bun.exe'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
join(homedir(), '.bun', 'bin', 'bun'),
|
||||
'/usr/local/bin/bun',
|
||||
'/usr/bin/bun',
|
||||
'/opt/homebrew/bin/bun',
|
||||
'/home/linuxbrew/.linuxbrew/bin/bun',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to locate the Bun executable.
|
||||
*
|
||||
* 1. Check PATH via `which` / `where`.
|
||||
* 2. Probe well-known installation directories.
|
||||
*
|
||||
* Returns the absolute path to the binary, `'bun'` if it is in PATH,
|
||||
* or `null` if Bun cannot be found.
|
||||
*/
|
||||
export function resolveBunBinaryPath(): string | null {
|
||||
// Try PATH first
|
||||
const whichCommand = IS_WINDOWS ? 'where' : 'which';
|
||||
const pathCheck = spawnSync(whichCommand, ['bun'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS,
|
||||
});
|
||||
|
||||
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
|
||||
return 'bun'; // Available in PATH — use short name
|
||||
}
|
||||
|
||||
// Probe known install locations
|
||||
for (const candidatePath of bunCandidatePaths()) {
|
||||
if (candidatePath && existsSync(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the Bun executable, falling back to the literal string 'bun'
|
||||
* so callers that must emit a bun invocation always get *something* to run.
|
||||
*
|
||||
* Use this when writing hook scripts / config files: the installation should
|
||||
* succeed even if Bun isn't detected at install time, and the user sees a
|
||||
* clear error at hook-run time if bun is still missing.
|
||||
*/
|
||||
export function resolveBunBinaryPathOrDefault(): string {
|
||||
return resolveBunBinaryPath() ?? 'bun';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the installed Bun version string (e.g. `"1.2.3"`), or `null`
|
||||
* if Bun is not available.
|
||||
*/
|
||||
export function getBunVersionString(): string | null {
|
||||
const bunPath = resolveBunBinaryPath();
|
||||
if (!bunPath) return null;
|
||||
|
||||
const result = spawnSync(bunPath, ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS,
|
||||
});
|
||||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
}
|
||||
Reference in New Issue
Block a user