Merge branch 'main' into fix/hook-resilience-worker-lifecycle
# Conflicts: # plugin/scripts/mcp-server.cjs # plugin/scripts/worker-service.cjs
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "9.1.1",
|
"version": "10.0.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"
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-53
@@ -2,6 +2,102 @@
|
|||||||
|
|
||||||
All notable changes to claude-mem.
|
All notable changes to claude-mem.
|
||||||
|
|
||||||
|
## [v10.0.1] - 2026-02-11
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### OpenClaw Observation Feed
|
||||||
|
- Enabled SSE observation feed for OpenClaw agent sessions, allowing real-time streaming of observations to connected OpenClaw clients
|
||||||
|
- Fixed `ObservationSSEPayload.project` type to be nullable, preventing type errors when project context is unavailable
|
||||||
|
- Added `EnvManager` support for OpenClaw environment configuration
|
||||||
|
|
||||||
|
### Build Artifacts
|
||||||
|
- Rebuilt worker service and MCP server with latest changes
|
||||||
|
|
||||||
|
## [v10.0.0] - 2026-02-11
|
||||||
|
|
||||||
|
## OpenClaw Plugin — Persistent Memory for OpenClaw Agents
|
||||||
|
|
||||||
|
Claude-mem now has an official [OpenClaw](https://openclaw.ai) plugin, bringing persistent memory to agents running on the OpenClaw gateway. This is a major milestone — claude-mem's memory system is no longer limited to Claude Code sessions.
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
|
||||||
|
The plugin bridges claude-mem's observation pipeline with OpenClaw's embedded runner (`pi-embedded`), which calls the Anthropic API directly without spawning a `claude` process. Three core capabilities:
|
||||||
|
|
||||||
|
1. **Observation Recording** — Captures every tool call from OpenClaw agents and sends it to the claude-mem worker for AI-powered compression and storage
|
||||||
|
2. **MEMORY.md Live Sync** — Writes a continuously-updated memory timeline to each agent's workspace, so agents start every session with full context from previous work
|
||||||
|
3. **Observation Feed** — Streams new observations to messaging channels (Telegram, Discord, Slack, Signal, WhatsApp, LINE) in real-time via SSE
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
Add claude-mem to your OpenClaw gateway config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"claude-mem": {
|
||||||
|
"enabled": true,
|
||||||
|
"config": {
|
||||||
|
"project": "my-project",
|
||||||
|
"syncMemoryFile": true,
|
||||||
|
"observationFeed": {
|
||||||
|
"enabled": true,
|
||||||
|
"channel": "telegram",
|
||||||
|
"to": "your-chat-id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The claude-mem worker service must be running on the same machine (`localhost:37777`).
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
- `/claude-mem-status` — Worker health check, active sessions, feed connection state
|
||||||
|
- `/claude-mem-feed` — Show/toggle observation feed status
|
||||||
|
- `/claude-mem-feed on|off` — Enable/disable feed
|
||||||
|
|
||||||
|
### How the Event Lifecycle Works
|
||||||
|
|
||||||
|
```
|
||||||
|
OpenClaw Gateway
|
||||||
|
├── session_start ──────────→ Init claude-mem session
|
||||||
|
├── before_agent_start ─────→ Sync MEMORY.md + track workspace
|
||||||
|
├── tool_result_persist ────→ Record observation + re-sync MEMORY.md
|
||||||
|
├── agent_end ──────────────→ Summarize + complete session
|
||||||
|
├── session_end ────────────→ Clean up session tracking
|
||||||
|
└── gateway_start ──────────→ Reset all tracking
|
||||||
|
```
|
||||||
|
|
||||||
|
All observation recording and MEMORY.md syncs are fire-and-forget — they never block the agent.
|
||||||
|
|
||||||
|
📖 Full documentation: [OpenClaw Integration Guide](https://docs.claude-mem.ai/docs/openclaw-integration)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Windows Platform Improvements
|
||||||
|
|
||||||
|
- **ProcessManager**: Migrated daemon spawning from deprecated WMIC to PowerShell `Start-Process` with `-WindowStyle Hidden`
|
||||||
|
- **ChromaSync**: Re-enabled vector search on Windows (was previously disabled entirely)
|
||||||
|
- **Worker Service**: Added unified DB-ready gate middleware — all DB-dependent endpoints now wait for initialization instead of returning "Database not initialized" errors
|
||||||
|
- **EnvManager**: Switched from fragile allowlist to simple blocklist for subprocess env vars (only strips `ANTHROPIC_API_KEY` per Issue #733)
|
||||||
|
|
||||||
|
## Session Management Fixes
|
||||||
|
|
||||||
|
- Fixed unbounded session tracking map growth — maps are now cleaned up on `session_end`
|
||||||
|
- Session init moved to `session_start` and `after_compaction` hooks for correct lifecycle handling
|
||||||
|
|
||||||
|
## SSE Fixes
|
||||||
|
|
||||||
|
- Fixed stream URL consistency across the codebase
|
||||||
|
- Fixed multi-line SSE data frame parsing (concatenates `data:` lines per SSE spec)
|
||||||
|
|
||||||
|
## Issue Triage
|
||||||
|
|
||||||
|
Closed 37+ duplicate/stale/invalid issues across multiple triage phases, significantly cleaning up the issue tracker.
|
||||||
|
|
||||||
## [v9.1.1] - 2026-02-07
|
## [v9.1.1] - 2026-02-07
|
||||||
|
|
||||||
## Critical Bug Fix: Worker Initialization Failure
|
## Critical Bug Fix: Worker Initialization Failure
|
||||||
@@ -1410,56 +1506,3 @@ Set in ~/.claude-mem/settings.json:
|
|||||||
|
|
||||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||||
|
|
||||||
## [v8.0.0] - 2025-12-23
|
|
||||||
|
|
||||||
## 🌍 Major Features
|
|
||||||
|
|
||||||
### **Mode System**: Context-aware observation capture tailored to different workflows
|
|
||||||
- **Code Development mode** (default): Tracks bugfixes, features, refactors, and more
|
|
||||||
- **Email Investigation mode**: Optimized for email analysis workflows
|
|
||||||
- Extensible architecture for custom domains
|
|
||||||
|
|
||||||
### **28 Language Support**: Full multilingual memory
|
|
||||||
- Arabic, Bengali, Chinese, Czech, Danish, Dutch, Finnish, French, German, Greek
|
|
||||||
- Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Norwegian, Polish
|
|
||||||
- Portuguese (Brazilian), Romanian, Russian, Spanish, Swedish, Thai, Turkish
|
|
||||||
- Ukrainian, Vietnamese
|
|
||||||
- All observations, summaries, and narratives generated in your chosen language
|
|
||||||
|
|
||||||
### **Inheritance Architecture**: Language modes inherit from base modes
|
|
||||||
- Consistent observation types across languages
|
|
||||||
- Locale-specific output while maintaining structural integrity
|
|
||||||
- JSON-based configuration for easy customization
|
|
||||||
|
|
||||||
## 🔧 Technical Improvements
|
|
||||||
|
|
||||||
- **ModeManager**: Centralized mode loading and configuration validation
|
|
||||||
- **Dynamic Prompts**: SDK prompts now adapt based on active mode
|
|
||||||
- **Mode-Specific Icons**: Observation types display contextual icons/emojis per mode
|
|
||||||
- **Fail-Fast Error Handling**: Complete removal of silent failures across all layers
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
- New docs/public/modes.mdx documenting the mode system
|
|
||||||
- 28 translated README files for multilingual community support
|
|
||||||
- Updated configuration guide for mode selection
|
|
||||||
|
|
||||||
## 🔨 Breaking Changes
|
|
||||||
|
|
||||||
- **None** - Mode system is fully backward compatible
|
|
||||||
- Default mode is 'code' (existing behavior)
|
|
||||||
- Settings: New `CLAUDE_MEM_MODE` option (defaults to 'code')
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.4.5...v8.0.0
|
|
||||||
**View PR**: https://github.com/thedotmack/claude-mem/pull/412
|
|
||||||
|
|
||||||
## [v7.4.5] - 2025-12-21
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
- Fix missing `formatDateTime` import in SearchManager that broke `get_context_timeline` mem-search function
|
|
||||||
|
|
||||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "claude-mem",
|
"id": "claude-mem",
|
||||||
"name": "Claude-Mem (Persistent Memory)",
|
"name": "Claude-Mem (Persistent Memory)",
|
||||||
"description": "Official OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
|
"description": "Official OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
|
||||||
"kind": "memory",
|
"kind": "integration",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "thedotmack",
|
"author": "thedotmack",
|
||||||
"homepage": "https://claude-mem.com",
|
"homepage": "https://claude-mem.com",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@claude-mem/openclaw-plugin",
|
"name": "@openclaw/claude-mem",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -11,5 +11,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.2.1",
|
"@types/node": "^25.2.1",
|
||||||
"typescript": "^5.3.0"
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"openclaw": {
|
||||||
|
"extensions": [
|
||||||
|
"./dist/index.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ describe("Observation I/O event handlers", () => {
|
|||||||
assert.equal(initRequests.length, 1, "should re-init after compaction");
|
assert.equal(initRequests.length, 1, "should re-init after compaction");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("before_agent_start does not call init", async () => {
|
it("before_agent_start calls init for session privacy check", async () => {
|
||||||
const { api, fireEvent } = createMockApi({ workerPort });
|
const { api, fireEvent } = createMockApi({ workerPort });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ describe("Observation I/O event handlers", () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init");
|
const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init");
|
||||||
assert.equal(initRequests.length, 0, "before_agent_start should not init");
|
assert.equal(initRequests.length, 1, "before_agent_start should init session");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tool_result_persist sends observation to worker", async () => {
|
it("tool_result_persist sends observation to worker", async () => {
|
||||||
|
|||||||
+67
-8
@@ -124,7 +124,7 @@ interface ObservationSSEPayload {
|
|||||||
concepts: string | null;
|
concepts: string | null;
|
||||||
files_read: string | null;
|
files_read: string | null;
|
||||||
files_modified: string | null;
|
files_modified: string | null;
|
||||||
project: string;
|
project: string | null;
|
||||||
prompt_number: number;
|
prompt_number: number;
|
||||||
created_at_epoch: number;
|
created_at_epoch: number;
|
||||||
}
|
}
|
||||||
@@ -160,6 +160,44 @@ const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
|
|||||||
const DEFAULT_WORKER_PORT = 37777;
|
const DEFAULT_WORKER_PORT = 37777;
|
||||||
const TOOL_RESULT_MAX_LENGTH = 1000;
|
const TOOL_RESULT_MAX_LENGTH = 1000;
|
||||||
|
|
||||||
|
// Agent emoji map for observation feed messages.
|
||||||
|
// When creating a new OpenClaw agent, add its agentId and emoji here.
|
||||||
|
const AGENT_EMOJI_MAP: Record<string, string> = {
|
||||||
|
"main": "🦞",
|
||||||
|
"openclaw": "🦞",
|
||||||
|
"devops": "🔧",
|
||||||
|
"architect": "📐",
|
||||||
|
"researcher": "🔍",
|
||||||
|
"code-reviewer": "🔎",
|
||||||
|
"coder": "💻",
|
||||||
|
"tester": "🧪",
|
||||||
|
"debugger": "🐛",
|
||||||
|
"opsec": "🛡️",
|
||||||
|
"cloudfarm": "☁️",
|
||||||
|
"extractor": "📦",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Project prefixes that indicate Claude Code sessions (not OpenClaw agents)
|
||||||
|
const CLAUDE_CODE_EMOJI = "⌨️";
|
||||||
|
const OPENCLAW_DEFAULT_EMOJI = "🦀";
|
||||||
|
|
||||||
|
function getSourceLabel(project: string | null | undefined): string {
|
||||||
|
if (!project) return OPENCLAW_DEFAULT_EMOJI;
|
||||||
|
// OpenClaw agent projects are formatted as "openclaw-<agentId>"
|
||||||
|
if (project.startsWith("openclaw-")) {
|
||||||
|
const agentId = project.slice("openclaw-".length);
|
||||||
|
const emoji = AGENT_EMOJI_MAP[agentId] || OPENCLAW_DEFAULT_EMOJI;
|
||||||
|
return `${emoji} ${agentId}`;
|
||||||
|
}
|
||||||
|
// OpenClaw project without agent suffix
|
||||||
|
if (project === "openclaw") {
|
||||||
|
return `🦞 openclaw`;
|
||||||
|
}
|
||||||
|
// Everything else is from Claude Code (project = working directory name)
|
||||||
|
const emoji = CLAUDE_CODE_EMOJI;
|
||||||
|
return `${emoji} ${project}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Worker HTTP Client
|
// Worker HTTP Client
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -233,7 +271,8 @@ async function workerGetText(
|
|||||||
|
|
||||||
function formatObservationMessage(observation: ObservationSSEPayload): string {
|
function formatObservationMessage(observation: ObservationSSEPayload): string {
|
||||||
const title = observation.title || "Untitled";
|
const title = observation.title || "Untitled";
|
||||||
let message = `🧠 Claude-Mem Observation\n**${title}**`;
|
const source = getSourceLabel(observation.project);
|
||||||
|
let message = `${source}\n**${title}**`;
|
||||||
if (observation.subtitle) {
|
if (observation.subtitle) {
|
||||||
message += `\n${observation.subtitle}`;
|
message += `\n${observation.subtitle}`;
|
||||||
}
|
}
|
||||||
@@ -387,7 +426,14 @@ async function connectToSSEStream(
|
|||||||
export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||||
const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig;
|
const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig;
|
||||||
const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT;
|
const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT;
|
||||||
const projectName = userConfig.project || "openclaw";
|
const baseProjectName = userConfig.project || "openclaw";
|
||||||
|
|
||||||
|
function getProjectName(ctx: EventContext): string {
|
||||||
|
if (ctx.agentId) {
|
||||||
|
return `openclaw-${ctx.agentId}`;
|
||||||
|
}
|
||||||
|
return baseProjectName;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Session tracking for observation I/O
|
// Session tracking for observation I/O
|
||||||
@@ -407,7 +453,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
||||||
const contextText = await workerGetText(
|
const contextText = await workerGetText(
|
||||||
workerPort,
|
workerPort,
|
||||||
`/api/context/inject?projects=${encodeURIComponent(projectName)}`,
|
`/api/context/inject?projects=${encodeURIComponent(baseProjectName)}`,
|
||||||
api.logger
|
api.logger
|
||||||
);
|
);
|
||||||
if (contextText && contextText.trim().length > 0) {
|
if (contextText && contextText.trim().length > 0) {
|
||||||
@@ -429,7 +475,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
|
|
||||||
await workerPost(workerPort, "/api/sessions/init", {
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
contentSessionId,
|
contentSessionId,
|
||||||
project: projectName,
|
project: getProjectName(ctx),
|
||||||
prompt: "",
|
prompt: "",
|
||||||
}, api.logger);
|
}, api.logger);
|
||||||
|
|
||||||
@@ -444,7 +490,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
|
|
||||||
await workerPost(workerPort, "/api/sessions/init", {
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
contentSessionId,
|
contentSessionId,
|
||||||
project: projectName,
|
project: getProjectName(ctx),
|
||||||
prompt: "",
|
prompt: "",
|
||||||
}, api.logger);
|
}, api.logger);
|
||||||
|
|
||||||
@@ -452,7 +498,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Event: before_agent_start — sync MEMORY.md + track workspace
|
// Event: before_agent_start — init session + sync MEMORY.md + track workspace
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
api.on("before_agent_start", async (_event, ctx) => {
|
api.on("before_agent_start", async (_event, ctx) => {
|
||||||
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
||||||
@@ -460,6 +506,15 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize session in the worker so observations are not skipped
|
||||||
|
// (the privacy check requires a stored user prompt to exist)
|
||||||
|
const contentSessionId = getContentSessionId(ctx.sessionKey);
|
||||||
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
|
contentSessionId,
|
||||||
|
project: getProjectName(ctx),
|
||||||
|
prompt: ctx.sessionKey || "agent run",
|
||||||
|
}, api.logger);
|
||||||
|
|
||||||
// Sync MEMORY.md before agent runs (provides context to agent)
|
// Sync MEMORY.md before agent runs (provides context to agent)
|
||||||
if (syncMemoryFile && ctx.workspaceDir) {
|
if (syncMemoryFile && ctx.workspaceDir) {
|
||||||
await syncMemoryToWorkspace(ctx.workspaceDir);
|
await syncMemoryToWorkspace(ctx.workspaceDir);
|
||||||
@@ -470,6 +525,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
// Event: tool_result_persist — record tool observations + sync MEMORY.md
|
// Event: tool_result_persist — record tool observations + sync MEMORY.md
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
api.on("tool_result_persist", (event, ctx) => {
|
api.on("tool_result_persist", (event, ctx) => {
|
||||||
|
api.logger.info(`[claude-mem] tool_result_persist fired: tool=${event.toolName ?? "unknown"} agent=${ctx.agentId ?? "none"} session=${ctx.sessionKey ?? "none"}`);
|
||||||
const toolName = event.toolName;
|
const toolName = event.toolName;
|
||||||
if (!toolName || toolName.startsWith("memory_")) return;
|
if (!toolName || toolName.startsWith("memory_")) return;
|
||||||
|
|
||||||
@@ -527,7 +583,10 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
workerPostFireAndForget(workerPort, "/api/sessions/summarize", {
|
// Await summarize so the worker receives it before complete.
|
||||||
|
// This also gives in-flight tool_result_persist observations time to arrive
|
||||||
|
// (they use fire-and-forget and may still be in transit).
|
||||||
|
await workerPost(workerPort, "/api/sessions/summarize", {
|
||||||
contentSessionId,
|
contentSessionId,
|
||||||
last_assistant_message: lastAssistantMessage,
|
last_assistant_message: lastAssistantMessage,
|
||||||
}, api.logger);
|
}, api.logger);
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "9.1.1",
|
"version": "10.0.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": "9.1.1",
|
"version": "10.0.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": "9.1.1",
|
"version": "10.0.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",
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export async function getChildProcesses(parentPid: number): Promise<number[]> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// PowerShell Get-Process instead of WMIC (deprecated in Windows 11)
|
// PowerShell Get-Process instead of WMIC (deprecated in Windows 11)
|
||||||
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { \\$_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty Id"`;
|
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty Id"`;
|
||||||
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
|
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
|
||||||
// PowerShell outputs just numbers (one per line), simpler than WMIC's "ProcessId=1234" format
|
// PowerShell outputs just numbers (one per line), simpler than WMIC's "ProcessId=1234" format
|
||||||
return stdout
|
return stdout
|
||||||
@@ -230,10 +230,10 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
|
|||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
// Windows: Use PowerShell Get-CimInstance with JSON output for age filtering
|
// Windows: Use PowerShell Get-CimInstance with JSON output for age filtering
|
||||||
const patternConditions = ORPHAN_PROCESS_PATTERNS
|
const patternConditions = ORPHAN_PROCESS_PATTERNS
|
||||||
.map(p => `\\$_.CommandLine -like '*${p}*'`)
|
.map(p => `$_.CommandLine -like '*${p}*'`)
|
||||||
.join(' -or ');
|
.join(' -or ');
|
||||||
|
|
||||||
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { (${patternConditions}) -and \\$_.ProcessId -ne ${currentPid} } | Select-Object ProcessId, CreationDate | ConvertTo-Json"`;
|
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { (${patternConditions}) -and $_.ProcessId -ne ${currentPid} } | Select-Object ProcessId, CreationDate | ConvertTo-Json"`;
|
||||||
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
|
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
|
||||||
|
|
||||||
if (!stdout.trim() || stdout.trim() === 'null') {
|
if (!stdout.trim() || stdout.trim() === 'null') {
|
||||||
@@ -343,9 +343,9 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
|
|||||||
* Spawn a detached daemon process
|
* Spawn a detached daemon process
|
||||||
* Returns the child PID or undefined if spawn failed
|
* Returns the child PID or undefined if spawn failed
|
||||||
*
|
*
|
||||||
* On Windows, uses WMIC to spawn a truly independent process that
|
* On Windows, uses PowerShell Start-Process with -WindowStyle Hidden to spawn
|
||||||
* survives parent exit without console popups. WMIC creates processes
|
* a truly independent process without console popups. Unlike WMIC, PowerShell
|
||||||
* that are not associated with the parent's console.
|
* inherits environment variables from the parent process.
|
||||||
*
|
*
|
||||||
* On Unix, uses standard detached spawn.
|
* On Unix, uses standard detached spawn.
|
||||||
*
|
*
|
||||||
@@ -365,21 +365,19 @@ export function spawnDaemon(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
// Use WMIC to spawn a process that's independent of the parent console
|
// Use PowerShell Start-Process to spawn a hidden, independent process
|
||||||
// This avoids the console popup that occurs with detached: true
|
// Unlike WMIC, PowerShell inherits environment variables from parent
|
||||||
// Paths must be individually quoted for WMIC when they contain spaces
|
// -WindowStyle Hidden prevents console popup
|
||||||
const execPath = process.execPath;
|
const execPath = process.execPath;
|
||||||
const script = scriptPath;
|
const script = scriptPath;
|
||||||
// WMIC command format: wmic process call create "\"path1\" \"path2\" args"
|
const psCommand = `Start-Process -FilePath '${execPath}' -ArgumentList '${script}','--daemon' -WindowStyle Hidden`;
|
||||||
const command = `wmic process call create "\\"${execPath}\\" \\"${script}\\" --daemon"`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(command, {
|
execSync(`powershell -NoProfile -Command "${psCommand}"`, {
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
windowsHide: true
|
windowsHide: true,
|
||||||
|
env
|
||||||
});
|
});
|
||||||
// WMIC returns immediately, we can't get the spawned PID easily
|
|
||||||
// Worker will write its own PID file after listen()
|
|
||||||
return 0;
|
return 0;
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -85,25 +85,15 @@ export class ChromaSync {
|
|||||||
private readonly VECTOR_DB_DIR: string;
|
private readonly VECTOR_DB_DIR: string;
|
||||||
private readonly BATCH_SIZE = 100;
|
private readonly BATCH_SIZE = 100;
|
||||||
|
|
||||||
// Windows: Chroma disabled due to MCP SDK spawning console popups
|
// Windows popup concern resolved: the worker daemon starts with -WindowStyle Hidden,
|
||||||
// See: https://github.com/anthropics/claude-mem/issues/675
|
// so child processes (uvx/chroma-mcp) inherit the hidden console and don't create new windows.
|
||||||
// Will be re-enabled when we migrate to persistent HTTP server
|
// MCP SDK's StdioClientTransport uses shell:false and no detached flag, so console is inherited.
|
||||||
private readonly disabled: boolean;
|
private readonly disabled: boolean = false;
|
||||||
|
|
||||||
constructor(project: string) {
|
constructor(project: string) {
|
||||||
this.project = project;
|
this.project = project;
|
||||||
this.collectionName = `cm__${project}`;
|
this.collectionName = `cm__${project}`;
|
||||||
this.VECTOR_DB_DIR = path.join(os.homedir(), '.claude-mem', 'vector-db');
|
this.VECTOR_DB_DIR = path.join(os.homedir(), '.claude-mem', 'vector-db');
|
||||||
|
|
||||||
// Disable on Windows to prevent console popups from MCP subprocess spawning
|
|
||||||
// The MCP SDK's StdioClientTransport spawns Python processes that create visible windows
|
|
||||||
this.disabled = process.platform === 'win32';
|
|
||||||
if (this.disabled) {
|
|
||||||
logger.warn('CHROMA_SYNC', 'Vector search disabled on Windows (prevents console popups)', {
|
|
||||||
project: this.project,
|
|
||||||
reason: 'MCP SDK subprocess spawning causes visible console windows'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,7 +193,6 @@ 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;
|
||||||
const isWindows = process.platform === 'win32';
|
|
||||||
|
|
||||||
// Get combined SSL certificate bundle for Zscaler/corporate proxy environments
|
// Get combined SSL certificate bundle for Zscaler/corporate proxy environments
|
||||||
const combinedCertPath = this.getCombinedCertPath();
|
const combinedCertPath = this.getCombinedCertPath();
|
||||||
@@ -232,12 +221,9 @@ export class ChromaSync {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL: On Windows, try to hide console window to prevent PowerShell popups
|
// Note: windowsHide is not needed here because the worker daemon starts with
|
||||||
// Note: windowsHide may not be supported by MCP SDK's StdioClientTransport
|
// -WindowStyle Hidden, so child processes inherit the hidden console.
|
||||||
if (isWindows) {
|
// The MCP SDK ignores custom windowsHide anyway (overridden internally).
|
||||||
transportOptions.windowsHide = true;
|
|
||||||
logger.debug('CHROMA_SYNC', 'Windows detected, attempting to hide console window', { project: this.project });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.transport = new StdioClientTransport(transportOptions);
|
this.transport = new StdioClientTransport(transportOptions);
|
||||||
|
|
||||||
|
|||||||
+40
-42
@@ -18,33 +18,15 @@ import { logger } from '../utils/logger.js';
|
|||||||
const DATA_DIR = join(homedir(), '.claude-mem');
|
const DATA_DIR = join(homedir(), '.claude-mem');
|
||||||
export const ENV_FILE_PATH = join(DATA_DIR, '.env');
|
export const ENV_FILE_PATH = join(DATA_DIR, '.env');
|
||||||
|
|
||||||
// Essential system environment variables that subprocesses need to function
|
// Environment variables to STRIP from subprocess environment (blocklist approach)
|
||||||
const ESSENTIAL_SYSTEM_VARS = [
|
// Only ANTHROPIC_API_KEY is stripped because it's the specific variable that causes
|
||||||
'PATH',
|
// Issue #733: project .env files set ANTHROPIC_API_KEY which the SDK auto-discovers,
|
||||||
'HOME',
|
// causing memory operations to bill personal API accounts instead of CLI subscription.
|
||||||
'USER',
|
//
|
||||||
'SHELL',
|
// All other env vars (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, system vars, etc.)
|
||||||
'TMPDIR',
|
// are passed through to avoid breaking CLI authentication, proxies, and platform features.
|
||||||
'TMP',
|
const BLOCKED_ENV_VARS = [
|
||||||
'TEMP',
|
'ANTHROPIC_API_KEY', // Issue #733: Prevent auto-discovery from project .env files
|
||||||
'LANG',
|
|
||||||
'LC_ALL',
|
|
||||||
'LC_CTYPE',
|
|
||||||
// Node.js specific
|
|
||||||
'NODE_ENV',
|
|
||||||
'NODE_PATH',
|
|
||||||
// Platform specific
|
|
||||||
'SYSTEMROOT', // Windows
|
|
||||||
'WINDIR', // Windows
|
|
||||||
'PROGRAMFILES', // Windows
|
|
||||||
'APPDATA', // Windows
|
|
||||||
'LOCALAPPDATA', // Windows
|
|
||||||
'XDG_RUNTIME_DIR', // Linux
|
|
||||||
'XDG_CONFIG_HOME', // Linux
|
|
||||||
'XDG_DATA_HOME', // Linux
|
|
||||||
// Claude Code specific (not credentials)
|
|
||||||
'CLAUDE_CONFIG_DIR',
|
|
||||||
'CLAUDE_CODE_DEBUG_LOGS_DIR',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Credential keys that claude-mem manages
|
// Credential keys that claude-mem manages
|
||||||
@@ -191,45 +173,58 @@ export function saveClaudeMemEnv(env: ClaudeMemEnv): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a clean, isolated environment for spawning SDK subprocesses
|
* Build a clean environment for spawning SDK subprocesses
|
||||||
*
|
*
|
||||||
* This is the key function that prevents Issue #733:
|
* Uses a BLOCKLIST approach: inherits the full process environment but strips
|
||||||
* - Includes only essential system variables (PATH, HOME, etc.)
|
* only ANTHROPIC_API_KEY to prevent Issue #733 (accidental billing from project .env files).
|
||||||
* - Adds credentials ONLY from claude-mem's .env file
|
|
||||||
* - Does NOT inherit random ANTHROPIC_API_KEY from user's shell
|
|
||||||
*
|
*
|
||||||
* @param includeCredentials - Whether to include API keys (default: true)
|
* All other variables pass through, including:
|
||||||
|
* - ANTHROPIC_AUTH_TOKEN (CLI subscription auth)
|
||||||
|
* - ANTHROPIC_BASE_URL (custom proxy endpoints)
|
||||||
|
* - Platform-specific vars (USERPROFILE, XDG_*, etc.)
|
||||||
|
*
|
||||||
|
* If claude-mem has an explicit ANTHROPIC_API_KEY in ~/.claude-mem/.env, it's re-injected
|
||||||
|
* after stripping, so the managed credential takes precedence over any ambient value.
|
||||||
|
*
|
||||||
|
* @param includeCredentials - Whether to include API keys from ~/.claude-mem/.env (default: true)
|
||||||
*/
|
*/
|
||||||
export function buildIsolatedEnv(includeCredentials: boolean = true): Record<string, string> {
|
export function buildIsolatedEnv(includeCredentials: boolean = true): Record<string, string> {
|
||||||
|
// 1. Start with full process environment
|
||||||
const isolatedEnv: Record<string, string> = {};
|
const isolatedEnv: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(process.env)) {
|
||||||
// 1. Copy essential system variables from current process
|
if (value !== undefined && !BLOCKED_ENV_VARS.includes(key)) {
|
||||||
for (const key of ESSENTIAL_SYSTEM_VARS) {
|
|
||||||
const value = process.env[key];
|
|
||||||
if (value !== undefined) {
|
|
||||||
isolatedEnv[key] = value;
|
isolatedEnv[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Add SDK entrypoint marker
|
// 2. Override SDK entrypoint marker
|
||||||
isolatedEnv.CLAUDE_CODE_ENTRYPOINT = 'sdk-ts';
|
isolatedEnv.CLAUDE_CODE_ENTRYPOINT = 'sdk-ts';
|
||||||
|
|
||||||
// 3. Add credentials from claude-mem's .env file (NOT from process.env)
|
// 3. Re-inject managed credentials from claude-mem's .env file
|
||||||
if (includeCredentials) {
|
if (includeCredentials) {
|
||||||
const credentials = loadClaudeMemEnv();
|
const credentials = loadClaudeMemEnv();
|
||||||
|
|
||||||
// Only add ANTHROPIC_API_KEY if explicitly configured in claude-mem
|
// Only add ANTHROPIC_API_KEY if explicitly configured in claude-mem
|
||||||
// If not configured, CLI billing will be used (via pathToClaudeCodeExecutable)
|
// If not configured, CLI billing will be used (via ANTHROPIC_AUTH_TOKEN passthrough)
|
||||||
if (credentials.ANTHROPIC_API_KEY) {
|
if (credentials.ANTHROPIC_API_KEY) {
|
||||||
isolatedEnv.ANTHROPIC_API_KEY = credentials.ANTHROPIC_API_KEY;
|
isolatedEnv.ANTHROPIC_API_KEY = credentials.ANTHROPIC_API_KEY;
|
||||||
}
|
}
|
||||||
// Note: GEMINI_API_KEY and OPENROUTER_API_KEY are handled by their respective agents
|
// Note: GEMINI_API_KEY and OPENROUTER_API_KEY pass through from process.env,
|
||||||
|
// but claude-mem's .env takes precedence if configured
|
||||||
if (credentials.GEMINI_API_KEY) {
|
if (credentials.GEMINI_API_KEY) {
|
||||||
isolatedEnv.GEMINI_API_KEY = credentials.GEMINI_API_KEY;
|
isolatedEnv.GEMINI_API_KEY = credentials.GEMINI_API_KEY;
|
||||||
}
|
}
|
||||||
if (credentials.OPENROUTER_API_KEY) {
|
if (credentials.OPENROUTER_API_KEY) {
|
||||||
isolatedEnv.OPENROUTER_API_KEY = credentials.OPENROUTER_API_KEY;
|
isolatedEnv.OPENROUTER_API_KEY = credentials.OPENROUTER_API_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Pass through Claude CLI's OAuth token if available (fallback for CLI subscription billing)
|
||||||
|
// When no ANTHROPIC_API_KEY is configured, the spawned CLI uses subscription billing
|
||||||
|
// which requires either ~/.claude/.credentials.json or CLAUDE_CODE_OAUTH_TOKEN.
|
||||||
|
// The worker inherits this token from the Claude Code session that started it.
|
||||||
|
if (!isolatedEnv.ANTHROPIC_API_KEY && process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||||
|
isolatedEnv.CLAUDE_CODE_OAUTH_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isolatedEnv;
|
return isolatedEnv;
|
||||||
@@ -270,5 +265,8 @@ export function getAuthMethodDescription(): string {
|
|||||||
if (hasAnthropicApiKey()) {
|
if (hasAnthropicApiKey()) {
|
||||||
return 'API key (from ~/.claude-mem/.env)';
|
return 'API key (from ~/.claude-mem/.env)';
|
||||||
}
|
}
|
||||||
|
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||||
|
return 'Claude Code OAuth token (from parent process)';
|
||||||
|
}
|
||||||
return 'Claude Code CLI (subscription billing)';
|
return 'Claude Code CLI (subscription billing)';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user