Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 130abe04a9 | |||
| bff10d49c9 | |||
| 40a71d3250 | |||
| ae3d20c71a | |||
| 54ef9662c1 | |||
| 9aec461e14 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.3.5",
|
||||
"version": "7.3.7",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [7.3.6] - 2025-12-17
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Enhanced SDKAgent response handling and message processing
|
||||
|
||||
## [7.3.5] - 2025-12-17
|
||||
|
||||
## What's Changed
|
||||
* fix(windows): solve zombie port problem with wrapper architecture by @ToxMox in https://github.com/thedotmack/claude-mem/pull/372
|
||||
* chore: bump version to 7.3.5 by @thedotmack in https://github.com/thedotmack/claude-mem/pull/375
|
||||
|
||||
## New Contributors
|
||||
* @ToxMox made their first contribution in https://github.com/thedotmack/claude-mem/pull/372
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.3.4...v7.3.5
|
||||
|
||||
## [7.3.4] - 2025-12-17
|
||||
|
||||
Patch release for bug fixes and minor improvements
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.3.5",
|
||||
"version": "7.3.7",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.3.5",
|
||||
"version": "7.3.7",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem-plugin",
|
||||
"version": "7.3.5",
|
||||
"version": "7.3.7",
|
||||
"private": true,
|
||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||
"type": "module",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+26
-12
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+190
-252
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -6,12 +6,12 @@
|
||||
* native module dependencies.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import { stdin } from "process";
|
||||
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
||||
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
|
||||
import { handleWorkerError } from "../shared/hook-error-handler.js";
|
||||
import { handleFetchError } from "./shared/error-handler.js";
|
||||
import { getProjectName } from "../utils/project-name.js";
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id: string;
|
||||
@@ -25,7 +25,7 @@ async function contextHook(input?: SessionStartInput): Promise<string> {
|
||||
await ensureWorkerRunning();
|
||||
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : "unknown-project";
|
||||
const project = getProjectName(cwd);
|
||||
const port = getWorkerPort();
|
||||
|
||||
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from 'path';
|
||||
import { stdin } from 'process';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||
import { handleFetchError } from './shared/error-handler.js';
|
||||
import { getProjectName } from '../utils/project-name.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
session_id: string;
|
||||
@@ -24,7 +24,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
}
|
||||
|
||||
const { session_id, cwd, prompt } = input;
|
||||
const project = path.basename(cwd);
|
||||
const project = getProjectName(cwd);
|
||||
|
||||
const port = getWorkerPort();
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
toRelativePath,
|
||||
extractFirstFile
|
||||
} from '../shared/timeline-formatting.js';
|
||||
import { getProjectName } from '../utils/project-name.js';
|
||||
|
||||
// Version marker path - use homedir-based path that works in both CJS and ESM contexts
|
||||
const VERSION_MARKER_PATH = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', '.install-version');
|
||||
@@ -222,7 +223,7 @@ function extractPriorMessages(transcriptPath: string): { userMessage: string; as
|
||||
export async function generateContext(input?: ContextInput, useColors: boolean = false): Promise<string> {
|
||||
const config = loadContextConfig();
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
const project = getProjectName(cwd);
|
||||
|
||||
let db: SessionStore | null = null;
|
||||
try {
|
||||
|
||||
@@ -271,29 +271,39 @@ export class ProcessManager {
|
||||
|
||||
private static async waitForHealth(pid: number, port: number, timeoutMs: number = HEALTH_CHECK_TIMEOUT_MS): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||
const startTime = Date.now();
|
||||
const isWindows = process.platform === 'win32';
|
||||
// Increase timeout on Windows to account for slower process startup
|
||||
const adjustedTimeout = isWindows ? timeoutMs * 2 : timeoutMs;
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
while (Date.now() - startTime < adjustedTimeout) {
|
||||
// Check if process is still alive
|
||||
if (!this.isProcessAlive(pid)) {
|
||||
return { success: false, error: 'Process died during startup' };
|
||||
const errorMsg = isWindows
|
||||
? `Process died during startup\n\nTroubleshooting:\n1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes\n2. Verify port ${port} is not in use: netstat -ano | findstr ${port}\n3. Check worker logs in ~/.claude-mem/logs/\n4. See GitHub issues: #363, #367, #371, #373\n5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`
|
||||
: 'Process died during startup';
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
|
||||
// Try health check
|
||||
// Try readiness check (changed from /health to /api/readiness)
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`, {
|
||||
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
|
||||
});
|
||||
if (response.ok) {
|
||||
return { success: true, pid };
|
||||
}
|
||||
} catch {
|
||||
// Not ready yet
|
||||
// Not ready yet, continue polling
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
|
||||
}
|
||||
|
||||
return { success: false, error: 'Health check timed out' };
|
||||
const timeoutMsg = isWindows
|
||||
? `Worker failed to start on Windows (readiness check timed out after ${adjustedTimeout}ms)\n\nTroubleshooting:\n1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes\n2. Verify port ${port} is not in use: netstat -ano | findstr ${port}\n3. Check worker logs in ~/.claude-mem/logs/\n4. See GitHub issues: #363, #367, #371, #373\n5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`
|
||||
: `Readiness check timed out after ${adjustedTimeout}ms`;
|
||||
|
||||
return { success: false, error: timeoutMsg };
|
||||
}
|
||||
|
||||
private static async waitForExit(pid: number, timeout: number): Promise<void> {
|
||||
|
||||
@@ -101,7 +101,9 @@ export class ChromaSync {
|
||||
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
||||
this.transport = new StdioClientTransport({
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
const transportOptions: any = {
|
||||
command: 'uvx',
|
||||
args: [
|
||||
'--python', pythonVersion,
|
||||
@@ -110,7 +112,16 @@ export class ChromaSync {
|
||||
'--data-dir', this.VECTOR_DB_DIR
|
||||
],
|
||||
stderr: 'ignore'
|
||||
});
|
||||
};
|
||||
|
||||
// CRITICAL: On Windows, try to hide console window to prevent PowerShell popups
|
||||
// Note: windowsHide may not be supported by MCP SDK's StdioClientTransport
|
||||
if (isWindows) {
|
||||
transportOptions.windowsHide = true;
|
||||
logger.debug('CHROMA_SYNC', 'Windows detected, attempting to hide console window', { project: this.project });
|
||||
}
|
||||
|
||||
this.transport = new StdioClientTransport(transportOptions);
|
||||
|
||||
this.client = new Client({
|
||||
name: 'claude-mem-chroma-sync',
|
||||
|
||||
+105
-23
@@ -14,7 +14,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { exec } from 'child_process';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -45,6 +45,10 @@ export class WorkerService {
|
||||
private startTime: number = Date.now();
|
||||
private mcpClient: Client;
|
||||
|
||||
// Initialization flags for MCP/SDK readiness tracking
|
||||
private mcpReady: boolean = false;
|
||||
private initializationCompleteFlag: boolean = false;
|
||||
|
||||
// Domain services
|
||||
private dbManager: DatabaseManager;
|
||||
private sessionManager: SessionManager;
|
||||
@@ -128,17 +132,36 @@ export class WorkerService {
|
||||
hasIpc: typeof process.send === 'function',
|
||||
platform: process.platform,
|
||||
pid: process.pid,
|
||||
initialized: this.initializationCompleteFlag,
|
||||
mcpReady: this.mcpReady,
|
||||
});
|
||||
});
|
||||
|
||||
// Readiness check endpoint - returns 503 until full initialization completes
|
||||
// Used by ProcessManager and worker-utils to ensure worker is fully ready before routing requests
|
||||
this.app.get('/api/readiness', (_req, res) => {
|
||||
if (this.initializationCompleteFlag) {
|
||||
res.status(200).json({
|
||||
status: 'ready',
|
||||
mcpReady: this.mcpReady,
|
||||
});
|
||||
} else {
|
||||
res.status(503).json({
|
||||
status: 'initializing',
|
||||
message: 'Worker is still initializing, please retry',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Version endpoint - returns the worker's current version
|
||||
this.app.get('/api/version', (_req, res) => {
|
||||
const { homedir } = require('os');
|
||||
const { readFileSync } = require('fs');
|
||||
const marketplaceRoot = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||
const packageJsonPath = path.join(marketplaceRoot, 'package.json');
|
||||
|
||||
try {
|
||||
// Read version from marketplace package.json
|
||||
const { homedir } = require('os');
|
||||
const { readFileSync } = require('fs');
|
||||
const marketplaceRoot = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||
const packageJsonPath = path.join(marketplaceRoot, 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||
res.status(200).json({ version: packageJson.version });
|
||||
} catch (error) {
|
||||
@@ -295,25 +318,47 @@ export class WorkerService {
|
||||
*/
|
||||
private async cleanupOrphanedProcesses(): Promise<void> {
|
||||
try {
|
||||
// Find all chroma-mcp processes
|
||||
const { stdout } = await execAsync('ps aux | grep "chroma-mcp" | grep -v grep || true');
|
||||
|
||||
if (!stdout.trim()) {
|
||||
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = stdout.trim().split('\n');
|
||||
const isWindows = process.platform === 'win32';
|
||||
const pids: number[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length > 1) {
|
||||
const pid = parseInt(parts[1], 10);
|
||||
if (!isNaN(pid)) {
|
||||
if (isWindows) {
|
||||
// Windows: Use PowerShell Get-CimInstance to find chroma-mcp processes
|
||||
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`;
|
||||
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||
|
||||
if (!stdout.trim()) {
|
||||
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Windows)');
|
||||
return;
|
||||
}
|
||||
|
||||
const pidStrings = stdout.trim().split('\n');
|
||||
for (const pidStr of pidStrings) {
|
||||
const pid = parseInt(pidStr.trim(), 10);
|
||||
// SECURITY: Validate PID is positive integer before adding to list
|
||||
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
|
||||
pids.push(pid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unix: Use ps aux | grep
|
||||
const { stdout } = await execAsync('ps aux | grep "chroma-mcp" | grep -v grep || true');
|
||||
|
||||
if (!stdout.trim()) {
|
||||
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Unix)');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = stdout.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length > 1) {
|
||||
const pid = parseInt(parts[1], 10);
|
||||
// SECURITY: Validate PID is positive integer before adding to list
|
||||
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
|
||||
pids.push(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pids.length === 0) {
|
||||
@@ -321,12 +366,28 @@ export class WorkerService {
|
||||
}
|
||||
|
||||
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
|
||||
platform: isWindows ? 'Windows' : 'Unix',
|
||||
count: pids.length,
|
||||
pids
|
||||
});
|
||||
|
||||
// Kill all found processes
|
||||
await execAsync(`kill ${pids.join(' ')}`);
|
||||
if (isWindows) {
|
||||
for (const pid of pids) {
|
||||
// SECURITY: Double-check PID validation before using in taskkill command
|
||||
if (!Number.isInteger(pid) || pid <= 0) {
|
||||
logger.warn('SYSTEM', 'Skipping invalid PID', { pid });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 5000, stdio: 'ignore' });
|
||||
} catch (error) {
|
||||
logger.warn('SYSTEM', 'Failed to kill orphaned process', { pid }, error as Error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await execAsync(`kill ${pids.join(' ')}`);
|
||||
}
|
||||
|
||||
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pids.length });
|
||||
} catch (error) {
|
||||
@@ -380,7 +441,7 @@ export class WorkerService {
|
||||
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
||||
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
||||
|
||||
// Connect to MCP server
|
||||
// Connect to MCP server with timeout guard
|
||||
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
@@ -388,10 +449,19 @@ export class WorkerService {
|
||||
env: process.env
|
||||
});
|
||||
|
||||
await this.mcpClient.connect(transport);
|
||||
// Add timeout guard to prevent hanging on MCP connection (15 seconds)
|
||||
const MCP_INIT_TIMEOUT_MS = 15000;
|
||||
const mcpConnectionPromise = this.mcpClient.connect(transport);
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('MCP connection timeout after 15s')), MCP_INIT_TIMEOUT_MS)
|
||||
);
|
||||
|
||||
await Promise.race([mcpConnectionPromise, timeoutPromise]);
|
||||
this.mcpReady = true;
|
||||
logger.success('WORKER', 'Connected to MCP server');
|
||||
|
||||
// Signal that initialization is complete
|
||||
this.initializationCompleteFlag = true;
|
||||
this.resolveInitialization();
|
||||
logger.info('SYSTEM', 'Background initialization complete');
|
||||
} catch (error) {
|
||||
@@ -492,6 +562,12 @@ export class WorkerService {
|
||||
return [];
|
||||
}
|
||||
|
||||
// SECURITY: Validate PID is a positive integer to prevent command injection
|
||||
if (!Number.isInteger(parentPid) || parentPid <= 0) {
|
||||
logger.warn('SYSTEM', 'Invalid parent PID for child process enumeration', { parentPid });
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
|
||||
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||
@@ -499,7 +575,7 @@ export class WorkerService {
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(s => parseInt(s.trim(), 10))
|
||||
.filter(n => !isNaN(n));
|
||||
.filter(n => !isNaN(n) && Number.isInteger(n) && n > 0); // SECURITY: Validate each PID
|
||||
} catch (error) {
|
||||
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
|
||||
return [];
|
||||
@@ -510,6 +586,12 @@ export class WorkerService {
|
||||
* Force kill a process by PID (Windows: uses taskkill /F /T)
|
||||
*/
|
||||
private async forceKillProcess(pid: number): Promise<void> {
|
||||
// SECURITY: Validate PID is a positive integer to prevent command injection
|
||||
if (!Number.isInteger(pid) || pid <= 0) {
|
||||
logger.warn('SYSTEM', 'Invalid PID for force kill', { pid });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// /T kills entire process tree, /F forces termination
|
||||
|
||||
@@ -113,7 +113,7 @@ export class SDKAgent {
|
||||
// Calculate discovery tokens (delta for this response only)
|
||||
const discoveryTokens = (session.cumulativeInputTokens + session.cumulativeOutputTokens) - tokensBeforeResponse;
|
||||
|
||||
// Only log non-empty responses (filter out noise)
|
||||
// Process response (empty or not) and mark messages as processed
|
||||
if (responseSize > 0) {
|
||||
const truncatedResponse = responseSize > 100
|
||||
? textContent.substring(0, 100) + '...'
|
||||
@@ -125,6 +125,9 @@ export class SDKAgent {
|
||||
|
||||
// Parse and process response with discovery token delta
|
||||
await this.processSDKResponse(session, textContent, worker, discoveryTokens);
|
||||
} else {
|
||||
// Empty response - still need to mark pending messages as processed
|
||||
await this.markMessagesProcessed(session, worker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,8 +399,15 @@ export class SDKAgent {
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Mark ALL pending messages as successfully processed
|
||||
// This prevents message loss if worker crashes before SDK finishes
|
||||
// Mark messages as processed after successful observation/summary storage
|
||||
await this.markMessagesProcessed(session, worker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all pending messages as successfully processed
|
||||
* CRITICAL: Prevents message loss and duplicate processing
|
||||
*/
|
||||
private async markMessagesProcessed(session: ActiveSession, worker: any | undefined): Promise<void> {
|
||||
const pendingMessageStore = this.sessionManager.getPendingMessageStore();
|
||||
if (session.pendingProcessingIds.size > 0) {
|
||||
for (const messageId of session.pendingProcessingIds) {
|
||||
|
||||
@@ -58,17 +58,18 @@ export function getWorkerHost(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if worker is responsive by trying the health endpoint
|
||||
* Check if worker is responsive and fully initialized by trying the readiness endpoint
|
||||
* Changed from /health to /api/readiness to ensure MCP initialization is complete
|
||||
*/
|
||||
async function isWorkerHealthy(): Promise<boolean> {
|
||||
try {
|
||||
const port = getWorkerPort();
|
||||
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`, {
|
||||
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
logger.debug('SYSTEM', 'Worker health check failed', {
|
||||
logger.debug('SYSTEM', 'Worker readiness check failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
errorType: error?.constructor?.name
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import path from 'path';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* Extract project name from working directory path
|
||||
* Handles edge cases: null/undefined cwd, drive roots, trailing slashes
|
||||
*
|
||||
* @param cwd - Current working directory (absolute path)
|
||||
* @returns Project name or "unknown-project" if extraction fails
|
||||
*/
|
||||
export function getProjectName(cwd: string | null | undefined): string {
|
||||
if (!cwd || cwd.trim() === '') {
|
||||
logger.warn('PROJECT_NAME', 'Empty cwd provided, using fallback', { cwd });
|
||||
return 'unknown-project';
|
||||
}
|
||||
|
||||
// Extract basename (handles trailing slashes automatically)
|
||||
const basename = path.basename(cwd);
|
||||
|
||||
// Edge case: Drive roots on Windows (C:\, J:\) or Unix root (/)
|
||||
// path.basename('C:\') returns '' (empty string)
|
||||
if (basename === '') {
|
||||
// Extract drive letter on Windows, or use 'root' on Unix
|
||||
const isWindows = process.platform === 'win32';
|
||||
if (isWindows && cwd.match(/^[A-Z]:\\/i)) {
|
||||
const driveLetter = cwd[0].toUpperCase();
|
||||
const projectName = `drive-${driveLetter}`;
|
||||
logger.info('PROJECT_NAME', 'Drive root detected', { cwd, projectName });
|
||||
return projectName;
|
||||
} else {
|
||||
logger.warn('PROJECT_NAME', 'Root directory detected, using fallback', { cwd });
|
||||
return 'unknown-project';
|
||||
}
|
||||
}
|
||||
|
||||
return basename;
|
||||
}
|
||||
Reference in New Issue
Block a user