Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8040c6d559 | |||
| 7187220b24 | |||
| ca52950b2a | |||
| ee1441f462 | |||
| c4af31f48d | |||
| c2742d5664 | |||
| 0c45919261 | |||
| a3ab898e04 | |||
| dea67c0d86 | |||
| d13a2c237c | |||
| c592f0aa69 | |||
| 85a2472e4e | |||
| 0cb3256b2d | |||
| 44029862b1 | |||
| 130abe04a9 | |||
| bff10d49c9 | |||
| 40a71d3250 | |||
| ae3d20c71a | |||
| 54ef9662c1 | |||
| 9aec461e14 |
@@ -10,7 +10,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.3.5",
|
"version": "7.4.1",
|
||||||
"source": "./plugin",
|
"source": "./plugin",
|
||||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
datasets/
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -4,6 +4,89 @@ 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/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [7.4.0] - 2025-12-18
|
||||||
|
|
||||||
|
## What's New
|
||||||
|
|
||||||
|
### MCP Tool Token Reduction
|
||||||
|
|
||||||
|
Optimized MCP tool definitions for reduced token consumption in Claude Code sessions through progressive parameter disclosure.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Streamlined MCP tool schemas with minimal inline definitions
|
||||||
|
- Added `get_schema()` tool for on-demand parameter documentation
|
||||||
|
- Enhanced worker API with operation-based instruction loading
|
||||||
|
|
||||||
|
This release improves session efficiency by reducing the token overhead of MCP tool definitions while maintaining full functionality through progressive disclosure.
|
||||||
|
|
||||||
|
## [7.3.9] - 2025-12-18
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fix MCP server compatibility and web UI path resolution
|
||||||
|
|
||||||
|
This patch release addresses compatibility issues with the MCP server and resolves path resolution problems in the web UI.
|
||||||
|
|
||||||
|
## [7.3.8] - 2025-12-18
|
||||||
|
|
||||||
|
## Security Fix
|
||||||
|
|
||||||
|
Added localhost-only protection for admin endpoints to prevent DoS attacks when worker service is bound to 0.0.0.0 for remote UI access.
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Created `requireLocalhost` middleware to restrict admin endpoints
|
||||||
|
- Applied to `/api/admin/restart` and `/api/admin/shutdown`
|
||||||
|
- Returns 403 Forbidden for non-localhost requests
|
||||||
|
|
||||||
|
### Security Impact
|
||||||
|
Prevents unauthorized shutdown/restart of worker service when exposed on network.
|
||||||
|
|
||||||
|
Fixes security concern raised in #368.
|
||||||
|
|
||||||
|
## [7.3.7] - 2025-12-17
|
||||||
|
|
||||||
|
## Windows Platform Stabilization
|
||||||
|
|
||||||
|
This patch release includes comprehensive improvements for Windows platform stability and reliability.
|
||||||
|
|
||||||
|
### Key Improvements
|
||||||
|
|
||||||
|
- **Worker Readiness Tracking**: Added `/api/readiness` endpoint with MCP/SDK initialization flags to prevent premature connection attempts
|
||||||
|
- **Process Tree Cleanup**: Implemented recursive process enumeration on Windows to prevent zombie socket processes
|
||||||
|
- **Bun Runtime Migration**: Migrated worker wrapper from Node.js to Bun for consistency and reliability
|
||||||
|
- **Centralized Project Name Utility**: Consolidated duplicate project name extraction logic with Windows drive root handling
|
||||||
|
- **Enhanced Error Messages**: Added platform-aware logging and detailed Windows troubleshooting guidance
|
||||||
|
- **Subprocess Console Hiding**: Standardized `windowsHide: true` across all child process spawns to prevent console window flashing
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Worker service tracks MCP and SDK readiness states separately
|
||||||
|
- ChromaSync service properly tracks subprocess PIDs for Windows cleanup
|
||||||
|
- Worker wrapper uses Bun runtime with enhanced socket cleanup via process tree enumeration
|
||||||
|
- Increased timeouts on Windows platform (30s worker startup, 10s hook timeouts)
|
||||||
|
- Logger utility includes platform and PID information for better debugging
|
||||||
|
|
||||||
|
This represents a major reliability improvement for Windows users, eliminating common issues with worker startup failures, orphaned processes, and zombie sockets.
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.3.6...v7.3.7
|
||||||
|
|
||||||
|
## [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
|
## [7.3.4] - 2025-12-17
|
||||||
|
|
||||||
Patch release for bug fixes and minor improvements
|
Patch release for bug fixes and minor improvements
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.3.5",
|
"version": "7.4.1",
|
||||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.3.5",
|
"version": "7.4.1",
|
||||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Alex Newman"
|
"name": "Alex Newman"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem-plugin",
|
"name": "claude-mem-plugin",
|
||||||
"version": "7.3.5",
|
"version": "7.4.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||||
"type": "module",
|
"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.
@@ -212,3 +212,118 @@ help(topic="all") # Get complete guide
|
|||||||
- ALWAYS get timeline context to understand what was happening
|
- ALWAYS get timeline context to understand what was happening
|
||||||
- ALWAYS use `get_observations` when fetching 2+ observations
|
- ALWAYS use `get_observations` when fetching 2+ observations
|
||||||
- The workflow is optimized: search → timeline → batch fetch = 10-100x faster
|
- The workflow is optimized: search → timeline → batch fetch = 10-100x faster
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Reference
|
||||||
|
|
||||||
|
Comprehensive parameter documentation for all memory tools. For MCP usage, call `help(topic="search")` to load specific tool docs.
|
||||||
|
|
||||||
|
### search
|
||||||
|
|
||||||
|
Search across all memory types (observations, sessions, prompts).
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `query` (string, optional) - Search term for full-text search
|
||||||
|
- `limit` (number, optional) - Maximum results to return. Default: 20, Max: 100
|
||||||
|
- `offset` (number, optional) - Number of results to skip. Default: 0
|
||||||
|
- `project` (string, required) - Project name to filter by
|
||||||
|
- `type` (string, optional) - Filter by type: "observations", "sessions", "prompts"
|
||||||
|
- `dateStart` (string, optional) - Start date filter (YYYY-MM-DD or epoch ms)
|
||||||
|
- `dateEnd` (string, optional) - End date filter (YYYY-MM-DD or epoch ms)
|
||||||
|
- `obs_type` (string, optional) - Filter observations by type (comma-separated): bugfix, feature, decision, discovery, change
|
||||||
|
- `orderBy` (string, optional) - Sort order: "date_desc" (default), "date_asc", "relevance"
|
||||||
|
|
||||||
|
**Returns:** Table of results with IDs, timestamps, types, titles
|
||||||
|
|
||||||
|
### timeline
|
||||||
|
|
||||||
|
Get chronological context around a specific point in time or observation.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `anchor` (number, optional) - Observation ID to center timeline around. If not provided, uses most recent result from query
|
||||||
|
- `query` (string, optional) - Search term to find anchor automatically (if anchor not provided)
|
||||||
|
- `depth_before` (number, optional) - Items before anchor. Default: 5, Max: 20
|
||||||
|
- `depth_after` (number, optional) - Items after anchor. Default: 5, Max: 20
|
||||||
|
- `project` (string, required) - Project name to filter by
|
||||||
|
|
||||||
|
**Returns:** Exactly `depth_before + 1 + depth_after` items in chronological order, with observations, sessions, and prompts interleaved
|
||||||
|
|
||||||
|
### get_recent_context
|
||||||
|
|
||||||
|
Get the most recent observations from current or recent sessions.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `limit` (number, optional) - Maximum observations to return. Default: 10, Max: 50
|
||||||
|
- `project` (string, required) - Project name to filter by
|
||||||
|
|
||||||
|
**Returns:** Recent observations in reverse chronological order
|
||||||
|
|
||||||
|
### get_context_timeline
|
||||||
|
|
||||||
|
Get timeline context around a specific observation ID.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `anchor` (number, required) - Observation ID to center timeline around
|
||||||
|
- `depth_before` (number, optional) - Items before anchor. Default: 5, Max: 20
|
||||||
|
- `depth_after` (number, optional) - Items after anchor. Default: 5, Max: 20
|
||||||
|
- `project` (string, optional) - Project name to filter by
|
||||||
|
|
||||||
|
**Returns:** Timeline items centered on the anchor observation
|
||||||
|
|
||||||
|
### get_observation
|
||||||
|
|
||||||
|
Fetch a single observation by ID with full details.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `id` (number, required) - Observation ID to fetch
|
||||||
|
|
||||||
|
**Returns:** Complete observation object with title, subtitle, narrative, facts, concepts, files, timestamps
|
||||||
|
|
||||||
|
### get_observations
|
||||||
|
|
||||||
|
Batch fetch multiple observations by IDs. Always prefer this over individual fetches for 2+ observations.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `ids` (array of numbers, required) - Array of observation IDs to fetch
|
||||||
|
- `orderBy` (string, optional) - Sort order: "date_desc" (default), "date_asc"
|
||||||
|
- `limit` (number, optional) - Maximum observations to return. Default: no limit
|
||||||
|
- `project` (string, optional) - Project name to filter by
|
||||||
|
|
||||||
|
**Returns:** Array of complete observation objects, 10-100x faster than individual fetches
|
||||||
|
|
||||||
|
### get_session
|
||||||
|
|
||||||
|
Fetch a single session by ID with metadata.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `id` (number, required) - Session ID to fetch (just the number, not "S2005" format)
|
||||||
|
|
||||||
|
**Returns:** Session object with ID, start time, end time, project, model info
|
||||||
|
|
||||||
|
### get_prompt
|
||||||
|
|
||||||
|
Fetch a single prompt by ID with full text.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `id` (number, required) - Prompt ID to fetch
|
||||||
|
|
||||||
|
**Returns:** Prompt object with ID, text, timestamp, session reference
|
||||||
|
|
||||||
|
### help
|
||||||
|
|
||||||
|
Load detailed instructions for specific topics or all documentation.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `topic` (string, optional) - Specific topic to load: "workflow", "search", "timeline", "get_recent_context", "get_context_timeline", "get_observation", "get_observations", "get_session", "get_prompt", "all". Default: "all"
|
||||||
|
|
||||||
|
**Returns:** Formatted documentation for the requested topic
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ async function buildHooks() {
|
|||||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||||
},
|
},
|
||||||
banner: {
|
banner: {
|
||||||
js: '#!/usr/bin/env bun'
|
js: '#!/usr/bin/env node'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
* native module dependencies.
|
* native module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
|
||||||
import { stdin } from "process";
|
import { stdin } from "process";
|
||||||
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
||||||
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
|
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
|
||||||
import { handleWorkerError } from "../shared/hook-error-handler.js";
|
import { handleWorkerError } from "../shared/hook-error-handler.js";
|
||||||
import { handleFetchError } from "./shared/error-handler.js";
|
import { handleFetchError } from "./shared/error-handler.js";
|
||||||
|
import { getProjectName } from "../utils/project-name.js";
|
||||||
|
|
||||||
export interface SessionStartInput {
|
export interface SessionStartInput {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
@@ -25,7 +25,7 @@ async function contextHook(input?: SessionStartInput): Promise<string> {
|
|||||||
await ensureWorkerRunning();
|
await ensureWorkerRunning();
|
||||||
|
|
||||||
const cwd = input?.cwd ?? process.cwd();
|
const cwd = input?.cwd ?? process.cwd();
|
||||||
const project = cwd ? path.basename(cwd) : "unknown-project";
|
const project = getProjectName(cwd);
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
|
|
||||||
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
|
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 { stdin } from 'process';
|
||||||
import { createHookResponse } from './hook-response.js';
|
import { createHookResponse } from './hook-response.js';
|
||||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||||
import { handleFetchError } from './shared/error-handler.js';
|
import { handleFetchError } from './shared/error-handler.js';
|
||||||
|
import { getProjectName } from '../utils/project-name.js';
|
||||||
|
|
||||||
export interface UserPromptSubmitInput {
|
export interface UserPromptSubmitInput {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
@@ -24,7 +24,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { session_id, cwd, prompt } = input;
|
const { session_id, cwd, prompt } = input;
|
||||||
const project = path.basename(cwd);
|
const project = getProjectName(cwd);
|
||||||
|
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
|
|
||||||
|
|||||||
+186
-72
@@ -6,14 +6,18 @@
|
|||||||
* Maintains MCP protocol handling and tool schemas
|
* Maintains MCP protocol handling and tool schemas
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// CRITICAL: Redirect console.log to stderr BEFORE any imports
|
||||||
|
// MCP uses stdio transport where stdout is reserved for JSON-RPC protocol messages.
|
||||||
|
// Any logs to stdout break the protocol (Claude Desktop parses "[2025..." as JSON array).
|
||||||
|
const _originalConsoleLog = console.log;
|
||||||
|
console.log = (...args: any[]) => console.error(...args);
|
||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import {
|
import {
|
||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { z } from 'zod';
|
|
||||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||||
|
|
||||||
@@ -35,6 +39,72 @@ const TOOL_ENDPOINT_MAP: Record<string, string> = {
|
|||||||
'help': '/api/instructions'
|
'help': '/api/instructions'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailed parameter schemas for each tool
|
||||||
|
*/
|
||||||
|
const TOOL_SCHEMAS: Record<string, any> = {
|
||||||
|
search: {
|
||||||
|
query: { type: 'string', description: 'Full-text search query' },
|
||||||
|
type: { type: 'string', description: 'Filter by type: tool_use, tool_result, prompt, summary' },
|
||||||
|
obs_type: { type: 'string', description: 'Observation type filter' },
|
||||||
|
concepts: { type: 'string', description: 'Comma-separated concept tags' },
|
||||||
|
files: { type: 'string', description: 'Comma-separated file paths' },
|
||||||
|
project: { type: 'string', description: 'Project name filter' },
|
||||||
|
dateStart: { type: ['string', 'number'], description: 'Start date (ISO or timestamp)' },
|
||||||
|
dateEnd: { type: ['string', 'number'], description: 'End date (ISO or timestamp)' },
|
||||||
|
limit: { type: 'number', description: 'Max results (default: 10)' },
|
||||||
|
offset: { type: 'number', description: 'Result offset for pagination' },
|
||||||
|
orderBy: { type: 'string', description: 'Sort order: created_at, relevance' }
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
query: { type: 'string', description: 'Search query to find anchor point' },
|
||||||
|
anchor: { type: 'number', description: 'Observation ID as timeline center' },
|
||||||
|
depth_before: { type: 'number', description: 'Observations before anchor (default: 5)' },
|
||||||
|
depth_after: { type: 'number', description: 'Observations after anchor (default: 5)' },
|
||||||
|
type: { type: 'string', description: 'Filter by type' },
|
||||||
|
concepts: { type: 'string', description: 'Comma-separated concept tags' },
|
||||||
|
files: { type: 'string', description: 'Comma-separated file paths' },
|
||||||
|
project: { type: 'string', description: 'Project name filter' }
|
||||||
|
},
|
||||||
|
get_recent_context: {
|
||||||
|
limit: { type: 'number', description: 'Max results (default: 20)' },
|
||||||
|
type: { type: 'string', description: 'Filter by type' },
|
||||||
|
concepts: { type: 'string', description: 'Comma-separated concept tags' },
|
||||||
|
files: { type: 'string', description: 'Comma-separated file paths' },
|
||||||
|
project: { type: 'string', description: 'Project name filter' },
|
||||||
|
dateStart: { type: ['string', 'number'], description: 'Start date' },
|
||||||
|
dateEnd: { type: ['string', 'number'], description: 'End date' }
|
||||||
|
},
|
||||||
|
get_context_timeline: {
|
||||||
|
anchor: { type: 'number', description: 'Observation ID (required)', required: true },
|
||||||
|
depth_before: { type: 'number', description: 'Observations before anchor' },
|
||||||
|
depth_after: { type: 'number', description: 'Observations after anchor' },
|
||||||
|
type: { type: 'string', description: 'Filter by type' },
|
||||||
|
concepts: { type: 'string', description: 'Comma-separated concept tags' },
|
||||||
|
files: { type: 'string', description: 'Comma-separated file paths' },
|
||||||
|
project: { type: 'string', description: 'Project name filter' }
|
||||||
|
},
|
||||||
|
get_observations: {
|
||||||
|
ids: { type: 'array', items: { type: 'number' }, description: 'Array of observation IDs (required)', required: true },
|
||||||
|
orderBy: { type: 'string', description: 'Sort order' },
|
||||||
|
limit: { type: 'number', description: 'Max results' },
|
||||||
|
project: { type: 'string', description: 'Project filter' }
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
operation: { type: 'string', description: 'Operation type: "observations", "timeline", "sessions", etc.' },
|
||||||
|
topic: { type: 'string', description: 'Specific topic for help' }
|
||||||
|
},
|
||||||
|
get_observation: {
|
||||||
|
id: { type: 'number', description: 'Observation ID (required)', required: true }
|
||||||
|
},
|
||||||
|
get_session: {
|
||||||
|
id: { type: 'number', description: 'Session ID (required)', required: true }
|
||||||
|
},
|
||||||
|
get_prompt: {
|
||||||
|
id: { type: 'number', description: 'Prompt ID (required)', required: true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call Worker HTTP API endpoint
|
* Call Worker HTTP API endpoint
|
||||||
*/
|
*/
|
||||||
@@ -182,25 +252,47 @@ async function verifyWorkerConnection(): Promise<boolean> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool definitions with HTTP-based handlers
|
* Tool definitions with HTTP-based handlers
|
||||||
* Descriptions removed - use progressive_description tool for parameter documentation
|
* Minimal descriptions - use help() tool with operation parameter for detailed docs
|
||||||
*/
|
*/
|
||||||
const tools = [
|
const tools = [
|
||||||
|
{
|
||||||
|
name: 'get_schema',
|
||||||
|
description: 'Get parameter schema for a tool. Call get_schema(tool_name) for details',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: { tool_name: { type: 'string' } },
|
||||||
|
required: ['tool_name']
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
// Validate tool_name to prevent prototype pollution
|
||||||
|
const toolName = args.tool_name;
|
||||||
|
if (typeof toolName !== 'string' || !Object.hasOwn(TOOL_SCHEMAS, toolName)) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Unknown tool: ${toolName}\n\nAvailable tools: ${Object.keys(TOOL_SCHEMAS).join(', ')}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = TOOL_SCHEMAS[toolName];
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `# ${toolName} Parameters\n\n${JSON.stringify(schema, null, 2)}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'search',
|
name: 'search',
|
||||||
description: 'Search memory',
|
description: 'Search memory. All parameters optional - call get_schema("search") for details',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
query: z.string().optional(),
|
type: 'object',
|
||||||
type: z.enum(['observations', 'sessions', 'prompts']).optional(),
|
properties: {},
|
||||||
obs_type: z.string().optional(),
|
additionalProperties: true
|
||||||
concepts: z.string().optional(),
|
},
|
||||||
files: z.string().optional(),
|
|
||||||
project: z.string().optional(),
|
|
||||||
dateStart: z.union([z.string(), z.number()]).optional(),
|
|
||||||
dateEnd: z.union([z.string(), z.number()]).optional(),
|
|
||||||
limit: z.number().min(1).max(100).default(20),
|
|
||||||
offset: z.number().min(0).default(0),
|
|
||||||
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc')
|
|
||||||
}),
|
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['search'];
|
const endpoint = TOOL_ENDPOINT_MAP['search'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -208,17 +300,12 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeline',
|
name: 'timeline',
|
||||||
description: 'Timeline context',
|
description: 'Timeline context. All parameters optional - call get_schema("timeline") for details',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
query: z.string().optional(),
|
type: 'object',
|
||||||
anchor: z.number().optional(),
|
properties: {},
|
||||||
depth_before: z.number().min(0).max(100).default(10),
|
additionalProperties: true
|
||||||
depth_after: z.number().min(0).max(100).default(10),
|
},
|
||||||
type: z.string().optional(),
|
|
||||||
concepts: z.string().optional(),
|
|
||||||
files: z.string().optional(),
|
|
||||||
project: z.string().optional()
|
|
||||||
}),
|
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['timeline'];
|
const endpoint = TOOL_ENDPOINT_MAP['timeline'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -226,16 +313,12 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_recent_context',
|
name: 'get_recent_context',
|
||||||
description: 'Recent context',
|
description: 'Recent context. All parameters optional - call get_schema("get_recent_context") for details',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
limit: z.number().min(1).max(100).default(30),
|
type: 'object',
|
||||||
type: z.string().optional(),
|
properties: {},
|
||||||
concepts: z.string().optional(),
|
additionalProperties: true
|
||||||
files: z.string().optional(),
|
},
|
||||||
project: z.string().optional(),
|
|
||||||
dateStart: z.union([z.string(), z.number()]).optional(),
|
|
||||||
dateEnd: z.union([z.string(), z.number()]).optional()
|
|
||||||
}),
|
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['get_recent_context'];
|
const endpoint = TOOL_ENDPOINT_MAP['get_recent_context'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -243,16 +326,18 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_context_timeline',
|
name: 'get_context_timeline',
|
||||||
description: 'Timeline around ID',
|
description: 'Timeline around observation ID',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
anchor: z.number(),
|
type: 'object',
|
||||||
depth_before: z.number().min(0).max(100).default(10),
|
properties: {
|
||||||
depth_after: z.number().min(0).max(100).default(10),
|
anchor: {
|
||||||
type: z.string().optional(),
|
type: 'number',
|
||||||
concepts: z.string().optional(),
|
description: 'Observation ID (required). Optional params: get_schema("get_context_timeline")'
|
||||||
files: z.string().optional(),
|
}
|
||||||
project: z.string().optional()
|
},
|
||||||
}),
|
required: ['anchor'],
|
||||||
|
additionalProperties: true
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['get_context_timeline'];
|
const endpoint = TOOL_ENDPOINT_MAP['get_context_timeline'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -260,10 +345,12 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'help',
|
name: 'help',
|
||||||
description: 'Usage help',
|
description: 'Get detailed docs. All parameters optional - call get_schema("help") for details',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
topic: z.enum(['workflow', 'search_params', 'examples', 'all']).default('all')
|
type: 'object',
|
||||||
}),
|
properties: {},
|
||||||
|
additionalProperties: true
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
const endpoint = TOOL_ENDPOINT_MAP['help'];
|
const endpoint = TOOL_ENDPOINT_MAP['help'];
|
||||||
return await callWorkerAPI(endpoint, args);
|
return await callWorkerAPI(endpoint, args);
|
||||||
@@ -271,43 +358,70 @@ const tools = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_observation',
|
name: 'get_observation',
|
||||||
description: 'Fetch by ID',
|
description: 'Fetch observation by ID',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
id: z.number()
|
type: 'object',
|
||||||
}),
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Observation ID (required)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIWithPath('/api/observation', args.id);
|
return await callWorkerAPIWithPath('/api/observation', args.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_observations',
|
name: 'get_observations',
|
||||||
description: 'Batch fetch',
|
description: 'Batch fetch observations',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
ids: z.array(z.number()),
|
type: 'object',
|
||||||
orderBy: z.enum(['date_desc', 'date_asc']).optional(),
|
properties: {
|
||||||
limit: z.number().optional(),
|
ids: {
|
||||||
project: z.string().optional()
|
type: 'array',
|
||||||
}),
|
items: { type: 'number' },
|
||||||
|
description: 'Array of observation IDs (required). Optional params: get_schema("get_observations")'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['ids'],
|
||||||
|
additionalProperties: true
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIPost('/api/observations/batch', args);
|
return await callWorkerAPIPost('/api/observations/batch', args);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_session',
|
name: 'get_session',
|
||||||
description: 'Session by ID',
|
description: 'Fetch session by ID',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
id: z.number()
|
type: 'object',
|
||||||
}),
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Session ID (required)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIWithPath('/api/session', args.id);
|
return await callWorkerAPIWithPath('/api/session', args.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_prompt',
|
name: 'get_prompt',
|
||||||
description: 'Prompt by ID',
|
description: 'Fetch prompt by ID',
|
||||||
inputSchema: z.object({
|
inputSchema: {
|
||||||
id: z.number()
|
type: 'object',
|
||||||
}),
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Prompt ID (required)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
handler: async (args: any) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIWithPath('/api/prompt', args.id);
|
return await callWorkerAPIWithPath('/api/prompt', args.id);
|
||||||
}
|
}
|
||||||
@@ -333,7 +447,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||||||
tools: tools.map(tool => ({
|
tools: tools.map(tool => ({
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
inputSchema: zodToJsonSchema(tool.inputSchema) as Record<string, unknown>
|
inputSchema: tool.inputSchema
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
toRelativePath,
|
toRelativePath,
|
||||||
extractFirstFile
|
extractFirstFile
|
||||||
} from '../shared/timeline-formatting.js';
|
} 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
|
// 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');
|
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> {
|
export async function generateContext(input?: ContextInput, useColors: boolean = false): Promise<string> {
|
||||||
const config = loadContextConfig();
|
const config = loadContextConfig();
|
||||||
const cwd = input?.cwd ?? process.cwd();
|
const cwd = input?.cwd ?? process.cwd();
|
||||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
const project = getProjectName(cwd);
|
||||||
|
|
||||||
let db: SessionStore | null = null;
|
let db: SessionStore | null = null;
|
||||||
try {
|
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 }> {
|
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 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
|
// Check if process is still alive
|
||||||
if (!this.isProcessAlive(pid)) {
|
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 {
|
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)
|
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return { success: true, pid };
|
return { success: true, pid };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not ready yet
|
// Not ready yet, continue polling
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
|
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> {
|
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)
|
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
||||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||||
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
||||||
this.transport = new StdioClientTransport({
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
|
const transportOptions: any = {
|
||||||
command: 'uvx',
|
command: 'uvx',
|
||||||
args: [
|
args: [
|
||||||
'--python', pythonVersion,
|
'--python', pythonVersion,
|
||||||
@@ -110,7 +112,16 @@ export class ChromaSync {
|
|||||||
'--data-dir', this.VECTOR_DB_DIR
|
'--data-dir', this.VECTOR_DB_DIR
|
||||||
],
|
],
|
||||||
stderr: 'ignore'
|
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({
|
this.client = new Client({
|
||||||
name: 'claude-mem-chroma-sync',
|
name: 'claude-mem-chroma-sync',
|
||||||
|
|||||||
+125
-34
@@ -14,7 +14,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
import { exec } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -32,7 +32,7 @@ import { TimelineService } from './worker/TimelineService.js';
|
|||||||
import { SessionEventBroadcaster } from './worker/events/SessionEventBroadcaster.js';
|
import { SessionEventBroadcaster } from './worker/events/SessionEventBroadcaster.js';
|
||||||
|
|
||||||
// Import HTTP layer
|
// Import HTTP layer
|
||||||
import { createMiddleware, summarizeRequestBody as summarizeBody } from './worker/http/middleware.js';
|
import { createMiddleware, summarizeRequestBody as summarizeBody, requireLocalhost } from './worker/http/middleware.js';
|
||||||
import { ViewerRoutes } from './worker/http/routes/ViewerRoutes.js';
|
import { ViewerRoutes } from './worker/http/routes/ViewerRoutes.js';
|
||||||
import { SessionRoutes } from './worker/http/routes/SessionRoutes.js';
|
import { SessionRoutes } from './worker/http/routes/SessionRoutes.js';
|
||||||
import { DataRoutes } from './worker/http/routes/DataRoutes.js';
|
import { DataRoutes } from './worker/http/routes/DataRoutes.js';
|
||||||
@@ -45,6 +45,10 @@ export class WorkerService {
|
|||||||
private startTime: number = Date.now();
|
private startTime: number = Date.now();
|
||||||
private mcpClient: Client;
|
private mcpClient: Client;
|
||||||
|
|
||||||
|
// Initialization flags for MCP/SDK readiness tracking
|
||||||
|
private mcpReady: boolean = false;
|
||||||
|
private initializationCompleteFlag: boolean = false;
|
||||||
|
|
||||||
// Domain services
|
// Domain services
|
||||||
private dbManager: DatabaseManager;
|
private dbManager: DatabaseManager;
|
||||||
private sessionManager: SessionManager;
|
private sessionManager: SessionManager;
|
||||||
@@ -128,17 +132,36 @@ export class WorkerService {
|
|||||||
hasIpc: typeof process.send === 'function',
|
hasIpc: typeof process.send === 'function',
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
pid: process.pid,
|
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
|
// Version endpoint - returns the worker's current version
|
||||||
this.app.get('/api/version', (_req, res) => {
|
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 {
|
try {
|
||||||
// Read version from marketplace package.json
|
// 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'));
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||||
res.status(200).json({ version: packageJson.version });
|
res.status(200).json({ version: packageJson.version });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -155,26 +178,35 @@ export class WorkerService {
|
|||||||
// Instructions endpoint - loads SKILL.md sections on-demand for progressive instruction loading
|
// Instructions endpoint - loads SKILL.md sections on-demand for progressive instruction loading
|
||||||
this.app.get('/api/instructions', async (req, res) => {
|
this.app.get('/api/instructions', async (req, res) => {
|
||||||
const topic = (req.query.topic as string) || 'all';
|
const topic = (req.query.topic as string) || 'all';
|
||||||
// Read SKILL.md from plugin directory
|
const operation = req.query.operation as string | undefined;
|
||||||
|
|
||||||
// Path resolution: __dirname is build output directory (plugin/scripts/)
|
// Path resolution: __dirname is build output directory (plugin/scripts/)
|
||||||
// SKILL.md is at plugin/skills/mem-search/SKILL.md
|
// SKILL.md is at plugin/skills/mem-search/SKILL.md
|
||||||
const skillPath = path.join(__dirname, '../skills/mem-search/SKILL.md');
|
// Operations are at plugin/skills/mem-search/operations/*.md
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fullContent = await fs.promises.readFile(skillPath, 'utf-8');
|
let content: string;
|
||||||
|
|
||||||
// Extract section based on topic
|
if (operation) {
|
||||||
const section = this.extractInstructionSection(fullContent, topic);
|
// Load specific operation file
|
||||||
|
const operationPath = path.join(__dirname, '../skills/mem-search/operations', `${operation}.md`);
|
||||||
|
content = await fs.promises.readFile(operationPath, 'utf-8');
|
||||||
|
} else {
|
||||||
|
// Load SKILL.md and extract section based on topic (backward compatibility)
|
||||||
|
const skillPath = path.join(__dirname, '../skills/mem-search/SKILL.md');
|
||||||
|
const fullContent = await fs.promises.readFile(skillPath, 'utf-8');
|
||||||
|
content = this.extractInstructionSection(fullContent, topic);
|
||||||
|
}
|
||||||
|
|
||||||
// Return in MCP format
|
// Return in MCP format
|
||||||
res.json({
|
res.json({
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: section
|
text: content
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('WORKER', 'Failed to load instructions', { topic, skillPath }, error as Error);
|
logger.error('WORKER', 'Failed to load instructions', { topic, operation }, error as Error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -185,8 +217,8 @@ export class WorkerService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin endpoints for process management
|
// Admin endpoints for process management (localhost-only)
|
||||||
this.app.post('/api/admin/restart', async (_req, res) => {
|
this.app.post('/api/admin/restart', requireLocalhost, async (_req, res) => {
|
||||||
res.json({ status: 'restarting' });
|
res.json({ status: 'restarting' });
|
||||||
|
|
||||||
// On Windows, if managed by wrapper, send message to parent to handle restart
|
// On Windows, if managed by wrapper, send message to parent to handle restart
|
||||||
@@ -207,7 +239,7 @@ export class WorkerService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.post('/api/admin/shutdown', async (_req, res) => {
|
this.app.post('/api/admin/shutdown', requireLocalhost, async (_req, res) => {
|
||||||
res.json({ status: 'shutting_down' });
|
res.json({ status: 'shutting_down' });
|
||||||
|
|
||||||
// On Windows, if managed by wrapper, send message to parent to handle shutdown
|
// On Windows, if managed by wrapper, send message to parent to handle shutdown
|
||||||
@@ -295,25 +327,47 @@ export class WorkerService {
|
|||||||
*/
|
*/
|
||||||
private async cleanupOrphanedProcesses(): Promise<void> {
|
private async cleanupOrphanedProcesses(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Find all chroma-mcp processes
|
const isWindows = process.platform === 'win32';
|
||||||
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 pids: number[] = [];
|
const pids: number[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
if (isWindows) {
|
||||||
const parts = line.trim().split(/\s+/);
|
// Windows: Use PowerShell Get-CimInstance to find chroma-mcp processes
|
||||||
if (parts.length > 1) {
|
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`;
|
||||||
const pid = parseInt(parts[1], 10);
|
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||||
if (!isNaN(pid)) {
|
|
||||||
|
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);
|
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) {
|
if (pids.length === 0) {
|
||||||
@@ -321,12 +375,28 @@ export class WorkerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
|
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
|
||||||
|
platform: isWindows ? 'Windows' : 'Unix',
|
||||||
count: pids.length,
|
count: pids.length,
|
||||||
pids
|
pids
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kill all found processes
|
// 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 });
|
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pids.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -380,7 +450,7 @@ export class WorkerService {
|
|||||||
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
||||||
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
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 mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
command: 'node',
|
command: 'node',
|
||||||
@@ -388,10 +458,19 @@ export class WorkerService {
|
|||||||
env: process.env
|
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');
|
logger.success('WORKER', 'Connected to MCP server');
|
||||||
|
|
||||||
// Signal that initialization is complete
|
// Signal that initialization is complete
|
||||||
|
this.initializationCompleteFlag = true;
|
||||||
this.resolveInitialization();
|
this.resolveInitialization();
|
||||||
logger.info('SYSTEM', 'Background initialization complete');
|
logger.info('SYSTEM', 'Background initialization complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -492,6 +571,12 @@ export class WorkerService {
|
|||||||
return [];
|
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 {
|
try {
|
||||||
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
|
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
|
||||||
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||||
@@ -499,7 +584,7 @@ export class WorkerService {
|
|||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(s => parseInt(s.trim(), 10))
|
.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) {
|
} catch (error) {
|
||||||
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
|
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
|
||||||
return [];
|
return [];
|
||||||
@@ -510,6 +595,12 @@ export class WorkerService {
|
|||||||
* Force kill a process by PID (Windows: uses taskkill /F /T)
|
* Force kill a process by PID (Windows: uses taskkill /F /T)
|
||||||
*/
|
*/
|
||||||
private async forceKillProcess(pid: number): Promise<void> {
|
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 {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
// /T kills entire process tree, /F forces termination
|
// /T kills entire process tree, /F forces termination
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export class SDKAgent {
|
|||||||
// Calculate discovery tokens (delta for this response only)
|
// Calculate discovery tokens (delta for this response only)
|
||||||
const discoveryTokens = (session.cumulativeInputTokens + session.cumulativeOutputTokens) - tokensBeforeResponse;
|
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) {
|
if (responseSize > 0) {
|
||||||
const truncatedResponse = responseSize > 100
|
const truncatedResponse = responseSize > 100
|
||||||
? textContent.substring(0, 100) + '...'
|
? textContent.substring(0, 100) + '...'
|
||||||
@@ -125,6 +125,9 @@ export class SDKAgent {
|
|||||||
|
|
||||||
// Parse and process response with discovery token delta
|
// Parse and process response with discovery token delta
|
||||||
await this.processSDKResponse(session, textContent, worker, discoveryTokens);
|
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
|
// Mark messages as processed after successful observation/summary storage
|
||||||
// This prevents message loss if worker crashes before SDK finishes
|
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();
|
const pendingMessageStore = this.sessionManager.getPendingMessageStore();
|
||||||
if (session.pendingProcessingIds.size > 0) {
|
if (session.pendingProcessingIds.size > 0) {
|
||||||
for (const messageId of session.pendingProcessingIds) {
|
for (const messageId of session.pendingProcessingIds) {
|
||||||
|
|||||||
@@ -60,6 +60,34 @@ export function createMiddleware(
|
|||||||
return middlewares;
|
return middlewares;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to require localhost-only access
|
||||||
|
* Used for admin endpoints that should not be exposed when binding to 0.0.0.0
|
||||||
|
*/
|
||||||
|
export function requireLocalhost(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
const clientIp = req.ip || req.connection.remoteAddress || '';
|
||||||
|
const isLocalhost =
|
||||||
|
clientIp === '127.0.0.1' ||
|
||||||
|
clientIp === '::1' ||
|
||||||
|
clientIp === '::ffff:127.0.0.1' ||
|
||||||
|
clientIp === 'localhost';
|
||||||
|
|
||||||
|
if (!isLocalhost) {
|
||||||
|
logger.warn('SECURITY', 'Admin endpoint access denied - not localhost', {
|
||||||
|
endpoint: req.path,
|
||||||
|
clientIp,
|
||||||
|
method: req.method
|
||||||
|
});
|
||||||
|
res.status(403).json({
|
||||||
|
error: 'Forbidden',
|
||||||
|
message: 'Admin endpoints are only accessible from localhost'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Summarize request body for logging
|
* Summarize request body for logging
|
||||||
* Used to avoid logging sensitive data or large payloads
|
* Used to avoid logging sensitive data or large payloads
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync, existsSync } from 'fs';
|
||||||
import { getPackageRoot } from '../../../../shared/paths.js';
|
import { getPackageRoot } from '../../../../shared/paths.js';
|
||||||
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
||||||
import { DatabaseManager } from '../../DatabaseManager.js';
|
import { DatabaseManager } from '../../DatabaseManager.js';
|
||||||
@@ -41,7 +41,19 @@ export class ViewerRoutes extends BaseRouteHandler {
|
|||||||
*/
|
*/
|
||||||
private handleViewerUI = this.wrapHandler((req: Request, res: Response): void => {
|
private handleViewerUI = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
const packageRoot = getPackageRoot();
|
const packageRoot = getPackageRoot();
|
||||||
const viewerPath = path.join(packageRoot, 'plugin', 'ui', 'viewer.html');
|
|
||||||
|
// Try cache structure first (ui/viewer.html), then marketplace structure (plugin/ui/viewer.html)
|
||||||
|
const viewerPaths = [
|
||||||
|
path.join(packageRoot, 'ui', 'viewer.html'),
|
||||||
|
path.join(packageRoot, 'plugin', 'ui', 'viewer.html')
|
||||||
|
];
|
||||||
|
|
||||||
|
const viewerPath = viewerPaths.find(p => existsSync(p));
|
||||||
|
|
||||||
|
if (!viewerPath) {
|
||||||
|
throw new Error('Viewer UI not found at any expected location');
|
||||||
|
}
|
||||||
|
|
||||||
const html = readFileSync(viewerPath, 'utf-8');
|
const html = readFileSync(viewerPath, 'utf-8');
|
||||||
res.setHeader('Content-Type', 'text/html');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.send(html);
|
res.send(html);
|
||||||
|
|||||||
@@ -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> {
|
async function isWorkerHealthy(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const port = getWorkerPort();
|
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)
|
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug('SYSTEM', 'Worker health check failed', {
|
logger.debug('SYSTEM', 'Worker readiness check failed', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
errorType: error?.constructor?.name
|
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