Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5de728612e | |||
| 64019ee28d | |||
| 6cad77328b | |||
| 26ac35ad40 | |||
| 31514d1943 | |||
| 52ea452010 | |||
| c469e0acc3 | |||
| f05f9ca735 | |||
| 1d76f93304 | |||
| 05e904e613 | |||
| 095f6fde47 | |||
| 1130bbc090 | |||
| 76f984ce7c | |||
| 6535ad597f | |||
| 40f81b4d2b | |||
| 54ca601e8f | |||
| de549cac05 | |||
| 83d474b13d | |||
| 9480ef06ab | |||
| c099e8eb27 | |||
| 1325f05432 | |||
| cfd19ae232 | |||
| cb6ff8738b | |||
| 81ebb8b6c0 | |||
| 9bdd00ea5a | |||
| 6f35e543ca | |||
| ee61270e1b | |||
| e902b74267 | |||
| 3eb6d9ea8e | |||
| 98d87d7573 | |||
| f7fea1f779 | |||
| 1f834863a7 | |||
| e4846a2046 | |||
| 0dda593c45 | |||
| 1bfb473c19 | |||
| 3f01baebfe | |||
| 46b61857ab | |||
| 2c5c99c0c7 | |||
| a3f9e7f638 |
@@ -10,7 +10,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "10.0.2",
|
"version": "10.0.6",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-49
@@ -2,6 +2,114 @@
|
|||||||
|
|
||||||
All notable changes to claude-mem.
|
All notable changes to claude-mem.
|
||||||
|
|
||||||
|
## [v10.0.5] - 2026-02-13
|
||||||
|
|
||||||
|
## OpenClaw Installer & Distribution
|
||||||
|
|
||||||
|
This release introduces the OpenClaw one-liner installer and fixes several OpenClaw plugin issues.
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- **OpenClaw Installer** (`openclaw/install.sh`): Full cross-platform installer script with `curl | bash` support
|
||||||
|
- Platform detection (macOS, Linux, WSL)
|
||||||
|
- Automatic dependency management (Bun, uv, Node.js)
|
||||||
|
- Interactive AI provider setup with settings writer
|
||||||
|
- OpenClaw gateway detection, plugin install, and memory slot configuration
|
||||||
|
- Worker startup and health verification with rich diagnostics
|
||||||
|
- TTY detection, `--provider`/`--api-key` CLI flags
|
||||||
|
- Error recovery and upgrade handling for existing installations
|
||||||
|
- jq/python3/node fallback chain for JSON config writing
|
||||||
|
- **Distribution readiness tests** (`openclaw/test-install.sh`): Comprehensive test suite for the installer
|
||||||
|
- **Enhanced `/api/health` endpoint**: Now returns version, uptime, workerPath, and AI status
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: use `event.prompt` instead of `ctx.sessionKey` for prompt storage in OpenClaw plugin
|
||||||
|
- Fix: detect both `openclaw` and `openclaw.mjs` binary names in gateway discovery
|
||||||
|
- Fix: pass file paths via env vars instead of bash interpolation in `node -e` calls
|
||||||
|
- Fix: handle stale plugin config that blocks OpenClaw CLI during reinstall
|
||||||
|
- Fix: remove stale memory slot reference during reinstall cleanup
|
||||||
|
- Fix: remove opinionated filters from OpenClaw plugin
|
||||||
|
|
||||||
|
## [v10.0.4] - 2026-02-12
|
||||||
|
|
||||||
|
## Revert: v10.0.3 chroma-mcp spawn storm fix
|
||||||
|
|
||||||
|
v10.0.3 introduced regressions. This release reverts the codebase to the stable v10.0.2 state.
|
||||||
|
|
||||||
|
### What was reverted
|
||||||
|
|
||||||
|
- Connection mutex via promise memoization
|
||||||
|
- Pre-spawn process count guard
|
||||||
|
- Hardened `close()` with try-finally + Unix `pkill -P` fallback
|
||||||
|
- Count-based orphan reaper in `ProcessManager`
|
||||||
|
- Circuit breaker (3 failures → 60s cooldown)
|
||||||
|
- `etime`-based sorting for process guards
|
||||||
|
|
||||||
|
### Files restored to v10.0.2
|
||||||
|
|
||||||
|
- `src/services/sync/ChromaSync.ts`
|
||||||
|
- `src/services/infrastructure/GracefulShutdown.ts`
|
||||||
|
- `src/services/infrastructure/ProcessManager.ts`
|
||||||
|
- `src/services/worker-service.ts`
|
||||||
|
- `src/services/worker/ProcessRegistry.ts`
|
||||||
|
- `tests/infrastructure/process-manager.test.ts`
|
||||||
|
- `tests/integration/chroma-vector-sync.test.ts`
|
||||||
|
|
||||||
|
## [v10.0.3] - 2026-02-11
|
||||||
|
|
||||||
|
## Fix: Prevent chroma-mcp spawn storm (PR #1065)
|
||||||
|
|
||||||
|
Fixes a critical bug where killing the worker daemon during active sessions caused **641 chroma-mcp Python processes** to spawn in ~5 minutes, consuming 75%+ CPU and ~64GB virtual memory.
|
||||||
|
|
||||||
|
### Root Cause
|
||||||
|
|
||||||
|
`ChromaSync.ensureConnection()` had no connection mutex. Concurrent fire-and-forget `syncObservation()` calls from multiple sessions raced through the check-then-act guard, each spawning a chroma-mcp subprocess via `StdioClientTransport`. Error-driven reconnection created a positive feedback loop.
|
||||||
|
|
||||||
|
### 5-Layer Defense
|
||||||
|
|
||||||
|
| Layer | Mechanism | Purpose |
|
||||||
|
|-------|-----------|---------|
|
||||||
|
| **0** | Connection mutex via promise memoization | Coalesces concurrent callers onto a single spawn attempt |
|
||||||
|
| **1** | Pre-spawn process count guard (`execFileSync('ps')`) | Kills excess chroma-mcp processes before spawning new ones |
|
||||||
|
| **2** | Hardened `close()` with try-finally + Unix `pkill -P` fallback | Guarantees state reset even on error, kills orphaned children |
|
||||||
|
| **3** | Count-based orphan reaper in `ProcessManager` | Kills by count (not age), catches spawn storms where all processes are young |
|
||||||
|
| **4** | Circuit breaker (3 failures → 60s cooldown) | Stops error-driven reconnection positive feedback loop |
|
||||||
|
|
||||||
|
### Additional Fix
|
||||||
|
|
||||||
|
- Process guards now use `etime`-based sorting instead of PID ordering for reliable age determination (PIDs wrap and don't guarantee ordering)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- 16 new tests for mutex, circuit breaker, close() hardening, and count guard
|
||||||
|
- All tests pass (947 pass, 3 skip)
|
||||||
|
|
||||||
|
Closes #1063, closes #695. Relates to #1010, #707.
|
||||||
|
|
||||||
|
**Contributors:** @rodboev
|
||||||
|
|
||||||
|
## [v10.0.2] - 2026-02-11
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
- **Prevent daemon silent death from SIGHUP + unhandled errors** — Worker process could silently die when receiving SIGHUP signals or encountering unhandled errors, leaving hooks without a backend. Now properly handles these signals and prevents silent crashes.
|
||||||
|
- **Hook resilience and worker lifecycle improvements** — Comprehensive fixes for hook command error classification, addressing issues #957, #923, #984, #987, and #1042. Hooks now correctly distinguish between worker unavailability errors and other failures.
|
||||||
|
- **Clarify TypeError order dependency in error classifier** — Fixed error classification logic to properly handle TypeError ordering edge cases.
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
- **Project-scoped statusline counter utility** — Added `statusline-counts.js` for tracking observation counts per project in the Claude Code status line.
|
||||||
|
|
||||||
|
## Internal
|
||||||
|
|
||||||
|
- Added test coverage for hook command error classification and process manager
|
||||||
|
- Worker service and MCP server lifecycle improvements
|
||||||
|
- Process manager enhancements for better cross-platform stability
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
- @rodboev — Hook resilience and worker lifecycle fixes (PR #1056)
|
||||||
|
|
||||||
## [v10.0.1] - 2026-02-11
|
## [v10.0.1] - 2026-02-11
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
@@ -1457,52 +1565,3 @@ This patch release improves stability by adding proper error handling to Chroma
|
|||||||
|
|
||||||
Refactored context loading logic to differentiate between code and non-code modes, resolving issues where mode-specific observations were filtered by stale settings.
|
Refactored context loading logic to differentiate between code and non-code modes, resolving issues where mode-specific observations were filtered by stale settings.
|
||||||
|
|
||||||
## [v8.0.4] - 2025-12-23
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
- Changed worker start script
|
|
||||||
|
|
||||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
||||||
|
|
||||||
## [v8.0.3] - 2025-12-23
|
|
||||||
|
|
||||||
Fix critical worker crashes on startup (v8.0.2 regression)
|
|
||||||
|
|
||||||
## [v8.0.2] - 2025-12-23
|
|
||||||
|
|
||||||
New "chill" remix of code mode for users who want fewer, more selective observations.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **code--chill mode**: A behavioral variant that produces fewer observations
|
|
||||||
- Only records things "painful to rediscover" - shipped features, architectural decisions, non-obvious gotchas
|
|
||||||
- Skips routine work, straightforward implementations, and obvious changes
|
|
||||||
- Philosophy: "When in doubt, skip it"
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- Updated modes.mdx with all 28 language modes (was 10)
|
|
||||||
- Added Code Mode Variants section documenting chill mode
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Set in ~/.claude-mem/settings.json:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"CLAUDE_MEM_MODE": "code--chill"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## [v8.0.1] - 2025-12-23
|
|
||||||
|
|
||||||
## 🎨 UI Improvements
|
|
||||||
|
|
||||||
- **Header Redesign**: Moved documentation and X (Twitter) links from settings modal to main header for better accessibility
|
|
||||||
- **Removed Product Hunt Badge**: Cleaned up header layout by removing the Product Hunt badge
|
|
||||||
- **Icon Reorganization**: Reordered header icons for improved UX flow (Docs → X → Discord → GitHub)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
||||||
|
|
||||||
|
|||||||
+48
-8
@@ -1,8 +1,46 @@
|
|||||||
# Claude-Mem OpenClaw Plugin — Setup Guide
|
# Claude-Mem OpenClaw Plugin — Setup Guide
|
||||||
|
|
||||||
This guide walks through setting up the claude-mem plugin on an OpenClaw gateway from scratch. Follow every step in order. By the end, your agents will have persistent memory across sessions, a live-updating MEMORY.md in their workspace, and optionally a real-time observation feed streaming to a messaging channel.
|
This guide walks through setting up the claude-mem plugin on an OpenClaw gateway. By the end, your agents will have persistent memory across sessions, a live-updating MEMORY.md in their workspace, and optionally a real-time observation feed streaming to a messaging channel.
|
||||||
|
|
||||||
## Step 1: Clone the Claude-Mem Repo
|
## Quick Install (Recommended)
|
||||||
|
|
||||||
|
Run this one-liner to install everything automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer handles dependency checks (Bun, uv), plugin installation, memory slot configuration, AI provider setup, worker startup, and optional observation feed configuration — all interactively.
|
||||||
|
|
||||||
|
### Install with options
|
||||||
|
|
||||||
|
Pre-select your AI provider and API key to skip interactive prompts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --provider=gemini --api-key=YOUR_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
For fully unattended installation (defaults to Claude Max Plan, skips observation feed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
To upgrade an existing installation (preserves settings, updates plugin):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation, skip to [Step 4: Restart the Gateway and Verify](#step-4-restart-the-gateway-and-verify) to confirm everything is working.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Setup
|
||||||
|
|
||||||
|
The steps below are for manual installation if you prefer not to use the automated installer, or need to troubleshoot individual steps.
|
||||||
|
|
||||||
|
### Step 1: Clone the Claude-Mem Repo
|
||||||
|
|
||||||
First, clone the claude-mem repository to a location accessible by your OpenClaw gateway. This gives you the worker service source and the plugin code.
|
First, clone the claude-mem repository to a location accessible by your OpenClaw gateway. This gives you the worker service source and the plugin code.
|
||||||
|
|
||||||
@@ -20,11 +58,11 @@ You'll need **bun** installed for the worker service. If you don't have it:
|
|||||||
curl -fsSL https://bun.sh/install | bash
|
curl -fsSL https://bun.sh/install | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2: Get the Worker Running
|
### Step 2: Get the Worker Running
|
||||||
|
|
||||||
The claude-mem worker is an HTTP service on port 37777. It stores observations, generates summaries, and serves the context timeline. The plugin talks to it over HTTP — it doesn't matter where the worker is running, just that it's reachable on localhost:37777.
|
The claude-mem worker is an HTTP service on port 37777. It stores observations, generates summaries, and serves the context timeline. The plugin talks to it over HTTP — it doesn't matter where the worker is running, just that it's reachable on localhost:37777.
|
||||||
|
|
||||||
### Check if it's already running
|
#### Check if it's already running
|
||||||
|
|
||||||
If this machine also runs Claude Code with claude-mem installed, the worker may already be running:
|
If this machine also runs Claude Code with claude-mem installed, the worker may already be running:
|
||||||
|
|
||||||
@@ -36,7 +74,7 @@ curl http://localhost:37777/api/health
|
|||||||
|
|
||||||
**Got connection refused or no response?** The worker isn't running. Continue below.
|
**Got connection refused or no response?** The worker isn't running. Continue below.
|
||||||
|
|
||||||
### If Claude Code has claude-mem installed
|
#### If Claude Code has claude-mem installed
|
||||||
|
|
||||||
If claude-mem is installed as a Claude Code plugin (at `~/.claude/plugins/marketplaces/thedotmack/`), start the worker from that installation:
|
If claude-mem is installed as a Claude Code plugin (at `~/.claude/plugins/marketplaces/thedotmack/`), start the worker from that installation:
|
||||||
|
|
||||||
@@ -54,7 +92,7 @@ curl http://localhost:37777/api/health
|
|||||||
|
|
||||||
**Still not working?** Check `npm run worker:status` for error details, or check that bun is installed and on your PATH.
|
**Still not working?** Check `npm run worker:status` for error details, or check that bun is installed and on your PATH.
|
||||||
|
|
||||||
### If there's no Claude Code installation
|
#### If there's no Claude Code installation
|
||||||
|
|
||||||
Run the worker from the cloned repo:
|
Run the worker from the cloned repo:
|
||||||
|
|
||||||
@@ -77,7 +115,7 @@ curl http://localhost:37777/api/health
|
|||||||
- Check logs: `npm run worker:logs` (if available)
|
- Check logs: `npm run worker:logs` (if available)
|
||||||
- Try running it directly to see errors: `bun plugin/scripts/worker-service.cjs start`
|
- Try running it directly to see errors: `bun plugin/scripts/worker-service.cjs start`
|
||||||
|
|
||||||
## Step 3: Add the Plugin to Your Gateway
|
### Step 3: Add the Plugin to Your Gateway
|
||||||
|
|
||||||
Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
||||||
|
|
||||||
@@ -96,7 +134,7 @@ Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config fields explained
|
#### Config fields explained
|
||||||
|
|
||||||
- **`project`** (string, default: `"openclaw"`) — The project name that scopes all observations in the memory database. Use a unique name per gateway/use-case so observations don't mix. For example, if this gateway runs a coding bot, use `"coding-bot"`.
|
- **`project`** (string, default: `"openclaw"`) — The project name that scopes all observations in the memory database. Use a unique name per gateway/use-case so observations don't mix. For example, if this gateway runs a coding bot, use `"coding-bot"`.
|
||||||
|
|
||||||
@@ -104,6 +142,8 @@ Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
|||||||
|
|
||||||
- **`workerPort`** (number, default: `37777`) — The port where the claude-mem worker service is listening. Only change this if you configured the worker to use a different port.
|
- **`workerPort`** (number, default: `37777`) — The port where the claude-mem worker service is listening. Only change this if you configured the worker to use a different port.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 4: Restart the Gateway and Verify
|
## Step 4: Restart the Gateway and Verify
|
||||||
|
|
||||||
Restart your OpenClaw gateway so it picks up the new plugin configuration. After restart, check the gateway logs for:
|
Restart your OpenClaw gateway so it picks up the new plugin configuration. After restart, check the gateway logs for:
|
||||||
|
|||||||
Executable
+1801
File diff suppressed because it is too large
Load Diff
@@ -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": "integration",
|
"kind": "memory",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "thedotmack",
|
"author": "thedotmack",
|
||||||
"homepage": "https://claude-mem.com",
|
"homepage": "https://claude-mem.com",
|
||||||
@@ -41,6 +41,10 @@
|
|||||||
"to": {
|
"to": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Target chat/user ID to send observations to"
|
"description": "Target chat/user ID to send observations to"
|
||||||
|
},
|
||||||
|
"botToken": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional dedicated Telegram bot token for the feed (bypasses gateway channel)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-19
@@ -67,13 +67,27 @@ interface SessionEndEvent {
|
|||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MessageReceivedEvent {
|
||||||
|
from: string;
|
||||||
|
content: string;
|
||||||
|
timestamp?: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
interface EventContext {
|
interface EventContext {
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MessageContext {
|
||||||
|
channelId: string;
|
||||||
|
accountId?: string;
|
||||||
|
conversationId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
type EventCallback<T> = (event: T, ctx: EventContext) => void | Promise<void>;
|
type EventCallback<T> = (event: T, ctx: EventContext) => void | Promise<void>;
|
||||||
|
type MessageEventCallback<T> = (event: T, ctx: MessageContext) => void | Promise<void>;
|
||||||
|
|
||||||
interface OpenClawPluginApi {
|
interface OpenClawPluginApi {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -100,6 +114,7 @@ interface OpenClawPluginApi {
|
|||||||
((event: "agent_end", callback: EventCallback<AgentEndEvent>) => void) &
|
((event: "agent_end", callback: EventCallback<AgentEndEvent>) => void) &
|
||||||
((event: "session_start", callback: EventCallback<SessionStartEvent>) => void) &
|
((event: "session_start", callback: EventCallback<SessionStartEvent>) => void) &
|
||||||
((event: "session_end", callback: EventCallback<SessionEndEvent>) => void) &
|
((event: "session_end", callback: EventCallback<SessionEndEvent>) => void) &
|
||||||
|
((event: "message_received", callback: MessageEventCallback<MessageReceivedEvent>) => void) &
|
||||||
((event: "after_compaction", callback: EventCallback<AfterCompactionEvent>) => void) &
|
((event: "after_compaction", callback: EventCallback<AfterCompactionEvent>) => void) &
|
||||||
((event: "gateway_start", callback: EventCallback<Record<string, never>>) => void);
|
((event: "gateway_start", callback: EventCallback<Record<string, never>>) => void);
|
||||||
runtime: {
|
runtime: {
|
||||||
@@ -149,6 +164,7 @@ interface ClaudeMemPluginConfig {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
|
botToken?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +174,6 @@ interface ClaudeMemPluginConfig {
|
|||||||
|
|
||||||
const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
|
const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
|
||||||
const DEFAULT_WORKER_PORT = 37777;
|
const DEFAULT_WORKER_PORT = 37777;
|
||||||
const TOOL_RESULT_MAX_LENGTH = 1000;
|
|
||||||
|
|
||||||
// Agent emoji map for observation feed messages.
|
// Agent emoji map for observation feed messages.
|
||||||
// When creating a new OpenClaw agent, add its agentId and emoji here.
|
// When creating a new OpenClaw agent, add its agentId and emoji here.
|
||||||
@@ -291,12 +306,44 @@ const CHANNEL_SEND_MAP: Record<string, { namespace: string; functionName: string
|
|||||||
line: { namespace: "line", functionName: "sendMessageLine" },
|
line: { namespace: "line", functionName: "sendMessageLine" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function sendDirectTelegram(
|
||||||
|
botToken: string,
|
||||||
|
chatId: string,
|
||||||
|
text: string,
|
||||||
|
logger: PluginLogger
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: chatId,
|
||||||
|
text,
|
||||||
|
parse_mode: "Markdown",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
logger.warn(`[claude-mem] Direct Telegram send failed (${response.status}): ${body}`);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.warn(`[claude-mem] Direct Telegram send error: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendToChannel(
|
function sendToChannel(
|
||||||
api: OpenClawPluginApi,
|
api: OpenClawPluginApi,
|
||||||
channel: string,
|
channel: string,
|
||||||
to: string,
|
to: string,
|
||||||
text: string
|
text: string,
|
||||||
|
botToken?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// If a dedicated bot token is provided for Telegram, send directly
|
||||||
|
if (botToken && channel === "telegram") {
|
||||||
|
return sendDirectTelegram(botToken, to, text, api.logger);
|
||||||
|
}
|
||||||
|
|
||||||
const mapping = CHANNEL_SEND_MAP[channel];
|
const mapping = CHANNEL_SEND_MAP[channel];
|
||||||
if (!mapping) {
|
if (!mapping) {
|
||||||
api.logger.warn(`[claude-mem] Unsupported channel type: ${channel}`);
|
api.logger.warn(`[claude-mem] Unsupported channel type: ${channel}`);
|
||||||
@@ -332,7 +379,8 @@ async function connectToSSEStream(
|
|||||||
channel: string,
|
channel: string,
|
||||||
to: string,
|
to: string,
|
||||||
abortController: AbortController,
|
abortController: AbortController,
|
||||||
setConnectionState: (state: ConnectionState) => void
|
setConnectionState: (state: ConnectionState) => void,
|
||||||
|
botToken?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let backoffMs = 1000;
|
let backoffMs = 1000;
|
||||||
const maxBackoffMs = 30000;
|
const maxBackoffMs = 30000;
|
||||||
@@ -393,7 +441,7 @@ async function connectToSSEStream(
|
|||||||
if (parsed.type === "new_observation" && parsed.observation) {
|
if (parsed.type === "new_observation" && parsed.observation) {
|
||||||
const event = parsed as SSENewObservationEvent;
|
const event = parsed as SSENewObservationEvent;
|
||||||
const message = formatObservationMessage(event.observation);
|
const message = formatObservationMessage(event.observation);
|
||||||
await sendToChannel(api, channel, to, message);
|
await sendToChannel(api, channel, to, message, botToken);
|
||||||
}
|
}
|
||||||
} catch (parseError: unknown) {
|
} catch (parseError: unknown) {
|
||||||
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
||||||
@@ -450,10 +498,16 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
return sessionIds.get(key)!;
|
return sessionIds.get(key)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
async function syncMemoryToWorkspace(workspaceDir: string, ctx?: EventContext): Promise<void> {
|
||||||
|
// Include both the base project and agent-scoped project (e.g. "openclaw" + "openclaw-main")
|
||||||
|
const projects = [baseProjectName];
|
||||||
|
const agentProject = ctx ? getProjectName(ctx) : null;
|
||||||
|
if (agentProject && agentProject !== baseProjectName) {
|
||||||
|
projects.push(agentProject);
|
||||||
|
}
|
||||||
const contextText = await workerGetText(
|
const contextText = await workerGetText(
|
||||||
workerPort,
|
workerPort,
|
||||||
`/api/context/inject?projects=${encodeURIComponent(baseProjectName)}`,
|
`/api/context/inject?projects=${encodeURIComponent(projects.join(","))}`,
|
||||||
api.logger
|
api.logger
|
||||||
);
|
);
|
||||||
if (contextText && contextText.trim().length > 0) {
|
if (contextText && contextText.trim().length > 0) {
|
||||||
@@ -482,6 +536,20 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
api.logger.info(`[claude-mem] Session initialized: ${contentSessionId}`);
|
api.logger.info(`[claude-mem] Session initialized: ${contentSessionId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Event: message_received — capture inbound user prompts from channels
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
api.on("message_received", async (event, ctx) => {
|
||||||
|
const sessionKey = ctx.conversationId || ctx.channelId || "default";
|
||||||
|
const contentSessionId = getContentSessionId(sessionKey);
|
||||||
|
|
||||||
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
|
contentSessionId,
|
||||||
|
project: baseProjectName,
|
||||||
|
prompt: event.content || "[media prompt]",
|
||||||
|
}, api.logger);
|
||||||
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Event: after_compaction — re-init session after context compaction
|
// Event: after_compaction — re-init session after context compaction
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -500,7 +568,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Event: before_agent_start — init session + 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
|
||||||
if (ctx.workspaceDir) {
|
if (ctx.workspaceDir) {
|
||||||
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
||||||
@@ -512,12 +580,12 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
await workerPost(workerPort, "/api/sessions/init", {
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
contentSessionId,
|
contentSessionId,
|
||||||
project: getProjectName(ctx),
|
project: getProjectName(ctx),
|
||||||
prompt: ctx.sessionKey || "agent run",
|
prompt: event.prompt || "agent run",
|
||||||
}, api.logger);
|
}, 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, ctx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -527,20 +595,18 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
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"}`);
|
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) return;
|
||||||
|
|
||||||
const contentSessionId = getContentSessionId(ctx.sessionKey);
|
const contentSessionId = getContentSessionId(ctx.sessionKey);
|
||||||
|
|
||||||
// Extract result text from message content
|
// Extract result text from all content blocks
|
||||||
let toolResponseText = "";
|
let toolResponseText = "";
|
||||||
const content = event.message?.content;
|
const content = event.message?.content;
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
const textBlock = content.find(
|
toolResponseText = content
|
||||||
(block) => block.type === "tool_result" || block.type === "text"
|
.filter((block) => (block.type === "tool_result" || block.type === "text") && "text" in block)
|
||||||
);
|
.map((block) => String(block.text))
|
||||||
if (textBlock && "text" in textBlock) {
|
.join("\n");
|
||||||
toolResponseText = String(textBlock.text).slice(0, TOOL_RESULT_MAX_LENGTH);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire-and-forget: send observation + sync MEMORY.md in parallel
|
// Fire-and-forget: send observation + sync MEMORY.md in parallel
|
||||||
@@ -554,7 +620,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
|
|
||||||
const workspaceDir = ctx.workspaceDir || workspaceDirsBySessionKey.get(ctx.sessionKey || "default");
|
const workspaceDir = ctx.workspaceDir || workspaceDirsBySessionKey.get(ctx.sessionKey || "default");
|
||||||
if (syncMemoryFile && workspaceDir) {
|
if (syncMemoryFile && workspaceDir) {
|
||||||
syncMemoryToWorkspace(workspaceDir);
|
syncMemoryToWorkspace(workspaceDir, ctx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -653,7 +719,8 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
feedConfig.channel,
|
feedConfig.channel,
|
||||||
feedConfig.to,
|
feedConfig.to,
|
||||||
sseAbortController,
|
sseAbortController,
|
||||||
(state) => { connectionState = state; }
|
(state) => { connectionState = state; },
|
||||||
|
feedConfig.botToken
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
stop: async (_ctx) => {
|
stop: async (_ctx) => {
|
||||||
|
|||||||
Executable
+2339
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "10.0.2",
|
"version": "10.0.6",
|
||||||
"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": "10.0.2",
|
"version": "10.0.6",
|
||||||
"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": "10.0.2",
|
"version": "10.0.6",
|
||||||
"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
@@ -30,6 +30,19 @@ export interface RouteHandler {
|
|||||||
setupRoutes(app: Application): void;
|
setupRoutes(app: Application): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI provider status for health endpoint
|
||||||
|
*/
|
||||||
|
export interface AiStatus {
|
||||||
|
provider: string;
|
||||||
|
authMethod: string;
|
||||||
|
lastInteraction: {
|
||||||
|
timestamp: number;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for initializing the server
|
* Options for initializing the server
|
||||||
*/
|
*/
|
||||||
@@ -42,6 +55,10 @@ export interface ServerOptions {
|
|||||||
onShutdown: () => Promise<void>;
|
onShutdown: () => Promise<void>;
|
||||||
/** Restart function for admin endpoints */
|
/** Restart function for admin endpoints */
|
||||||
onRestart: () => Promise<void>;
|
onRestart: () => Promise<void>;
|
||||||
|
/** Filesystem path to the worker entry point */
|
||||||
|
workerPath: string;
|
||||||
|
/** Callback to get current AI provider status */
|
||||||
|
getAiStatus: () => AiStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,20 +157,20 @@ export class Server {
|
|||||||
* Setup core system routes (health, readiness, version, admin)
|
* Setup core system routes (health, readiness, version, admin)
|
||||||
*/
|
*/
|
||||||
private setupCoreRoutes(): void {
|
private setupCoreRoutes(): void {
|
||||||
// Test build ID for debugging which build is running
|
|
||||||
const TEST_BUILD_ID = 'TEST-008-wrapper-ipc';
|
|
||||||
|
|
||||||
// Health check endpoint - always responds, even during initialization
|
// Health check endpoint - always responds, even during initialization
|
||||||
this.app.get('/api/health', (_req: Request, res: Response) => {
|
this.app.get('/api/health', (_req: Request, res: Response) => {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
build: TEST_BUILD_ID,
|
version: BUILT_IN_VERSION,
|
||||||
|
workerPath: this.options.workerPath,
|
||||||
|
uptime: Date.now() - this.startTime,
|
||||||
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
|
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
|
||||||
hasIpc: typeof process.send === 'function',
|
hasIpc: typeof process.send === 'function',
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
initialized: this.options.getInitializationComplete(),
|
initialized: this.options.getInitializationComplete(),
|
||||||
mcpReady: this.options.getMcpReady(),
|
mcpReady: this.options.getMcpReady(),
|
||||||
|
ai: this.options.getAiStatus(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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 { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||||
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
||||||
|
import { getAuthMethodDescription } from '../shared/EnvManager.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
// Windows: avoid repeated spawn popups when startup fails (issue #921)
|
// Windows: avoid repeated spawn popups when startup fails (issue #921)
|
||||||
@@ -170,6 +171,14 @@ export class WorkerService {
|
|||||||
// Orphan reaper cleanup function (Issue #737)
|
// Orphan reaper cleanup function (Issue #737)
|
||||||
private stopOrphanReaper: (() => void) | null = null;
|
private stopOrphanReaper: (() => void) | null = null;
|
||||||
|
|
||||||
|
// AI interaction tracking for health endpoint
|
||||||
|
private lastAiInteraction: {
|
||||||
|
timestamp: number;
|
||||||
|
success: boolean;
|
||||||
|
provider: string;
|
||||||
|
error?: string;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize the promise that will resolve when background initialization completes
|
// Initialize the promise that will resolve when background initialization completes
|
||||||
this.initializationComplete = new Promise((resolve) => {
|
this.initializationComplete = new Promise((resolve) => {
|
||||||
@@ -206,7 +215,24 @@ export class WorkerService {
|
|||||||
getInitializationComplete: () => this.initializationCompleteFlag,
|
getInitializationComplete: () => this.initializationCompleteFlag,
|
||||||
getMcpReady: () => this.mcpReady,
|
getMcpReady: () => this.mcpReady,
|
||||||
onShutdown: () => this.shutdown(),
|
onShutdown: () => this.shutdown(),
|
||||||
onRestart: () => this.shutdown()
|
onRestart: () => this.shutdown(),
|
||||||
|
workerPath: __filename,
|
||||||
|
getAiStatus: () => {
|
||||||
|
let provider = 'claude';
|
||||||
|
if (isOpenRouterSelected() && isOpenRouterAvailable()) provider = 'openrouter';
|
||||||
|
else if (isGeminiSelected() && isGeminiAvailable()) provider = 'gemini';
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
authMethod: getAuthMethodDescription(),
|
||||||
|
lastInteraction: this.lastAiInteraction
|
||||||
|
? {
|
||||||
|
timestamp: this.lastAiInteraction.timestamp,
|
||||||
|
success: this.lastAiInteraction.success,
|
||||||
|
...(this.lastAiInteraction.error && { error: this.lastAiInteraction.error }),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register route handlers
|
// Register route handlers
|
||||||
@@ -459,6 +485,7 @@ export class WorkerService {
|
|||||||
|
|
||||||
// Track whether generator failed with an unrecoverable error to prevent infinite restart loops
|
// Track whether generator failed with an unrecoverable error to prevent infinite restart loops
|
||||||
let hadUnrecoverableError = false;
|
let hadUnrecoverableError = false;
|
||||||
|
let sessionFailed = false;
|
||||||
|
|
||||||
logger.info('SYSTEM', `Starting generator (${source}) using ${providerName}`, { sessionId: sid });
|
logger.info('SYSTEM', `Starting generator (${source}) using ${providerName}`, { sessionId: sid });
|
||||||
|
|
||||||
@@ -473,9 +500,16 @@ export class WorkerService {
|
|||||||
'CLAUDE_CODE_PATH',
|
'CLAUDE_CODE_PATH',
|
||||||
'ENOENT',
|
'ENOENT',
|
||||||
'spawn',
|
'spawn',
|
||||||
|
'Invalid API key',
|
||||||
];
|
];
|
||||||
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
|
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
|
||||||
hadUnrecoverableError = true;
|
hadUnrecoverableError = true;
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: false,
|
||||||
|
provider: providerName,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
logger.error('SDK', 'Unrecoverable generator error - will NOT restart', {
|
logger.error('SDK', 'Unrecoverable generator error - will NOT restart', {
|
||||||
sessionId: session.sessionDbId,
|
sessionId: session.sessionDbId,
|
||||||
project: session.project,
|
project: session.project,
|
||||||
@@ -512,11 +546,27 @@ export class WorkerService {
|
|||||||
project: session.project,
|
project: session.project,
|
||||||
provider: providerName
|
provider: providerName
|
||||||
}, error as Error);
|
}, error as Error);
|
||||||
|
sessionFailed = true;
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: false,
|
||||||
|
provider: providerName,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
session.generatorPromise = null;
|
session.generatorPromise = null;
|
||||||
|
|
||||||
|
// Record successful AI interaction if no error occurred
|
||||||
|
if (!sessionFailed && !hadUnrecoverableError) {
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: true,
|
||||||
|
provider: providerName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Do NOT restart after unrecoverable errors - prevents infinite loops
|
// Do NOT restart after unrecoverable errors - prevents infinite loops
|
||||||
if (hadUnrecoverableError) {
|
if (hadUnrecoverableError) {
|
||||||
logger.warn('SYSTEM', 'Skipping restart due to unrecoverable error', {
|
logger.warn('SYSTEM', 'Skipping restart due to unrecoverable error', {
|
||||||
|
|||||||
@@ -246,6 +246,12 @@ export class SDKAgent {
|
|||||||
throw new Error('Claude session context overflow: prompt is too long');
|
throw new Error('Claude session context overflow: prompt is too long');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect invalid API key — SDK returns this as response text, not an error.
|
||||||
|
// Throw so it surfaces in health endpoint and prevents silent failures.
|
||||||
|
if (typeof textContent === 'string' && textContent.includes('Invalid API key')) {
|
||||||
|
throw new Error('Invalid API key: check your API key configuration in ~/.claude-mem/settings.json or ~/.claude-mem/.env');
|
||||||
|
}
|
||||||
|
|
||||||
// Parse and process response using shared ResponseProcessor
|
// Parse and process response using shared ResponseProcessor
|
||||||
await processAgentResponse(
|
await processAgentResponse(
|
||||||
textContent,
|
textContent,
|
||||||
|
|||||||
Reference in New Issue
Block a user