diff --git a/.claude-plugin/CLAUDE.md b/.claude-plugin/CLAUDE.md index 57e76b57..f1d5f402 100644 --- a/.claude-plugin/CLAUDE.md +++ b/.claude-plugin/CLAUDE.md @@ -1,8 +1,6 @@ # Recent Activity - - ### Oct 25, 2025 | ID | Time | T | Title | Read | diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c5aab315..f955e1d8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "claude-mem", - "version": "9.0.10", + "version": "10.0.6", "source": "./plugin", "description": "Persistent memory system for Claude Code - context compression across sessions" } diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index e2782952..00000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1,11 +0,0 @@ - -# Recent Activity - - - -### Nov 3, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #3366 | 3:40 PM | 🔵 | Claude Mem MCP Search Architecture and Timeline Tool Capabilities | ~438 | - \ No newline at end of file diff --git a/.claude/commands/CLAUDE.md b/.claude/commands/CLAUDE.md deleted file mode 100644 index 2aa3649b..00000000 --- a/.claude/commands/CLAUDE.md +++ /dev/null @@ -1,20 +0,0 @@ - -# Recent Activity - - - -### Oct 25, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #2484 | 6:33 PM | 🔴 | Removed slash commands from incorrect root .claude/commands directory | ~268 | - -### Jan 10, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #39054 | 3:45 PM | 🔄 | Development commands removed from root .claude directory | ~249 | -| #39053 | " | 🟣 | Added development commands to plugin distribution | ~276 | -| #39051 | 3:44 PM | 🔵 | Development commands confirmed in .claude/commands/ | ~315 | -| #39049 | " | 🔵 | Development commands located in .claude/commands/ directory | ~293 | - \ No newline at end of file diff --git a/.claude/plans/CLAUDE.md b/.claude/plans/CLAUDE.md deleted file mode 100644 index adfdcb11..00000000 --- a/.claude/plans/CLAUDE.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Recent Activity - - - -*No recent activity* - \ No newline at end of file diff --git a/.claude/plans/bugfix-env-auth.md b/.claude/plans/bugfix-env-auth.md new file mode 100644 index 00000000..284f1f48 --- /dev/null +++ b/.claude/plans/bugfix-env-auth.md @@ -0,0 +1,176 @@ +# Bugfix Plan: Observer Sessions Authentication Failure + +## Problem Summary + +Observer sessions fail with "Invalid API key · Please run /login" because the `CLAUDE_CONFIG_DIR` environment variable is being set to an isolated directory (`~/.claude-mem/observer-config/`) that lacks authentication credentials. + +## Root Cause + +**File:** `src/services/worker/ProcessRegistry.ts` (lines 207-211) + +```typescript +const isolatedEnv = { + ...spawnOptions.env, + CLAUDE_CONFIG_DIR: OBSERVER_CONFIG_DIR // <-- This isolates auth credentials! +}; +``` + +This was added in Issue #832 to prevent observer sessions from polluting the `claude --resume` list. However, it also isolates the authentication credentials, breaking the SDK's ability to authenticate with the Anthropic API. + +## Evidence + +1. Running Claude with alternate config dir reproduces the error: + ```bash + CLAUDE_CONFIG_DIR=/tmp/test-claude claude --print "hello" + # Output: Invalid API key · Please run /login + ``` + +2. The observer config directory exists but only has cached feature flags, no authentication: + - `~/.claude-mem/observer-config/.claude.json` - feature flags only + - No credentials copied from main `~/.claude/` directory + +## Solution + +The fix must allow authentication while still isolating session history. Claude Code stores different data types in `CLAUDE_CONFIG_DIR`: +- Authentication credentials (needed) +- Session history/resume list (should be isolated) +- Feature flags and settings (can be shared or isolated) + +**Approach:** Do NOT override `CLAUDE_CONFIG_DIR`. Instead, find an alternative solution for Issue #832. + +### Alternative Approaches for Session Isolation + +1. **Use `--no-resume` flag** (if SDK supports it) - Prevent observer sessions from being resumable +2. **Accept pollution** - Observer sessions in resume list may be acceptable tradeoff +3. **Post-hoc cleanup** - Clean up observer session entries from history after completion +4. **SDK parameter** - Check if SDK has a session isolation option that doesn't affect auth + +--- + +## Phase 0: Documentation Discovery + +### Objective +Understand SDK options for session isolation without breaking authentication. + +### Tasks +1. Read SDK documentation/source for: + - Available `query()` options + - Session isolation mechanisms + - Authentication handling + +2. Read Issue #832 context: + - What was the original problem? + - How bad was the pollution? + - Are there alternative solutions mentioned? + +### Verification +- [ ] List all `query()` options available +- [ ] Identify if `--no-resume` or equivalent exists +- [ ] Document the tradeoffs + +--- + +## Phase 1: Fix Authentication + +### Objective +Remove the `CLAUDE_CONFIG_DIR` override to restore authentication. + +### File to Modify +`src/services/worker/ProcessRegistry.ts` + +### Change +Remove lines 207-211 that override `CLAUDE_CONFIG_DIR`: + +**Before:** +```typescript +const isolatedEnv = { + ...spawnOptions.env, + CLAUDE_CONFIG_DIR: OBSERVER_CONFIG_DIR +}; +``` + +**After:** +```typescript +const isolatedEnv = { + ...spawnOptions.env + // CLAUDE_CONFIG_DIR removed - observer sessions need access to auth credentials + // Session isolation addressed via [alternative approach] +}; +``` + +### Verification +- [ ] Build succeeds: `npm run build` +- [ ] Observer sessions authenticate successfully +- [ ] Observations are saved to database + +--- + +## Phase 2: Address Session Isolation (Issue #832) + +### Objective +Find alternative solution to prevent observer sessions from polluting `claude --resume` list. + +### Options to Evaluate + +1. **Option A: Accept the tradeoff** + - Observer sessions appear in resume list but users can ignore them + - No code changes needed beyond Phase 1 + +2. **Option B: Use isSynthetic flag** + - If SDK has a flag to mark sessions as non-resumable, use it + - Requires SDK documentation review + +3. **Option C: Post-processing cleanup** + - After session ends, remove observer entries from history + - More complex, may have race conditions + +### Decision Point +After Phase 0 documentation review, choose the appropriate option. + +### Verification +- [ ] Chosen approach documented +- [ ] If code changes made, tests pass +- [ ] Observer sessions either isolated OR tradeoff accepted + +--- + +## Phase 3: Testing + +### Manual Tests +1. Start a new Claude Code session with the plugin +2. Verify observations are being saved (check logs) +3. Check that no "Invalid API key" errors appear +4. Verify `claude --resume` behavior (acceptable level of observer entries) + +### Verification Checklist +- [ ] `npm run build` succeeds +- [ ] Worker service starts without errors +- [ ] Observations save to database +- [ ] No authentication errors in logs +- [ ] Issue #832 regression acceptable or addressed + +--- + +## Anti-Patterns to Avoid + +1. **DO NOT** add `ANTHROPIC_API_KEY` to environment - authentication is handled by Claude Code's built-in credential management +2. **DO NOT** copy credential files to observer config dir - credentials may be in keychain or other secure storage +3. **DO NOT** try to "fix" authentication by adding API key loading - that creates Issue #588 (unexpected API charges) + +--- + +## Files Involved + +| File | Purpose | +|------|---------| +| `src/services/worker/ProcessRegistry.ts` | Contains the problematic `CLAUDE_CONFIG_DIR` override | +| `src/shared/paths.ts` | Defines `OBSERVER_CONFIG_DIR` constant | +| `src/services/worker/SDKAgent.ts` | Uses `createPidCapturingSpawn` which sets the env | + +--- + +## Risk Assessment + +**Low Risk:** Removing the `CLAUDE_CONFIG_DIR` override is a simple, targeted change. + +**Regression Risk (Issue #832):** Observer sessions may appear in `claude --resume` list again. This is a cosmetic issue vs. complete authentication failure, so the tradeoff favors removing the override. diff --git a/.claude/plans/fix-empty-claude-md-files.md b/.claude/plans/fix-empty-claude-md-files.md new file mode 100644 index 00000000..f7744918 --- /dev/null +++ b/.claude/plans/fix-empty-claude-md-files.md @@ -0,0 +1,266 @@ +# Plan: Fix Empty CLAUDE.md File Generation + +## Problem Statement + +Currently the CLAUDE.md generator creates files with wasteful content: +1. **Empty files with "No recent activity"** - Files are created even when there are zero observations for a folder +2. **Redundant HTML comment** - "" is unnecessary since the `` tag already conveys this information + +These issues create noisy, wasteful context that loads automatically and provides no value. + +## Phase 0: Documentation Discovery + +### Allowed APIs (from code analysis) +- `formatTimelineForClaudeMd(timelineText: string): string` - src/utils/claude-md-utils.ts:139 +- `formatObservationsForClaudeMd(observations, folderPath): string` - scripts/regenerate-claude-md.ts:238 +- `writeClaudeMdToFolder(folderPath, newContent): void` - src/utils/claude-md-utils.ts:94 +- `updateFolderClaudeMdFiles(filePaths, project, port, projectRoot): Promise` - src/utils/claude-md-utils.ts:257 +- `replaceTaggedContent(existingContent, newContent): string` - src/utils/claude-md-utils.ts:64 + +### Key Locations +| File | Lines | Purpose | +|------|-------|---------| +| `src/utils/claude-md-utils.ts` | 139-235 | Main formatting function | +| `src/utils/claude-md-utils.ts` | 143 | HTML comment generation | +| `src/utils/claude-md-utils.ts` | 209-211 | "No recent activity" handling | +| `src/utils/claude-md-utils.ts` | 322-323 | Write decision point | +| `scripts/regenerate-claude-md.ts` | 238-286 | Regeneration script formatting | +| `scripts/regenerate-claude-md.ts` | 242 | HTML comment generation (duplicate) | +| `scripts/regenerate-claude-md.ts` | 245-247 | "No recent activity" handling | +| `scripts/regenerate-claude-md.ts` | 452-453 | Write decision point | +| `tests/utils/claude-md-utils.test.ts` | 96-109 | Tests for "No recent activity" behavior | + +### Anti-patterns to avoid +- Do NOT add new configuration options for this behavior - just fix it +- Do NOT add logging for skipped files (unnecessary noise) + +--- + +## Phase 1: Modify formatTimelineForClaudeMd to Return Empty on No Observations + +### Task 1.1: Update formatTimelineForClaudeMd return behavior +**File:** `src/utils/claude-md-utils.ts` +**Lines:** 139-235 + +**Changes:** +1. Remove HTML comment line at line 143 +2. Change the empty observations case (lines 209-211) to return an empty string instead of "No recent activity" + +**Before (lines 141-144):** +```typescript +lines.push('# Recent Activity'); +lines.push(''); +lines.push(''); +lines.push(''); +``` + +**After:** +```typescript +lines.push('# Recent Activity'); +lines.push(''); +``` + +**Before (lines 209-212):** +```typescript +if (observations.length === 0) { + lines.push('*No recent activity*'); + return lines.join('\n'); +} +``` + +**After:** +```typescript +if (observations.length === 0) { + return ''; +} +``` + +### Verification +- Run `bun test tests/utils/claude-md-utils.test.ts` +- Tests at lines 96-109 will FAIL (expected - they test for "No recent activity") +- Update tests to expect empty string for empty input + +--- + +## Phase 2: Update updateFolderClaudeMdFiles to Skip Empty Content + +### Task 2.1: Add empty content check before writing +**File:** `src/utils/claude-md-utils.ts` +**Lines:** 322-323 + +**Changes:** +After formatting, check if result is empty and skip writing if so. + +**Before (lines 321-325):** +```typescript +const formatted = formatTimelineForClaudeMd(result.content[0].text); +writeClaudeMdToFolder(folderPath, formatted); + +logger.debug('FOLDER_INDEX', 'Updated CLAUDE.md', { folderPath }); +``` + +**After:** +```typescript +const formatted = formatTimelineForClaudeMd(result.content[0].text); +if (!formatted) { + logger.debug('FOLDER_INDEX', 'No observations for folder, skipping', { folderPath }); + continue; +} +writeClaudeMdToFolder(folderPath, formatted); + +logger.debug('FOLDER_INDEX', 'Updated CLAUDE.md', { folderPath }); +``` + +### Verification +- Grep for files containing "No recent activity": should find none after running + +--- + +## Phase 3: Update Regeneration Script + +### Task 3.1: Remove HTML comment from formatObservationsForClaudeMd +**File:** `scripts/regenerate-claude-md.ts` +**Lines:** 238-286 + +**Changes:** +1. Remove HTML comment line at line 242 +2. Change empty observations case (lines 245-247) to return empty string + +**Before (lines 240-244):** +```typescript +lines.push('# Recent Activity'); +lines.push(''); +lines.push(''); +lines.push(''); +``` + +**After:** +```typescript +lines.push('# Recent Activity'); +lines.push(''); +``` + +**Before (lines 245-248):** +```typescript +if (observations.length === 0) { + lines.push('*No recent activity*'); + return lines.join('\n'); +} +``` + +**After:** +```typescript +if (observations.length === 0) { + return ''; +} +``` + +### Task 3.2: Update regenerateFolder to handle empty formatted content +**File:** `scripts/regenerate-claude-md.ts` +**Lines:** 432-459 + +The script already skips folders with no observations (lines 443-444), so this change is already compatible. The `formatObservationsForClaudeMd` returning empty string doesn't change behavior since observations are checked before calling it. + +### Verification +- Run `bun scripts/regenerate-claude-md.ts --dry-run` in the project +- Should NOT show any folders with 0 observations + +--- + +## Phase 4: Update Tests + +### Task 4.1: Update tests for new empty behavior +**File:** `tests/utils/claude-md-utils.test.ts` +**Lines:** 96-109 + +**Changes:** +Update the two tests that expect "No recent activity" to expect empty string instead. + +**Before (lines 96-101):** +```typescript +it('should return "No recent activity" for empty input', () => { + const result = formatTimelineForClaudeMd(''); + + expect(result).toContain('# Recent Activity'); + expect(result).toContain('*No recent activity*'); +}); +``` + +**After:** +```typescript +it('should return empty string for empty input', () => { + const result = formatTimelineForClaudeMd(''); + + expect(result).toBe(''); +}); +``` + +**Before (lines 103-109):** +```typescript +it('should return "No recent activity" when no table rows exist', () => { + const input = 'Just some plain text without table rows'; + + const result = formatTimelineForClaudeMd(input); + + expect(result).toContain('*No recent activity*'); +}); +``` + +**After:** +```typescript +it('should return empty string when no table rows exist', () => { + const input = 'Just some plain text without table rows'; + + const result = formatTimelineForClaudeMd(input); + + expect(result).toBe(''); +}); +``` + +### Task 4.2: Remove HTML comment assertions from any other tests +Search for tests that assert on "auto-generated" comment and update accordingly. + +### Verification +- Run full test suite: `bun test` +- All tests should pass + +--- + +## Phase 5: Cleanup Existing Empty Files + +### Task 5.1: Run cleanup to remove existing empty CLAUDE.md files +**Command:** +```bash +bun scripts/regenerate-claude-md.ts --clean +``` + +This will: +- Find all CLAUDE.md files with `` tags +- Strip the tagged section +- Delete files that become empty after stripping +- Preserve files that have user content outside the tags + +### Verification +- `grep -r "No recent activity" . --include="CLAUDE.md"` should return no results +- `grep -r "auto-generated by claude-mem" . --include="CLAUDE.md"` should return no results + +--- + +## Summary of Changes + +| File | Change | +|------|--------| +| `src/utils/claude-md-utils.ts:143` | Remove HTML comment line | +| `src/utils/claude-md-utils.ts:209-211` | Return empty string instead of "No recent activity" | +| `src/utils/claude-md-utils.ts:322` | Skip writing if formatted content is empty | +| `scripts/regenerate-claude-md.ts:242` | Remove HTML comment line | +| `scripts/regenerate-claude-md.ts:245-247` | Return empty string instead of "No recent activity" | +| `tests/utils/claude-md-utils.test.ts:96-109` | Update tests to expect empty string | + +## Final Verification Checklist +- [ ] `bun test` passes +- [ ] No "No recent activity" CLAUDE.md files exist +- [ ] No "auto-generated" comments in CLAUDE.md files +- [ ] Build succeeds: `npm run build-and-sync` +- [ ] New observations correctly generate CLAUDE.md files with content +- [ ] Folders without observations get no CLAUDE.md file created diff --git a/.claude/plans/remove-worker-start-calls.md b/.claude/plans/remove-worker-start-calls.md new file mode 100644 index 00000000..c51f0c24 --- /dev/null +++ b/.claude/plans/remove-worker-start-calls.md @@ -0,0 +1,394 @@ +# Plan: Remove Worker Start Calls - In-Process Architecture + +## Problem Statement + +Current architecture has problematic spawn patterns: +1. `hooks.json` calls `worker-service.cjs start` which spawns a daemon +2. Spawning is buggy on Windows - **HARD RULE: NO SPAWN** +3. `user-message` hook is deprecated +4. `smart-install` was supposed to chain: `smart-install && stop && context` + +## Target Architecture + +**NO SPAWN - Worker runs in-process within hook command** + +``` +SessionStart: + smart-install && stop && context +``` + +Flow: +1. `smart-install` - Install dependencies if needed +2. `stop` - Kill any existing worker (clean slate) +3. `context` - Hook starts worker IN-PROCESS, becomes the worker + +**Key insight:** The first hook that needs the worker **becomes** the worker. No spawn, no daemon. The hook process IS the worker process. + +--- + +## Current vs Target hooks.json + +### Current (BROKEN) +```json +"SessionStart": [ + { "hooks": [ + { "command": "node smart-install.js" }, + { "command": "bun worker-service.cjs start" }, // REMOVE - spawn + { "command": "bun worker-service.cjs hook ... context" }, + { "command": "bun worker-service.cjs hook ... user-message" } // REMOVE - deprecated + ]} +] +``` + +### Target +```json +"SessionStart": [ + { "hooks": [ + { "command": "node smart-install.js && bun worker-service.cjs stop && bun worker-service.cjs hook claude-code context" } + ]} +] +``` + +--- + +## Files Involved + +| File | Changes | +|------|---------| +| `plugin/hooks/hooks.json` | Restructure to chained commands, remove start/user-message | +| `src/services/worker-service.ts` | `hook` case: start worker in-process if not running | +| `src/cli/handlers/*.ts` | May need adjustment for in-process execution | +| `src/shared/worker-utils.ts` | `ensureWorkerRunning()` → adapt for in-process | + +--- + +## Phase 0: Documentation Discovery + +### Available APIs + +**From `src/services/infrastructure/HealthMonitor.ts`:** +- `isPortInUse(port): Promise` +- `waitForHealth(port, timeoutMs): Promise` +- `httpShutdown(port): Promise` + +**From `src/services/worker-service.ts`:** +- `WorkerService` class - the actual worker +- `stop` command - shuts down worker via HTTP +- `--daemon` case - starts WorkerService (currently only used after spawn) + +**BANNED (spawn patterns):** +- ~~`spawnDaemon()`~~ - NO SPAWN +- ~~`fork()`~~ - NO SPAWN +- ~~`spawn()` with detached~~ - NO SPAWN + +### Anti-Patterns +- **NO SPAWN** - Hard rule, Windows buggy +- No `restart` command - removed for same reason +- No detached processes + +--- + +## Phase 1: Modify `hook` Case for In-Process Worker + +### Location +`src/services/worker-service.ts:564-576` + +### Current Code +```typescript +case 'hook': { + const platform = process.argv[3]; + const event = process.argv[4]; + if (!platform || !event) { + console.error('Usage: claude-mem hook '); + process.exit(1); + } + const { hookCommand } = await import('../cli/hook-command.js'); + await hookCommand(platform, event); + break; +} +``` + +### Target Code +```typescript +case 'hook': { + const platform = process.argv[3]; + const event = process.argv[4]; + if (!platform || !event) { + console.error('Usage: claude-mem hook '); + process.exit(1); + } + + // Check if worker already running (port in use = valid, another process has it) + const portInUse = await isPortInUse(port); + if (portInUse) { + // Port in use - either healthy worker or something else + // Proceed with hook via HTTP to existing worker + const { hookCommand } = await import('../cli/hook-command.js'); + await hookCommand(platform, event); + break; + } + + // Port free - start worker IN THIS PROCESS (no spawn!) + logger.info('SYSTEM', 'Starting worker in-process for hook'); + const worker = new WorkerService(); + + // Start worker (non-blocking, returns when server listening) + await worker.start(); + + // Now execute hook logic - worker is running in this process + // Can call handler directly (in-process) or via HTTP to self + const { hookCommand } = await import('../cli/hook-command.js'); + await hookCommand(platform, event); + + // DON'T exit - this process IS the worker now + // Worker stays alive serving requests + break; +} +``` + +### Key Behavior +- If port in use → hook runs via HTTP to existing worker, then exits +- If port free → start worker in-process, run hook, process stays alive as worker + +### Verification +- [ ] Stop worker, run hook command → should start worker and stay alive +- [ ] Worker already running, run hook command → should complete and exit +- [ ] `lsof -i :37777` shows hook process IS the worker + +--- + +## Phase 2: Update hooks.json - Chained Commands + +### Location +`plugin/hooks/hooks.json` + +### Target Structure +```json +{ + "description": "Claude-mem memory system hooks", + "hooks": { + "SessionStart": [ + { + "matcher": "startup|clear|compact", + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\" && bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" stop && bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code context", + "timeout": 300 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-init", + "timeout": 60 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code observation", + "timeout": 120 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize", + "timeout": 120 + } + ] + } + ] + } +} +``` + +### Changes Summary +1. SessionStart: Chain `smart-install && stop && context` in single command +2. Remove `user-message` hook (deprecated) +3. Remove all separate `start` commands +4. Other hooks unchanged (just hook command, auto-starts if needed) + +### Verification +- [ ] JSON valid: `cat plugin/hooks/hooks.json | jq .` +- [ ] No `start` command: `grep -c '"start"' plugin/hooks/hooks.json` = 0 +- [ ] No `user-message`: `grep -c 'user-message' plugin/hooks/hooks.json` = 0 + +--- + +## Phase 3: Handle "Port In Use" Gracefully + +### Scenario +Another process has port 37777 (not our worker). Hook should handle gracefully. + +### Current Behavior +`ensureWorkerRunning()` polls for 15 seconds, then throws error. + +### Target Behavior +If port in use but not healthy (not our worker): +- Hook is "valid" - don't block Claude Code +- Return graceful response (empty context, etc.) +- Log warning for debugging + +### Location +`src/shared/worker-utils.ts:117-141` + +### Changes +```typescript +export async function ensureWorkerRunning(): Promise { + const port = getWorkerPort(); + + // Quick health check (2 seconds max) + try { + if (await isWorkerHealthy()) { + await checkWorkerVersion(); + return true; // Worker healthy + } + } catch (e) { + // Not healthy + } + + // Port might be in use by something else + // Return false but don't throw - let caller decide + logger.warn('SYSTEM', 'Worker not healthy, hook will proceed gracefully'); + return false; +} +``` + +### Handler Updates +Update handlers to handle `ensureWorkerRunning()` returning false: +```typescript +const workerReady = await ensureWorkerRunning(); +if (!workerReady) { + // Return graceful empty response + return { output: '', exitCode: HOOK_EXIT_CODES.SUCCESS }; +} +``` + +### Verification +- [ ] Start non-worker process on 37777, run hook → completes gracefully +- [ ] No 15-second hang when port blocked + +--- + +## Phase 4: Remove Deprecated Code + +### Remove `user-message` Handler (if unused elsewhere) +- [ ] Check if `user-message.ts` is used anywhere else +- [ ] Remove from `src/cli/handlers/index.ts` if safe +- [ ] Consider keeping file but removing from hooks.json only + +### Remove `start` Command (optional) +The `start` command in worker-service.ts can stay for manual use: +```bash +bun worker-service.cjs start # Manual start if needed +``` +But it should NOT be called from hooks.json. + +### Verification +- [ ] `npm run build` succeeds +- [ ] No references to removed handlers in hooks.json + +--- + +## Phase 5: Update Handler `ensureWorkerRunning()` Calls + +### Context +Each handler currently calls `ensureWorkerRunning()` which polls for 15 seconds. + +With in-process architecture: +- If hook started worker in-process → worker is THIS process, no HTTP needed +- If worker already running → HTTP to existing worker + +### Decision +**Keep handler calls** but modify `ensureWorkerRunning()` to: +1. Return quickly if port is in use (assume valid) +2. Return true if in-process worker (detect via global flag?) +3. Graceful false return instead of throwing + +### Files +- `src/cli/handlers/context.ts:15` +- `src/cli/handlers/session-init.ts:15` +- `src/cli/handlers/observation.ts:14` +- `src/cli/handlers/summarize.ts:17` +- `src/cli/handlers/file-edit.ts:15` + +### Verification +- [ ] Handlers don't hang on port-in-use scenarios +- [ ] In-process worker scenario works + +--- + +## Phase 6: Final Verification + +### Tests +- [ ] `bun test` - All tests pass +- [ ] `npm run build-and-sync` - Build succeeds + +### Manual Tests + +**Test 1: Clean Start** +```bash +bun plugin/scripts/worker-service.cjs stop +# Start new Claude Code session +# Verify: context hook starts worker in-process +# Verify: lsof -i :37777 shows the hook process +``` + +**Test 2: Worker Already Running** +```bash +bun plugin/scripts/worker-service.cjs stop +bun plugin/scripts/worker-service.cjs hook claude-code context & +# Wait for worker to start +bun plugin/scripts/worker-service.cjs hook claude-code observation +# Verify: observation hook exits after completing (doesn't stay alive) +``` + +**Test 3: Port Blocked** +```bash +bun plugin/scripts/worker-service.cjs stop +nc -l 37777 & # Block port with netcat +bun plugin/scripts/worker-service.cjs hook claude-code context +# Verify: completes gracefully, doesn't hang +kill %1 # Clean up netcat +``` + +**Test 4: Full Session** +```bash +# Start fresh Claude Code session +# Do some work (creates observations) +# End session (Ctrl+C or /exit) +# Verify: summarize hook ran, observations saved +``` + +--- + +## Risk Assessment + +| Risk | Mitigation | +|------|------------| +| Hook stays alive forever | Expected - it's the worker now | +| Multiple hooks compete for port | First one wins, others use HTTP | +| Graceful shutdown on session end | Stop command in chain handles this | +| Windows compatibility | No spawn = no Windows issues | + +## Rollback Plan + +If issues arise: +1. Restore hooks.json with separate start commands +2. Revert worker-service.ts hook case changes +3. No database changes to rollback diff --git a/.claude/reports/CLAUDE.md b/.claude/reports/CLAUDE.md deleted file mode 100644 index 40df664c..00000000 --- a/.claude/reports/CLAUDE.md +++ /dev/null @@ -1,22 +0,0 @@ - -# Recent Activity - - - -### Jan 5, 2026 - -**CLAUDE.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38082 | 10:13 PM | ✅ | Merge Conflict Resolution - Kept Feature Branch Versions | ~431 | - -**test-audit-2026-01-05.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #37776 | 6:35 PM | 🔵 | Test Audit Reveals Quality Issues and Architecture Recommendations | ~372 | -| #37775 | " | 🔵 | Test Audit Identifies Zero Coverage for Logger FormatTool Tests | ~280 | -| #37747 | 6:20 PM | 🔵 | Comprehensive Test Suite Audit Completed: 41 Files Analyzed | ~664 | -| #37736 | 6:16 PM | 🔵 | Test Suite Audit Reveals Critical Test Failure Root Cause | ~660 | -| #37735 | " | ✅ | Test Suite Audit Report Generated: 41 Tests Scored and Analyzed | ~634 | -| #37732 | 6:15 PM | 🔵 | Test Quality Audit Completed: Identified Critical Mock Pollution Issue | ~490 | - \ No newline at end of file diff --git a/.claude/skills/CLAUDE.md b/.claude/skills/CLAUDE.md index 36e5ab21..9be5808a 100644 --- a/.claude/skills/CLAUDE.md +++ b/.claude/skills/CLAUDE.md @@ -26,49 +26,4 @@ Manages semantic versioning for the claude-mem project itself. Handles updating ## Adding New Skills **For claude-mem development** → Add to `.claude/skills/` -**For end users** → Add to `plugin/skills/` (gets distributed with plugin) - - - -# Recent Activity - - - -### Nov 9, 2025 - -**CLAUDE.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #5901 | 6:54 PM | ✅ | Project Skills Documentation Created | ~317 | - -### Dec 13, 2025 - -**CLAUDE.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #24725 | 4:07 PM | 🔵 | Claude Skills Infrastructure for Automation | ~220 | - -### Dec 14, 2025 - -**CLAUDE.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #26354 | 9:20 PM | 🔵 | PR #317 Second CLAUDE.md Compliance Review Confirms No Violations | ~442 | -| #26353 | " | 🔵 | PR #317 CLAUDE.md Compliance Review Completed | ~402 | -| #26193 | 8:15 PM | 🔵 | PR spans 21 files with net addition of 374 lines across codebase | ~375 | -| #26173 | 8:08 PM | ✅ | Updated Skills CLAUDE.md Documentation for Version Bump | ~277 | - -### Dec 28, 2025 - -**CLAUDE.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #33311 | 3:09 PM | ✅ | Version 8.2.3 Release Deployed with Worker Stability Improvements | ~434 | - -### Jan 5, 2026 - -**CLAUDE.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38082 | 10:13 PM | ✅ | Merge Conflict Resolution - Kept Feature Branch Versions | ~431 | - \ No newline at end of file +**For end users** → Add to `plugin/skills/` (gets distributed with plugin) \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/CLAUDE.md b/.github/ISSUE_TEMPLATE/CLAUDE.md deleted file mode 100644 index 8ae61fb3..00000000 --- a/.github/ISSUE_TEMPLATE/CLAUDE.md +++ /dev/null @@ -1,21 +0,0 @@ - -# Recent Activity - - - -### Dec 13, 2025 - -**feature_request.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #25012 | 6:41 PM | 🟣 | Auto-Convert Feature Requests to GitHub Discussions | ~298 | -| #25011 | " | ✅ | Staged GitHub Feature Request Automation Files | ~206 | -| #25009 | 6:40 PM | ✅ | Feature Request Template Auto-Labeling Configured | ~241 | -| #24995 | 6:26 PM | 🔵 | Standard Feature Request Template Configuration | ~260 | - -**bug_report.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #24994 | 6:26 PM | 🔵 | Standard Bug Report Template Configuration | ~258 | -| #24992 | " | 🔵 | GitHub Issue Templates Located | ~188 | - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2314b658..79075ca3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,12 @@ assignees: '' --- +## Before submitting + +- [ ] I searched [existing issues](https://github.com/thedotmack/claude-mem/issues) and confirmed this is not a duplicate + +--- + ## ⚡ Quick Bug Report (Recommended) **Use the automated bug report generator** for comprehensive diagnostics: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 982a4dc0..eae73a33 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,6 +7,12 @@ assignees: '' --- +## Before submitting + +- [ ] I searched [existing issues](https://github.com/thedotmack/claude-mem/issues) and confirmed this is not a duplicate + +--- + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] diff --git a/.github/issues/CLAUDE.md b/.github/issues/CLAUDE.md deleted file mode 100644 index adfdcb11..00000000 --- a/.github/issues/CLAUDE.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Recent Activity - - - -*No recent activity* - \ No newline at end of file diff --git a/.github/workflows/CLAUDE.md b/.github/workflows/CLAUDE.md deleted file mode 100644 index 9697204e..00000000 --- a/.github/workflows/CLAUDE.md +++ /dev/null @@ -1,82 +0,0 @@ - -# Recent Activity - - - -### Dec 13, 2025 - -**convert-feature-requests.yml** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #25022 | 6:48 PM | ✅ | Workflow Fix Committed to Repository | ~289 | -| #25021 | " | 🔴 | Fixed Issue Number Reference in Workflow Steps | ~277 | -| #25020 | " | 🔴 | Workflow String Interpolation Fixed by Consolidating Steps | ~339 | -| #25019 | 6:47 PM | 🔵 | GitHub Workflow Automates Feature Request Triage | ~328 | -| #25012 | 6:41 PM | 🟣 | Auto-Convert Feature Requests to GitHub Discussions | ~298 | -| #25011 | " | ✅ | Staged GitHub Feature Request Automation Files | ~206 | -| #25010 | 6:40 PM | 🟣 | GitHub Action Workflow for Feature Request Auto-Conversion | ~414 | - -**summary.yml** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #25002 | 6:38 PM | 🔵 | AI Summary Workflow for New Issues | ~239 | - -**claude.yml** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #24997 | 6:27 PM | 🔵 | Claude Code Action Workflow for Issue and PR Comments | ~242 | -| #24727 | 4:08 PM | 🔵 | GitHub Automation Baseline Assessment | ~312 | -| #24722 | 4:06 PM | 🔵 | Existing Claude Workflow Trigger Configuration | ~233 | - -**claude-code-review.yml** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #24996 | 6:27 PM | 🔵 | Existing GitHub Actions Workflows Identified | ~199 | -| #24723 | 4:06 PM | 🔵 | Automated PR Review Workflow Pattern | ~268 | -| #24720 | " | 🔵 | GitHub Workflows Inventory | ~142 | - -### Dec 17, 2025 - -**issue-list-query** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #28918 | 7:27 PM | 🔵 | Four open issues identified - MCP connection, Bun PATH, web UI path, and endless mode | ~432 | - -**pr-list-query** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #28917 | 7:27 PM | 🔵 | Recent PRs audit reveals comprehensive Windows stabilization and MCP fixes | ~414 | - -**windows-ci.yml** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #28655 | 5:30 PM | ✅ | Windows CI Removal Committed to Repository | ~253 | -| #28654 | " | ✅ | Windows CI Workflow File Removed | ~174 | -| #28650 | 5:26 PM | ✅ | Committed Windows CI Workflow Simplification | ~213 | -| #28649 | " | ✅ | Removed Build and Install Steps from Windows CI | ~278 | -| #28648 | " | 🔵 | Windows CI Workflow Includes Build Step | ~288 | -| #28644 | 5:24 PM | ✅ | Modified 27 files with 693 additions and 239 deletions for Windows support | ~447 | -| #28625 | 5:19 PM | 🟣 | Windows CI Testing Workflow Deployed | ~303 | -| #28624 | " | ✅ | Windows CI Workflow File Staged for Commit | ~163 | -| #28623 | " | 🔵 | Windows CI Workflow File Present But Untracked | ~178 | -| #28622 | 5:18 PM | 🟣 | Windows CI Pipeline with Worker Lifecycle Testing | ~326 | - -### Dec 31, 2025 - -**claude-code-review.yml** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34627 | 3:01 PM | 🔵 | Claude Code Review GitHub Action Provides Automated PR Review Integration | ~478 | - -**claude.yml** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34626 | 3:01 PM | 🔵 | Test-Driven Validation Agent Performing Extensive Infrastructure Analysis | ~501 | - -### Jan 6, 2026 - -**windows-ci.yml** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38108 | 12:15 AM | 🔵 | Complete Windows Zombie Port Bug Technical Deep Dive | ~935 | - \ No newline at end of file diff --git a/.github/workflows/deploy-install-scripts.yml b/.github/workflows/deploy-install-scripts.yml new file mode 100644 index 00000000..f9f2de12 --- /dev/null +++ b/.github/workflows/deploy-install-scripts.yml @@ -0,0 +1,29 @@ +name: Deploy Install Scripts + +on: + push: + branches: [main] + paths: + - 'openclaw/install.sh' + - 'install/**' + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Copy install scripts to deploy directory + run: | + mkdir -p install/public + cp openclaw/install.sh install/public/openclaw.sh + + - name: Deploy to Vercel + uses: amondnet/vercel-action@v25 + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + vercel-args: '--prod' + working-directory: ./install diff --git a/.gitignore b/.gitignore index d7ecb202..a7c4e1d9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ package-lock.json bun.lock private/ datasets/ +Auto Run Docs/ # Generated UI files (built from viewer-template.html) src/ui/viewer.html @@ -30,4 +31,7 @@ src/ui/viewer.html # Prevent other malformed path directories http*/ -https*/ \ No newline at end of file +https*/ + +# Ignore WebStorm project files (for dinosaur IDE users) +.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a2db6b..41c000e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,489 @@ All notable changes to claude-mem. +## [v10.0.6] - 2026-02-13 + +## Bug Fixes + +- **OpenClaw: Fix MEMORY.md project query mismatch** — `syncMemoryToWorkspace` now includes both the base project name and the agent-scoped project name (e.g., both "openclaw" and "openclaw-main") when querying for context injection, ensuring the correct observations are pulled into MEMORY.md. + +- **OpenClaw: Add feed botToken support for Telegram** — Feeds can now configure a dedicated `botToken` for direct Telegram message delivery, bypassing the OpenClaw gateway channel. This fixes scenarios where the gateway bot token couldn't be used for feed messages. + +## Other + +- Changed OpenClaw plugin kind from "integration" to "memory" for accuracy. + +## [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 + +## 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 + +## Critical Bug Fix: Worker Initialization Failure + +**v9.1.0 was unable to initialize its database on existing installations.** This patch fixes the root cause and several related issues. + +### Bug Fixes + +- **Fix FOREIGN KEY constraint failure during migration** — The `addOnUpdateCascadeToForeignKeys` migration (schema v21) crashed when orphaned observations existed (observations whose `memory_session_id` has no matching row in `sdk_sessions`). Fixed by disabling FK checks (`PRAGMA foreign_keys = OFF`) during table recreation, following SQLite's recommended migration pattern. + +- **Remove hardcoded CHECK constraints on observation type column** — Multiple locations enforced `CHECK(type IN ('decision', 'bugfix', ...))` but the mode system (v8.0.0+) allows custom observation types, causing constraint violations. Removed all 5 occurrences across `SessionStore.ts`, `migrations.ts`, and `migrations/runner.ts`. + +- **Fix Express middleware ordering for initialization guard** — The `/api/*` guard middleware that waits for DB initialization was registered AFTER routes, so Express matched routes before the guard. Moved guard middleware registration BEFORE route registrations. Added dedicated early handler for `/api/context/inject` to fail-open during init. + +### New + +- **Restored mem-search skill** — Recreated `plugin/skills/mem-search/SKILL.md` with the 3-layer workflow (search → timeline → batch fetch) updated for the current MCP tool set. + +## [v9.1.0] - 2026-02-07 + +## v9.1.0 — The Great PR Triage + +100 open PRs reviewed, triaged, and resolved. 157 commits, 123 files changed, +6,104/-721 lines. This release focuses on stability, security, and community contributions. + +### Highlights + +- **100 PR triage**: Reviewed every open PR — merged 48, cherry-picked 13, closed 39 (stale/duplicate/YAGNI) +- **Fail-open hook architecture**: Hooks no longer block Claude Code prompts when the worker is starting up +- **DB initialization guard**: All API endpoints now wait for database initialization instead of crashing with "Database not initialized" +- **Security hardening**: CORS restricted to localhost, XSS defense-in-depth via DOMPurify +- **3 new features**: Manual memory save, project exclusion, folder exclude setting + +--- + +### Security + +- **CORS restricted to localhost** — Worker API no longer accepts cross-origin requests from arbitrary websites. Only localhost/127.0.0.1 origins allowed. (PR #917 by @Spunky84) +- **XSS defense-in-depth** — Added DOMPurify sanitization to TerminalPreview.tsx viewer component (concept from PR #896) + +### New Features + +- **Manual memory storage** — New \`save_memory\` MCP tool and \`POST /api/memory/save\` endpoint for explicit memory capture (PR #662 by @darconada, closes #645) +- **Project exclusion setting** — \`CLAUDE_MEM_EXCLUDED_PROJECTS\` glob patterns to exclude entire projects from tracking (PR #920 by @Spunky84) +- **Folder exclude setting** — \`CLAUDE_MEM_FOLDER_MD_EXCLUDE\` JSON array to exclude paths from CLAUDE.md generation, fixing Xcode/drizzle build conflicts (PR #699 by @leepokai, closes #620) +- **Folder CLAUDE.md opt-in** — \`CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED\` now defaults to \`false\` (opt-in) instead of always-on (PR #913 by @superbiche) +- **Generate/clean CLI commands** — \`generate\` and \`clean\` commands for CLAUDE.md management with \`--dry-run\` support (PR #657 by @thedotmack) +- **Ragtime email investigation** — Batch processor for email investigation workflows (PR #863 by @thedotmack) + +### Hook Resilience (Fail-Open Architecture) + +Hooks no longer block Claude Code when the worker is unavailable or slow: + +- **Graceful hook failures** — Hooks exit 0 with empty responses instead of crashing with exit 2 (PR #973 by @farikh) +- **Fail-open context injection** — Returns empty context during initialization instead of 503 (PR #959 by @rodboev) +- **Fetch timeouts** — All hook fetch calls have timeouts via \`fetchWithTimeout()\` helper (PR #964 by @rodboev) +- **Removed stale user-message hook** — Eliminated startup error from incorrectly bundled hook (PR #960 by @rodboev) +- **DB initialization middleware** — All \`/api/*\` routes now wait for DB init with 30s timeout instead of crashing + +### Windows Stability + +- **Path spaces fix** — bun-runner.js no longer fails for Windows usernames with spaces (PR #972 by @farikh) +- **Spawn guard** — 2-minute cooldown prevents repeated worker popup windows on startup failure + +### Process & Zombie Management + +- **Daemon children cleanup** — Orphan reaper now catches idle daemon child processes (PR #879 by @boaz-robopet) +- **Expanded orphan cleanup** — Startup cleanup now targets mcp-server.cjs and worker-service.cjs processes +- **Session-complete hook** — New Stop phase 2 hook removes sessions from active map, enabling effective orphan reaper cleanup (PR #844 by @thusdigital, fixes #842) + +### Session Management + +- **Prompt-too-long termination** — Sessions terminate cleanly instead of infinite retry loops (PR #934 by @jayvenn21) +- **Infinite restart prevention** — Max 3 restart attempts with exponential backoff, prevents runaway API costs (PR #693 by @ajbmachon) +- **Orphaned message fallback** — Messages from terminated sessions drain via Gemini/OpenRouter fallback (PR #937 by @jayvenn21, fixes #936) +- **Project field backfill** — Sessions correctly scoped when PostToolUse creates session before UserPromptSubmit (PR #940 by @miclip) +- **Provider-aware recovery** — Startup recovery uses correct provider instead of hardcoding SDKAgent (PR #741 by @licutis) +- **AbortController reset** — Prevents infinite "Generator aborted" loops after session abort (PR #627 by @TranslateMe) +- **Stateless provider IDs** — Synthetic memorySessionId generation for Gemini/OpenRouter (concept from PR #615 by @JiehoonKwak) +- **Duplicate generator prevention** — Legacy init endpoint uses idempotent \`ensureGeneratorRunning()\` (PR #932 by @jayvenn21) +- **DB readiness wait** — Session-init endpoint waits for database initialization (PR #828 by @rajivsinclair) +- **Image-only prompt support** — Empty/media prompts use \`[media prompt]\` placeholder (concept from PR #928 by @iammike) + +### CLAUDE.md Path & Generation + +- **Race condition fix** — Two-pass detection prevents corruption when Claude Code edits CLAUDE.md (concept from PR #974 by @cheapsteak) +- **Duplicate path prevention** — Detects \`frontend/frontend/\` style nested duplicates (concept from PR #836 by @Glucksberg) +- **Unsafe directory exclusion** — Blocks generation in \`res/\`, \`.git/\`, \`build/\`, \`node_modules/\`, \`__pycache__/\` (concept from PR #929 by @jayvenn21) + +### Chroma/Vector Search + +- **ID/metadata alignment fix** — Search results no longer misaligned after deduplication (PR #887 by @abkrim) +- **Transport zombie prevention** — Connection error handlers now close transport (PR #769 by @jenyapoyarkov) +- **Zscaler SSL support** — Enterprise environments with SSL inspection now work via combined cert path (PR #884 by @RClark4958) + +### Parser & Config + +- **Nested XML tag handling** — Parser correctly extracts fields with nested XML content (PR #835 by @Glucksberg) +- **Graceful empty transcripts** — Transcript parser returns empty string instead of crashing (PR #862 by @DennisHartrampf) +- **Gemini model name fix** — Corrected \`gemini-3-flash\` → \`gemini-3-flash-preview\` (PR #831 by @Glucksberg) +- **CLAUDE_CONFIG_DIR support** — Plugin paths respect custom config directory (PR #634 by @Kuroakira, fixes #626) +- **Env var priority** — \`env > file > defaults\` ordering via \`applyEnvOverrides()\` (PR #712 by @cjpeterein) +- **Minimum Bun version check** — smart-install.js enforces Bun 1.1.14+ (PR #524 by @quicktime, fixes #519) +- **Stdin timeout** — JSON self-delimiting detection with 30s safety timeout prevents hook hangs (PR #771 by @rajivsinclair, fixes #727) +- **FK constraint prevention** — \`ensureMemorySessionIdRegistered()\` guard + \`ON UPDATE CASCADE\` schema migration (PR #889 by @Et9797, fixes #846) +- **Cursor bun runtime** — Cursor hooks use bun instead of node, fixing bun:sqlite crashes (PR #721 by @polux0) + +### Documentation + +- **9 README PRs merged**: formatting fixes, Korean/Japanese/Chinese render fixes, documentation link updates, Traditional Chinese + Urdu translations (PRs #953, #898, #864, #637, #636, #894, #907, #691 by @Leonard013, @youngsu5582, @eltociear, @WuMingDao, @fengluodb, @PeterDaveHello, @yasirali646) +- **Windows setup note** — npm PATH instructions (PR #919 by @kamran-khalid-v9) +- **Issue templates** — Duplicate check checkbox added (PR #970 by @bmccann36) + +### Community Contributors + +Thank you to the 35+ contributors whose PRs were reviewed in this release: + +@Spunky84, @farikh, @rodboev, @boaz-robopet, @jayvenn21, @ajbmachon, @miclip, @licutis, @TranslateMe, @JiehoonKwak, @rajivsinclair, @iammike, @cheapsteak, @Glucksberg, @abkrim, @jenyapoyarkov, @RClark4958, @DennisHartrampf, @Kuroakira, @cjpeterein, @quicktime, @polux0, @Et9797, @thusdigital, @superbiche, @darconada, @leepokai, @Leonard013, @youngsu5582, @eltociear, @WuMingDao, @fengluodb, @PeterDaveHello, @yasirali646, @kamran-khalid-v9, @bmccann36 + +--- + +**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v9.0.17...v9.1.0 + +## [v9.0.17] - 2026-02-05 + +## Bug Fixes + +### Fix Fresh Install Bun PATH Resolution (#818) + +On fresh installations, hooks would fail because Bun wasn't in PATH until terminal restart. The `smart-install.js` script installs Bun to `~/.bun/bin/bun`, but the current shell session doesn't have it in PATH. + +**Fix:** Introduced `bun-runner.js` — a Node.js wrapper that searches common Bun installation locations across all platforms: +- PATH (via `which`/`where`) +- `~/.bun/bin/bun` (default install location) +- `/usr/local/bin/bun` +- `/opt/homebrew/bin/bun` (macOS Homebrew) +- `/home/linuxbrew/.linuxbrew/bin/bun` (Linuxbrew) +- Windows: `%LOCALAPPDATA%\bun` or fallback paths + +All 9 hook definitions updated to use `node bun-runner.js` instead of direct `bun` calls. + +**Files changed:** +- `plugin/scripts/bun-runner.js` — New 88-line Bun discovery script +- `plugin/hooks/hooks.json` — All hook commands now route through bun-runner + +Fixes #818 | PR #827 by @bigphoot + +## [v9.0.16] - 2026-02-05 + +## Bug Fixes + +### Fix Worker Startup Timeout (#811, #772, #729) + +Resolves the "Worker did not become ready within 15 seconds" timeout error that could prevent hooks from communicating with the worker service. + +**Root cause:** `isWorkerHealthy()` and `waitForHealth()` were checking `/api/readiness`, which returns 503 until full initialization completes — including MCP connection setup that can take 5+ minutes. Hooks only have a 15-second timeout window. + +**Fix:** Switched to `/api/health` (liveness check), which returns 200 as soon as the HTTP server is listening. This is sufficient for hook communication since the worker accepts requests while background initialization continues. + +**Files changed:** +- `src/shared/worker-utils.ts` — `isWorkerHealthy()` now checks `/api/health` +- `src/services/infrastructure/HealthMonitor.ts` — `waitForHealth()` now checks `/api/health` +- `tests/infrastructure/health-monitor.test.ts` — Updated test expectations + +### PR Merge Tasks +- PR #820 merged with full verification pipeline (rebase, code review, build verification, test, manual verification) + +## [v9.0.15] - 2026-02-05 + +## Security Fix + +### Isolated Credentials (#745) +- **Prevents API key hijacking** from random project `.env` files +- Credentials now sourced exclusively from `~/.claude-mem/.env` +- Only whitelisted environment variables passed to SDK `query()` calls +- Authentication method logging shows whether using Claude Code CLI subscription billing or explicit API key + +This is a security-focused patch release that hardens credential handling to prevent unintended API key usage from project directories. + +## [v9.0.14] - 2026-02-05 + +## In-Process Worker Architecture + +This release includes the merged in-process worker architecture from PR #722, which fundamentally improves how hooks interact with the worker service. + +### Changes + +- **In-process worker architecture** - Hook processes now become the worker when port 37777 is available, eliminating Windows spawn issues +- **Hook command improvements** - Added `skipExit` option to `hook-command.ts` for chained command execution +- **Worker health checks** - `worker-utils.ts` now returns boolean status for cleaner health monitoring +- **Massive CLAUDE.md cleanup** - Removed 76 redundant documentation files (4,493 lines removed) +- **Chained hook configuration** - `hooks.json` now supports chained commands for complex workflows + +### Technical Details + +The in-process architecture means hooks no longer need to spawn separate worker processes. When port 37777 is available, the hook itself becomes the worker, providing: +- Faster startup times +- Better resource utilization +- Elimination of process spawn failures on Windows + +Full PR: https://github.com/thedotmack/claude-mem/pull/722 + +## [v9.0.13] - 2026-02-05 + +## Bug Fixes + +### Zombie Observer Prevention (#856) + +Fixed a critical issue where observer processes could become "zombies" - lingering indefinitely without activity. This release adds: + +- **3-minute idle timeout**: SessionQueueProcessor now automatically terminates after 3 minutes of inactivity +- **Race condition fix**: Resolved spurious wakeup issues by resetting `lastActivityTime` on queue activity +- **Comprehensive test coverage**: Added 11 new tests for the idle timeout mechanism + +This fix prevents resource leaks from orphaned observer processes that could accumulate over time. + +## [v9.0.12] - 2026-01-28 + +## Fix: Authentication failure from observer session isolation + +**Critical bugfix** for users who upgraded to v9.0.11. + +### Problem + +v9.0.11 introduced observer session isolation using `CLAUDE_CONFIG_DIR` override, which inadvertently broke authentication: + +``` +Invalid API key · Please run /login +``` + +This happened because Claude Code stores credentials in the config directory, and overriding it prevented access to existing auth tokens. + +### Solution + +Observer sessions now use the SDK's `cwd` option instead: +- Sessions stored under `~/.claude-mem/observer-sessions/` project +- Auth credentials in `~/.claude/` remain accessible +- Observer sessions still won't pollute `claude --resume` lists + +### Affected Users + +Anyone running v9.0.11 who saw "Invalid API key" errors should upgrade immediately. + +--- + +🤖 Generated with [Claude Code](https://claude.ai/code) + +## [v9.0.11] - 2026-01-28 + +## Bug Fixes + +### Observer Session Isolation (#837) +Observer sessions created by claude-mem were polluting the `claude --resume` list, cluttering it with internal plugin sessions that users never intend to resume. In one user's case, 74 observer sessions out of ~220 total (34% noise). + +**Solution**: Observer processes now use a dedicated config directory (`~/.claude-mem/observer-config/`) to isolate their session files from user sessions. + +Thanks to @Glucksberg for this fix! Fixes #832. + +### Stale memory_session_id Crash Prevention (#839) +After a worker restart, stale `memory_session_id` values in the database could cause crashes when attempting to resume SDK conversations. The existing guard didn't protect against this because session data was loaded from the database. + +**Solution**: Clear `memory_session_id` when loading sessions from the database (not from cache). The key insight: if a session isn't in memory, any database `memory_session_id` is definitely stale. + +Thanks to @bigph00t for this fix! Fixes #817. + +--- +**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v9.0.10...v9.0.11 + ## [v9.0.10] - 2026-01-26 ## Bug Fix @@ -1084,233 +1567,3 @@ Since we're now explicit about recovery instead of silently papering over proble This patch release improves stability by adding proper error handling to Chroma vector database sync operations, preventing worker crashes when sync operations timeout. -## [v8.0.5] - 2025-12-24 - -## Bug Fixes - -- **Context Loading**: Fixed observation filtering for non-code modes, ensuring observations are properly retrieved across all mode types - -## Technical Details - -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) - -## [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) - -## [v7.4.4] - 2025-12-21 - -## What's Changed - -* Code quality: comprehensive nonsense audit cleanup (20 issues) by @thedotmack in #400 - -**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.4.3...v7.4.4 - -## [v7.4.3] - 2025-12-20 - -Added Discord notification script for release announcements. - -### Added -- `scripts/discord-release-notify.js` - Posts formatted release notifications to Discord using webhook URL from `.env` -- `npm run discord:notify ` - New npm script to trigger Discord notifications -- Updated version-bump skill workflow to include Discord notification step - -### Configuration -Set `DISCORD_UPDATES_WEBHOOK` in your `.env` file to enable release notifications. - ---- - -🤖 Generated with [Claude Code](https://claude.com/claude-code) - -## [v7.4.2] - 2025-12-20 - -Patch release v7.4.2 - -## Changes -- Refactored worker commands from npm scripts to claude-mem CLI -- Added path alias script -- Fixed Windows worker stop/restart reliability (#395) -- Simplified build commands section in CLAUDE.md - -## [v7.4.1] - 2025-12-19 - -## Bug Fixes - -- **MCP Server**: Redirect logs to stderr to preserve JSON-RPC protocol (#396) - - MCP uses stdio transport where stdout is reserved for JSON-RPC messages - - Console.log was writing startup logs to stdout, causing Claude Desktop to parse log lines as JSON and fail - -## [v7.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. - ---- - -🤖 Generated with [Claude Code](https://claude.com/claude-code) - -## [v7.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. - -## [v7.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. - -## [v7.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 - -## [v7.3.6] - 2025-12-17 - -## Bug Fixes - -- Enhanced SDKAgent response handling and message processing - diff --git a/README.md b/README.md index 4f4769e3..6d4b7db7 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -41,6 +42,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -109,13 +111,23 @@ Start a new Claude Code session in the terminal and enter the following commands: ``` -> /plugin marketplace add thedotmack/claude-mem +/plugin marketplace add thedotmack/claude-mem -> /plugin install claude-mem +/plugin install claude-mem ``` Restart Claude Code. Context from previous sessions will automatically appear in new sessions. +### 🦞 OpenClaw Gateway + +Install claude-mem as a persistent memory plugin on [OpenClaw](https://openclaw.ai) gateways with a single command: + +```bash +curl -fsSL https://install.cmem.ai/openclaw.sh | bash +``` + +The installer handles dependencies, plugin setup, AI provider configuration, worker startup, and optional real-time observation feeds to Telegram, Discord, Slack, and more. See the [OpenClaw Integration Guide](https://docs.claude-mem.ai/openclaw-integration) for details. + **Key Features:** - 🧠 **Persistent Memory** - Context survives across sessions @@ -133,7 +145,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in ## Documentation -📚 **[View Full Documentation](docs/)** - Browse markdown docs on GitHub +📚 **[View Full Documentation](https://docs.claude-mem.ai/)** - Browse on official website ### Getting Started @@ -182,7 +194,7 @@ See [Architecture Overview](https://docs.claude-mem.ai/architecture/overview) fo ## MCP Search Tools -Claude-Mem provides intelligent memory search through **4 MCP tools** following a token-efficient **3-layer workflow pattern**: +Claude-Mem provides intelligent memory search through **5 MCP tools** following a token-efficient **3-layer workflow pattern**: **The 3-Layer Workflow:** @@ -195,6 +207,7 @@ Claude-Mem provides intelligent memory search through **4 MCP tools** following - Start with `search` to get an index of results - Use `timeline` to see what was happening around specific observations - Use `get_observations` to fetch full details for relevant IDs +- Use `save_memory` to manually store important information - **~10x token savings** by filtering before fetching details **Available MCP Tools:** @@ -202,7 +215,8 @@ Claude-Mem provides intelligent memory search through **4 MCP tools** following 1. **`search`** - Search memory index with full-text queries, filters by type/date/project 2. **`timeline`** - Get chronological context around a specific observation or query 3. **`get_observations`** - Fetch full observation details by IDs (always batch multiple IDs) -4. **`__IMPORTANT`** - Workflow documentation (always visible to Claude) +4. **`save_memory`** - Manually save a memory/observation for semantic search +5. **`__IMPORTANT`** - Workflow documentation (always visible to Claude) **Example Usage:** @@ -214,6 +228,9 @@ search(query="authentication bug", type="bugfix", limit=10) // Step 3: Fetch full details get_observations(ids=[123, 456]) + +// Save important information manually +save_memory(text="API requires auth header X-API-Key", title="API Auth") ``` See [Search Tools Guide](https://docs.claude-mem.ai/usage/search-tools) for detailed examples. @@ -236,6 +253,17 @@ See **[Beta Features Documentation](https://docs.claude-mem.ai/beta-features)** - **uv**: Python package manager for vector search (auto-installed if missing) - **SQLite 3**: For persistent storage (bundled) +--- +### Windows Setup Notes + +If you see an error like: + +```powershell +npm : The term 'npm' is not recognized as the name of a cmdlet +``` + +Make sure Node.js and npm are installed and added to your PATH. Download the latest Node.js installer from https://nodejs.org and restart your terminal after installation. + --- ## Configuration diff --git a/cursor-hooks/CLAUDE.md b/cursor-hooks/CLAUDE.md deleted file mode 100644 index 2fbb492d..00000000 --- a/cursor-hooks/CLAUDE.md +++ /dev/null @@ -1,131 +0,0 @@ - -# Recent Activity - - - -### Dec 29, 2025 - -**save-file-edit.sh** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34270 | 10:45 PM | 🔵 | Save File Edit Hook Captures File Modifications as Tool Observations | ~495 | - -**session-summary.sh** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34268 | 10:44 PM | 🔵 | Session Summary Hook Generates Summaries and Updates Context on Stop | ~498 | - -**save-observation.sh** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34267 | 10:44 PM | 🔵 | Save Observation Hook Captures MCP and Shell Executions | ~494 | - -**context-inject.sh** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34266 | 10:44 PM | 🔵 | Context Inject Hook Refreshes Memory Context Before Prompt Submission | ~498 | -| #34165 | 9:41 PM | 🔵 | Context Injection Hook Implementation for Cursor | ~466 | - -**session-init.sh** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34264 | 10:43 PM | 🔵 | Session Init Hook Initializes Sessions on Prompt Submission | ~514 | - -**common.sh** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34261 | 10:43 PM | 🔵 | Cursor Hooks Common Shell Library Provides Core Utilities | ~381 | -| #34237 | 10:31 PM | 🔄 | Removed arbitrary array index validation from json_get function | ~421 | - -**STANDALONE-SETUP.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34258 | 10:39 PM | ✅ | Updated STANDALONE-SETUP.md to recommend user-level installation | ~265 | -| #34257 | " | 🔵 | Claude-Mem Command Reference for Cursor Integration | ~245 | -| #34256 | " | ✅ | Updated STANDALONE-SETUP.md to recommend user-level installation | ~305 | -| #34252 | 10:38 PM | 🔵 | Cursor Hooks Installation and Worker Setup Process | ~258 | -| #34224 | 10:14 PM | ✅ | Completed Bun Migration by Updating All Windows Commands | ~392 | -| #34223 | " | ✅ | Updated Windows Installation Commands to Use Bun | ~354 | -| #34222 | 10:13 PM | ✅ | Updated Quick Reference Table to Use Bun Commands | ~361 | -| #34221 | " | ✅ | Updated Step 5 Status Check Command to Use Bun | ~353 | -| #34220 | " | ✅ | Updated Step 4 Worker Start Command to Use Bun | ~360 | -| #34219 | 10:12 PM | ✅ | Updated Step 3 Hook Installation Commands to Use Bun | ~322 | -| #34218 | " | ✅ | Updated STANDALONE-SETUP Step 1 Commands to Use Bun Instead of NPM | ~357 | -| #34217 | " | ✅ | Updated STANDALONE-SETUP Prerequisites to Require Bun Runtime | ~341 | -| #34215 | 10:08 PM | 🔵 | Retrieved Detailed Cursor Integration Implementation History | ~676 | -| #34214 | 10:07 PM | 🔵 | Cursor Integration Feature Set Discovered via Memory Search | ~427 | - -**QUICKSTART.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34255 | 10:39 PM | ✅ | Quickstart Documentation Reordered to Recommend User-Level Installation First | ~265 | -| #34251 | 10:38 PM | 🔵 | Quickstart Documentation Shows CLI-Based Installation Method | ~257 | -| #34225 | 10:14 PM | ✅ | Updated QUICKSTART Worker Restart Command to Use Bun | ~308 | - -**README.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34254 | 10:38 PM | ✅ | Updated README to recommend user-level installation over project-level | ~345 | -| #34253 | " | 🔵 | Cursor hooks installation documented with quick install CLI and manual options | ~327 | -| #34250 | " | 🔵 | Documentation references installation types and project-level concepts | ~311 | -| #34226 | 10:14 PM | ✅ | Updated README Quick Install Commands to Use Bun | ~311 | - -**install.sh** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34249 | 10:37 PM | ✅ | Install Script Usage Message Updated to Recommend User-Level Installation | ~256 | -| #34248 | " | ✅ | Marked user-level installation as recommended in install script | ~255 | -| #34246 | " | ✅ | Simplified path rewriting logic after enterprise mode removal | ~291 | -| #34245 | " | ✅ | Simplified conditional logic after removing enterprise installation code | ~266 | -| #34244 | 10:36 PM | ✅ | Removed enterprise installation mode from cursor-hooks installer | ~298 | -| #34243 | " | ✅ | Removed enterprise installation option from Cursor hooks installer | ~292 | -| #34242 | " | 🔵 | Cursor hooks installation script copies and configures hooks with path adjustments | ~387 | -| #34240 | 10:33 PM | 🔵 | Cursor hooks installation paths and requirements vary by deployment mode | ~312 | -| #34239 | " | 🔵 | Cursor hooks installation supports enterprise mode | ~240 | -| #34233 | 10:26 PM | ⚖️ | Implemented PR 493 fixes with mixed necessity and complexity trade-offs | ~610 | -| #34232 | " | 🔵 | PR 493 review identified security and concurrency issues requiring fixes | ~560 | -| #34228 | 10:21 PM | 🔴 | Fixed sed portability issue in install.sh | ~318 | - -**common.ps1** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34238 | 10:31 PM | 🔄 | Rollback complete: simplified over-engineered concurrency and validation code | ~434 | -| #34231 | 10:25 PM | 🔴 | Fixed race conditions and security vulnerabilities in Cursor integration | ~562 | - -**hooks.json** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34184 | 9:51 PM | 🔵 | Cursor Hooks Configuration Schema | ~375 | - -### Dec 31, 2025 - -**session-init.sh** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34675 | 3:37 PM | 🔵 | API Endpoint /api/sessions/init Expects contentSessionId Parameter | ~401 | - -### Jan 5, 2026 - -**CLAUDE.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38078 | 9:54 PM | ✅ | CLAUDE.md Documentation Cleanup - 1,233 Lines Removed Across 18 Files | ~590 | - -**INTEGRATION.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #37995 | 9:01 PM | 🔵 | CLAUDE_MEM_WORKER_HOST setting implementation pattern | ~304 | - -**README.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #37990 | 9:00 PM | 🔵 | CLAUDE_MEM_WORKER_HOST setting used across 19 files | ~289 | - -### Jan 7, 2026 - -**CLAUDE.md** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38195 | 7:35 PM | ✅ | Context-hook enhanced with promotional footer and user-message-hook removed from SessionStart | ~376 | -| #38194 | " | 🔵 | Working tree contains 10 modified files ready for commit | ~303 | - \ No newline at end of file diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index adfdcb11..4a15d484 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -3,5 +3,81 @@ -*No recent activity* +### Nov 6, 2025 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #4241 | 11:19 PM | 🟣 | Object-Oriented Architecture Design Document Created | ~662 | +| #4240 | 11:11 PM | 🟣 | Worker Service Rewrite Blueprint Created | ~541 | +| #4239 | 11:07 PM | 🟣 | Comprehensive Worker Service Performance Analysis Document Created | ~541 | +| #4238 | 10:59 PM | 🔵 | Overhead Analysis Document Checked | ~203 | + +### Nov 7, 2025 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #4609 | 6:33 PM | ✅ | PR #69 Successfully Merged to Main Branch | ~516 | +| #4600 | 6:31 PM | 🟣 | Added Worker Service Documentation Suite | ~441 | +| #4597 | " | 🔄 | Worker Service Refactored to Object-Oriented Architecture | ~473 | + +### Nov 8, 2025 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #5539 | 10:20 PM | 🔵 | Harsh critical audit of context-hook reveals systematic anti-patterns | ~3154 | +| #5497 | 9:29 PM | 🔵 | Harsh critical audit of context-hook reveals systematic anti-patterns | ~2815 | +| #5495 | 9:28 PM | 🔵 | Context Hook Audit Reveals Project Anti-Patterns | ~660 | +| #5476 | 9:17 PM | 🔵 | Critical Code Audit Identified 14 Anti-Patterns in Context Hook | ~887 | +| #5391 | 8:45 PM | 🔵 | Critical Code Quality Audit of Context Hook Implementation | ~720 | +| #5150 | 7:37 PM | 🟣 | Troubleshooting Skill Added to Claude-Mem Plugin | ~427 | + +### Nov 9, 2025 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #6161 | 11:55 PM | 🔵 | YC W26 Application Research and Preparation Completed for Claude-Mem | ~1628 | +| #6155 | 11:47 PM | ✅ | Comprehensive Y Combinator Winter 2026 Application Notes Created | ~1045 | +| #5979 | 7:58 PM | 🔵 | Smart Contextualization Feature Architecture | ~560 | +| #5971 | 7:49 PM | 🔵 | Hooks Reference Documentation Structure | ~448 | +| #5929 | 7:08 PM | ✅ | Documentation Updates for v5.4.0 Skill-Based Search Migration | ~604 | +| #5927 | " | ✅ | Updated Configuration Documentation for Skill-Based Search | ~497 | +| #5920 | 7:05 PM | ✅ | Renamed Architecture Documentation File Reference | ~271 | + +### Nov 18, 2025 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #11515 | 8:22 PM | 🔵 | Smart Contextualization Architecture Retrieved with Command Hook Pattern Details | ~502 | + +### Dec 8, 2025 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #22294 | 9:43 PM | 🔵 | Documentation Site Structure Located | ~359 | + +### Dec 12, 2025 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #24430 | 8:27 PM | ✅ | Removed Final Platform Check Reference from Linux Section | ~320 | +| #24429 | " | ✅ | Final Platform Check Reference Removal from Linux Section | ~274 | +| #24428 | " | ✅ | Corrected Second Line Number Reference for Migration Marker Logic | ~267 | +| #24427 | 8:26 PM | ✅ | Updated Line Number Reference for PM2 Cleanup Implementation | ~260 | +| #24426 | " | ✅ | Removed Platform Check from Manual Marker Deletion Scenario | ~338 | +| #24425 | " | ✅ | Removed Platform Check from Fresh Install Scenario Flow | ~314 | +| #24424 | 8:25 PM | ✅ | Renumbered Manual Marker Deletion Scenario | ~285 | +| #24423 | " | ✅ | Renumbered Fresh Install Scenario | ~243 | +| #24422 | " | ✅ | Removed Obsolete Windows Platform Detection Scenario | ~311 | +| #24421 | " | ✅ | Removed Platform Check from macOS Migration Documentation | ~294 | +| #24420 | 8:24 PM | ✅ | Platform Check Removed from Migration Documentation | ~288 | +| #24417 | 8:16 PM | ✅ | Code Reference Example Updated to Reflect Actual Cross-Platform Implementation | ~366 | +| #24416 | " | ✅ | Architecture Decision Documentation Updated to Reflect Cross-Platform PM2 Cleanup Rationale | ~442 | +| #24415 | 8:15 PM | ✅ | Migration Marker Lifecycle Documentation Updated for Unified Cross-Platform Behavior | ~463 | +| #24414 | " | ✅ | Platform Comparison Table Updated to Reflect Unified Cross-Platform Migration | ~351 | +| #24413 | " | ✅ | Windows Platform-Specific Documentation Completely Rewritten for Unified Migration | ~428 | +| #24412 | " | ✅ | User Experience Timeline Updated for Cross-Platform PM2 Cleanup | ~291 | +| #24411 | 8:14 PM | ✅ | Migration Marker Lifecycle Documentation Updated for All Platforms | ~277 | +| #24410 | " | ✅ | Marker File Platform Behavior Documentation Updated for Unified Migration | ~282 | +| #24409 | " | ✅ | Migration Steps Documentation Updated for Cross-Platform PM2 Cleanup | ~278 | +| #24408 | 8:13 PM | ✅ | PM2 Migration Documentation Updated to Remove Windows Platform Check | ~280 | \ No newline at end of file diff --git a/docs/PR-SHIPPING-REPORT.md b/docs/PR-SHIPPING-REPORT.md new file mode 100644 index 00000000..20011ca8 --- /dev/null +++ b/docs/PR-SHIPPING-REPORT.md @@ -0,0 +1,213 @@ +# Claude-Mem PR Shipping Report +*Generated: 2026-02-04* + +## Executive Summary + +6 PRs analyzed for shipping readiness. **1 is ready to merge**, 4 have conflicts, 1 is too large for easy review. + +| PR | Title | Status | Recommendation | +|----|-------|--------|----------------| +| **#856** | Idle timeout for zombie processes | ✅ **MERGEABLE** | **Ship it** | +| #700 | Windows Terminal popup fix | ⚠️ Conflicts | Rebase, then ship | +| #722 | In-process worker architecture | ⚠️ Conflicts | Rebase, high impact | +| #657 | generate/clean CLI commands | ⚠️ Conflicts | Rebase, then ship | +| #863 | Ragtime email investigation | 🔍 Needs review | Research pending | +| #464 | Sleep Agent Pipeline (contributor) | 🔴 Too large | Request split or dedicated review | + +--- + +## Ready to Ship + +### PR #856: Idle Timeout for Zombie Observer Processes +**Status:** ✅ MERGEABLE (no conflicts) + +| Metric | Value | +|--------|-------| +| Additions | 928 | +| Deletions | 171 | +| Files | 8 | +| Risk | Low-Medium | + +**What it does:** +- Adds 3-minute idle timeout to `SessionQueueProcessor` +- Prevents zombie observer processes that were causing 13.4GB swap usage +- Processes exit gracefully after inactivity instead of waiting forever + +**Why ship it:** +- Fixes real user-reported issue (79 zombie processes) +- Well-tested (11 new tests, 440 lines of test coverage) +- Clean implementation, preventive approach +- Supersedes PR #848's reactive cleanup +- No conflicts, ready to merge + +**Review notes:** +- 1 Greptile bot comment (addressed) +- Race condition fix included +- Enhanced logging added + +--- + +## Needs Rebase (Have Conflicts) + +### PR #700: Windows Terminal Popup Fix +**Status:** ⚠️ CONFLICTING + +| Metric | Value | +|--------|-------| +| Additions | 187 | +| Deletions | 399 | +| Files | 8 | +| Risk | Medium | + +**What it does:** +- Eliminates Windows Terminal popup by removing spawn-based daemon +- Worker `start` command becomes daemon directly (no child spawn) +- Removes `restart` command (users do `stop` then `start`) +- Net simplification: -212 lines + +**Breaking changes:** +- `restart` command removed + +**Review status:** +- ✅ 1 APPROVAL from @volkanfirat (Jan 15, 2026) + +**Action needed:** Resolve conflicts, then ready to ship. + +--- + +### PR #722: In-Process Worker Architecture +**Status:** ⚠️ CONFLICTING + +| Metric | Value | +|--------|-------| +| Additions | 869 | +| Deletions | 4,658 | +| Files | 112 | +| Risk | High | + +**What it does:** +- Hook processes become the worker (no separate daemon spawning) +- First hook that needs worker becomes the worker +- Eliminates Windows spawn issues ("NO SPAWN" rule) +- 761 tests pass + +**Architectural impact:** HIGH +- Fundamentally changes worker lifecycle +- Hook processes stay alive (they ARE the worker) +- First hook wins port 37777, others use HTTP + +**Action needed:** Resolve conflicts. Consider relationship with PR #700 (both touch worker architecture). + +--- + +### PR #657: Generate/Clean CLI Commands +**Status:** ⚠️ CONFLICTING + +| Metric | Value | +|--------|-------| +| Additions | 1,184 | +| Deletions | 5,057 | +| Files | 104 | +| Risk | Medium | + +**What it does:** +- Adds `claude-mem generate` and `claude-mem clean` CLI commands +- Fixes validation bugs (deleted folders recreated from stale DB) +- Fixes Windows path handling +- Adds automatic shell alias installation +- Disables subdirectory CLAUDE.md files by default + +**Breaking changes:** +- Default behavior change: folder CLAUDE.md now disabled by default + +**Action needed:** Resolve conflicts, complete Windows testing. + +--- + +## Needs Attention + +### PR #863: Ragtime Email Investigation +**Status:** 🔍 Research pending + +Research agent did not return results. Manual review needed. + +--- + +### PR #464: Sleep Agent Pipeline (Contributor: @laihenyi) +**Status:** 🔴 Too large for effective review + +| Metric | Value | +|--------|-------| +| Additions | 15,430 | +| Deletions | 469 | +| Files | 73 | +| Wait time | 37+ days | +| Risk | High | + +**What it does:** +- Sleep Agent Pipeline with memory tiering +- Supersession detection +- Session Statistics API (`/api/session/:id/stats`) +- StatusLine + PreCompact hooks +- Context Generator improvements +- Self-healing CI workflow + +**Concerns:** +| Issue | Details | +|-------|---------| +| 🔴 Size | 15K+ lines is too large for effective review | +| 🔴 SupersessionDetector | Single file with 1,282 additions | +| 🟡 No tests visible | Test plan checkboxes unchecked | +| 🟡 Self-healing CI | Auto-fix workflow could cause infinite commit loops | +| 🟡 Serena config | Adds `.serena/` tooling | + +**Recommendation:** +1. **Option A:** Request contributor split into 4-5 smaller PRs +2. **Option B:** Allocate dedicated review time (several hours) +3. **Option C:** Cherry-pick specific features (hooks, stats API) + +**Note:** Contributor has been waiting 37+ days. Deserves response either way. + +--- + +## Shipping Strategy + +### Phase 1: Quick Wins (This Week) +1. **Merge #856** — Ready now, fixes real user issue +2. **Rebase #700** — Has approval, Windows fix needed +3. **Rebase #657** — Useful CLI commands + +### Phase 2: Architecture (Careful Review) +4. **Review #722** — High impact, conflicts with #700 approach? + - Both PRs eliminate spawning but in different ways + - May need to pick one approach + +### Phase 3: Contributor PR +5. **Respond to #464** — Options: + - Ask for split + - Schedule dedicated review + - Cherry-pick subset + +### Phase 4: Investigation +6. **Manual review #863** — Ragtime email feature + +--- + +## Conflict Resolution Order + +Since multiple PRs have conflicts, suggested rebase order: + +1. **#856** (merge first — no conflicts) +2. **#700** (rebase onto main after #856) +3. **#657** (rebase onto main after #700) +4. **#722** (rebase last — may conflict with #700 architecturally) + +--- + +## Summary + +| Ready | Conflicts | Needs Work | +|-------|-----------|------------| +| 1 PR (#856) | 3 PRs (#700, #722, #657) | 2 PRs (#464, #863) | + +**Immediate action:** Merge #856, then rebase the conflict PRs in order. diff --git a/docs/context/CLAUDE.md b/docs/context/CLAUDE.md deleted file mode 100644 index 3d88f650..00000000 --- a/docs/context/CLAUDE.md +++ /dev/null @@ -1,77 +0,0 @@ - -# Recent Activity - - - -### Nov 13, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #7806 | 4:54 PM | 🔵 | PR #101 Enhancement: Continuation Prompt Token Reduction | ~634 | - -### Nov 16, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #9976 | 11:35 PM | 🔵 | Endless Mode Architecture Plan Documented | ~661 | -| #9967 | 11:18 PM | ⚖️ | Endless Mode Architecture: Immutable Storage with Ephemeral Transform | ~217 | - -### Nov 17, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #10131 | 1:22 AM | 🔵 | Endless Mode Token Economics Analysis Output: Complete Infrastructure Impact | ~542 | -| #10130 | " | ✅ | Integration of Actual Compute Savings Analysis into Main Execution Flow | ~258 | -| #10129 | " | 🔵 | Prompt Caching Economics: User Cost vs. Anthropic Compute Cost Divergence | ~451 | -| #10126 | 1:19 AM | 🔴 | Fix Return Statement Variable Names in playTheTapeThrough Function | ~313 | -| #10125 | " | ✅ | Redesign Timeline Display to Show Fresh/Cached Token Breakdown and Real Dollar Costs | ~501 | -| #10124 | " | ✅ | Replace Estimated Cost Model with Actual Caching-Based Costs in Anthropic Scale Analysis | ~516 | -| #10123 | " | ✅ | Pivot Session Length Comparison Table from Token to Cost Metrics | ~413 | -| #10122 | " | ✅ | Add Dual Reporting: Token Count vs Actual Cost in Comparison Output | ~410 | -| #10121 | 1:18 AM | ✅ | Apply Prompt Caching Cost Model to Endless Mode Calculation Function | ~501 | -| #10120 | " | ✅ | Integrate Prompt Caching Cost Calculations into Without-Endless-Mode Function | ~426 | -| #10119 | " | ✅ | Display Prompt Caching Pricing in Initial Calculator Output | ~297 | -| #10118 | " | ✅ | Add Prompt Caching Pricing Model to Token Economics Calculator | ~316 | -| #10115 | 1:15 AM | 🟣 | Token Economics Calculator for Endless Mode Sessions | ~465 | -| #10013 | 12:13 AM | 🔵 | Duplicate Agent SDK TypeScript Reference Documentation | ~340 | -| #10012 | " | 🔵 | Agent SDK TypeScript API Reference Complete | ~349 | - -### Nov 18, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #11738 | 11:51 PM | ⚖️ | Comprehensive Architecture Document Created for Phase 1 | ~868 | -| #11711 | 11:44 PM | 🔵 | Language Model Tool Documentation Index | ~282 | -| #11710 | " | 🔵 | Language Model Tool API Implementation Guide | ~718 | -| #11709 | 11:43 PM | 🔵 | Comprehensive Copilot Extension Implementation Plan | ~624 | -| #11708 | " | 🔵 | VS Code Chat Sample Documentation Unavailable | ~327 | -| #11707 | " | 🔵 | VS Code Language Model API Structure and Capabilities | ~515 | -| #11705 | " | ⚖️ | VS Code Extension Development Planning Phase Initiated | ~327 | -| #11206 | 3:01 PM | 🔵 | mem-search skill architecture and migration details retrieved in full format | ~538 | - -### Nov 25, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #15538 | 8:36 PM | 🔵 | Context Document for Landing Page Refinements | ~381 | -| #15314 | 5:04 PM | 🔵 | Endless Mode Documentation Post Retrieved with 156 Lines | ~671 | - -### Dec 20, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #31257 | 8:58 PM | ⚖️ | Eight Conflict Detection Hypotheses Evaluated with Simulation Results | ~525 | -| #31256 | " | 🔵 | Supersession vs Conflict Detection Feature Analysis | ~515 | - -### Dec 30, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34520 | 2:34 PM | 🔵 | V2 Example Code Demonstrates All Key Patterns | ~537 | - -### Jan 7, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38209 | 7:39 PM | 🔵 | Claude Code Hooks System Architecture and Usage | ~491 | - \ No newline at end of file diff --git a/docs/i18n/CLAUDE.md b/docs/i18n/CLAUDE.md deleted file mode 100644 index adfdcb11..00000000 --- a/docs/i18n/CLAUDE.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Recent Activity - - - -*No recent activity* - \ No newline at end of file diff --git a/docs/i18n/README.ar.md b/docs/i18n/README.ar.md index 4e060680..fc3bf733 100644 --- a/docs/i18n/README.ar.md +++ b/docs/i18n/README.ar.md @@ -13,6 +13,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -32,6 +33,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -129,7 +131,7 @@ Claude-Mem هو نظام متطور مصمم لضغط وحفظ الذاكرة ل ## المستندات -📚 **[عرض التوثيق الكامل](docs/)** - تصفح مستندات markdown على GitHub +📚 **[عرض التوثيق الكامل](https://docs.claude-mem.ai/)** - تصفح على الموقع الرسمي ### البدء diff --git a/docs/i18n/README.bn.md b/docs/i18n/README.bn.md index 4a838025..592de8a1 100644 --- a/docs/i18n/README.bn.md +++ b/docs/i18n/README.bn.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Claude Code পুনরায় চালু করুন। পূর্ব ## ডকুমেন্টেশন -📚 **[সম্পূর্ণ ডকুমেন্টেশন দেখুন](docs/)** - GitHub-এ markdown ডক্স ব্রাউজ করুন +📚 **[সম্পূর্ণ ডকুমেন্টেশন দেখুন](https://docs.claude-mem.ai/)** - অফিসিয়াল ওয়েবসাইটে ব্রাউজ করুন ### শুরু করা diff --git a/docs/i18n/README.cs.md b/docs/i18n/README.cs.md index 1498130e..424951d0 100644 --- a/docs/i18n/README.cs.md +++ b/docs/i18n/README.cs.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Restartujte Claude Code. Kontext z předchozích sezení se automaticky objeví ## Dokumentace -📚 **[Zobrazit kompletní dokumentaci](docs/)** - Procházejte dokumentaci v markdown na GitHubu +📚 **[Zobrazit kompletní dokumentaci](https://docs.claude-mem.ai/)** - Procházet na oficiálních stránkách ### Začínáme diff --git a/docs/i18n/README.da.md b/docs/i18n/README.da.md index e993c3db..f11e9c5a 100644 --- a/docs/i18n/README.da.md +++ b/docs/i18n/README.da.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Genstart Claude Code. Kontekst fra tidligere sessioner vil automatisk vises i ny ## Dokumentation -📚 **[Se Fuld Dokumentation](docs/)** - Gennemse markdown-dokumenter på GitHub +📚 **[Se Fuld Dokumentation](https://docs.claude-mem.ai/)** - Gennemse på den officielle hjemmeside ### Kom Godt I Gang diff --git a/docs/i18n/README.de.md b/docs/i18n/README.de.md index 918eb11e..ce7bcb72 100644 --- a/docs/i18n/README.de.md +++ b/docs/i18n/README.de.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Starten Sie Claude Code neu. Kontext aus vorherigen Sitzungen wird automatisch i ## Dokumentation -📚 **[Vollständige Dokumentation anzeigen](docs/)** - Markdown-Dokumentation auf GitHub durchsuchen +📚 **[Vollständige Dokumentation anzeigen](https://docs.claude-mem.ai/)** - Auf der offiziellen Website durchsuchen ### Erste Schritte diff --git a/docs/i18n/README.el.md b/docs/i18n/README.el.md index 3118db81..0264e21f 100644 --- a/docs/i18n/README.el.md +++ b/docs/i18n/README.el.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ ## Τεκμηρίωση -📚 **[Προβολή Πλήρους Τεκμηρίωσης](docs/)** - Περιήγηση στα markdown έγγραφα στο GitHub +📚 **[Προβολή Πλήρους Τεκμηρίωσης](https://docs.claude-mem.ai/)** - Περιήγηση στον επίσημο ιστότοπο ### Ξεκινώντας diff --git a/docs/i18n/README.es.md b/docs/i18n/README.es.md index 97879546..72bdb98e 100644 --- a/docs/i18n/README.es.md +++ b/docs/i18n/README.es.md @@ -16,6 +16,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -35,6 +36,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -127,7 +129,7 @@ Reinicia Claude Code. El contexto de sesiones anteriores aparecerá automáticam ## Documentación -📚 **[Ver Documentación Completa](docs/)** - Explora documentos markdown en GitHub +📚 **[Ver Documentación Completa](https://docs.claude-mem.ai/)** - Navegar en el sitio web oficial ### Primeros Pasos diff --git a/docs/i18n/README.fi.md b/docs/i18n/README.fi.md index a9f66fd9..9fcb24ab 100644 --- a/docs/i18n/README.fi.md +++ b/docs/i18n/README.fi.md @@ -14,6 +14,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -33,6 +34,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -125,7 +127,7 @@ Käynnistä Claude Code uudelleen. Aiempien istuntojen konteksti ilmestyy automa ## Dokumentaatio -📚 **[Näytä täydellinen dokumentaatio](docs/)** - Selaa markdown-dokumentteja GitHubissa +📚 **[Näytä täydellinen dokumentaatio](https://docs.claude-mem.ai/)** - Selaa virallisella verkkosivustolla ### Aloitus diff --git a/docs/i18n/README.fr.md b/docs/i18n/README.fr.md index 11b21d9c..d0c3ef2b 100644 --- a/docs/i18n/README.fr.md +++ b/docs/i18n/README.fr.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Redémarrez Claude Code. Le contexte des sessions précédentes apparaîtra auto ## Documentation -📚 **[Voir la documentation complète](docs/)** - Parcourez la documentation markdown sur GitHub +📚 **[Voir la documentation complète](https://docs.claude-mem.ai/)** - Parcourir sur le site officiel ### Pour commencer diff --git a/docs/i18n/README.he.md b/docs/i18n/README.he.md index 20d8fcab..8b7c5e4b 100644 --- a/docs/i18n/README.he.md +++ b/docs/i18n/README.he.md @@ -14,6 +14,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -33,6 +34,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -125,7 +127,7 @@ ## תיעוד -📚 **[צפה בתיעוד המלא](docs/)** - עיין במסמכי markdown ב-GitHub +📚 **[צפה בתיעוד המלא](https://docs.claude-mem.ai/)** - דפדף באתר הרשמי ### תחילת העבודה diff --git a/docs/i18n/README.hi.md b/docs/i18n/README.hi.md index eed1f1f5..1abf14de 100644 --- a/docs/i18n/README.hi.md +++ b/docs/i18n/README.hi.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Claude Code को पुनः आरंभ करें। पिछले स ## दस्तावेज़ीकरण -📚 **[पूर्ण दस्तावेज़ीकरण देखें](docs/)** - GitHub पर markdown दस्तावेज़ ब्राउज़ करें +📚 **[पूर्ण दस्तावेज़ीकरण देखें](https://docs.claude-mem.ai/)** - आधिकारिक वेबसाइट पर ब्राउज़ करें ### शुरुआत करना diff --git a/docs/i18n/README.hu.md b/docs/i18n/README.hu.md index 439c09a3..fc1c1746 100644 --- a/docs/i18n/README.hu.md +++ b/docs/i18n/README.hu.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Indítsa újra a Claude Code-ot. A korábbi munkamenetek kontextusa automatikusa ## Dokumentáció -📚 **[Teljes dokumentáció megtekintése](docs/)** - Markdown dokumentumok böngészése GitHub-on +📚 **[Teljes dokumentáció megtekintése](https://docs.claude-mem.ai/)** - Böngészés a hivatalos weboldalon ### Első lépések diff --git a/docs/i18n/README.id.md b/docs/i18n/README.id.md index 7cd4b8f9..a615a5a9 100644 --- a/docs/i18n/README.id.md +++ b/docs/i18n/README.id.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Restart Claude Code. Konteks dari sesi sebelumnya akan secara otomatis muncul di ## Dokumentasi -📚 **[Lihat Dokumentasi Lengkap](docs/)** - Telusuri dokumen markdown di GitHub +📚 **[Lihat Dokumentasi Lengkap](https://docs.claude-mem.ai/)** - Jelajahi di situs web resmi ### Memulai diff --git a/docs/i18n/README.it.md b/docs/i18n/README.it.md index 747a66b5..550d813d 100644 --- a/docs/i18n/README.it.md +++ b/docs/i18n/README.it.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Riavvia Claude Code. Il contesto delle sessioni precedenti apparirà automaticam ## Documentazione -📚 **[Visualizza Documentazione Completa](docs/)** - Sfoglia i documenti markdown su GitHub +📚 **[Visualizza Documentazione Completa](https://docs.claude-mem.ai/)** - Sfoglia sul sito ufficiale ### Per Iniziare diff --git a/docs/i18n/README.ja.md b/docs/i18n/README.ja.md index 8b64bc27..66c0e7ca 100644 --- a/docs/i18n/README.ja.md +++ b/docs/i18n/README.ja.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Claude Codeを再起動します。以前のセッションからのコンテキ ## ドキュメント -📚 **[完全なドキュメントを見る](docs/)** - GitHubでマークダウンドキュメントを閲覧 +📚 **[完全なドキュメントを見る](https://docs.claude-mem.ai/)** - 公式ウェブサイトで閲覧 ### はじめに @@ -212,7 +214,7 @@ Claude-Memは、過去の作業について尋ねると自動的に呼び出さ Claude-Memは、**Endless Mode**(拡張セッション用の生体模倣メモリアーキテクチャ)などの実験的機能を備えた**ベータチャネル**を提供します。http://localhost:37777 → SettingsのWebビューアUIから安定版とベータ版を切り替えます。 -Endless Modeと試用方法の詳細については、**[ベータ機能ドキュメント](https://docs.claude-mem.ai/beta-features)**を参照してください。 +Endless Modeと試用方法の詳細については、**[ベータ機能ドキュメント](https://docs.claude-mem.ai/beta-features)** を参照してください。 --- @@ -230,13 +232,13 @@ Endless Modeと試用方法の詳細については、**[ベータ機能ドキ 設定は`~/.claude-mem/settings.json`で管理されます(初回実行時にデフォルト値で自動作成)。AIモデル、ワーカーポート、データディレクトリ、ログレベル、コンテキスト注入設定を構成します。 -利用可能なすべての設定と例については、**[設定ガイド](https://docs.claude-mem.ai/configuration)**を参照してください。 +利用可能なすべての設定と例については、**[設定ガイド](https://docs.claude-mem.ai/configuration)** を参照してください。 --- ## 開発 -ビルド手順、テスト、コントリビューションワークフローについては、**[開発ガイド](https://docs.claude-mem.ai/development)**を参照してください。 +ビルド手順、テスト、コントリビューションワークフローについては、**[開発ガイド](https://docs.claude-mem.ai/development)** を参照してください。 --- @@ -244,7 +246,7 @@ Endless Modeと試用方法の詳細については、**[ベータ機能ドキ 問題が発生した場合は、Claudeに問題を説明すると、troubleshootスキルが自動的に診断して修正を提供します。 -よくある問題と解決策については、**[トラブルシューティングガイド](https://docs.claude-mem.ai/troubleshooting)**を参照してください。 +よくある問題と解決策については、**[トラブルシューティングガイド](https://docs.claude-mem.ai/troubleshooting)** を参照してください。 --- @@ -286,7 +288,7 @@ Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved. - 派生作品もAGPL-3.0の下でライセンスする必要があります - このソフトウェアには保証がありません -**Ragtimeに関する注意**: `ragtime/`ディレクトリは**PolyForm Noncommercial License 1.0.0**の下で個別にライセンスされています。詳細は[ragtime/LICENSE](ragtime/LICENSE)を参照してください。 +**Ragtimeに関する注意**: `ragtime/`ディレクトリは **PolyForm Noncommercial License 1.0.0** の下で個別にライセンスされています。詳細は[ragtime/LICENSE](ragtime/LICENSE)を参照してください。 --- @@ -299,4 +301,4 @@ Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved. --- -**Claude Agent SDKで構築** | **Claude Codeで動作** | **TypeScriptで作成** \ No newline at end of file +**Claude Agent SDKで構築** | **Claude Codeで動作** | **TypeScriptで作成** diff --git a/docs/i18n/README.ko.md b/docs/i18n/README.ko.md index 371f7a50..0d5724c3 100644 --- a/docs/i18n/README.ko.md +++ b/docs/i18n/README.ko.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -114,19 +116,19 @@ Claude Code를 재시작하세요. 이전 세션의 컨텍스트가 자동으로 - 🧠 **지속적인 메모리** - 세션 간 컨텍스트 유지 - 📊 **점진적 공개** - 토큰 비용 가시성을 갖춘 계층화된 메모리 검색 - 🔍 **스킬 기반 검색** - mem-search 스킬로 프로젝트 기록 쿼리 -- 🖥️ **웹 뷰어 UI** - http://localhost:37777에서 실시간 메모리 스트림 확인 +- 🖥️ **웹 뷰어 UI** - http://localhost:37777 에서 실시간 메모리 스트림 확인 - 💻 **Claude Desktop 스킬** - Claude Desktop 대화에서 메모리 검색 - 🔒 **개인정보 제어** - `` 태그를 사용하여 민감한 콘텐츠를 저장소에서 제외 - ⚙️ **컨텍스트 설정** - 주입되는 컨텍스트에 대한 세밀한 제어 - 🤖 **자동 작동** - 수동 개입 불필요 -- 🔗 **인용** - ID로 과거 관찰 참조 (http://localhost:37777/api/observation/{id}를 통해 액세스하거나 http://localhost:37777의 웹 뷰어에서 모두 보기) +- 🔗 **인용** - ID로 과거 관찰 참조 (http://localhost:37777/api/observation/{id} 를 통해 액세스하거나 http://localhost:37777 의 웹 뷰어에서 모두 보기) - 🧪 **베타 채널** - 버전 전환을 통해 Endless Mode와 같은 실험적 기능 사용 --- ## 문서 -📚 **[전체 문서 보기](docs/)** - GitHub에서 마크다운 문서 탐색 +📚 **[전체 문서 보기](https://docs.claude-mem.ai/)** - 공식 웹사이트에서 찾아보기 ### 시작하기 diff --git a/docs/i18n/README.nl.md b/docs/i18n/README.nl.md index 67a6c2bb..3437cdbf 100644 --- a/docs/i18n/README.nl.md +++ b/docs/i18n/README.nl.md @@ -14,6 +14,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -33,6 +34,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -125,7 +127,7 @@ Herstart Claude Code. Context van eerdere sessies verschijnt automatisch in nieu ## Documentatie -📚 **[Bekijk Volledige Documentatie](docs/)** - Blader door markdown documenten op GitHub +📚 **[Bekijk Volledige Documentatie](https://docs.claude-mem.ai/)** - Bladeren op de officiële website ### Aan de Slag diff --git a/docs/i18n/README.no.md b/docs/i18n/README.no.md index 678f4f0d..a50b5995 100644 --- a/docs/i18n/README.no.md +++ b/docs/i18n/README.no.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Start Claude Code på nytt. Kontekst fra tidligere økter vil automatisk vises i ## Dokumentasjon -📚 **[Se Full Dokumentasjon](docs/)** - Bla gjennom markdown-dokumenter på GitHub +📚 **[Se Full Dokumentasjon](https://docs.claude-mem.ai/)** - Bla gjennom på det offisielle nettstedet ### Komme I Gang diff --git a/docs/i18n/README.pl.md b/docs/i18n/README.pl.md index 97fc3fe1..fcef5ebd 100644 --- a/docs/i18n/README.pl.md +++ b/docs/i18n/README.pl.md @@ -14,6 +14,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -33,6 +34,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -125,7 +127,7 @@ Uruchom ponownie Claude Code. Kontekst z poprzednich sesji automatycznie pojawi ## Dokumentacja -📚 **[Wyświetl Pełną Dokumentację](docs/)** - Przeglądaj dokumentację markdown na GitHub +📚 **[Wyświetl Pełną Dokumentację](https://docs.claude-mem.ai/)** - Przeglądaj na oficjalnej stronie ### Pierwsze Kroki diff --git a/docs/i18n/README.pt-br.md b/docs/i18n/README.pt-br.md index f7df0381..bcb2c43a 100644 --- a/docs/i18n/README.pt-br.md +++ b/docs/i18n/README.pt-br.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Reinicie o Claude Code. O contexto de sessões anteriores aparecerá automaticam ## Documentação -📚 **[Ver Documentação Completa](docs/)** - Navegue pelos documentos markdown no GitHub +📚 **[Ver Documentação Completa](https://docs.claude-mem.ai/)** - Navegar no site oficial ### Começando diff --git a/docs/i18n/README.ro.md b/docs/i18n/README.ro.md index c52e4d2f..69ad6e0d 100644 --- a/docs/i18n/README.ro.md +++ b/docs/i18n/README.ro.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Reporniți Claude Code. Contextul din sesiunile anterioare va apărea automat î ## Documentație -📚 **[Vizualizați Documentația Completă](docs/)** - Răsfoiți documentele markdown pe GitHub +📚 **[Vizualizați Documentația Completă](https://docs.claude-mem.ai/)** - Răsfoiți pe site-ul oficial ### Introducere diff --git a/docs/i18n/README.ru.md b/docs/i18n/README.ru.md index 2b53f3fa..7bc3ac07 100644 --- a/docs/i18n/README.ru.md +++ b/docs/i18n/README.ru.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ ## Документация -📚 **[Просмотреть полную документацию](docs/)** - Просмотр markdown-документов на GitHub +📚 **[Просмотреть полную документацию](https://docs.claude-mem.ai/)** - Просмотр на официальном сайте ### Начало работы diff --git a/docs/i18n/README.sv.md b/docs/i18n/README.sv.md index a2c9d52e..39c9cb3b 100644 --- a/docs/i18n/README.sv.md +++ b/docs/i18n/README.sv.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Starta om Claude Code. Kontext från tidigare sessioner kommer automatiskt att v ## Dokumentation -📚 **[Visa fullständig dokumentation](docs/)** - Bläddra bland markdown-dokument på GitHub +📚 **[Visa fullständig dokumentation](https://docs.claude-mem.ai/)** - Bläddra på den officiella webbplatsen ### Komma igång diff --git a/docs/i18n/README.th.md b/docs/i18n/README.th.md index fe6dddca..cb77f90a 100644 --- a/docs/i18n/README.th.md +++ b/docs/i18n/README.th.md @@ -14,6 +14,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -33,6 +34,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -125,7 +127,7 @@ ## เอกสาร -📚 **[ดูเอกสารฉบับเต็ม](docs/)** - เรียกดูเอกสาร markdown บน GitHub +📚 **[ดูเอกสารฉบับเต็ม](https://docs.claude-mem.ai/)** - เรียกดูบนเว็บไซต์อย่างเป็นทางการ ### เริ่มต้นใช้งาน diff --git a/docs/i18n/README.tr.md b/docs/i18n/README.tr.md index 8860367d..3c36f6ab 100644 --- a/docs/i18n/README.tr.md +++ b/docs/i18n/README.tr.md @@ -14,6 +14,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -33,6 +34,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -125,7 +127,7 @@ Claude Code'u yeniden başlatın. Önceki oturumlardaki bağlam otomatik olarak ## Dokümantasyon -📚 **[Tam Dokümantasyonu Görüntüle](docs/)** - GitHub'da markdown dökümanlarına göz atın +📚 **[Tam Dokümantasyonu Görüntüle](https://docs.claude-mem.ai/)** - Resmi web sitesinde göz atın ### Başlarken diff --git a/docs/i18n/README.uk.md b/docs/i18n/README.uk.md index 10310d2e..b7aa2a39 100644 --- a/docs/i18n/README.uk.md +++ b/docs/i18n/README.uk.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ ## Документація -📚 **[Переглянути повну документацію](docs/)** - Переглядайте markdown документи на GitHub +📚 **[Переглянути повну документацію](https://docs.claude-mem.ai/)** - Переглянути на офіційному сайті ### Початок роботи diff --git a/docs/i18n/README.ur.md b/docs/i18n/README.ur.md new file mode 100644 index 00000000..c67e593d --- /dev/null +++ b/docs/i18n/README.ur.md @@ -0,0 +1,311 @@ +

+🌐 یہ ایک خودکار ترجمہ ہے۔ کمیونٹی کی اصلاحات کا خیر مقدم ہے! + +--- +

+
+ + + + + Claude-Mem + + +
+

+ +

+ 🇨🇳 中文 • + 🇯🇵 日本語 • + 🇧🇷 Português • + 🇰🇷 한국어 • + 🇪🇸 Español • + 🇩🇪 Deutsch • + 🇫🇷 Français + 🇮🇱 עברית • + 🇸🇦 العربية • + 🇷🇺 Русский • + 🇵🇱 Polski • + 🇨🇿 Čeština • + 🇳🇱 Nederlands • + 🇹🇷 Türkçe • + 🇺🇦 Українська • + 🇻🇳 Tiếng Việt • + 🇮🇩 Indonesia • + 🇹🇭 ไทย • + 🇮🇳 हिन्दी • + 🇧🇩 বাংলা • + 🇵🇰 اردو • + 🇷🇴 Română • + 🇸🇪 Svenska • + 🇮🇹 Italiano • + 🇬🇷 Ελληνικά • + 🇭🇺 Magyar • + 🇫🇮 Suomi • + 🇩🇰 Dansk • + 🇳🇴 Norsk +

+ +

Claude Code کے لیے بنایا گیا مستقل میموری کمپریشن سسٹم۔

+ +

+ + License + + + Version + + + Node + + + Mentioned in Awesome Claude Code + +

+ +

+ + + + + thedotmack/claude-mem | Trendshift + + +

+ +
+ +

+ + + Claude-Mem Preview + + +

+ +

+ تیز رفتار شروعات • + یہ کیسے کام کرتا ہے • + تلاش کے اوزار • + دستاویزات • + ترتیبات • + مسائل کی تشخیص • + لائسنس +

+ +

+ Claude-Mem خودکار طور پر ٹول کے استعمال کے بعد کے مشاہدات کو ریکارڈ کرتا ہے، سیمانٹک خلاصے تیار کرتا ہے اور انہیں مستقبل کے سیشنز میں دستیاب کرتا ہے تاکہ آپ سیشن میں براہ راست تناسب محفوظ رہے۔ یہ Claude کو سیشن ختم ہونے یا دوبارہ جڑنے کے بعد بھی منصوبے کے بارے میں معلومات کی مسلسلیت برقرار رکھنے کے قابل بناتا ہے۔ +

+ +--- + +## تیز رفتار شروعات + +ٹرمنل میں نیا Claude Code سیشن شروع کریں اور ہیں کمانڈز درج کریں: + +``` +> /plugin marketplace add thedotmack/claude-mem + +> /plugin install claude-mem +``` + +Claude Code کو دوبارہ شروع کریں۔ سابقہ سیشن کا تناسب خودکار طور پر نئے سیشن میں موجود ہوگا۔ + +**اہم خصوصیات:** + +- 🧠 **مستقل میموری** - تناسب سیشن کے دوران برقرار رہتا ہے +- 📊 **بتدریج ظہور** - لیئرڈ میموری کی بازیافت ٹوکن کی لاگت کی نمائندگی کے ساتھ +- 🔍 **کمکردہ تلاش** - mem-search مہارت کے ساتھ اپنے منصوبے کی تاریخ میں تلاش کریں +- 🖥️ **ویب ویور یو آئی** - http://localhost:37777 پر حقیقی وقت میموری اسٹریم +- 💻 **Claude Desktop مہارت** - Claude Desktop بات چیت سے میموری تلاش کریں +- 🔒 **رازداری کے کنٹرولز** - حساس مواد کو ذخیرہ سے خارج کرنے کے لیے `` ٹیگ استعمال کریں +- ⚙️ **تناسب کی ترتیبات** - کون سا تناسب انجیکٹ کیا جائے اس پر باریک کنٹرول +- 🤖 **خودکار آپریشن** - کسی دستی مداخلت کی ضرورت نہیں +- 🔗 **حوالہ** - ID کے ذریعے سابقہ مشاہدات کا حوالہ دیں (http://localhost:37777/api/observation/{id} کے ذریعے رسائی حاصل کریں یا تمام کو http://localhost:37777 پر ویب ویور میں دیکھیں) +- 🧪 **بیٹا چینل** - ورژن تبدیل کرنے کے ذریعے Endless Mode جیسی تجرباتی خصوصیات آزمائیں + +--- + +## دستاویزات + +📚 **[مکمل دستاویزات دیکھیں](docs/)** - GitHub پر markdown ڈاکس کو براؤز کریں + +### شروعات کرنا + +- **[انسٹالیشن گائیڈ](https://docs.claude-mem.ai/installation)** - تیز رفتار شروعات اور اعلیٰ درجے کی انسٹالیشن +- **[استعمال گائیڈ](https://docs.claude-mem.ai/usage/getting-started)** - Claude-Mem خودکار طور پر کیسے کام کرتا ہے +- **[تلاش کے اوزار](https://docs.claude-mem.ai/usage/search-tools)** - قدرتی زبان کے ساتھ اپنے منصوبے کی تاریخ میں تلاش کریں +- **[بیٹا خصوصیات](https://docs.claude-mem.ai/beta-features)** - Endless Mode جیسی تجرباتی خصوصیات آزمائیں + +### بہترین طریقہ کار + +- **[تناسب انجینیئرنگ](https://docs.claude-mem.ai/context-engineering)** - AI ایجنٹ کے تناسب کی اہمیت کے اصول +- **[بتدریج ظہور](https://docs.claude-mem.ai/progressive-disclosure)** - Claude-Mem کے تناسب کی تیاری کی حکمت عملی کے پیچھے فلسفہ + +### تعمیر + +- **[جائزہ](https://docs.claude-mem.ai/architecture/overview)** - نظام کے اجزاء اور ڈیٹا کے بہاؤ +- **[تعمیر کا ارتقاء](https://docs.claude-mem.ai/architecture-evolution)** - v3 سے v5 تک کا سفر +- **[ہکس تعمیر](https://docs.claude-mem.ai/hooks-architecture)** - Claude-Mem لائف سائیکل ہکس کا استعمال کیسے کرتا ہے +- **[ہکس حوالہ](https://docs.claude-mem.ai/architecture/hooks)** - 7 ہک اسکرپٹس کی تشریح +- **[ورکر سروس](https://docs.claude-mem.ai/architecture/worker-service)** - HTTP API اور Bun انتظام +- **[ڈیٹا بیس](https://docs.claude-mem.ai/architecture/database)** - SQLite اسکیما اور FTS5 تلاش +- **[تلاش تعمیر](https://docs.claude-mem.ai/architecture/search-architecture)** - Chroma ویکٹر ڈیٹا بیس کے ساتھ ہائبرڈ تلاش + +### ترتیبات اور ترقی + +- **[ترتیبات](https://docs.claude-mem.ai/configuration)** - ماحول کے متغیرات اور سیٹنگز +- **[ترقی](https://docs.claude-mem.ai/development)** - تعمیر، جانچ، حصہ داری +- **[مسائل کی تشخیص](https://docs.claude-mem.ai/troubleshooting)** - عام مسائل اور حل + +--- + +## یہ کیسے کام کرتا ہے + +**اہم اجزاء:** + +1. **5 لائف سائیکل ہکس** - SessionStart، UserPromptSubmit، PostToolUse، Stop، SessionEnd (6 ہک اسکرپٹس) +2. **سمارٹ انسٹالیشن** - کیش شدہ منحصرات چیکر (پری ہک اسکرپٹ، لائف سائیکل ہک نہیں) +3. **ورکر سروس** - ویب ویور UI اور 10 تلاش کے endpoints کے ساتھ پورٹ 37777 پر HTTP API، Bun کے ذریعے برتاؤ +4. **SQLite ڈیٹا بیس** - سیشنز، مشاہدات، خلاصہ ذخیرہ کرتا ہے +5. **mem-search مہارت** - بتدریج ظہور کے ساتھ قدرتی زبان کے سوالات +6. **Chroma ویکٹر ڈیٹا بیس** - ہائبرڈ سیمانٹک + کلیدی لفظ تلاش ذہین تناسب کی بازیافت کے لیے + +تفصیلات کے لیے [تعمیر کا جائزہ](https://docs.claude-mem.ai/architecture/overview) دیکھیں۔ + +--- + +## MCP تلاش کے اوزار + +Claude-Mem ٹوکن-موثر **3-لیئر ورک فلو پیٹرن** کی پیروی کرتے ہوئے **4 MCP اوزار** کے ذریعے ذہین میموری تلاش فراہم کرتا ہے: + +**3-لیئر ورک فلو:** + +1. **`search`** - IDs کے ساتھ کمپیکٹ انڈیکس حاصل کریں (~50-100 ٹوکن/نتیجہ) +2. **`timeline`** - دلچسپ نتائج کے ارد گرد زمانی تناسب حاصل کریں +3. **`get_observations`** - فلٹر شدہ IDs کے لیے صرف مکمل تفصیلات حاصل کریں (~500-1,000 ٹوکن/نتیجہ) + +**یہ کیسے کام کرتا ہے:** +- Claude آپ کی میموری میں تلاش کے لیے MCP اوزار استعمال کرتا ہے +- نتائج کا انڈیکس حاصل کرنے کے لیے `search` سے شروع کریں +- مخصوص مشاہدات کے ارد گرد کیا ہو رہا تھا دیکھنے کے لیے `timeline` استعمال کریں +- متعلقہ IDs کے لیے مکمل تفصیلات حاصل کرنے کے لیے `get_observations` استعمال کریں +- تفصیلات حاصل کرنے سے پہلے فلٹرنگ کے ذریعے **~10x ٹوکن کی بچت** + +**دستیاب MCP اوزار:** + +1. **`search`** - مکمل متن کی تلاش کے سوالات کے ساتھ میموری انڈیکس تلاش کریں، قسم/تاریخ/منصوبے کے لحاظ سے فلٹر کریں +2. **`timeline`** - مخصوص مشاہدہ یا سوال کے ارد گرد زمانی تناسب حاصل کریں +3. **`get_observations`** - IDs کے ذریعے مکمل مشاہدہ تفصیلات حاصل کریں (ہمیشہ متعدد IDs کو بیچ کریں) +4. **`__IMPORTANT`** - ورک فلو دستاویزات (ہمیشہ Claude کو نظر آتی ہے) + +**استعمال کی مثال:** + +```typescript +// مرحلہ 1: انڈیکس کے لیے تلاش کریں +search(query="authentication bug", type="bugfix", limit=10) + +// مرحلہ 2: انڈیکس کا جائزہ لیں، متعلقہ IDs کی شناخت کریں (مثلاً، #123, #456) + +// مرحلہ 3: مکمل تفصیلات حاصل کریں +get_observations(ids=[123, 456]) +``` + +تفصیلی مثالوں کے لیے [تلاش کے اوزار گائیڈ](https://docs.claude-mem.ai/usage/search-tools) دیکھیں۔ + +--- + +## بیٹا خصوصیات + +Claude-Mem ایک **بیٹا چینل** فراہم کرتا ہے جس میں **Endless Mode** جیسی تجرباتی خصوصیات ہیں (بڑھی ہوئی سیشنز کے لیے حیاتی نقل میموری کی تعمیر)۔ http://localhost:37777 → Settings میں ویب ویور UI سے مستحکم اور بیٹا ورژن کے درمیان سوئچ کریں۔ + +Endless Mode اور اسے کیسے آزمائیں اس کے بارے میں تفصیلات کے لیے **[بیٹا خصوصیات دستاویزات](https://docs.claude-mem.ai/beta-features)** دیکھیں۔ + +--- + +## نظام کی ضروریات + +- **Node.js**: 18.0.0 یا اس سے اوپر +- **Claude Code**: پلگ ان سپورٹ کے ساتھ جدید ترین ورژن +- **Bun**: JavaScript رن ٹائم اور پروسیس مینیجر (غیر موجود ہو تو خودکار طور پر انسٹال ہوگا) +- **uv**: ویکٹر تلاش کے لیے Python پیکج مینیجر (غیر موجود ہو تو خودکار طور پر انسٹال ہوگا) +- **SQLite 3**: مستقل اسٹوریج کے لیے (بنڈل شدہ) + +--- + +## ترتیبات + +سیٹنگز `~/.claude-mem/settings.json` میں منظم ہیں (پہلی رن میں ڈیفالٹ کے ساتھ خودکار طور پر بنائی جاتی ہے)۔ AI ماڈل، ورکر پورٹ، ڈیٹا ڈائریکٹری، لاگ لیول اور تناسب انجیکشن سیٹنگز کو ترتیب دیں۔ + +تمام دستیاب سیٹنگز اور مثالوں کے لیے **[ترتیبات گائیڈ](https://docs.claude-mem.ai/configuration)** دیکھیں۔ + +--- + +## ترقی + +تعمیر کی ہدایات، جانچ اور حصہ داری کے کام کے بہاؤ کے لیے **[ترقی گائیڈ](https://docs.claude-mem.ai/development)** دیکھیں۔ + +--- + +## مسائل کی تشخیص + +اگر مسائل کا سامنا ہو تو Claude کو مسئلہ بتائیں اور troubleshoot مہارت خودکار طور پر تشخیص دے گی اور حل فراہم کرے گی۔ + +عام مسائل اور حل کے لیے **[مسائل کی تشخیص گائیڈ](https://docs.claude-mem.ai/troubleshooting)** دیکھیں۔ + +--- + +## خرابی کی رپورٹ + +خودکار جنریٹر کے ساتھ تفصیلی خرابی کی رپورٹ تیار کریں: + +```bash +cd ~/.claude/plugins/marketplaces/thedotmack +npm run bug-report +``` + +## حصہ داری + +حصہ داری کا خیر مقدم ہے! براہ کرم: + +1. رپوزیٹری کو فورک کریں +2. ایک خصوصیت کی برانچ بنائیں +3. ٹیسٹ کے ساتھ اپنی تبدیلیاں کریں +4. دستاویزات کو اپڈیٹ کریں +5. ایک Pull Request جمع کریں + +حصہ داری کے کام کے بہاؤ کے لیے [ترقی گائیڈ](https://docs.claude-mem.ai/development) دیکھیں۔ + +--- + +## لائسنس + +یہ منصوبہ **GNU Affero General Public License v3.0** (AGPL-3.0) کے تحت لائسنس ہے۔ + +Copyright (C) 2025 Alex Newman (@thedotmack)۔ تمام حقوق محفوظ ہیں۔ + +مکمل تفصیلات کے لیے [LICENSE](LICENSE) فائل دیکھیں۔ + +**اس کا مطلب کیا ہے:** + +- آپ اس سافٹ ویئر کو آزادی سے استعمال، تبدیل اور تقسیم کر سکتے ہیں +- اگر آپ اسے تبدیل کریں اور نیٹ ورک سرور میں نشر کریں تو آپ کو اپنا سورس کوڈ دستیاب کرنا ہوگا +- ماخوذ کام بھی AGPL-3.0 کے تحت لائسنس ہونے چاہیں +- اس سافٹ ویئر کے لیے کوئی وارنٹی نہیں + +**Ragtime کے بارے میں نوٹ**: `ragtime/` ڈائریکٹری الگ سے **PolyForm Noncommercial License 1.0.0** کے تحت لائسنس ہے۔ تفصیلات کے لیے [ragtime/LICENSE](ragtime/LICENSE) دیکھیں۔ + +--- + +## معاونت + +- **دستاویزات**: [docs/](docs/) +- **مسائل**: [GitHub Issues](https://github.com/thedotmack/claude-mem/issues) +- **رپوزیٹری**: [github.com/thedotmack/claude-mem](https://github.com/thedotmack/claude-mem) +- **مصنف**: Alex Newman ([@thedotmack](https://github.com/thedotmack)) + +--- + +**Claude Agent SDK کے ساتھ بنایا گیا** | **Claude Code کے ذریعے طاقت ور** | **TypeScript کے ساتھ بنایا گیا** + +
diff --git a/docs/i18n/README.vi.md b/docs/i18n/README.vi.md index 8dadb339..a0a99a48 100644 --- a/docs/i18n/README.vi.md +++ b/docs/i18n/README.vi.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ Khởi động lại Claude Code. Ngữ cảnh từ các phiên trước sẽ t ## Tài Liệu -📚 **[Xem Tài Liệu Đầy Đủ](docs/)** - Duyệt tài liệu markdown trên GitHub +📚 **[Xem Tài Liệu Đầy Đủ](https://docs.claude-mem.ai/)** - Duyệt trên trang web chính thức ### Bắt Đầu diff --git a/docs/i18n/README.zh-tw.md b/docs/i18n/README.zh-tw.md new file mode 100644 index 00000000..47406a7f --- /dev/null +++ b/docs/i18n/README.zh-tw.md @@ -0,0 +1,311 @@ +🌐 這是自動翻譯。歡迎社群貢獻修正! + +--- +

+
+ + + + + Claude-Mem + + +
+

+ +

+ 🇨🇳 中文 • + 🇹🇼 繁體中文 • + 🇯🇵 日本語 • + 🇧🇷 Português • + 🇰🇷 한국어 • + 🇪🇸 Español • + 🇩🇪 Deutsch • + 🇫🇷 Français + 🇮🇱 עברית • + 🇸🇦 العربية • + 🇷🇺 Русский • + 🇵🇱 Polski • + 🇨🇿 Čeština • + 🇳🇱 Nederlands • + 🇹🇷 Türkçe • + 🇺🇦 Українська • + 🇻🇳 Tiếng Việt • + 🇮🇩 Indonesia • + 🇹🇭 ไทย • + 🇮🇳 हिन्दी • + 🇧🇩 বাংলা • + 🇷🇴 Română • + 🇸🇪 Svenska • + 🇮🇹 Italiano • + 🇬🇷 Ελληνικά • + 🇭🇺 Magyar • + 🇫🇮 Suomi • + 🇩🇰 Dansk • + 🇳🇴 Norsk +

+ +

Claude Code 打造的持久記憶壓縮系統

+ +

+ + License + + + Version + + + Node + + + Mentioned in Awesome Claude Code + +

+ +

+ + + + + thedotmack/claude-mem | Trendshift + + +

+ +
+ +

+ + + Claude-Mem Preview + + +

+ +

+ 快速開始 • + 運作原理 • + 搜尋工具 • + 文件 • + 設定 • + 疑難排解 • + 授權條款 +

+ +

+ Claude-Mem 透過自動擷取工具使用觀察、產生語意摘要並在未來的工作階段中提供使用,無縫保留跨工作階段的脈絡。這使 Claude 即使在工作階段結束或重新連線後,仍能維持對專案的知識連續性。 +

+ +--- + +## 快速開始 + +在終端機中開啟新的 Claude Code 工作階段,並輸入以下指令: + +``` +> /plugin marketplace add thedotmack/claude-mem + +> /plugin install claude-mem +``` + +重新啟動 Claude Code。先前工作階段的脈絡將自動出現在新的工作階段中。 + +**主要功能:** + +- 🧠 **持久記憶** - 脈絡跨工作階段保留 +- 📊 **漸進式揭露** - 具有 Token 成本可見性的分層記憶擷取 +- 🔍 **技能式搜尋** - 使用 mem-search 技能查詢專案歷史 +- 🖥️ **網頁檢視介面** - 在 http://localhost:37777 即時檢視記憶串流 +- 💻 **Claude Desktop 技能** - 從 Claude Desktop 對話中搜尋記憶 +- 🔒 **隱私控制** - 使用 `` 標籤排除敏感內容的儲存 +- ⚙️ **脈絡設定** - 精細控制注入哪些脈絡 +- 🤖 **自動運作** - 無需手動介入 +- 🔗 **引用** - 使用 ID 參考過去的觀察(透過 http://localhost:37777/api/observation/{id} 存取,或在 http://localhost:37777 的網頁檢視器中檢視全部) +- 🧪 **Beta 通道** - 透過版本切換試用 Endless Mode 等實驗性功能 + +--- + +## 文件 + +📚 **[檢視完整文件](docs/)** - 在 GitHub 上瀏覽 Markdown 文件 + +### 入門指南 + +- **[安裝指南](https://docs.claude-mem.ai/installation)** - 快速開始與進階安裝 +- **[使用指南](https://docs.claude-mem.ai/usage/getting-started)** - Claude-Mem 如何自動運作 +- **[搜尋工具](https://docs.claude-mem.ai/usage/search-tools)** - 使用自然語言查詢專案歷史 +- **[Beta 功能](https://docs.claude-mem.ai/beta-features)** - 試用 Endless Mode 等實驗性功能 + +### 最佳實務 + +- **[脈絡工程](https://docs.claude-mem.ai/context-engineering)** - AI 代理脈絡最佳化原則 +- **[漸進式揭露](https://docs.claude-mem.ai/progressive-disclosure)** - Claude-Mem 脈絡啟動策略背後的理念 + +### 架構 + +- **[概覽](https://docs.claude-mem.ai/architecture/overview)** - 系統元件與資料流程 +- **[架構演進](https://docs.claude-mem.ai/architecture-evolution)** - 從 v3 到 v5 的旅程 +- **[Hooks 架構](https://docs.claude-mem.ai/hooks-architecture)** - Claude-Mem 如何使用生命週期掛鉤 +- **[Hooks 參考](https://docs.claude-mem.ai/architecture/hooks)** - 7 個掛鉤腳本說明 +- **[Worker 服務](https://docs.claude-mem.ai/architecture/worker-service)** - HTTP API 與 Bun 管理 +- **[資料庫](https://docs.claude-mem.ai/architecture/database)** - SQLite 結構描述與 FTS5 搜尋 +- **[搜尋架構](https://docs.claude-mem.ai/architecture/search-architecture)** - 使用 Chroma 向量資料庫的混合搜尋 + +### 設定與開發 + +- **[設定](https://docs.claude-mem.ai/configuration)** - 環境變數與設定 +- **[開發](https://docs.claude-mem.ai/development)** - 建置、測試、貢獻 +- **[疑難排解](https://docs.claude-mem.ai/troubleshooting)** - 常見問題與解決方案 + +--- + +## 運作原理 + +**核心元件:** + +1. **5 個生命週期掛鉤** - SessionStart、UserPromptSubmit、PostToolUse、Stop、SessionEnd(6 個掛鉤腳本) +2. **智慧安裝** - 快取的相依性檢查器(pre-hook 腳本,非生命週期掛鉤) +3. **Worker 服務** - 連接埠 37777 上的 HTTP API,含網頁檢視介面與 10 個搜尋端點,由 Bun 管理 +4. **SQLite 資料庫** - 儲存工作階段、觀察、摘要 +5. **mem-search 技能** - 具有漸進式揭露的自然語言查詢 +6. **Chroma 向量資料庫** - 用於智慧脈絡擷取的混合語意 + 關鍵字搜尋 + +詳情請參閱[架構概覽](https://docs.claude-mem.ai/architecture/overview)。 + +--- + +## MCP 搜尋工具 + +Claude-Mem 透過遵循 Token 高效的 **3 層工作流程模式**,以 **4 個 MCP 工具**提供智慧記憶搜尋: + +**3 層工作流程:** + +1. **`search`** - 取得精簡索引與 ID(每筆結果約 50-100 tokens) +2. **`timeline`** - 取得有趣結果周圍的時間脈絡 +3. **`get_observations`** - 僅為過濾後的 ID 擷取完整詳情(每筆結果約 500-1,000 tokens) + +**運作方式:** + +- Claude 使用 MCP 工具搜尋您的記憶 +- 從 `search` 開始取得結果索引 +- 使用 `timeline` 檢視特定觀察周圍發生的事情 +- 使用 `get_observations` 擷取相關 ID 的完整詳情 +- 透過在擷取詳情前過濾,**節省約 10 倍 token** + +**可用的 MCP 工具:** + +1. **`search`** - 使用全文查詢搜尋記憶索引,依類型/日期/專案過濾 +2. **`timeline`** - 取得特定觀察或查詢周圍的時間脈絡 +3. **`get_observations`** - 依 ID 擷取完整觀察詳情(批次處理多個 ID) +4. **`__IMPORTANT`** - 工作流程文件(Claude 永遠可見) + +**使用範例:** + +```typescript +// 步驟 1:搜尋索引 +search(query="authentication bug", type="bugfix", limit=10) + +// 步驟 2:檢閱索引,識別相關 ID(例如 #123、#456) + +// 步驟 3:擷取完整詳情 +get_observations(ids=[123, 456]) +``` + +詳細範例請參閱[搜尋工具指南](https://docs.claude-mem.ai/usage/search-tools)。 + +--- + +## Beta 功能 + +Claude-Mem 提供具有實驗性功能的 **Beta 通道**,例如 **Endless Mode**(用於延長工作階段的仿生記憶架構)。在 http://localhost:37777 → Settings 的網頁檢視介面中切換穩定版與 Beta 版。 + +有關 Endless Mode 與如何試用的詳情,請參閱 **[Beta 功能文件](https://docs.claude-mem.ai/beta-features)**。 + +--- + +## 系統需求 + +- **Node.js**:18.0.0 或更高版本 +- **Claude Code**:具有外掛支援的最新版本 +- **Bun**:JavaScript 執行環境與程序管理員(如缺少將自動安裝) +- **uv**:用於向量搜尋的 Python 套件管理員(如缺少將自動安裝) +- **SQLite 3**:用於持久儲存(已內建) + +--- + +## 設定 + +設定在 `~/.claude-mem/settings.json` 中管理(首次執行時自動以預設值建立)。設定 AI 模型、Worker 連接埠、資料目錄、日誌層級與脈絡注入設定。 + +所有可用設定與範例請參閱 **[設定指南](https://docs.claude-mem.ai/configuration)**。 + +--- + +## 開發 + +建置說明、測試與貢獻工作流程請參閱 **[開發指南](https://docs.claude-mem.ai/development)**。 + +--- + +## 疑難排解 + +如遇問題,向 Claude 描述問題,troubleshoot 技能將自動診斷並提供修正。 + +常見問題與解決方案請參閱 **[疑難排解指南](https://docs.claude-mem.ai/troubleshooting)**。 + +--- + +## 錯誤回報 + +使用自動產生器建立完整的錯誤回報: + +```bash +cd ~/.claude/plugins/marketplaces/thedotmack +npm run bug-report +``` + +## 貢獻 + +歡迎貢獻!請依照以下步驟: + +1. Fork 儲存庫 +2. 建立功能分支 +3. 加入測試並進行變更 +4. 更新文件 +5. 提交 Pull Request + +貢獻工作流程請參閱[開發指南](https://docs.claude-mem.ai/development)。 + +--- + +## 授權條款 + +本專案採用 **GNU Affero 通用公共授權條款 v3.0**(AGPL-3.0)授權。 + +Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved. + +完整詳情請參閱 [LICENSE](LICENSE) 檔案。 + +**這代表什麼:** + +- 您可以自由使用、修改與散佈此軟體 +- 如果您修改並部署於網路伺服器上,您必須公開您的原始碼 +- 衍生作品也必須採用 AGPL-3.0 授權 +- 本軟體不提供任何擔保 + +**關於 Ragtime 的說明**:`ragtime/` 目錄採用 **PolyForm Noncommercial License 1.0.0** 另行授權。詳情請參閱 [ragtime/LICENSE](ragtime/LICENSE)。 + +--- + +## 支援 + +- **文件**:[docs/](docs/) +- **Issues**:[GitHub Issues](https://github.com/thedotmack/claude-mem/issues) +- **儲存庫**:[github.com/thedotmack/claude-mem](https://github.com/thedotmack/claude-mem) +- **官方 X 帳號**:[@Claude_Memory](https://x.com/Claude_Memory) +- **官方 Discord**:[加入 Discord](https://discord.com/invite/J4wttp9vDu) +- **作者**:Alex Newman ([@thedotmack](https://github.com/thedotmack)) + +--- + +**使用 Claude Agent SDK 建置** | **由 Claude Code 驅動** | **以 TypeScript 開發** diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index 389ea32d..75c26a5d 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -15,6 +15,7 @@

🇨🇳 中文 • + 🇹🇼 繁體中文🇯🇵 日本語🇧🇷 Português🇰🇷 한국어 • @@ -34,6 +35,7 @@ 🇹🇭 ไทย🇮🇳 हिन्दी🇧🇩 বাংলা • + 🇵🇰 اردو🇷🇴 Română🇸🇪 Svenska🇮🇹 Italiano • @@ -126,7 +128,7 @@ ## 文档 -📚 **[查看完整文档](docs/)** - 在 GitHub 上浏览 Markdown 文档 +📚 **[查看完整文档](https://docs.claude-mem.ai/)** - 在官方网站浏览 ### 入门指南 @@ -212,7 +214,7 @@ Claude-Mem 通过 mem-search 技能提供智能搜索,当您询问过去的工 Claude-Mem 提供**测试版渠道**,包含实验性功能,如**无尽模式**(用于扩展会话的仿生记忆架构)。从 Web 查看器界面 http://localhost:37777 → 设置 切换稳定版和测试版。 -详见**[测试版功能文档](https://docs.claude-mem.ai/beta-features)**了解无尽模式的详细信息和试用方法。 +详见 **[测试版功能文档](https://docs.claude-mem.ai/beta-features)** 了解无尽模式的详细信息和试用方法。 --- @@ -230,13 +232,13 @@ Claude-Mem 提供**测试版渠道**,包含实验性功能,如**无尽模式**( 设置在 `~/.claude-mem/settings.json` 中管理(首次运行时自动创建默认设置)。可配置 AI 模型、worker 端口、数据目录、日志级别和上下文注入设置。 -详见**[配置指南](https://docs.claude-mem.ai/configuration)**了解所有可用设置和示例。 +详见 **[配置指南](https://docs.claude-mem.ai/configuration)** 了解所有可用设置和示例。 --- ## 开发 -详见**[开发指南](https://docs.claude-mem.ai/development)**了解构建说明、测试和贡献工作流程。 +详见 **[开发指南](https://docs.claude-mem.ai/development)** 了解构建说明、测试和贡献工作流程。 --- @@ -244,7 +246,7 @@ Claude-Mem 提供**测试版渠道**,包含实验性功能,如**无尽模式**( 如果遇到问题,向 Claude 描述问题,troubleshoot 技能将自动诊断并提供修复方案。 -详见**[故障排除指南](https://docs.claude-mem.ai/troubleshooting)**了解常见问题和解决方案。 +详见 **[故障排除指南](https://docs.claude-mem.ai/troubleshooting)** 了解常见问题和解决方案。 --- @@ -301,4 +303,4 @@ Copyright (C) 2025 Alex Newman (@thedotmack)。保留所有权利。 **使用 Claude Agent SDK 构建** | **由 Claude Code 驱动** | **使用 TypeScript 制作** ---- \ No newline at end of file +--- diff --git a/docs/public/CLAUDE.md b/docs/public/CLAUDE.md index 36246e89..bb075855 100644 --- a/docs/public/CLAUDE.md +++ b/docs/public/CLAUDE.md @@ -85,92 +85,4 @@ npx mintlify dev **Simple Rule**: - `/docs/public/` = Official user documentation (Mintlify .mdx files) ← YOU ARE HERE -- `/docs/context/` = Internal docs, plans, references, audits - - - -# Recent Activity - - - -### Nov 18, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #11206 | 3:01 PM | 🔵 | mem-search skill architecture and migration details retrieved in full format | ~538 | - -### Nov 21, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #13221 | 2:01 AM | 🔴 | Fixed broken markdown link to Viewer UI documentation | ~316 | -| #13220 | 2:00 AM | 🔴 | Escaped HTML less-than symbol in universal architecture timeout documentation | ~316 | -| #13216 | 1:54 AM | ✅ | Universal Architecture Added to Navigation | ~330 | -| #13215 | " | 🟣 | Universal AI Memory Architecture Documentation Created | ~732 | -| #13213 | 1:50 AM | 🔵 | Introduction Page Content and Recent v6.0.0 Release | ~495 | -| #13212 | " | 🔵 | Architecture Evolution Documentation Structure | ~408 | -| #13211 | " | 🔵 | Mintlify Documentation Site Configuration | ~430 | -| #13209 | 1:48 AM | 🔵 | Public Documentation Structure and Guidelines | ~383 | - -### Nov 25, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #14994 | 2:22 PM | ✅ | Version Channel Section Added to Configuration Documentation | ~301 | -| #14993 | " | ✅ | Beta Features Added to Documentation Navigation | ~188 | -| #14992 | 2:21 PM | 🟣 | Beta Features Documentation Page Created | ~488 | -| #14991 | " | 🔵 | Mintlify Navigation Structure and Documentation Groups | ~394 | -| #14989 | " | 🔵 | Installation Documentation with Quick Start and Verification Steps | ~383 | -| #14988 | " | 🔵 | Configuration Documentation Structure and Environment Variables | ~338 | - -### Nov 26, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #16190 | 10:22 PM | 🔵 | RAGTIME Search Retrieved Five Observations About Claude-Mem vs RAG Architecture | ~637 | - -### Dec 3, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #19884 | 9:42 PM | 🔵 | Configuration system and environment variables | ~701 | -| #19878 | 9:40 PM | 🔵 | Installation process and system architecture | ~486 | - -### Dec 8, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #22335 | 10:26 PM | 🔵 | Mintlify documentation configuration analyzed | ~534 | -| #22311 | 9:47 PM | 🔵 | Comprehensive Hooks Architecture Documentation Review | ~263 | -| #22297 | 9:43 PM | 🔵 | Mintlify Documentation Framework Configuration | ~446 | -| #22294 | " | 🔵 | Documentation Site Structure Located | ~359 | - -### Dec 9, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #23179 | 10:44 PM | ✅ | Removed explanatory reasons from tool exclusion documentation | ~297 | - -### Dec 15, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #27038 | 6:02 PM | 🔵 | 95% token reduction claims found only in private experimental documents, not in main public docs | ~513 | -| #27037 | " | 🔵 | Branch switching functionality exists in SettingsRoutes with UI switcher removal intent | ~463 | -| #26986 | 5:24 PM | ✅ | Updated Endless Mode latency warning in beta features documentation | ~299 | - -### Dec 29, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #33938 | 6:27 PM | 🔵 | Relevant CLAUDE.md Context Identified for PR #492 | ~435 | -| #33750 | 12:25 AM | ✅ | Documentation Update: Removed Version Number from Architecture Evolution | ~281 | - -### Jan 7, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38233 | 7:42 PM | ✅ | Renumbered SessionEnd Hook from 6 to 5 | ~315 | -| #38229 | 7:41 PM | ✅ | Renumbered PostToolUse Hook from 4 to 3 | ~278 | -| #38225 | " | ✅ | Updated Hook Count Description in Hooks Architecture Documentation | ~352 | - \ No newline at end of file +- `/docs/context/` = Internal docs, plans, references, audits \ No newline at end of file diff --git a/docs/public/architecture/CLAUDE.md b/docs/public/architecture/CLAUDE.md deleted file mode 100644 index 446aaa12..00000000 --- a/docs/public/architecture/CLAUDE.md +++ /dev/null @@ -1,38 +0,0 @@ - -# Recent Activity - - - -### Nov 18, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #11206 | 3:01 PM | 🔵 | mem-search skill architecture and migration details retrieved in full format | ~538 | - -### Nov 21, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #13218 | 1:58 AM | 🔴 | Escaped HTML special character in MDX documentation | ~261 | - -### Dec 3, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #19891 | 9:43 PM | 🔵 | Seven hook scripts across five lifecycle events | ~713 | - -### Dec 15, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #27040 | 6:03 PM | 🔵 | Comprehensive search confirms no 95% claims exist in main branch public documentation | ~508 | -| #27037 | 6:02 PM | 🔵 | Branch switching functionality exists in SettingsRoutes with UI switcher removal intent | ~463 | - -### Jan 7, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38221 | 7:41 PM | ✅ | Removed User Message Hook Documentation Section | ~339 | -| #38218 | 7:40 PM | ✅ | Updated Hook Configuration Documentation to Match Implementation | ~382 | -| #38212 | " | 🔵 | 5-Stage Hook Lifecycle Architecture for Memory Agent | ~668 | - \ No newline at end of file diff --git a/docs/public/configuration.mdx b/docs/public/configuration.mdx index 67c273d9..2e31a59b 100644 --- a/docs/public/configuration.mdx +++ b/docs/public/configuration.mdx @@ -26,7 +26,7 @@ Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created | Setting | Default | Description | |-------------------------------|---------------------------------|---------------------------------------| | `CLAUDE_MEM_GEMINI_API_KEY` | — | Gemini API key ([get free key](https://aistudio.google.com/app/apikey)) | -| `CLAUDE_MEM_GEMINI_MODEL` | `gemini-2.5-flash-lite` | Gemini model: `gemini-2.5-flash-lite`, `gemini-2.5-flash`, `gemini-3-flash` | +| `CLAUDE_MEM_GEMINI_MODEL` | `gemini-2.5-flash-lite` | Gemini model: `gemini-2.5-flash-lite`, `gemini-2.5-flash`, `gemini-3-flash-preview` | See [Gemini Provider](usage/gemini-provider) for detailed configuration and free tier information. diff --git a/docs/public/cursor/CLAUDE.md b/docs/public/cursor/CLAUDE.md deleted file mode 100644 index 2af9f843..00000000 --- a/docs/public/cursor/CLAUDE.md +++ /dev/null @@ -1,51 +0,0 @@ - -# Recent Activity - - - -### Dec 29, 2025 - -**gemini-setup.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34346 | 11:11 PM | 🟣 | Gemini Free Tier Integration Guide | ~413 | -| #34337 | 11:10 PM | 🔵 | Cursor Documentation Available | ~161 | -| #34331 | 11:05 PM | 🔴 | Fixed Broken Links in cursor/gemini-setup.mdx | ~253 | -| #34326 | 11:04 PM | 🔵 | Broken Links in Cursor Gemini Setup Documentation | ~324 | -| #34320 | 11:03 PM | 🔵 | Mintlify Broken Links Detected in Documentation | ~292 | -| #34215 | 10:08 PM | 🔵 | Retrieved Detailed Cursor Integration Implementation History | ~676 | -| #34214 | 10:07 PM | 🔵 | Cursor Integration Feature Set Discovered via Memory Search | ~427 | -| #34148 | 9:28 PM | 🟣 | Cursor IDE Integration with Cross-Platform Hooks and Documentation | ~514 | -| #34112 | 9:07 PM | 🟣 | Committed Cursor Public Documentation to Repository | ~427 | -| #34106 | 9:05 PM | 🟣 | Created Cursor-Specific Gemini Setup Guide | ~563 | - -**index.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34339 | 11:10 PM | 🟣 | Cursor IDE Integration with Persistent Memory | ~394 | -| #34335 | 11:06 PM | 🟣 | Mintlify Documentation Linting Successfully Completed | ~409 | -| #34330 | 11:05 PM | 🔴 | Fixed Remaining Broken Links in cursor/index.mdx Next Steps Section | ~284 | -| #34329 | " | 🔴 | Fixed Broken Links in cursor/index.mdx Detailed Guides Section | ~269 | -| #34325 | 11:04 PM | 🔵 | Multiple Broken Links in Cursor Index Documentation | ~329 | -| #34216 | 10:08 PM | 🔵 | Additional Cursor Integration Details Retrieved for Post Writing | ~600 | -| #34105 | 9:05 PM | 🟣 | Created Cursor Integration Landing Page | ~522 | - -**openrouter-setup.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34332 | 11:05 PM | 🔴 | Fixed Broken Links in cursor/openrouter-setup.mdx | ~283 | -| #34324 | 11:04 PM | 🔵 | Broken Link Syntax Identified in Cursor Documentation | ~329 | -| #34107 | 9:06 PM | 🟣 | Created Cursor-Specific OpenRouter Setup Guide | ~573 | - -**cursor** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34322 | 11:03 PM | 🔵 | Cursor Directory Files Confirmed to Exist | ~224 | - -### Jan 4, 2026 - -**gemini-setup.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #36751 | 12:32 AM | 🔵 | Gemini-Related Files Located Across Project | ~242 | - \ No newline at end of file diff --git a/docs/public/cursor/gemini-setup.mdx b/docs/public/cursor/gemini-setup.mdx index a66458fc..81c509c7 100644 --- a/docs/public/cursor/gemini-setup.mdx +++ b/docs/public/cursor/gemini-setup.mdx @@ -103,7 +103,7 @@ Open http://localhost:37777 to see the memory viewer. |-------|---------------|-------| | `gemini-2.5-flash-lite` | 10 (4,000 with billing) | **Default.** Fastest, highest free tier RPM | | `gemini-2.5-flash` | 5 (1,000 with billing) | Higher capability | -| `gemini-3-flash` | 5 (1,000 with billing) | Latest model | +| `gemini-3-flash-preview` | 5 (1,000 with billing) | Latest model | To change the model, update your settings: diff --git a/docs/public/docs.json b/docs/public/docs.json index 28a68e84..d9503182 100644 --- a/docs/public/docs.json +++ b/docs/public/docs.json @@ -73,7 +73,8 @@ "modes", "development", "troubleshooting", - "platform-integration" + "platform-integration", + "openclaw-integration" ] }, { diff --git a/docs/public/modes.mdx b/docs/public/modes.mdx index b1715f57..fed9fdb1 100644 --- a/docs/public/modes.mdx +++ b/docs/public/modes.mdx @@ -65,6 +65,7 @@ Inherits all behavior from Code Mode but instructs Claude to generate **all** me | **Hindi** | `code--hi` | हिन्दी | | **Hungarian** | `code--hu` | Magyar | | **Indonesian** | `code--id` | Bahasa Indonesia | +| **Urdu** | `code--ur` | اردو | | **Italian** | `code--it` | Italiano | | **Japanese** | `code--ja` | 日本語 | | **Korean** | `code--ko` | 한국어 | diff --git a/docs/public/openclaw-integration.mdx b/docs/public/openclaw-integration.mdx new file mode 100644 index 00000000..e52083a7 --- /dev/null +++ b/docs/public/openclaw-integration.mdx @@ -0,0 +1,385 @@ +--- +title: OpenClaw Integration +description: Persistent memory for OpenClaw agents — observation recording, MEMORY.md live sync, and real-time observation feeds +icon: dragon +--- + +## Overview + +The OpenClaw plugin gives claude-mem persistent memory to agents running on the [OpenClaw](https://openclaw.ai) gateway. It handles three things: + +1. **Observation recording** — Captures tool usage from OpenClaw's embedded runner and sends it to the claude-mem worker for AI processing +2. **MEMORY.md live sync** — Writes a continuously-updated timeline to each agent's workspace so agents always have context from previous sessions +3. **Observation feed** — Streams new observations to messaging channels (Telegram, Discord, Slack, etc.) in real-time via SSE + + +OpenClaw's embedded runner (`pi-embedded`) calls the Anthropic API directly without spawning a `claude` process, so claude-mem's standard hooks never fire. This plugin bridges that gap by using OpenClaw's event system to capture the same data. + + +## How It Works + +```plaintext +OpenClaw Gateway + │ + ├── before_agent_start ──→ Sync MEMORY.md + Init session + ├── tool_result_persist ──→ Record observation + Re-sync MEMORY.md + ├── agent_end ────────────→ Summarize + Complete session + └── gateway_start ────────→ Reset session tracking + │ + ▼ + Claude-Mem Worker (localhost:37777) + ├── POST /api/sessions/init + ├── POST /api/sessions/observations + ├── POST /api/sessions/summarize + ├── POST /api/sessions/complete + ├── GET /api/context/inject ──→ MEMORY.md content + └── GET /stream ─────────────→ SSE → Messaging channels +``` + +### Event Lifecycle + + + + When an OpenClaw agent starts, the plugin does two things: + + 1. **Syncs MEMORY.md** — Fetches the latest timeline from the worker's `/api/context/inject` endpoint and writes it to `MEMORY.md` in the agent's workspace directory. This gives the agent context from all previous sessions before it starts working. + + 2. **Initializes a session** — Sends the user prompt to `POST /api/sessions/init` so the worker can create a new session and start processing. + + Short prompts (under 10 characters) skip session init but still sync MEMORY.md. + + + Every time the agent uses a tool (Read, Write, Bash, etc.), the plugin: + + 1. **Sends the observation** to `POST /api/sessions/observations` with the tool name, input, and truncated response (max 1000 chars) + 2. **Re-syncs MEMORY.md** with the latest timeline from the worker + + Both operations are fire-and-forget — they don't block the agent from continuing work. The MEMORY.md file gets progressively richer as the session continues. + + Tools prefixed with `memory_` are skipped to avoid recursive recording. + + + When the agent completes, the plugin extracts the last assistant message and sends it to `POST /api/sessions/summarize`, then calls `POST /api/sessions/complete` to close the session. Both are fire-and-forget. + + + Clears all session tracking (session IDs, workspace directory mappings) so agents get fresh state after a gateway restart. + + + +### MEMORY.md Live Sync + +The plugin writes a `MEMORY.md` file to each agent's workspace directory containing the full timeline of observations and summaries from previous sessions. This file is updated: + +- On every `before_agent_start` event (agent gets fresh context before starting) +- On every `tool_result_persist` event (context stays current during the session) + +The content comes from the worker's `GET /api/context/inject?projects=` endpoint, which generates a formatted markdown timeline from the SQLite database. + + +MEMORY.md updates are fire-and-forget. They run in the background without blocking the agent. The file reflects whatever the worker has processed so far — it doesn't wait for the current observation to be fully processed before writing. + + +### Observation Feed (SSE → Messaging) + +The plugin runs a background service that connects to the worker's SSE stream (`GET /stream`) and forwards `new_observation` events to a configured messaging channel. This lets you monitor what your agents are learning in real-time from Telegram, Discord, Slack, or any supported OpenClaw channel. + +The SSE connection uses exponential backoff (1s → 30s) for automatic reconnection. + +## Setting Up the Observation Feed + +The observation feed sends a formatted message to your OpenClaw channel every time claude-mem creates a new observation. Each message includes the observation title and subtitle so you can follow along as your agents work. + +Messages look like this in your channel: + +``` +🧠 Claude-Mem Observation +**Implemented retry logic for API client** +Added exponential backoff with configurable max retries to handle transient failures +``` + +### Step 1: Choose your channel + +The observation feed works with any channel that your OpenClaw gateway has configured. You need two pieces of information: + +- **Channel type** — The name of the channel plugin registered with OpenClaw (e.g., `telegram`, `discord`, `slack`, `signal`, `whatsapp`, `line`) +- **Target ID** — The chat ID, channel ID, or user ID where messages should be sent + + + + **Channel type:** `telegram` + + **Target ID:** Your Telegram chat ID (numeric). To find it: + 1. Message [@userinfobot](https://t.me/userinfobot) on Telegram + 2. It will reply with your chat ID (e.g., `123456789`) + 3. For group chats, the ID is negative (e.g., `-1001234567890`) + + ```json + "observationFeed": { + "enabled": true, + "channel": "telegram", + "to": "123456789" + } + ``` + + + + **Channel type:** `discord` + + **Target ID:** The Discord channel ID. To find it: + 1. Enable Developer Mode in Discord (Settings → Advanced → Developer Mode) + 2. Right-click the channel → Copy Channel ID + + ```json + "observationFeed": { + "enabled": true, + "channel": "discord", + "to": "1234567890123456789" + } + ``` + + + + **Channel type:** `slack` + + **Target ID:** The Slack channel ID (not the channel name). To find it: + 1. Open the channel in Slack + 2. Click the channel name at the top + 3. Scroll to the bottom of the channel details — the ID looks like `C01ABC2DEFG` + + ```json + "observationFeed": { + "enabled": true, + "channel": "slack", + "to": "C01ABC2DEFG" + } + ``` + + + + **Channel type:** `signal` + + **Target ID:** The Signal phone number or group ID configured in your OpenClaw gateway. + + ```json + "observationFeed": { + "enabled": true, + "channel": "signal", + "to": "+1234567890" + } + ``` + + + + **Channel type:** `whatsapp` + + **Target ID:** The WhatsApp phone number or group JID configured in your OpenClaw gateway. + + ```json + "observationFeed": { + "enabled": true, + "channel": "whatsapp", + "to": "+1234567890" + } + ``` + + + + **Channel type:** `line` + + **Target ID:** The LINE user ID or group ID from the LINE Developer Console. + + ```json + "observationFeed": { + "enabled": true, + "channel": "line", + "to": "U1234567890abcdef" + } + ``` + + + +### Step 2: Add the config to your gateway + +Add the `observationFeed` block to your claude-mem plugin config in your OpenClaw gateway configuration: + +```json +{ + "plugins": { + "claude-mem": { + "enabled": true, + "config": { + "project": "my-project", + "observationFeed": { + "enabled": true, + "channel": "telegram", + "to": "123456789" + } + } + } + } +} +``` + + +The `channel` value must match a channel plugin that is already configured and running on your OpenClaw gateway. If the channel isn't registered, you'll see `Unknown channel type: ` in the logs. + + +### Step 3: Verify the connection + +After starting the gateway, check that the feed is connected: + +1. **Check the logs** — You should see: + ``` + [claude-mem] Observation feed starting — channel: telegram, target: 123456789 + [claude-mem] Connecting to SSE stream at http://localhost:37777/stream + [claude-mem] Connected to SSE stream + ``` + +2. **Use the status command** — Run `/claude-mem-feed` in any OpenClaw chat to see: + ``` + Claude-Mem Observation Feed + Enabled: yes + Channel: telegram + Target: 123456789 + Connection: connected + ``` + +3. **Trigger a test** — Have an agent do some work. When the worker processes the tool usage into an observation, you'll receive a message in your configured channel. + + +The feed only sends `new_observation` events — not raw tool usage. Observations are generated asynchronously by the worker's AI agent, so there's a 1-2 second delay between tool use and the observation message appearing in your channel. + + +### Troubleshooting the Feed + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `Connection: disconnected` | Worker not running or wrong port | Check `workerPort` config, run `npm run worker:status` | +| `Connection: reconnecting` | Worker was running but connection dropped | The plugin auto-reconnects with backoff — wait up to 30s | +| `Unknown channel type` in logs | Channel plugin not loaded on gateway | Verify your OpenClaw gateway has the channel plugin configured | +| No messages appearing | Feed connected but no observations being created | Check that agents are running and the worker is processing observations | +| `Observation feed disabled` in logs | `enabled` is `false` or missing | Set `observationFeed.enabled` to `true` | +| `Observation feed misconfigured` in logs | Missing `channel` or `to` | Both `channel` and `to` are required | + +## Installation + +Run this one-liner to install everything automatically: + +```bash +curl -fsSL https://install.cmem.ai/openclaw.sh | bash +``` + +The installer handles dependency checks (Bun, uv), plugin installation, memory slot configuration, AI provider setup, worker startup, and optional observation feed configuration. + +You can also pre-select options: + +```bash +# With a specific AI provider +curl -fsSL https://install.cmem.ai/openclaw.sh | bash -s -- --provider=gemini --api-key=YOUR_KEY + +# Fully unattended (defaults to Claude Max Plan) +curl -fsSL https://install.cmem.ai/openclaw.sh | bash -s -- --non-interactive + +# Upgrade existing installation +curl -fsSL https://install.cmem.ai/openclaw.sh | bash -s -- --upgrade +``` + +### Manual Configuration + +Add `claude-mem` to your OpenClaw gateway's plugin configuration: + +```json +{ + "plugins": { + "claude-mem": { + "enabled": true, + "config": { + "project": "my-project", + "syncMemoryFile": true, + "workerPort": 37777, + "observationFeed": { + "enabled": true, + "channel": "telegram", + "to": "your-chat-id" + } + } + } + } +} +``` + + +The claude-mem worker service must be running on the same machine as the OpenClaw gateway. The plugin communicates with it via HTTP on `localhost:37777`. + + +## Configuration + + + Project name for scoping observations in the memory database. All observations from this gateway will be stored under this project name. + + + + Enable automatic MEMORY.md sync to agent workspaces. Set to `false` if you don't want the plugin writing files to workspace directories. + + + + Port for the claude-mem worker service. Override if your worker runs on a non-default port. + + + + Enable live observation streaming to messaging channels. + + + + Channel type: `telegram`, `discord`, `signal`, `slack`, `whatsapp`, `line` + + + + Target chat/user/channel ID to send observations to. + + +## Commands + +### /claude-mem-feed + +Show or toggle the observation feed status. + +``` +/claude-mem-feed # Show current status +/claude-mem-feed on # Request enable +/claude-mem-feed off # Request disable +``` + +### /claude-mem-status + +Check worker health and session status. + +``` +/claude-mem-status +``` + +Returns worker status, port, active session count, and observation feed connection state. + +## Architecture + +The plugin uses HTTP calls to the already-running claude-mem worker service rather than spawning subprocesses. This means: + +- No `bun` dependency required on the gateway +- No process spawn overhead per event +- Uses the same worker API that Claude Code hooks use +- All operations are non-blocking (fire-and-forget where possible) + +### Session Tracking + +Each OpenClaw agent session gets a unique `contentSessionId` (format: `openclaw--`) that maps to a claude-mem session in the worker. The plugin tracks: + +- `sessionIds` — Maps OpenClaw session keys to content session IDs +- `workspaceDirsBySessionKey` — Maps session keys to workspace directories so `tool_result_persist` events can sync MEMORY.md even when the event context doesn't include `workspaceDir` + +Both maps are cleared on `gateway_start`. + +## Requirements + +- Claude-mem worker service running on `localhost:37777` (or configured port) +- OpenClaw gateway with plugin support +- Network access between gateway and worker (localhost only) diff --git a/docs/public/usage/CLAUDE.md b/docs/public/usage/CLAUDE.md deleted file mode 100644 index a80914b6..00000000 --- a/docs/public/usage/CLAUDE.md +++ /dev/null @@ -1,131 +0,0 @@ - -# Recent Activity - - - -### Dec 25, 2025 - -**gemini-provider.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #32789 | 9:49 PM | 🟣 | Gemini AI Provider Integration Merged to Main | ~409 | - -**manual-recovery.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #32654 | 8:51 PM | 🔵 | Identified multiple files related to queue recovery | ~375 | - -### Dec 26, 2025 - -**openrouter-provider.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #32925 | 10:26 PM | 🔵 | OpenRouter Provider Integration Proposed in PR 448 | ~543 | -| #32924 | 10:21 PM | 🟣 | OpenRouter Provider Documentation | ~501 | - -### Dec 28, 2025 - -**claude-desktop.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #33651 | 11:44 PM | 🔴 | Migration 17 Wrapped in Transaction with Documentation Updates | ~331 | -| #33650 | 11:43 PM | 🔵 | Code Changes Ready for Token Optimizations PR | ~292 | -| #33648 | " | ✅ | Documentation Installation Steps Renumbered | ~283 | -| #33647 | 11:42 PM | ✅ | Removed Skill Installation Steps from Claude Desktop Documentation | ~347 | -| #33646 | " | ✅ | Updated Documentation to Reflect Streamlined 3-Tool MCP Architecture | ~391 | -| #33643 | 11:41 PM | 🔵 | Documentation Uses Inconsistent Naming for MCP Server | ~403 | -| #33639 | " | 🔵 | Pull Request Review Identified Critical Migration Risk | ~457 | -| #33638 | 11:40 PM | 🔵 | Pull Request Review Identified Critical Migration Risk and Token Optimization Success | ~415 | -| #33636 | 11:35 PM | ✅ | Major Documentation and Code Cleanup Removed 4,929 Lines | ~381 | -| #33598 | 11:15 PM | 🔵 | Filtered MCP search query successfully returning rename history with type constraints | ~386 | -| #33597 | 11:14 PM | 🔵 | MCP search tool successfully retrieving mem-search to mcp-search rename history | ~361 | -| #33539 | 10:54 PM | ✅ | Updated configuration examples to use mcp-search as MCP server key | ~449 | -| #33538 | " | ✅ | Updated Step 3 installation instructions to reference mcp-search MCP server | ~250 | -| #33537 | " | ✅ | Updated prerequisites documentation to reference mcp-search MCP server | ~266 | -| #33536 | 10:53 PM | 🔵 | Identified documentation file requiring MCP server name update | ~451 | -| #33526 | 10:47 PM | 🔵 | Claude Desktop skill installation guide references mem-search server and skill | ~388 | - -**search-tools.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #33540 | 10:55 PM | 🔵 | Grep search found mem-search references in internationalized documentation | ~577 | - -**openrouter-provider.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #33312 | 3:09 PM | ✅ | OpenRouter Provider Documentation | ~497 | - -### Dec 29, 2025 - -**gemini-provider.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34335 | 11:06 PM | 🟣 | Mintlify Documentation Linting Successfully Completed | ~409 | -| #34333 | 11:05 PM | 🔴 | Fixed Broken Links in usage/gemini-provider.mdx | ~285 | -| #34328 | 11:04 PM | 🔵 | Broken Link in Usage Gemini Provider Documentation | ~330 | -| #34320 | 11:03 PM | 🔵 | Mintlify Broken Links Detected in Documentation | ~292 | -| #34103 | 9:05 PM | 🔵 | Gemini Provider Documentation Covers Free Tier and Configuration | ~480 | - -**openrouter-provider.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34334 | 11:05 PM | 🔴 | Fixed All Broken Links in usage/openrouter-provider.mdx | ~339 | -| #34327 | 11:04 PM | 🔵 | Broken Links in Usage OpenRouter Provider Documentation | ~337 | - -**usage** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #34323 | 11:03 PM | 🔵 | Usage Directory Files Confirmed to Exist | ~280 | - -**search-tools.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #33763 | 12:27 AM | ✅ | Pull request #480 created for MCP architecture documentation updates | ~423 | -| #33760 | 12:26 AM | ✅ | Major documentation overhaul across 6 files with 908 additions | ~367 | -| #33702 | 12:09 AM | ⚖️ | Documentation Update Strategy Finalized for MCP Architecture Transition | ~845 | -| #33694 | 12:06 AM | 🔵 | Search Tools Documentation Describes Deleted Skill Architecture | ~615 | -| #33679 | 12:03 AM | 🔵 | Search Tools Documentation Structure and Skill-Based Architecture | ~473 | - -**claude-desktop.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #33703 | 12:10 AM | 🔵 | Final Documentation Review Confirms Update Requirements | ~756 | -| #33699 | 12:08 AM | ✅ | Claude Desktop Documentation Successfully Updated for MCP Tools | ~583 | -| #33689 | 12:05 AM | 🔴 | Migration 17 Transaction Safety and Documentation Updates | ~436 | -| #33681 | 12:03 AM | ✅ | Claude Desktop Documentation Updated for MCP Tools Workflow | ~491 | -| #33675 | 12:02 AM | 🔄 | Major Documentation and Code Cleanup in MCP Clarity Branch | ~491 | - -### Jan 4, 2026 - -**gemini-provider.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #36751 | 12:32 AM | 🔵 | Gemini-Related Files Located Across Project | ~242 | - -### Jan 5, 2026 - -**folder-context.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38086 | 10:42 PM | ✅ | Merged PR with comprehensive CLAUDE.md documentation system | ~478 | -| #38066 | 9:50 PM | ✅ | v9.0 Documentation Audit Completed with 14 Files Updated | ~547 | -| #38064 | " | ⚖️ | 9.0 Release Documentation Audit Complete - Major Gaps Identified | ~997 | -| #38053 | 9:47 PM | 🔵 | Folder Context Documentation Exists But Marked As Disabled By Default | ~616 | - -**getting-started.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38042 | 9:44 PM | 🔵 | Getting Started Documentation Review for Live Context Gap | ~411 | - -**claude-desktop.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #37617 | 5:32 PM | ⚖️ | PR #558 Review Requirements Categorized by Priority | ~637 | -| #37561 | 4:50 PM | 🔵 | Claude Desktop mem-search Skill Documentation Confirms Platform-Specific Feature | ~393 | - -**private-tags.mdx** -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #37512 | 3:22 PM | 🔵 | Privacy Tag System Release History and Documentation Evolution | ~749 | -| #37505 | 3:21 PM | 🔵 | Comprehensive Dual-Tag Privacy System Architecture and Implementation Details | ~915 | - \ No newline at end of file diff --git a/docs/public/usage/gemini-provider.mdx b/docs/public/usage/gemini-provider.mdx index 4ca36b13..7696f6cf 100644 --- a/docs/public/usage/gemini-provider.mdx +++ b/docs/public/usage/gemini-provider.mdx @@ -39,7 +39,7 @@ Claude-mem supports Google's Gemini API as an alternative to the Claude Agent SD |---------|--------|---------|-------------| | `CLAUDE_MEM_PROVIDER` | `claude`, `gemini` | `claude` | AI provider for observation extraction | | `CLAUDE_MEM_GEMINI_API_KEY` | string | — | Your Gemini API key | -| `CLAUDE_MEM_GEMINI_MODEL` | `gemini-2.5-flash-lite`, `gemini-2.5-flash`, `gemini-3-flash` | `gemini-2.5-flash-lite` | Gemini model to use | +| `CLAUDE_MEM_GEMINI_MODEL` | `gemini-2.5-flash-lite`, `gemini-2.5-flash`, `gemini-3-flash-preview` | `gemini-2.5-flash-lite` | Gemini model to use | | `CLAUDE_MEM_GEMINI_BILLING_ENABLED` | `true`, `false` | `false` | Skip rate limiting if billing is enabled on Google Cloud | ### Using the Settings UI @@ -79,7 +79,7 @@ The settings file takes precedence over the environment variable. |-------|--------------|-------| | `gemini-2.5-flash-lite` | 10 | Default, recommended for free tier (highest RPM) | | `gemini-2.5-flash` | 5 | Higher capability, lower rate limit | -| `gemini-3-flash` | 5 | Latest model, lower rate limit | +| `gemini-3-flash-preview` | 5 | Latest model, lower rate limit | ## Provider Switching @@ -139,7 +139,7 @@ Google has two rate limit tiers for free usage: |-------|-----|-----| | gemini-2.5-flash-lite | 10 | 250K | | gemini-2.5-flash | 5 | 250K | -| gemini-3-flash | 5 | 250K | +| gemini-3-flash-preview | 5 | 250K | Claude-mem enforces these limits automatically with built-in delays between requests. Processing may be slower but stays within limits. @@ -149,7 +149,7 @@ Claude-mem enforces these limits automatically with built-in delays between requ |-------|-----|-----| | gemini-2.5-flash-lite | 4,000 | 4M | | gemini-2.5-flash | 1,000 | 1M | -| gemini-3-flash | 1,000 | 1M | +| gemini-3-flash-preview | 1,000 | 1M | **Recommended**: Enable billing on your Google Cloud project to unlock much higher rate limits. You won't be charged unless you exceed the generous free quota. This allows claude-mem to process observations instantly instead of waiting between requests. diff --git a/docs/reports/CLAUDE.md b/docs/reports/CLAUDE.md deleted file mode 100644 index 1de0fb72..00000000 --- a/docs/reports/CLAUDE.md +++ /dev/null @@ -1,17 +0,0 @@ - -# Recent Activity - - - -### Jan 3, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #36651 | 11:03 PM | 🔵 | Critical Design Decision Documented: Memory Session ID Must Never Equal Content Session ID | ~481 | - -### Jan 8, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #38731 | 6:49 PM | 🟣 | Comprehensive Sonnet vs Opus Behavioral Analysis Report Generated and Saved | ~700 | - \ No newline at end of file diff --git a/install/.gitignore b/install/.gitignore new file mode 100644 index 00000000..e940dd69 --- /dev/null +++ b/install/.gitignore @@ -0,0 +1,2 @@ +.vercel +public/openclaw.sh diff --git a/install/vercel.json b/install/vercel.json new file mode 100644 index 00000000..d9504cd5 --- /dev/null +++ b/install/vercel.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "headers": [ + { + "source": "/(.*)\\.sh", + "headers": [ + { "key": "Content-Type", "value": "text/plain; charset=utf-8" }, + { "key": "Cache-Control", "value": "public, max-age=300, s-maxage=60" } + ] + } + ] +} diff --git a/openclaw/.gitignore b/openclaw/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/openclaw/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/openclaw/Dockerfile.e2e b/openclaw/Dockerfile.e2e new file mode 100644 index 00000000..fcc0bcbd --- /dev/null +++ b/openclaw/Dockerfile.e2e @@ -0,0 +1,69 @@ +# Dockerfile.e2e — End-to-end test: install claude-mem plugin on a real OpenClaw instance +# Simulates the complete plugin installation flow a user would follow. +# +# Usage: +# docker build -f Dockerfile.e2e -t openclaw-e2e-test . && docker run --rm openclaw-e2e-test +# +# Interactive (for human testing): +# docker run --rm -it openclaw-e2e-test /bin/bash + +FROM ghcr.io/openclaw/openclaw:main + +USER root + +# Install curl for health checks in e2e-verify.sh, and TypeScript for building +RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* +RUN npm install -g typescript@5 + +# Create staging directory for the plugin source +WORKDIR /tmp/claude-mem-plugin + +# Copy plugin source files +COPY package.json tsconfig.json openclaw.plugin.json ./ +COPY src/ ./src/ + +# Build the plugin (TypeScript → JavaScript) +# NODE_ENV=production is set in the base image; override to install devDependencies +RUN NODE_ENV=development npm install && npx tsc + +# Create the installable plugin package: +# OpenClaw `plugins install` expects package.json with openclaw.extensions field. +# The package name must match the plugin ID in openclaw.plugin.json (claude-mem). +# Only include the main plugin entry point, not test/mock files. +RUN mkdir -p /tmp/claude-mem-installable/dist && \ + cp dist/index.js /tmp/claude-mem-installable/dist/ && \ + cp dist/index.d.ts /tmp/claude-mem-installable/dist/ 2>/dev/null || true && \ + cp openclaw.plugin.json /tmp/claude-mem-installable/ && \ + node -e " \ + const pkg = { \ + name: 'claude-mem', \ + version: '1.0.0', \ + type: 'module', \ + main: 'dist/index.js', \ + openclaw: { extensions: ['./dist/index.js'] } \ + }; \ + require('fs').writeFileSync('/tmp/claude-mem-installable/package.json', JSON.stringify(pkg, null, 2)); \ + " + +# Switch back to app directory and node user for installation +WORKDIR /app +USER node + +# Create the OpenClaw config directory +RUN mkdir -p /home/node/.openclaw + +# Install the plugin using OpenClaw's official CLI +RUN node openclaw.mjs plugins install /tmp/claude-mem-installable + +# Enable the plugin +RUN node openclaw.mjs plugins enable claude-mem + +# Copy the e2e verification script and mock worker +COPY --chown=node:node e2e-verify.sh /app/e2e-verify.sh +USER root +RUN chmod +x /app/e2e-verify.sh && \ + cp /tmp/claude-mem-plugin/dist/mock-worker.js /app/mock-worker.js +USER node + +# Default: run the automated verification +CMD ["/bin/bash", "/app/e2e-verify.sh"] diff --git a/openclaw/SKILL.md b/openclaw/SKILL.md new file mode 100644 index 00000000..47cea087 --- /dev/null +++ b/openclaw/SKILL.md @@ -0,0 +1,458 @@ +# Claude-Mem OpenClaw Plugin — Setup Guide + +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. + +## Quick Install (Recommended) + +Run this one-liner to install everything automatically: + +```bash +curl -fsSL https://install.cmem.ai/openclaw.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://install.cmem.ai/openclaw.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://install.cmem.ai/openclaw.sh | bash -s -- --non-interactive +``` + +To upgrade an existing installation (preserves settings, updates plugin): + +```bash +curl -fsSL https://install.cmem.ai/openclaw.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. + +```bash +cd /opt # or wherever you want to keep it +git clone https://github.com/thedotmack/claude-mem.git +cd claude-mem +npm install +npm run build +``` + +You'll need **bun** installed for the worker service. If you don't have it: + +```bash +curl -fsSL https://bun.sh/install | bash +``` + +### 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. + +#### Check if it's already running + +If this machine also runs Claude Code with claude-mem installed, the worker may already be running: + +```bash +curl http://localhost:37777/api/health +``` + +**Got `{"status":"ok"}`?** The worker is already running. Skip to Step 3. + +**Got connection refused or no response?** The worker isn't running. Continue below. + +#### 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: + +```bash +cd ~/.claude/plugins/marketplaces/thedotmack +npm run worker:restart +``` + +Verify: +```bash +curl http://localhost:37777/api/health +``` + +**Got `{"status":"ok"}`?** You're set. Skip to Step 3. + +**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 + +Run the worker from the cloned repo: + +```bash +cd /opt/claude-mem # wherever you cloned it +npm run worker:start +``` + +Verify: +```bash +curl http://localhost:37777/api/health +``` + +**Got `{"status":"ok"}`?** You're set. Move to Step 3. + +**Still not working?** Debug steps: +- Check that bun is installed: `bun --version` +- Check the worker status: `npm run worker:status` +- Check if something else is using port 37777: `lsof -i :37777` +- Check logs: `npm run worker:logs` (if available) +- Try running it directly to see errors: `bun plugin/scripts/worker-service.cjs start` + +### Step 3: Add the Plugin to Your Gateway + +Add the `claude-mem` plugin to your OpenClaw gateway configuration: + +```json +{ + "plugins": { + "claude-mem": { + "enabled": true, + "config": { + "project": "my-project", + "syncMemoryFile": true, + "workerPort": 37777 + } + } + } +} +``` + +#### 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"`. + +- **`syncMemoryFile`** (boolean, default: `true`) — When enabled, the plugin writes a `MEMORY.md` file to each agent's workspace directory. This file contains the full timeline of observations and summaries from previous sessions, and it updates on every tool use so agents always have fresh context. Set to `false` only if you don't want the plugin writing files to agent workspaces. + +- **`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 + +Restart your OpenClaw gateway so it picks up the new plugin configuration. After restart, check the gateway logs for: + +``` +[claude-mem] OpenClaw plugin loaded — v1.0.0 (worker: 127.0.0.1:37777) +``` + +If you see this, the plugin is loaded. You can also verify by running `/claude-mem-status` in any OpenClaw chat: + +``` +Claude-Mem Worker Status +Status: ok +Port: 37777 +Active sessions: 0 +Observation feed: disconnected +``` + +The observation feed shows `disconnected` because we haven't configured it yet. That's next. + +## Step 5: Verify Observations Are Being Recorded + +Have an agent do some work. The plugin automatically records observations through these OpenClaw events: + +1. **`before_agent_start`** — Initializes a claude-mem session when the agent starts, syncs MEMORY.md to the workspace +2. **`tool_result_persist`** — Records each tool use (Read, Write, Bash, etc.) as an observation, re-syncs MEMORY.md +3. **`agent_end`** — Summarizes the session and marks it complete + +All of this happens automatically. No additional configuration needed. + +To verify it's working, check the agent's workspace directory for a `MEMORY.md` file after the agent runs. It should contain a formatted timeline of observations. + +You can also check the worker's viewer UI at http://localhost:37777 to see observations appearing in real time. + +## Step 6: Set Up the Observation Feed (Streaming to a Channel) + +The observation feed connects to the claude-mem worker's SSE (Server-Sent Events) stream and forwards every new observation to a messaging channel in real time. Your agents learn things, and you see them learning in your Telegram/Discord/Slack/etc. + +### What you'll see + +Every time claude-mem creates a new observation from your agent's tool usage, a message like this appears in your channel: + +``` +🧠 Claude-Mem Observation +**Implemented retry logic for API client** +Added exponential backoff with configurable max retries to handle transient failures +``` + +### Pick your channel + +You need two things: +- **Channel type** — Must match a channel plugin already running on your OpenClaw gateway +- **Target ID** — The chat/channel/user ID where messages go + +#### Telegram + +Channel type: `telegram` + +To find your chat ID: +1. Message @userinfobot on Telegram — https://t.me/userinfobot +2. It replies with your numeric chat ID (e.g., `123456789`) +3. For group chats, the ID is negative (e.g., `-1001234567890`) + +```json +"observationFeed": { + "enabled": true, + "channel": "telegram", + "to": "123456789" +} +``` + +#### Discord + +Channel type: `discord` + +To find your channel ID: +1. Enable Developer Mode in Discord: Settings → Advanced → Developer Mode +2. Right-click the target channel → Copy Channel ID + +```json +"observationFeed": { + "enabled": true, + "channel": "discord", + "to": "1234567890123456789" +} +``` + +#### Slack + +Channel type: `slack` + +To find your channel ID (not the channel name): +1. Open the channel in Slack +2. Click the channel name at the top +3. Scroll to the bottom of the channel details — the ID looks like `C01ABC2DEFG` + +```json +"observationFeed": { + "enabled": true, + "channel": "slack", + "to": "C01ABC2DEFG" +} +``` + +#### Signal + +Channel type: `signal` + +Use the phone number or group ID configured in your OpenClaw gateway's Signal plugin. + +```json +"observationFeed": { + "enabled": true, + "channel": "signal", + "to": "+1234567890" +} +``` + +#### WhatsApp + +Channel type: `whatsapp` + +Use the phone number or group JID configured in your OpenClaw gateway's WhatsApp plugin. + +```json +"observationFeed": { + "enabled": true, + "channel": "whatsapp", + "to": "+1234567890" +} +``` + +#### LINE + +Channel type: `line` + +Use the user ID or group ID from the LINE Developer Console. + +```json +"observationFeed": { + "enabled": true, + "channel": "line", + "to": "U1234567890abcdef" +} +``` + +### Add it to your config + +Your complete plugin config should now look like this (using Telegram as an example): + +```json +{ + "plugins": { + "claude-mem": { + "enabled": true, + "config": { + "project": "my-project", + "syncMemoryFile": true, + "workerPort": 37777, + "observationFeed": { + "enabled": true, + "channel": "telegram", + "to": "123456789" + } + } + } + } +} +``` + +### Restart and verify + +Restart the gateway. Check the logs for these three lines in order: + +``` +[claude-mem] Observation feed starting — channel: telegram, target: 123456789 +[claude-mem] Connecting to SSE stream at http://localhost:37777/stream +[claude-mem] Connected to SSE stream +``` + +Then run `/claude-mem-feed` in any OpenClaw chat: + +``` +Claude-Mem Observation Feed +Enabled: yes +Channel: telegram +Target: 123456789 +Connection: connected +``` + +If `Connection` shows `connected`, you're done. Have an agent do some work and watch observations stream to your channel. + +## Commands Reference + +The plugin registers two commands: + +### /claude-mem-status + +Reports worker health and current session state. + +``` +/claude-mem-status +``` + +Output: +``` +Claude-Mem Worker Status +Status: ok +Port: 37777 +Active sessions: 2 +Observation feed: connected +``` + +### /claude-mem-feed + +Shows observation feed status. Accepts optional `on`/`off` argument. + +``` +/claude-mem-feed — show status +/claude-mem-feed on — request enable (update config to persist) +/claude-mem-feed off — request disable (update config to persist) +``` + +## How It All Works + +``` +OpenClaw Gateway + │ + ├── before_agent_start ──→ Sync MEMORY.md + Init session + ├── tool_result_persist ──→ Record observation + Re-sync MEMORY.md + ├── agent_end ────────────→ Summarize + Complete session + └── gateway_start ────────→ Reset session tracking + │ + ▼ + Claude-Mem Worker (localhost:37777) + ├── POST /api/sessions/init + ├── POST /api/sessions/observations + ├── POST /api/sessions/summarize + ├── POST /api/sessions/complete + ├── GET /api/context/inject ──→ MEMORY.md content + └── GET /stream ─────────────→ SSE → Messaging channels +``` + +### MEMORY.md live sync + +The plugin writes `MEMORY.md` to each agent's workspace with the full observation timeline. It updates: +- On every `before_agent_start` — agent gets fresh context before starting +- On every `tool_result_persist` — context stays current as the agent works + +Updates are fire-and-forget (non-blocking). The agent is never held up waiting for MEMORY.md to write. + +### Observation recording + +Every tool use (Read, Write, Bash, etc.) is sent to the claude-mem worker as an observation. The worker's AI agent processes it into a structured observation with title, subtitle, facts, concepts, and narrative. Tools prefixed with `memory_` are skipped to avoid recursive recording. + +### Session lifecycle + +- **`before_agent_start`** — Creates a session in the worker, syncs MEMORY.md. Short prompts (under 10 chars) skip session init but still sync. +- **`tool_result_persist`** — Records observation (fire-and-forget), re-syncs MEMORY.md (fire-and-forget). Tool responses are truncated to 1000 characters. +- **`agent_end`** — Sends the last assistant message for summarization, then completes the session. Both fire-and-forget. +- **`gateway_start`** — Clears all session tracking (session IDs, workspace mappings) so agents start fresh. + +### Observation feed + +A background service connects to the worker's SSE stream and forwards `new_observation` events to a configured messaging channel. The connection auto-reconnects with exponential backoff (1s → 30s max). + +## Troubleshooting + +| Problem | What to check | +|---------|---------------| +| Worker health check fails | Is bun installed? (`bun --version`). Is something else on port 37777? (`lsof -i :37777`). Try running directly: `bun plugin/scripts/worker-service.cjs start` | +| Worker started from Claude Code install but not responding | Check `cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:status`. May need `npm run worker:restart`. | +| Worker started from cloned repo but not responding | Check `cd /path/to/claude-mem && npm run worker:status`. Make sure you ran `npm install && npm run build` first. | +| No MEMORY.md appearing | Check that `syncMemoryFile` is not set to `false`. Verify the agent's event context includes `workspaceDir`. | +| Observations not being recorded | Check gateway logs for `[claude-mem]` messages. The worker must be running and reachable on localhost:37777. | +| Feed shows `disconnected` | Worker's `/stream` endpoint not reachable. Check `workerPort` matches the actual worker port. | +| Feed shows `reconnecting` | Connection dropped. The plugin auto-reconnects — wait up to 30 seconds. | +| `Unknown channel type` in logs | The channel plugin (e.g., telegram) isn't loaded on your gateway. Make sure the channel is configured and running. | +| `Observation feed disabled` in logs | Set `observationFeed.enabled` to `true` in your config. | +| `Observation feed misconfigured` in logs | Both `observationFeed.channel` and `observationFeed.to` are required. | +| No messages in channel despite `connected` | The feed only sends processed observations, not raw tool usage. There's a 1-2 second delay. Make sure the worker is actually processing observations (check http://localhost:37777). | + +## Full Config Reference + +```json +{ + "plugins": { + "claude-mem": { + "enabled": true, + "config": { + "project": "openclaw", + "syncMemoryFile": true, + "workerPort": 37777, + "observationFeed": { + "enabled": false, + "channel": "telegram", + "to": "123456789" + } + } + } + } +} +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `project` | string | `"openclaw"` | Project name scoping observations in the database | +| `syncMemoryFile` | boolean | `true` | Write MEMORY.md to agent workspaces | +| `workerPort` | number | `37777` | Claude-mem worker service port | +| `observationFeed.enabled` | boolean | `false` | Stream observations to a messaging channel | +| `observationFeed.channel` | string | — | Channel type: `telegram`, `discord`, `slack`, `signal`, `whatsapp`, `line` | +| `observationFeed.to` | string | — | Target chat/channel/user ID | diff --git a/openclaw/TESTING.md b/openclaw/TESTING.md new file mode 100644 index 00000000..a3d4123e --- /dev/null +++ b/openclaw/TESTING.md @@ -0,0 +1,279 @@ +# OpenClaw Claude-Mem Plugin — Testing Guide + +## Quick Start (Docker) + +The fastest way to test the plugin is using the pre-built Docker E2E environment: + +```bash +cd openclaw + +# Automated test (builds, installs plugin on real OpenClaw, verifies everything) +./test-e2e.sh + +# Interactive shell (for manual exploration) +./test-e2e.sh --interactive + +# Just build the image +./test-e2e.sh --build-only +``` + +--- + +## Test Layers + +### 1. Unit Tests (fastest) + +```bash +cd openclaw +npm test # compiles TypeScript, runs 17 tests +``` + +Tests plugin registration, service lifecycle, command handling, SSE integration, and all 6 channel types. + +### 2. Smoke Test + +```bash +node test-sse-consumer.js +``` + +Quick check that the plugin loads and registers its service + command correctly. + +### 3. Container Unit Tests (fresh install) + +```bash +./test-container.sh # Unit tests in clean Docker +./test-container.sh --full # Integration tests with mock worker +``` + +### 4. E2E on Real OpenClaw (Docker) + +```bash +./test-e2e.sh +``` + +This is the most comprehensive test. It: +1. Uses the official `ghcr.io/openclaw/openclaw:main` Docker image +2. Installs the plugin via `openclaw plugins install` (same as a real user) +3. Enables the plugin via `openclaw plugins enable` +4. Starts a mock claude-mem worker on port 37777 +5. Starts the OpenClaw gateway with plugin config +6. Verifies the plugin loads, connects to SSE, and processes events + +**All 16 checks must pass.** + +--- + +## Human E2E Testing (Interactive Docker) + +For manual walkthrough testing, use the interactive Docker mode: + +```bash +./test-e2e.sh --interactive +``` + +This drops you into a fully-configured OpenClaw container with the plugin pre-installed. + +### Step-by-step inside the container + +#### 1. Verify plugin is installed + +```bash +node openclaw.mjs plugins list +node openclaw.mjs plugins info claude-mem +node openclaw.mjs plugins doctor +``` + +**Expected:** +- `claude-mem` appears in the plugins list as "enabled" or "loaded" +- Info shows version 1.0.0, source at `/home/node/.openclaw/extensions/claude-mem/` +- Doctor reports no issues + +#### 2. Inspect plugin files + +```bash +ls -la /home/node/.openclaw/extensions/claude-mem/ +cat /home/node/.openclaw/extensions/claude-mem/openclaw.plugin.json +cat /home/node/.openclaw/extensions/claude-mem/package.json +``` + +**Expected:** +- `dist/index.js` exists (compiled plugin) +- `openclaw.plugin.json` has `"id": "claude-mem"` and `"kind": "memory"` +- `package.json` has `openclaw.extensions` field pointing to `./dist/index.js` + +#### 3. Start mock worker + +```bash +node /app/mock-worker.js & +``` + +Verify it's running: + +```bash +curl -s http://localhost:37777/health +# → {"status":"ok"} + +curl -s --max-time 3 http://localhost:37777/stream +# → data: {"type":"connected","message":"Mock worker SSE stream"} +# → data: {"type":"new_observation","observation":{...}} +``` + +#### 4. Configure and start gateway + +```bash +cat > /home/node/.openclaw/openclaw.json << 'EOF' +{ + "gateway": { + "mode": "local", + "auth": { + "mode": "token", + "token": "e2e-test-token" + } + }, + "plugins": { + "slots": { + "memory": "claude-mem" + }, + "entries": { + "claude-mem": { + "enabled": true, + "config": { + "workerPort": 37777, + "observationFeed": { + "enabled": true, + "channel": "telegram", + "to": "test-chat-id-12345" + } + } + } + } + } +} +EOF + +node openclaw.mjs gateway --allow-unconfigured --verbose --token e2e-test-token +``` + +**Expected in gateway logs:** +- `[claude-mem] OpenClaw plugin loaded — v1.0.0` +- `[claude-mem] Observation feed starting — channel: telegram, target: test-chat-id-12345` +- `[claude-mem] Connecting to SSE stream at http://localhost:37777/stream` +- `[claude-mem] Connected to SSE stream` + +#### 5. Run automated verification (optional) + +From a second shell in the container (or after stopping the gateway): + +```bash +/bin/bash /app/e2e-verify.sh +``` + +--- + +## Manual E2E (Real OpenClaw + Real Worker) + +For testing with a real claude-mem worker and real messaging channel: + +### Prerequisites + +- OpenClaw gateway installed and configured +- Claude-Mem worker running on port 37777 +- Plugin built: `cd openclaw && npm run build` + +### 1. Install the plugin + +```bash +# Build the plugin +cd openclaw && npm run build + +# Install on OpenClaw (from the openclaw/ directory) +openclaw plugins install . + +# Enable it +openclaw plugins enable claude-mem +``` + +### 2. Configure + +Edit `~/.openclaw/openclaw.json` to add plugin config: + +```json +{ + "plugins": { + "entries": { + "claude-mem": { + "enabled": true, + "config": { + "workerPort": 37777, + "observationFeed": { + "enabled": true, + "channel": "telegram", + "to": "YOUR_CHAT_ID" + } + } + } + } + } +} +``` + +**Supported channels:** `telegram`, `discord`, `signal`, `slack`, `whatsapp`, `line` + +### 3. Restart gateway + +```bash +openclaw restart +``` + +**Look for in logs:** +- `[claude-mem] OpenClaw plugin loaded — v1.0.0` +- `[claude-mem] Connected to SSE stream` + +### 4. Trigger an observation + +Start a Claude Code session with claude-mem enabled and perform any action. The worker will emit a `new_observation` SSE event. + +### 5. Verify delivery + +Check the target messaging channel for: + +``` +🧠 Claude-Mem Observation +**Observation Title** +Optional subtitle +``` + +--- + +## Troubleshooting + +### `api.log is not a function` +The plugin was built against the wrong API. Ensure `src/index.ts` uses `api.logger.info()` not `api.log()`. Rebuild with `npm run build`. + +### Worker not running +- **Symptom:** `SSE stream error: fetch failed. Reconnecting in 1s` +- **Fix:** Start the worker: `cd /path/to/claude-mem && npm run build-and-sync` + +### Port mismatch +- **Fix:** Ensure `workerPort` in config matches the worker's actual port (default: 37777) + +### Channel not configured +- **Symptom:** `Observation feed misconfigured — channel or target missing` +- **Fix:** Add both `channel` and `to` to `observationFeed` in config + +### Unknown channel type +- **Fix:** Use: `telegram`, `discord`, `signal`, `slack`, `whatsapp`, or `line` + +### Feed disabled +- **Symptom:** `Observation feed disabled` +- **Fix:** Set `observationFeed.enabled: true` + +### Messages not arriving +1. Verify the bot/integration is configured in the target channel +2. Check the target ID (`to`) is correct +3. Look for `Failed to send to ` in logs +4. Test the channel via OpenClaw's built-in tools + +### Memory slot conflict +- **Symptom:** `plugin disabled (memory slot set to "memory-core")` +- **Fix:** Add `"slots": { "memory": "claude-mem" }` to plugins config diff --git a/openclaw/e2e-verify.sh b/openclaw/e2e-verify.sh new file mode 100755 index 00000000..464e08e6 --- /dev/null +++ b/openclaw/e2e-verify.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash +# e2e-verify.sh — Automated E2E verification for claude-mem plugin on OpenClaw +# +# This script verifies the complete plugin installation and operation flow: +# 1. Plugin is installed and visible in OpenClaw +# 2. Plugin loads correctly when gateway starts +# 3. Mock worker SSE stream is consumed by the plugin +# 4. Observations are received and formatted +# +# Exit 0 = all checks passed, Exit 1 = failure + +set -euo pipefail + +PASS=0 +FAIL=0 +TOTAL=0 + +pass() { + PASS=$((PASS + 1)) + TOTAL=$((TOTAL + 1)) + echo " PASS: $1" +} + +fail() { + FAIL=$((FAIL + 1)) + TOTAL=$((TOTAL + 1)) + echo " FAIL: $1" +} + +section() { + echo "" + echo "=== $1 ===" +} + +# ─── Phase 1: Plugin Discovery ─── + +section "Phase 1: Plugin Discovery" + +# Check plugin is listed +PLUGIN_LIST=$(node /app/openclaw.mjs plugins list 2>&1) +if echo "$PLUGIN_LIST" | grep -q "claude-mem"; then + pass "Plugin appears in 'plugins list'" +else + fail "Plugin NOT found in 'plugins list'" + echo "$PLUGIN_LIST" +fi + +# Check plugin info +PLUGIN_INFO=$(node /app/openclaw.mjs plugins info claude-mem 2>&1 || true) +if echo "$PLUGIN_INFO" | grep -qi "claude-mem"; then + pass "Plugin info shows claude-mem details" +else + fail "Plugin info failed" + echo "$PLUGIN_INFO" +fi + +# Check plugin is enabled +if echo "$PLUGIN_LIST" | grep -A1 "claude-mem" | grep -qi "enabled\|loaded"; then + pass "Plugin is enabled" +else + # Try to check via info + if echo "$PLUGIN_INFO" | grep -qi "enabled\|loaded"; then + pass "Plugin is enabled (via info)" + else + fail "Plugin does not appear enabled" + echo "$PLUGIN_INFO" + fi +fi + +# Check plugin doctor reports no issues +DOCTOR_OUT=$(node /app/openclaw.mjs plugins doctor 2>&1 || true) +if echo "$DOCTOR_OUT" | grep -qi "no.*issue\|0 issue"; then + pass "Plugin doctor reports no issues" +else + fail "Plugin doctor reports issues" + echo "$DOCTOR_OUT" +fi + +# ─── Phase 2: Plugin Files ─── + +section "Phase 2: Plugin Files" + +# Check extension directory exists +EXTENSIONS_DIR="/home/node/.openclaw/extensions/openclaw-plugin" +if [ ! -d "$EXTENSIONS_DIR" ]; then + # Try alternative naming + EXTENSIONS_DIR="/home/node/.openclaw/extensions/claude-mem" + if [ ! -d "$EXTENSIONS_DIR" ]; then + # Search for it + FOUND_DIR=$(find /home/node/.openclaw/extensions/ -name "openclaw.plugin.json" -exec dirname {} \; 2>/dev/null | head -1 || true) + if [ -n "$FOUND_DIR" ]; then + EXTENSIONS_DIR="$FOUND_DIR" + fi + fi +fi + +if [ -d "$EXTENSIONS_DIR" ]; then + pass "Plugin directory exists: $EXTENSIONS_DIR" +else + fail "Plugin directory not found under /home/node/.openclaw/extensions/" + ls -la /home/node/.openclaw/extensions/ 2>/dev/null || echo " (extensions dir not found)" +fi + +# Check key files exist +for FILE in "openclaw.plugin.json" "dist/index.js" "package.json"; do + if [ -f "$EXTENSIONS_DIR/$FILE" ]; then + pass "File exists: $FILE" + else + fail "File missing: $FILE" + fi +done + +# ─── Phase 3: Mock Worker + Plugin Integration ─── + +section "Phase 3: Mock Worker + Plugin Integration" + +# Start mock worker in background +echo " Starting mock claude-mem worker..." +node /app/mock-worker.js & +MOCK_PID=$! + +# Wait for mock worker to be ready +for i in $(seq 1 10); do + if curl -sf http://localhost:37777/health > /dev/null 2>&1; then + break + fi + sleep 0.5 +done + +if curl -sf http://localhost:37777/health > /dev/null 2>&1; then + pass "Mock worker health check passed" +else + fail "Mock worker health check failed" + kill $MOCK_PID 2>/dev/null || true +fi + +# Test SSE stream connectivity (curl with max-time to capture initial SSE frame) +SSE_TEST=$(curl -s --max-time 2 http://localhost:37777/stream 2>/dev/null || true) +if echo "$SSE_TEST" | grep -q "connected"; then + pass "SSE stream returns connected event" +else + fail "SSE stream did not return connected event" + echo " Got: $(echo "$SSE_TEST" | head -5)" +fi + +# ─── Phase 4: Gateway + Plugin Load ─── + +section "Phase 4: Gateway Startup with Plugin" + +# Create a minimal config that enables the plugin with the mock worker. +# The memory slot must be set to "claude-mem" to match what `plugins install` configured. +# Gateway auth is disabled via token for headless testing. +mkdir -p /home/node/.openclaw +cat > /home/node/.openclaw/openclaw.json << 'EOFCONFIG' +{ + "gateway": { + "mode": "local", + "auth": { + "mode": "token", + "token": "e2e-test-token" + } + }, + "plugins": { + "slots": { + "memory": "claude-mem" + }, + "entries": { + "claude-mem": { + "enabled": true, + "config": { + "workerPort": 37777, + "observationFeed": { + "enabled": true, + "channel": "telegram", + "to": "test-chat-id-12345" + } + } + } + } + } +} +EOFCONFIG + +pass "OpenClaw config written with plugin enabled" + +# Start gateway in background and capture output +GATEWAY_LOG="/tmp/gateway.log" +echo " Starting OpenClaw gateway (timeout 15s)..." +OPENCLAW_GATEWAY_TOKEN=e2e-test-token timeout 15 node /app/openclaw.mjs gateway --allow-unconfigured --verbose --token e2e-test-token > "$GATEWAY_LOG" 2>&1 & +GATEWAY_PID=$! + +# Give the gateway time to start and load plugins +sleep 5 + +# Check if gateway started +if kill -0 $GATEWAY_PID 2>/dev/null; then + pass "Gateway process is running" +else + fail "Gateway process exited early" + echo " Gateway log:" + cat "$GATEWAY_LOG" 2>/dev/null | tail -30 +fi + +# Check gateway log for plugin load messages +if grep -qi "claude-mem" "$GATEWAY_LOG" 2>/dev/null; then + pass "Gateway log mentions claude-mem plugin" +else + fail "Gateway log does not mention claude-mem" + echo " Gateway log (last 20 lines):" + tail -20 "$GATEWAY_LOG" 2>/dev/null +fi + +# Check for plugin loaded message +if grep -q "plugin loaded" "$GATEWAY_LOG" 2>/dev/null || grep -q "v1.0.0" "$GATEWAY_LOG" 2>/dev/null; then + pass "Plugin load message found in gateway log" +else + fail "Plugin load message not found" +fi + +# Check for observation feed messages +if grep -qi "observation feed" "$GATEWAY_LOG" 2>/dev/null; then + pass "Observation feed activity in gateway log" +else + fail "No observation feed activity detected" +fi + +# Check for SSE connection to mock worker +if grep -qi "connected.*SSE\|SSE.*stream\|connecting.*SSE" "$GATEWAY_LOG" 2>/dev/null; then + pass "SSE connection activity detected" +else + fail "No SSE connection activity in log" +fi + +# ─── Cleanup ─── + +section "Cleanup" +kill $GATEWAY_PID 2>/dev/null || true +kill $MOCK_PID 2>/dev/null || true +wait $GATEWAY_PID 2>/dev/null || true +wait $MOCK_PID 2>/dev/null || true +echo " Processes stopped." + +# ─── Summary ─── + +echo "" +echo "===============================" +echo " E2E Test Results" +echo "===============================" +echo " Total: $TOTAL" +echo " Passed: $PASS" +echo " Failed: $FAIL" +echo "===============================" + +if [ "$FAIL" -gt 0 ]; then + echo "" + echo " SOME TESTS FAILED" + echo "" + echo " Full gateway log:" + cat "$GATEWAY_LOG" 2>/dev/null + exit 1 +fi + +echo "" +echo " ALL TESTS PASSED" +exit 0 diff --git a/openclaw/install.sh b/openclaw/install.sh new file mode 100755 index 00000000..51da77b5 --- /dev/null +++ b/openclaw/install.sh @@ -0,0 +1,1801 @@ +#!/usr/bin/env bash +set -euo pipefail + +# claude-mem OpenClaw Plugin Installer +# Installs the claude-mem persistent memory plugin for OpenClaw gateways. +# +# Usage: +# curl -fsSL https://install.cmem.ai/openclaw.sh | bash +# # Or with options: +# curl -fsSL https://install.cmem.ai/openclaw.sh | bash -s -- --provider=gemini --api-key=YOUR_KEY +# # Direct execution: +# bash install.sh [--non-interactive] [--upgrade] [--provider=claude|gemini|openrouter] [--api-key=KEY] + +############################################################################### +# Constants +############################################################################### + +readonly MIN_BUN_VERSION="1.1.14" +readonly INSTALLER_VERSION="1.0.0" + +############################################################################### +# Argument parsing +############################################################################### + +NON_INTERACTIVE="" +CLI_PROVIDER="" +CLI_API_KEY="" +UPGRADE_MODE="" +CLI_BRANCH="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --non-interactive) + NON_INTERACTIVE="true" + shift + ;; + --upgrade) + UPGRADE_MODE="true" + shift + ;; + --branch=*) + CLI_BRANCH="${1#--branch=}" + shift + ;; + --branch) + CLI_BRANCH="${2:-}" + shift 2 + ;; + --provider=*) + CLI_PROVIDER="${1#--provider=}" + shift + ;; + --provider) + CLI_PROVIDER="${2:-}" + shift 2 + ;; + --api-key=*) + CLI_API_KEY="${1#--api-key=}" + shift + ;; + --api-key) + CLI_API_KEY="${2:-}" + shift 2 + ;; + *) + shift + ;; + esac +done + +############################################################################### +# TTY detection — ensure interactive prompts work under curl | bash +# When piped, stdin reads from curl's output, not the terminal. +# We open /dev/tty on fd 3 and read interactive input from there. +############################################################################### + +TTY_FD=0 + +setup_tty() { + if [[ -t 0 ]]; then + # stdin IS a terminal — use it directly + TTY_FD=0 + elif [[ -e /dev/tty ]]; then + # stdin is piped (curl | bash) but /dev/tty is available + exec 3&2 + echo "Use --non-interactive or run directly: bash install.sh" >&2 + exit 1 + fi + fi +} + +############################################################################### +# Color utilities — auto-detect terminal color support +############################################################################### + +if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then + readonly COLOR_RED='\033[0;31m' + readonly COLOR_GREEN='\033[0;32m' + readonly COLOR_YELLOW='\033[0;33m' + readonly COLOR_BLUE='\033[0;34m' + readonly COLOR_MAGENTA='\033[0;35m' + readonly COLOR_CYAN='\033[0;36m' + readonly COLOR_BOLD='\033[1m' + readonly COLOR_RESET='\033[0m' +else + readonly COLOR_RED='' + readonly COLOR_GREEN='' + readonly COLOR_YELLOW='' + readonly COLOR_BLUE='' + readonly COLOR_MAGENTA='' + readonly COLOR_CYAN='' + readonly COLOR_BOLD='' + readonly COLOR_RESET='' +fi + +info() { echo -e "${COLOR_BLUE}ℹ${COLOR_RESET} $*"; } +success() { echo -e "${COLOR_GREEN}✓${COLOR_RESET} $*"; } +warn() { echo -e "${COLOR_YELLOW}⚠${COLOR_RESET} $*"; } +error() { echo -e "${COLOR_RED}✗${COLOR_RESET} $*" >&2; } + +prompt_user() { + if [[ "$NON_INTERACTIVE" == "true" ]]; then + error "Cannot prompt in non-interactive mode: $*" + return 1 + fi + echo -en "${COLOR_CYAN}?${COLOR_RESET} $* " +} + +# Read a line from the terminal (works even when stdin is piped from curl) +# Callers always pass -r via $@; shellcheck can't see through the delegation +read_tty() { + # shellcheck disable=SC2162 + read "$@" <&"$TTY_FD" +} + +############################################################################### +# Global cleanup trap — removes temp directories on unexpected exit +############################################################################### + +CLEANUP_DIRS=() + +register_cleanup_dir() { + CLEANUP_DIRS+=("$1") +} + +cleanup_on_exit() { + local exit_code=$? + for dir in "${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}"; do + if [[ -d "$dir" ]]; then + rm -rf "$dir" + fi + done + if [[ $exit_code -ne 0 ]]; then + echo "" >&2 + error "Installation failed (exit code: ${exit_code})" + error "Any temporary files have been cleaned up." + error "Fix the issue above and re-run the installer." + fi +} + +trap cleanup_on_exit EXIT + +############################################################################### +# Prerequisite checks +############################################################################### + +check_git() { + if command -v git &>/dev/null; then + return 0 + fi + + error "git is not installed" + echo "" >&2 + case "${PLATFORM:-}" in + macos) + error "Install git on macOS with:" + error " xcode-select --install" + error " # or: brew install git" + ;; + linux) + error "Install git on Linux with:" + error " sudo apt install git # Debian/Ubuntu" + error " sudo dnf install git # Fedora/RHEL" + error " sudo pacman -S git # Arch" + ;; + *) + error "Please install git and re-run this installer." + ;; + esac + exit 1 +} + +############################################################################### +# Port conflict detection — check if port 37777 is already in use +############################################################################### + +check_port_37777() { + local port_in_use="" + + # Try lsof first (macOS/Linux) + if command -v lsof &>/dev/null; then + if lsof -i :37777 -sTCP:LISTEN &>/dev/null; then + port_in_use="true" + fi + # Fallback to ss (Linux) + elif command -v ss &>/dev/null; then + if ss -tlnp 2>/dev/null | grep -q ':37777 '; then + port_in_use="true" + fi + # Fallback to curl probe + elif command -v curl &>/dev/null; then + local response + response="$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:37777/api/health" 2>/dev/null)" || true + if [[ "$response" == "200" ]]; then + port_in_use="true" + fi + fi + + if [[ "$port_in_use" == "true" ]]; then + return 0 # port IS in use + fi + return 1 # port is free +} + +############################################################################### +# Upgrade detection — check if claude-mem is already installed +############################################################################### + +is_claude_mem_installed() { + # Check if the plugin directory exists with the worker script + if find_claude_mem_install_dir 2>/dev/null; then + return 0 + fi + return 1 +} + +############################################################################### +# JSON manipulation helper — jq with python3/node fallback +# Usage: ensure_jq_or_fallback [jq_args...] +# For simple read operations, returns the result on stdout. +# For write operations, updates the file in-place. +############################################################################### + +ensure_jq_or_fallback() { + local json_file="$1" + shift + local jq_filter="$1" + shift + # remaining args are passed as jq --arg pairs + + if command -v jq &>/dev/null; then + local tmp_file + tmp_file="$(mktemp)" + jq "$@" "$jq_filter" "$json_file" > "$tmp_file" && mv "$tmp_file" "$json_file" + return $? + fi + + if command -v python3 &>/dev/null; then + # For complex jq filters, fall back to node instead + # Python is used only for simple operations + : + fi + + # Fallback to node (always available — it's a dependency) + # This is a passthrough; callers that need node-specific logic + # should use node -e directly. This function is for jq compatibility. + warn "jq not found — using node for JSON manipulation" + return 1 +} + +############################################################################### +# Parse /api/health JSON response — extract worker metadata into globals +# Uses jq → python3 → node fallback chain (matching installer conventions) +# Sets: WORKER_VERSION, WORKER_AI_PROVIDER, WORKER_AI_AUTH_METHOD, +# WORKER_INITIALIZED, WORKER_REPORTED_PID, WORKER_UPTIME +############################################################################### + +parse_health_json() { + local raw_json="$1" + + # Reset all health globals before parsing + WORKER_VERSION="" + WORKER_AI_PROVIDER="" + WORKER_AI_AUTH_METHOD="" + WORKER_INITIALIZED="" + WORKER_REPORTED_PID="" + WORKER_UPTIME="" + + if [[ -z "$raw_json" ]]; then + return 0 + fi + + # Try jq first (fastest, most reliable) + if command -v jq &>/dev/null; then + WORKER_VERSION="$(echo "$raw_json" | jq -r '.version // empty' 2>/dev/null)" || true + WORKER_AI_PROVIDER="$(echo "$raw_json" | jq -r '.ai.provider // empty' 2>/dev/null)" || true + WORKER_AI_AUTH_METHOD="$(echo "$raw_json" | jq -r '.ai.authMethod // empty' 2>/dev/null)" || true + WORKER_INITIALIZED="$(echo "$raw_json" | jq -r '.initialized // empty' 2>/dev/null)" || true + WORKER_REPORTED_PID="$(echo "$raw_json" | jq -r '.pid // empty' 2>/dev/null)" || true + WORKER_UPTIME="$(echo "$raw_json" | jq -r '.uptime // empty' 2>/dev/null)" || true + return 0 + fi + + # Try python3 fallback + if command -v python3 &>/dev/null; then + local parsed + parsed="$(INSTALLER_HEALTH_JSON="$raw_json" python3 -c " +import json, os, sys +try: + data = json.loads(os.environ['INSTALLER_HEALTH_JSON']) + ai = data.get('ai') or {} + fields = [ + str(data.get('version', '')), + str(ai.get('provider', '')), + str(ai.get('authMethod', '')), + str(data.get('initialized', '')), + str(data.get('pid', '')), + str(data.get('uptime', '')), + ] + sys.stdout.write('\n'.join(fields)) +except Exception: + sys.stdout.write('\n\n\n\n\n') +" 2>/dev/null)" || true + + if [[ -n "$parsed" ]]; then + local -a health_fields + IFS=$'\n' read -r -d '' -a health_fields <<< "$parsed" || true + WORKER_VERSION="${health_fields[0]:-}" + WORKER_AI_PROVIDER="${health_fields[1]:-}" + WORKER_AI_AUTH_METHOD="${health_fields[2]:-}" + WORKER_INITIALIZED="${health_fields[3]:-}" + WORKER_REPORTED_PID="${health_fields[4]:-}" + WORKER_UPTIME="${health_fields[5]:-}" + # Normalize python's None/empty representations + [[ "$WORKER_VERSION" == "None" ]] && WORKER_VERSION="" + [[ "$WORKER_AI_PROVIDER" == "None" ]] && WORKER_AI_PROVIDER="" + [[ "$WORKER_AI_AUTH_METHOD" == "None" ]] && WORKER_AI_AUTH_METHOD="" + [[ "$WORKER_INITIALIZED" == "None" ]] && WORKER_INITIALIZED="" + [[ "$WORKER_REPORTED_PID" == "None" ]] && WORKER_REPORTED_PID="" + [[ "$WORKER_UPTIME" == "None" ]] && WORKER_UPTIME="" + fi + return 0 + fi + + # Fallback to node (always available — it's a dependency) + local parsed + parsed="$(INSTALLER_HEALTH_JSON="$raw_json" node -e " + try { + const data = JSON.parse(process.env.INSTALLER_HEALTH_JSON); + const ai = data.ai || {}; + const fields = [ + data.version ?? '', + ai.provider ?? '', + ai.authMethod ?? '', + data.initialized != null ? String(data.initialized) : '', + data.pid != null ? String(data.pid) : '', + data.uptime != null ? String(data.uptime) : '', + ]; + process.stdout.write(fields.join('\n')); + } catch (e) { + process.stdout.write('\n\n\n\n\n'); + } + " 2>/dev/null)" || true + + if [[ -n "$parsed" ]]; then + local -a health_fields + IFS=$'\n' read -r -d '' -a health_fields <<< "$parsed" || true + WORKER_VERSION="${health_fields[0]:-}" + WORKER_AI_PROVIDER="${health_fields[1]:-}" + WORKER_AI_AUTH_METHOD="${health_fields[2]:-}" + WORKER_INITIALIZED="${health_fields[3]:-}" + WORKER_REPORTED_PID="${health_fields[4]:-}" + WORKER_UPTIME="${health_fields[5]:-}" + fi +} + +############################################################################### +# Format uptime from milliseconds to human-readable (e.g., "2m 15s", "1h 23m") +############################################################################### + +format_uptime_ms() { + local ms="$1" + local secs=$((ms / 1000)) + if (( secs >= 3600 )); then + echo "$((secs / 3600))h $((secs % 3600 / 60))m" + elif (( secs >= 60 )); then + echo "$((secs / 60))m $((secs % 60))s" + else + echo "${secs}s" + fi +} + +############################################################################### +# Banner +############################################################################### + +print_banner() { + echo -e "${COLOR_MAGENTA}${COLOR_BOLD}" + cat << 'BANNER' + ┌─────────────────────────────────────────┐ + │ claude-mem × OpenClaw │ + │ Persistent Memory Plugin Installer │ + └─────────────────────────────────────────┘ +BANNER + echo -e "${COLOR_RESET}" + info "Installer v${INSTALLER_VERSION}" + echo "" +} + +############################################################################### +# Platform detection +############################################################################### + +PLATFORM="" +IS_WSL="" + +detect_platform() { + local uname_out + uname_out="$(uname -s)" + + case "${uname_out}" in + Darwin*) + PLATFORM="macos" + ;; + Linux*) + if grep -qi microsoft /proc/version 2>/dev/null; then + PLATFORM="linux" + IS_WSL="true" + else + PLATFORM="linux" + fi + ;; + MINGW*|MSYS*|CYGWIN*) + PLATFORM="windows" + ;; + *) + error "Unsupported platform: ${uname_out}" + exit 1 + ;; + esac + + info "Detected platform: ${PLATFORM}${IS_WSL:+ (WSL)}" +} + +############################################################################### +# Version comparison — returns 0 if $1 >= $2 +############################################################################### + +version_gte() { + local v1="$1" v2="$2" + local -a parts1 parts2 + IFS='.' read -ra parts1 <<< "$v1" + IFS='.' read -ra parts2 <<< "$v2" + + for i in 0 1 2; do + local p1="${parts1[$i]:-0}" + local p2="${parts2[$i]:-0}" + if (( p1 > p2 )); then return 0; fi + if (( p1 < p2 )); then return 1; fi + done + return 0 +} + +############################################################################### +# Bun detection and installation +# Translated from plugin/scripts/smart-install.js patterns +############################################################################### + +BUN_PATH="" + +find_bun_path() { + # Try PATH first + if command -v bun &>/dev/null; then + BUN_PATH="$(command -v bun)" + return 0 + fi + + # Check common installation paths (handles fresh installs before PATH reload) + local -a bun_paths=( + "${HOME}/.bun/bin/bun" + "/usr/local/bin/bun" + "/opt/homebrew/bin/bun" + ) + + for candidate in "${bun_paths[@]}"; do + if [[ -x "$candidate" ]]; then + BUN_PATH="$candidate" + return 0 + fi + done + + BUN_PATH="" + return 1 +} + +check_bun() { + if ! find_bun_path; then + return 1 + fi + + # Verify minimum version + local bun_version + bun_version="$("$BUN_PATH" --version 2>/dev/null)" || return 1 + + if version_gte "$bun_version" "$MIN_BUN_VERSION"; then + success "Bun ${bun_version} found at ${BUN_PATH}" + return 0 + else + warn "Bun ${bun_version} is below minimum required version ${MIN_BUN_VERSION}" + return 1 + fi +} + +install_bun() { + info "Installing Bun runtime..." + + if ! curl -fsSL https://bun.sh/install | bash; then + error "Failed to install Bun automatically" + error "Please install manually:" + error " curl -fsSL https://bun.sh/install | bash" + error " Or: brew install oven-sh/bun/bun (macOS)" + error "Then restart your terminal and re-run this installer." + exit 1 + fi + + # Re-detect after install (installer may have placed it in ~/.bun/bin) + if ! find_bun_path; then + error "Bun installation completed but binary not found in expected locations" + error "Please restart your terminal and re-run this installer." + exit 1 + fi + + local bun_version + bun_version="$("$BUN_PATH" --version 2>/dev/null)" || true + success "Bun ${bun_version} installed at ${BUN_PATH}" +} + +############################################################################### +# uv detection and installation +# Translated from plugin/scripts/smart-install.js patterns +############################################################################### + +UV_PATH="" + +find_uv_path() { + # Try PATH first + if command -v uv &>/dev/null; then + UV_PATH="$(command -v uv)" + return 0 + fi + + # Check common installation paths (handles fresh installs before PATH reload) + local -a uv_paths=( + "${HOME}/.local/bin/uv" + "${HOME}/.cargo/bin/uv" + "/usr/local/bin/uv" + "/opt/homebrew/bin/uv" + ) + + for candidate in "${uv_paths[@]}"; do + if [[ -x "$candidate" ]]; then + UV_PATH="$candidate" + return 0 + fi + done + + UV_PATH="" + return 1 +} + +check_uv() { + if ! find_uv_path; then + return 1 + fi + + local uv_version + uv_version="$("$UV_PATH" --version 2>/dev/null)" || return 1 + success "uv ${uv_version} found at ${UV_PATH}" + return 0 +} + +install_uv() { + info "Installing uv (Python package manager for Chroma support)..." + + if ! curl -LsSf https://astral.sh/uv/install.sh | sh; then + error "Failed to install uv automatically" + error "Please install manually:" + error " curl -LsSf https://astral.sh/uv/install.sh | sh" + error " Or: brew install uv (macOS)" + error "Then restart your terminal and re-run this installer." + exit 1 + fi + + # Re-detect after install + if ! find_uv_path; then + error "uv installation completed but binary not found in expected locations" + error "Please restart your terminal and re-run this installer." + exit 1 + fi + + local uv_version + uv_version="$("$UV_PATH" --version 2>/dev/null)" || true + success "uv ${uv_version} installed at ${UV_PATH}" +} + +############################################################################### +# OpenClaw gateway detection +############################################################################### + +OPENCLAW_PATH="" + +find_openclaw() { + # Try PATH first — check both "openclaw" and "openclaw.mjs" binary names + for bin_name in openclaw openclaw.mjs; do + if command -v "$bin_name" &>/dev/null; then + OPENCLAW_PATH="$(command -v "$bin_name")" + return 0 + fi + done + + # Check common installation paths + local -a openclaw_paths=( + "${HOME}/.openclaw/openclaw.mjs" + "/usr/local/bin/openclaw.mjs" + "/usr/local/bin/openclaw" + "/usr/local/lib/node_modules/openclaw/openclaw.mjs" + "${HOME}/.npm-global/lib/node_modules/openclaw/openclaw.mjs" + "${HOME}/.npm-global/bin/openclaw" + ) + + # Also check for node_modules in common project locations + if [[ -n "${NODE_PATH:-}" ]]; then + openclaw_paths+=("${NODE_PATH}/openclaw/openclaw.mjs") + fi + + for candidate in "${openclaw_paths[@]}"; do + if [[ -f "$candidate" ]]; then + OPENCLAW_PATH="$candidate" + return 0 + fi + done + + OPENCLAW_PATH="" + return 1 +} + +check_openclaw() { + if ! find_openclaw; then + error "OpenClaw gateway not found" + error "" + error "The claude-mem plugin requires an OpenClaw gateway to be installed." + error "Please install OpenClaw first:" + error "" + error " npm install -g openclaw" + error " # or visit: https://openclaw.dev/docs/installation" + error "" + error "Then re-run this installer." + exit 1 + fi + + success "OpenClaw gateway found at ${OPENCLAW_PATH}" +} + +# Run openclaw command — uses node for .mjs files, direct execution otherwise +run_openclaw() { + if [[ "$OPENCLAW_PATH" == *.mjs ]]; then + node "$OPENCLAW_PATH" "$@" + else + "$OPENCLAW_PATH" "$@" + fi +} + +############################################################################### +# Plugin installation — clone, build, install, enable +# Flow based on openclaw/Dockerfile.e2e +############################################################################### + +CLAUDE_MEM_REPO="https://github.com/thedotmack/claude-mem.git" +CLAUDE_MEM_BRANCH="${CLI_BRANCH:-main}" +PLUGIN_FRESHLY_INSTALLED="" + +install_plugin() { + # Check for git before attempting clone + check_git + + # Remove existing plugin installation to allow clean re-install + local existing_plugin_dir="${HOME}/.openclaw/extensions/claude-mem" + if [[ -d "$existing_plugin_dir" ]]; then + info "Removing existing claude-mem plugin at ${existing_plugin_dir}..." + rm -rf "$existing_plugin_dir" + fi + + local build_dir + build_dir="$(mktemp -d)" + register_cleanup_dir "$build_dir" + + info "Cloning claude-mem repository (branch: ${CLAUDE_MEM_BRANCH})..." + if ! git clone --depth 1 --branch "$CLAUDE_MEM_BRANCH" "$CLAUDE_MEM_REPO" "$build_dir/claude-mem" 2>&1; then + error "Failed to clone claude-mem repository" + error "Check your internet connection and try again." + exit 1 + fi + + local plugin_src="${build_dir}/claude-mem/openclaw" + + # Build the TypeScript plugin + info "Building TypeScript plugin..." + if ! (cd "$plugin_src" && NODE_ENV=development npm install --ignore-scripts 2>&1 && npx tsc 2>&1); then + error "Failed to build the claude-mem OpenClaw plugin" + error "Make sure Node.js and npm are installed." + exit 1 + fi + + # Create minimal installable package (matches Dockerfile.e2e pattern) + local installable_dir="${build_dir}/claude-mem-installable" + mkdir -p "${installable_dir}/dist" + + cp "${plugin_src}/dist/index.js" "${installable_dir}/dist/" + cp "${plugin_src}/dist/index.d.ts" "${installable_dir}/dist/" 2>/dev/null || true + cp "${plugin_src}/openclaw.plugin.json" "${installable_dir}/" + + # Generate the installable package.json with openclaw.extensions field + INSTALLER_PACKAGE_DIR="$installable_dir" node -e " + const pkg = { + name: 'claude-mem', + version: '1.0.0', + type: 'module', + main: 'dist/index.js', + openclaw: { extensions: ['./dist/index.js'] } + }; + require('fs').writeFileSync(process.env.INSTALLER_PACKAGE_DIR + '/package.json', JSON.stringify(pkg, null, 2)); + " + + # Clean up stale claude-mem plugin entry before installing. + # If the config references claude-mem but the plugin isn't installed, + # OpenClaw's config validator blocks ALL CLI commands (including plugins install). + # We temporarily remove the entry and save the config so `plugins install` can run, + # then `plugins install` + `plugins enable` will re-create it properly. + local oc_config="${HOME}/.openclaw/openclaw.json" + local saved_plugin_config="" + if [[ -f "$oc_config" ]]; then + saved_plugin_config=$(INSTALLER_CONFIG_FILE="$oc_config" node -e " + const fs = require('fs'); + const configPath = process.env.INSTALLER_CONFIG_FILE; + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + const entry = config?.plugins?.entries?.['claude-mem']; + if (entry || config?.plugins?.slots?.memory === 'claude-mem') { + // Save the config block so we can restore it after install + process.stdout.write(JSON.stringify(entry?.config || {})); + // Remove the stale entry so OpenClaw CLI can run + if (entry) delete config.plugins.entries['claude-mem']; + // Also remove the slot reference — if the slot points to a plugin + // that isn't in entries, OpenClaw's config validator rejects ALL commands + if (config?.plugins?.slots?.memory === 'claude-mem') { + delete config.plugins.slots.memory; + } + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + } + " 2>/dev/null) || true + fi + + # Install the plugin using OpenClaw's CLI + info "Installing claude-mem plugin into OpenClaw..." + if ! run_openclaw plugins install "$installable_dir" 2>&1; then + error "Failed to install claude-mem plugin" + error "Try manually: ${OPENCLAW_PATH} plugins install " + exit 1 + fi + + # Enable the plugin + info "Enabling claude-mem plugin..." + if ! run_openclaw plugins enable claude-mem 2>&1; then + error "Failed to enable claude-mem plugin" + error "Try manually: ${OPENCLAW_PATH} plugins enable claude-mem" + exit 1 + fi + + # Restore saved plugin config (workerPort, syncMemoryFile, observationFeed, etc.) + # from any pre-existing installation that was temporarily removed above. + if [[ -n "$saved_plugin_config" && "$saved_plugin_config" != "{}" ]]; then + info "Restoring previous plugin configuration..." + INSTALLER_CONFIG_FILE="$oc_config" INSTALLER_SAVED_CONFIG="$saved_plugin_config" node -e " + const fs = require('fs'); + const configPath = process.env.INSTALLER_CONFIG_FILE; + const savedConfig = JSON.parse(process.env.INSTALLER_SAVED_CONFIG); + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + if (config?.plugins?.entries?.['claude-mem']) { + config.plugins.entries['claude-mem'].config = savedConfig; + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + } + " 2>/dev/null || warn "Could not restore previous plugin config — configure manually" + fi + + success "claude-mem plugin installed and enabled" + + # ── Copy core plugin files (worker, hooks, scripts) to extension directory ── + # The OpenClaw extension only contains the gateway hook (dist/index.js). + # The actual worker service and Claude Code hooks live in the plugin/ directory + # of the main repo. We copy them so find_claude_mem_install_dir() can locate + # the worker-service.cjs and the worker runs the updated version. + local extension_dir="${HOME}/.openclaw/extensions/claude-mem" + local repo_root="${build_dir}/claude-mem" + + if [[ -d "$extension_dir" && -d "${repo_root}/plugin" ]]; then + info "Copying core plugin files to ${extension_dir}..." + + # Copy plugin/ directory (worker service, hooks, scripts, skills, UI) + cp -R "${repo_root}/plugin" "${extension_dir}/" + + # Copy root package.json (contains the canonical version number) + cp "${repo_root}/package.json" "${extension_dir}/package.json" + + success "Core plugin files updated at ${extension_dir}" + else + warn "Could not copy core plugin files — worker may need manual update" + fi + + PLUGIN_FRESHLY_INSTALLED="true" +} + +############################################################################### +# Memory slot configuration +# Sets plugins.slots.memory = "claude-mem" in ~/.openclaw/openclaw.json +############################################################################### + +configure_memory_slot() { + local config_dir="${HOME}/.openclaw" + local config_file="${config_dir}/openclaw.json" + + mkdir -p "$config_dir" + + if [[ ! -f "$config_file" ]]; then + # No config file exists — create one with the memory slot + info "Creating OpenClaw configuration with claude-mem memory slot..." + INSTALLER_CONFIG_FILE="$config_file" node -e " + const config = { + plugins: { + slots: { memory: 'claude-mem' }, + entries: { + 'claude-mem': { + enabled: true, + config: { + workerPort: 37777, + syncMemoryFile: true + } + } + } + } + }; + require('fs').writeFileSync(process.env.INSTALLER_CONFIG_FILE, JSON.stringify(config, null, 2)); + " + success "Created ${config_file} with memory slot set to claude-mem" + return 0 + fi + + # Config file exists — update it to set the memory slot + info "Updating OpenClaw configuration to use claude-mem memory slot..." + + # Use node for reliable JSON manipulation + INSTALLER_CONFIG_FILE="$config_file" node -e " + const fs = require('fs'); + const configPath = process.env.INSTALLER_CONFIG_FILE; + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + // Ensure plugins structure exists + if (!config.plugins) config.plugins = {}; + if (!config.plugins.slots) config.plugins.slots = {}; + if (!config.plugins.entries) config.plugins.entries = {}; + + // Set memory slot to claude-mem + config.plugins.slots.memory = 'claude-mem'; + + // Ensure claude-mem entry exists and is enabled + if (!config.plugins.entries['claude-mem']) { + config.plugins.entries['claude-mem'] = { + enabled: true, + config: { + workerPort: 37777, + syncMemoryFile: true + } + }; + } else { + config.plugins.entries['claude-mem'].enabled = true; + // Remove unrecognized keys that cause OpenClaw config validation errors + const allowedKeys = new Set(['enabled', 'config']); + for (const key of Object.keys(config.plugins.entries['claude-mem'])) { + if (!allowedKeys.has(key)) { + delete config.plugins.entries['claude-mem'][key]; + } + } + } + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + " + + success "Memory slot set to claude-mem in ${config_file}" +} + +############################################################################### +# AI Provider setup — interactive provider selection +# Reads defaults from SettingsDefaultsManager.ts (single source of truth) +############################################################################### + +AI_PROVIDER="" +AI_PROVIDER_API_KEY="" + +mask_api_key() { + local key="$1" + local len=${#key} + if (( len <= 4 )); then + echo "****" + else + local masked_len=$((len - 4)) + local mask="" + for (( i=0; i/dev/null | head -n 1)" || true + if [[ -n "$found" ]]; then + # Strip /plugin/scripts/worker-service.cjs to get the install dir + CLAUDE_MEM_INSTALL_DIR="${found%/plugin/scripts/worker-service.cjs}" + return 0 + fi + fi + done + + CLAUDE_MEM_INSTALL_DIR="" + return 1 +} + +############################################################################### +# Worker service startup +# Starts the claude-mem worker using bun in the background +############################################################################### + +WORKER_PID="" +WORKER_VERSION="" +WORKER_AI_PROVIDER="" +WORKER_AI_AUTH_METHOD="" +WORKER_INITIALIZED="" +WORKER_REPORTED_PID="" +WORKER_UPTIME="" + +start_worker() { + info "Starting claude-mem worker service..." + + if ! find_claude_mem_install_dir; then + error "Cannot find claude-mem plugin installation directory" + error "Expected worker-service.cjs in one of:" + error " ~/.openclaw/extensions/claude-mem/plugin/scripts/" + error " ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/" + error "" + error "Try reinstalling the plugin and re-running this installer." + return 1 + fi + + local worker_script="${CLAUDE_MEM_INSTALL_DIR}/plugin/scripts/worker-service.cjs" + local log_dir="${HOME}/.claude-mem/logs" + local log_date + log_date="$(date +%Y-%m-%d)" + local log_file="${log_dir}/worker-${log_date}.log" + + mkdir -p "$log_dir" + + # Ensure bun path is available + if [[ -z "$BUN_PATH" ]]; then + if ! find_bun_path; then + error "Bun not found — cannot start worker service" + return 1 + fi + fi + + # Start worker in background with nohup + CLAUDE_MEM_WORKER_PORT=37777 nohup "$BUN_PATH" "$worker_script" \ + >> "$log_file" 2>&1 & + WORKER_PID=$! + + # Write PID file for future management + local pid_file="${HOME}/.claude-mem/worker.pid" + mkdir -p "${HOME}/.claude-mem" + INSTALLER_PID_FILE="$pid_file" INSTALLER_WORKER_PID="$WORKER_PID" node -e " + const info = { + pid: parseInt(process.env.INSTALLER_WORKER_PID, 10), + port: 37777, + startedAt: new Date().toISOString(), + version: 'installer' + }; + require('fs').writeFileSync(process.env.INSTALLER_PID_FILE, JSON.stringify(info, null, 2)); + " + + success "Worker process started (PID: ${WORKER_PID})" + info "Logs: ${log_file}" +} + +############################################################################### +# Health verification — two-stage: health (alive) then readiness (initialized) +# Stage 1: Poll /api/health for HTTP 200 (worker process is running) +# Stage 2: Poll /api/readiness for HTTP 200 (worker is fully initialized) +# Total budget: 30 attempts (30 seconds) shared across both stages +############################################################################### + +verify_health() { + local max_attempts=30 + local attempt=1 + local health_url="http://127.0.0.1:37777/api/health" + local readiness_url="http://127.0.0.1:37777/api/readiness" + local health_alive=false + + info "Verifying worker health..." + + # ── Stage 1: Wait for /api/health to return HTTP 200 (worker is alive) ── + while (( attempt <= max_attempts )); do + local http_status + http_status="$(curl -s -o /dev/null -w "%{http_code}" "$health_url" 2>/dev/null)" || true + + if [[ "$http_status" == "200" ]]; then + health_alive=true + + # Fetch the full health response body and parse metadata + local body + body="$(curl -s "$health_url" 2>/dev/null)" || true + parse_health_json "$body" + + success "Worker is alive, waiting for initialization..." + + break + fi + + info "Waiting for worker to start... (attempt ${attempt}/${max_attempts})" + sleep 1 + attempt=$((attempt + 1)) + done + + # If health never responded, the worker is not running at all + if [[ "$health_alive" != "true" ]]; then + warn "Worker health check timed out after ${max_attempts} attempts" + warn "The worker may still be starting up. Check status with:" + warn " curl http://127.0.0.1:37777/api/health" + warn " Or check logs: ~/.claude-mem/logs/" + return 1 + fi + + # ── Stage 2: Wait for /api/readiness to return HTTP 200 (fully initialized) ── + attempt=$((attempt + 1)) + while (( attempt <= max_attempts )); do + local readiness_status + readiness_status="$(curl -s -o /dev/null -w "%{http_code}" "$readiness_url" 2>/dev/null)" || true + + if [[ "$readiness_status" == "200" ]]; then + success "Worker is ready!" + return 0 + fi + + info "Waiting for worker to initialize... (attempt ${attempt}/${max_attempts})" + sleep 1 + attempt=$((attempt + 1)) + done + + # Readiness timed out but health is OK — worker is running, just not fully initialized yet + warn "Worker is running but initialization is still in progress" + warn "This is normal on first run — the worker will finish initializing in the background." + warn "Check readiness with: curl http://127.0.0.1:37777/api/readiness" + return 0 +} + +############################################################################### +# Observation feed setup — optional interactive channel configuration +############################################################################### + +FEED_CHANNEL="" +FEED_TARGET_ID="" +FEED_CONFIGURED=false + +setup_observation_feed() { + echo "" + echo -e " ${COLOR_BOLD}Real-Time Observation Feed${COLOR_RESET}" + echo "" + echo " claude-mem can stream AI-compressed observations to a messaging" + echo " channel in real time. Every time an agent learns something," + echo " you'll see it in your chat." + echo "" + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + info "Non-interactive mode: skipping observation feed setup" + info "Configure later in ~/.openclaw/openclaw.json under" + info " plugins.entries.claude-mem.config.observationFeed" + return 0 + fi + + prompt_user "Would you like to set up real-time observation streaming to a messaging channel? (y/n)" + local answer + read_tty -r answer + answer="${answer:-n}" + + if [[ "$answer" != [yY] && "$answer" != [yY][eE][sS] ]]; then + echo "" + info "Skipped observation feed setup." + info "You can configure it later by re-running this installer or" + info "editing ~/.openclaw/openclaw.json under" + info " plugins.entries.claude-mem.config.observationFeed" + return 0 + fi + + echo "" + echo -e " ${COLOR_BOLD}Select your messaging channel:${COLOR_RESET}" + echo "" + echo -e " ${COLOR_BOLD}1)${COLOR_RESET} Telegram" + echo -e " ${COLOR_BOLD}2)${COLOR_RESET} Discord" + echo -e " ${COLOR_BOLD}3)${COLOR_RESET} Slack" + echo -e " ${COLOR_BOLD}4)${COLOR_RESET} Signal" + echo -e " ${COLOR_BOLD}5)${COLOR_RESET} WhatsApp" + echo -e " ${COLOR_BOLD}6)${COLOR_RESET} LINE" + echo "" + + local channel_choice + while true; do + prompt_user "Enter choice [1-6]:" + read_tty -r channel_choice + + case "$channel_choice" in + 1) + FEED_CHANNEL="telegram" + echo "" + echo -e " ${COLOR_CYAN}How to find your Telegram chat ID:${COLOR_RESET}" + echo " Message @userinfobot on Telegram (https://t.me/userinfobot)" + echo " — it replies with your numeric chat ID." + echo " For groups, the ID is negative (e.g., -1001234567890)." + break + ;; + 2) + FEED_CHANNEL="discord" + echo "" + echo -e " ${COLOR_CYAN}How to find your Discord channel ID:${COLOR_RESET}" + echo " Enable Developer Mode (Settings → Advanced → Developer Mode)," + echo " right-click the target channel → Copy Channel ID" + break + ;; + 3) + FEED_CHANNEL="slack" + echo "" + echo -e " ${COLOR_CYAN}How to find your Slack channel ID:${COLOR_RESET}" + echo " Open the channel, click the channel name at top," + echo " scroll to bottom — ID looks like C01ABC2DEFG" + break + ;; + 4) + FEED_CHANNEL="signal" + echo "" + echo -e " ${COLOR_CYAN}How to find your Signal target ID:${COLOR_RESET}" + echo " Use the phone number or group ID from your" + echo " OpenClaw Signal plugin config" + break + ;; + 5) + FEED_CHANNEL="whatsapp" + echo "" + echo -e " ${COLOR_CYAN}How to find your WhatsApp target ID:${COLOR_RESET}" + echo " Use the phone number or group JID from your" + echo " OpenClaw WhatsApp plugin config" + break + ;; + 6) + FEED_CHANNEL="line" + echo "" + echo -e " ${COLOR_CYAN}How to find your LINE target ID:${COLOR_RESET}" + echo " Use the user ID or group ID from the" + echo " LINE Developer Console" + break + ;; + *) + warn "Invalid choice. Please enter a number between 1 and 6." + ;; + esac + done + + echo "" + prompt_user "Enter your ${FEED_CHANNEL} target ID:" + read_tty -r FEED_TARGET_ID + + if [[ -z "$FEED_TARGET_ID" ]]; then + warn "No target ID provided — skipping observation feed setup." + warn "You can configure it later in ~/.openclaw/openclaw.json" + FEED_CHANNEL="" + return 0 + fi + + success "Observation feed: ${FEED_CHANNEL} → ${FEED_TARGET_ID}" + FEED_CONFIGURED=true +} + +############################################################################### +# Write observation feed config into ~/.openclaw/openclaw.json +############################################################################### + +write_observation_feed_config() { + if [[ "$FEED_CONFIGURED" != "true" ]]; then + return 0 + fi + + local config_file="${HOME}/.openclaw/openclaw.json" + + if [[ ! -f "$config_file" ]]; then + warn "OpenClaw config file not found at ${config_file}" + warn "Cannot write observation feed config." + return 1 + fi + + info "Writing observation feed configuration..." + + # Use jq if available, fall back to python3, then node for JSON manipulation + if command -v jq &>/dev/null; then + local tmp_file + tmp_file="$(mktemp)" + jq --arg channel "$FEED_CHANNEL" --arg target "$FEED_TARGET_ID" ' + .plugins //= {} | + .plugins.entries //= {} | + .plugins.entries["claude-mem"] //= {"enabled": true, "config": {}} | + .plugins.entries["claude-mem"].config //= {} | + .plugins.entries["claude-mem"].config.observationFeed = { + "enabled": true, + "channel": $channel, + "to": $target + } + ' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file" + elif command -v python3 &>/dev/null; then + INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \ + INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \ + INSTALLER_CONFIG_FILE="$config_file" \ + python3 -c " +import json, os +config_path = os.environ['INSTALLER_CONFIG_FILE'] +channel = os.environ['INSTALLER_FEED_CHANNEL'] +target_id = os.environ['INSTALLER_FEED_TARGET_ID'] + +with open(config_path) as f: + config = json.load(f) + +config.setdefault('plugins', {}) +config['plugins'].setdefault('entries', {}) +config['plugins']['entries'].setdefault('claude-mem', {'enabled': True, 'config': {}}) +config['plugins']['entries']['claude-mem'].setdefault('config', {}) +config['plugins']['entries']['claude-mem']['config']['observationFeed'] = { + 'enabled': True, + 'channel': channel, + 'to': target_id +} + +with open(config_path, 'w') as f: + json.dump(config, f, indent=2) +" + else + # Fallback to node (always available since it's a dependency) + INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \ + INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \ + INSTALLER_CONFIG_FILE="$config_file" \ + node -e " + const fs = require('fs'); + const configPath = process.env.INSTALLER_CONFIG_FILE; + const channel = process.env.INSTALLER_FEED_CHANNEL; + const targetId = process.env.INSTALLER_FEED_TARGET_ID; + + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + if (!config.plugins) config.plugins = {}; + if (!config.plugins.entries) config.plugins.entries = {}; + if (!config.plugins.entries['claude-mem']) { + config.plugins.entries['claude-mem'] = { enabled: true, config: {} }; + } + if (!config.plugins.entries['claude-mem'].config) { + config.plugins.entries['claude-mem'].config = {}; + } + + config.plugins.entries['claude-mem'].config.observationFeed = { + enabled: true, + channel: channel, + to: targetId + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + " + fi + + success "Observation feed config written to ${config_file}" + echo "" + echo -e " ${COLOR_BOLD}Observation feed summary:${COLOR_RESET}" + echo -e " Channel: ${COLOR_CYAN}${FEED_CHANNEL}${COLOR_RESET}" + echo -e " Target: ${COLOR_CYAN}${FEED_TARGET_ID}${COLOR_RESET}" + echo -e " Enabled: ${COLOR_GREEN}yes${COLOR_RESET}" + echo "" + info "Restart your OpenClaw gateway to activate the observation feed." + info "You should see these log lines:" + echo " [claude-mem] Observation feed starting — channel: ${FEED_CHANNEL}, target: ${FEED_TARGET_ID}" + echo "" + info "After restarting, run /claude-mem-feed in any OpenClaw chat to verify" + info "the feed is connected." +} + +############################################################################### +# Completion summary +############################################################################### + +print_completion_summary() { + local provider_display="" + case "$AI_PROVIDER" in + claude) provider_display="Claude Max Plan (CLI authentication)" ;; + gemini) provider_display="Gemini (gemini-2.5-flash-lite)" ;; + openrouter) provider_display="OpenRouter (xiaomi/mimo-v2-flash:free)" ;; + *) provider_display="$AI_PROVIDER" ;; + esac + + echo "" + echo -e "${COLOR_MAGENTA}${COLOR_BOLD}" + echo " ┌──────────────────────────────────────────┐" + echo " │ Installation Complete! │" + echo " └──────────────────────────────────────────┘" + echo -e "${COLOR_RESET}" + + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Dependencies installed (Bun, uv)" + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} OpenClaw gateway detected" + + # Show installed version from health data if available + if [[ -n "$WORKER_VERSION" ]]; then + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} claude-mem v${COLOR_BOLD}${WORKER_VERSION}${COLOR_RESET} installed and running" + else + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} claude-mem plugin installed and enabled" + fi + + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Memory slot configured" + + # Show AI provider with auth method from health data if available + if [[ -n "$WORKER_AI_AUTH_METHOD" ]]; then + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} AI provider: ${COLOR_BOLD}${WORKER_AI_PROVIDER} (${WORKER_AI_AUTH_METHOD})${COLOR_RESET}" + else + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} AI provider: ${COLOR_BOLD}${provider_display}${COLOR_RESET}" + fi + + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Settings written to ~/.claude-mem/settings.json" + + if [[ -n "$WORKER_PID" ]] && kill -0 "$WORKER_PID" 2>/dev/null; then + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Worker running on port ${COLOR_BOLD}37777${COLOR_RESET} (PID: ${WORKER_PID})" + elif [[ -n "$WORKER_UPTIME" && "$WORKER_UPTIME" =~ ^[0-9]+$ ]] && (( WORKER_UPTIME > 0 )); then + local uptime_formatted + uptime_formatted="$(format_uptime_ms "$WORKER_UPTIME")" + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Worker running on port ${COLOR_BOLD}37777${COLOR_RESET} (PID: ${WORKER_REPORTED_PID}, uptime: ${uptime_formatted})" + else + echo -e " ${COLOR_YELLOW}⚠${COLOR_RESET} Worker may not be running — check logs at ~/.claude-mem/logs/" + fi + + # Show initialization warning if worker is alive but not yet initialized + if [[ "$WORKER_INITIALIZED" != "true" ]] && { [[ -n "$WORKER_REPORTED_PID" ]] || { [[ -n "$WORKER_PID" ]] && kill -0 "$WORKER_PID" 2>/dev/null; }; }; then + echo -e " ${COLOR_YELLOW}⚠${COLOR_RESET} Worker is starting but still initializing (this is normal on first run)" + fi + + if [[ "$FEED_CONFIGURED" == "true" ]]; then + echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Observation feed: ${COLOR_BOLD}${FEED_CHANNEL}${COLOR_RESET} → ${FEED_TARGET_ID}" + else + echo -e " ${COLOR_YELLOW}─${COLOR_RESET} Observation feed: not configured (optional)" + echo -e " Configure later in ~/.openclaw/openclaw.json under" + echo -e " plugins.entries.claude-mem.config.observationFeed" + fi + + echo "" + echo -e " ${COLOR_BOLD}What's next?${COLOR_RESET}" + echo "" + echo -e " ${COLOR_CYAN}1.${COLOR_RESET} Restart your OpenClaw gateway to load the plugin" + echo -e " ${COLOR_CYAN}2.${COLOR_RESET} Verify with ${COLOR_BOLD}/claude-mem-status${COLOR_RESET} in any OpenClaw chat" + echo -e " ${COLOR_CYAN}3.${COLOR_RESET} Check the viewer UI at ${COLOR_BOLD}http://localhost:37777${COLOR_RESET}" + if [[ "$FEED_CONFIGURED" == "true" ]]; then + echo -e " ${COLOR_CYAN}4.${COLOR_RESET} Run ${COLOR_BOLD}/claude-mem-feed${COLOR_RESET} to check feed status" + fi + echo "" + echo -e " ${COLOR_BOLD}To re-run this installer:${COLOR_RESET}" + echo " bash <(curl -fsSL https://install.cmem.ai/openclaw.sh)" + echo "" +} + +############################################################################### +# Main +############################################################################### + +main() { + setup_tty + print_banner + detect_platform + + # --- Step 1: Dependencies --- + echo "" + info "${COLOR_BOLD}[1/8]${COLOR_RESET} Checking dependencies..." + echo "" + + if ! check_bun; then + install_bun + fi + + if ! check_uv; then + install_uv + fi + + echo "" + success "All dependencies satisfied" + + # --- Step 2: OpenClaw gateway --- + echo "" + info "${COLOR_BOLD}[2/8]${COLOR_RESET} Locating OpenClaw gateway..." + check_openclaw + + # --- Step 3: Plugin installation (skip if upgrading and already installed) --- + echo "" + info "${COLOR_BOLD}[3/8]${COLOR_RESET} Installing claude-mem plugin..." + + if [[ "$UPGRADE_MODE" == "true" ]] && is_claude_mem_installed; then + success "claude-mem already installed at ${CLAUDE_MEM_INSTALL_DIR}" + info "Upgrade mode: skipping clone/build/register, updating settings only" + else + install_plugin + fi + + # --- Step 4: Memory slot configuration --- + echo "" + info "${COLOR_BOLD}[4/8]${COLOR_RESET} Configuring memory slot..." + configure_memory_slot + + # --- Step 5: AI provider setup --- + echo "" + info "${COLOR_BOLD}[5/8]${COLOR_RESET} AI provider setup..." + setup_ai_provider + + # --- Step 6: Write settings --- + echo "" + info "${COLOR_BOLD}[6/8]${COLOR_RESET} Writing settings..." + write_settings + + # --- Step 7: Start worker and verify --- + echo "" + info "${COLOR_BOLD}[7/8]${COLOR_RESET} Starting worker service..." + + if check_port_37777; then + warn "Port 37777 is already in use (worker may already be running)" + info "Checking if the existing service is healthy..." + if verify_health; then + # verify_health already called parse_health_json — WORKER_* globals are set. + # Determine the expected version from the installed plugin's package.json. + local expected_version="" + if [[ -n "$CLAUDE_MEM_INSTALL_DIR" ]] || find_claude_mem_install_dir; then + expected_version="$(INSTALLER_PKG="${CLAUDE_MEM_INSTALL_DIR}/package.json" node -e " + try { process.stdout.write(JSON.parse(require('fs').readFileSync(process.env.INSTALLER_PKG, 'utf8')).version || ''); } + catch(e) {} + " 2>/dev/null)" || true + fi + + local needs_restart="" + + # If we just installed fresh plugin files, always restart the worker + # to pick up the new version — even if the old worker was healthy. + if [[ "$PLUGIN_FRESHLY_INSTALLED" == "true" ]]; then + if [[ -n "$WORKER_VERSION" && -n "$expected_version" && "$WORKER_VERSION" != "$expected_version" ]]; then + info "Upgrading worker from v${WORKER_VERSION} to v${expected_version}..." + else + info "Plugin files updated — restarting worker to load new code..." + fi + needs_restart="true" + fi + + # Check if worker version is outdated compared to installed version + if [[ "$needs_restart" != "true" && -n "$WORKER_VERSION" && -n "$expected_version" && "$WORKER_VERSION" != "$expected_version" ]]; then + info "Upgrading worker from v${WORKER_VERSION} to v${expected_version}..." + needs_restart="true" + fi + + # Check if AI provider doesn't match current configuration + if [[ "$needs_restart" != "true" && -n "$WORKER_AI_PROVIDER" && -n "$AI_PROVIDER" && "$WORKER_AI_PROVIDER" != "$AI_PROVIDER" ]]; then + warn "Worker is using ${WORKER_AI_PROVIDER} but you configured ${AI_PROVIDER} — restarting to apply" + needs_restart="true" + fi + + # Restart worker if needed: kill old process, start fresh + if [[ "$needs_restart" == "true" ]]; then + info "Stopping existing worker..." + # Try graceful shutdown via API first, fall back to SIGTERM + curl -s -X POST "http://127.0.0.1:37777/api/admin/shutdown" >/dev/null 2>&1 || true + sleep 2 + + # If still running, send SIGTERM to known PID + if check_port_37777; then + if [[ -n "$WORKER_REPORTED_PID" ]]; then + kill "$WORKER_REPORTED_PID" 2>/dev/null || true + sleep 1 + fi + # Check PID file as fallback + local pid_file="${HOME}/.claude-mem/worker.pid" + if [[ -f "$pid_file" ]]; then + local file_pid + file_pid="$(INSTALLER_PID_FILE="$pid_file" node -e " + try { process.stdout.write(String(JSON.parse(require('fs').readFileSync(process.env.INSTALLER_PID_FILE, 'utf8')).pid || '')); } + catch(e) {} + " 2>/dev/null)" || true + if [[ -n "$file_pid" ]]; then + kill "$file_pid" 2>/dev/null || true + sleep 1 + fi + fi + fi + + # Start fresh worker + if start_worker; then + verify_health || true + else + warn "Worker restart failed — you can start it manually later" + fi + else + # No restart needed — show healthy status + local uptime_display="" + if [[ -n "$WORKER_UPTIME" && "$WORKER_UPTIME" =~ ^[0-9]+$ && "$WORKER_UPTIME" != "0" ]]; then + uptime_display="$(format_uptime_ms "$WORKER_UPTIME")" + fi + + local status_parts="" + if [[ -n "$WORKER_VERSION" ]]; then + status_parts="v${WORKER_VERSION}" + fi + if [[ -n "$WORKER_AI_PROVIDER" ]]; then + status_parts="${status_parts:+${status_parts}, }${WORKER_AI_PROVIDER}" + fi + if [[ -n "$uptime_display" ]]; then + status_parts="${status_parts:+${status_parts}, }uptime: ${uptime_display}" + fi + + if [[ -n "$status_parts" ]]; then + success "Existing worker is healthy (${status_parts}) — skipping startup" + else + success "Existing worker is healthy — skipping startup" + fi + fi + else + warn "Port 37777 is occupied but not responding to health checks" + warn "Another process may be using this port. Stop it and re-run the installer," + warn "or change CLAUDE_MEM_WORKER_PORT in ~/.claude-mem/settings.json" + fi + else + if start_worker; then + verify_health || true + else + warn "Worker startup failed — you can start it manually later" + warn " cd ~/.openclaw/extensions/claude-mem && bun plugin/scripts/worker-service.cjs" + fi + fi + + # --- Step 8: Observation feed setup (optional) --- + echo "" + info "${COLOR_BOLD}[8/8]${COLOR_RESET} Observation feed setup..." + setup_observation_feed + write_observation_feed_config + + # --- Completion --- + print_completion_summary +} + +main "$@" diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json new file mode 100644 index 00000000..a304b670 --- /dev/null +++ b/openclaw/openclaw.plugin.json @@ -0,0 +1,53 @@ +{ + "id": "claude-mem", + "name": "Claude-Mem (Persistent Memory)", + "description": "Official OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.", + "kind": "memory", + "version": "1.0.0", + "author": "thedotmack", + "homepage": "https://claude-mem.com", + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "syncMemoryFile": { + "type": "boolean", + "default": true, + "description": "Automatically sync MEMORY.md on session start" + }, + "workerPort": { + "type": "number", + "default": 37777, + "description": "Port for Claude-Mem worker service" + }, + "project": { + "type": "string", + "default": "openclaw", + "description": "Project name for scoping observations in the memory database" + }, + "observationFeed": { + "type": "object", + "description": "Live observation feed — streams observations to any OpenClaw channel in real-time", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable live observation feed to messaging channels" + }, + "channel": { + "type": "string", + "description": "Channel type: telegram, discord, signal, slack, whatsapp, line" + }, + "to": { + "type": "string", + "description": "Target chat/user ID to send observations to" + }, + "botToken": { + "type": "string", + "description": "Optional dedicated Telegram bot token for the feed (bypasses gateway channel)" + } + } + } + } + } +} diff --git a/openclaw/package.json b/openclaw/package.json new file mode 100644 index 00000000..e74ec764 --- /dev/null +++ b/openclaw/package.json @@ -0,0 +1,20 @@ +{ + "name": "@openclaw/claude-mem", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "test": "tsc && node --test dist/index.test.js" + }, + "devDependencies": { + "@types/node": "^25.2.1", + "typescript": "^5.3.0" + }, + "openclaw": { + "extensions": [ + "./dist/index.js" + ] + } +} diff --git a/openclaw/src/index.test.ts b/openclaw/src/index.test.ts new file mode 100644 index 00000000..0c6bee6f --- /dev/null +++ b/openclaw/src/index.test.ts @@ -0,0 +1,962 @@ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http"; +import { mkdtemp, readFile, rm } from "fs/promises"; +import { join } from "path"; +import { tmpdir } from "os"; +import claudeMemPlugin from "./index.js"; + +function createMockApi(pluginConfigOverride: Record = {}) { + const logs: string[] = []; + const sentMessages: Array<{ to: string; text: string; channel: string; opts?: any }> = []; + + let registeredService: any = null; + const registeredCommands: Map = new Map(); + const eventHandlers: Map = new Map(); + + const api = { + id: "claude-mem", + name: "Claude-Mem (Persistent Memory)", + version: "1.0.0", + source: "/test/extensions/claude-mem/dist/index.js", + config: {}, + pluginConfig: pluginConfigOverride, + logger: { + info: (message: string) => { logs.push(message); }, + warn: (message: string) => { logs.push(message); }, + error: (message: string) => { logs.push(message); }, + debug: (message: string) => { logs.push(message); }, + }, + registerService: (service: any) => { + registeredService = service; + }, + registerCommand: (command: any) => { + registeredCommands.set(command.name, command); + }, + on: (event: string, callback: Function) => { + if (!eventHandlers.has(event)) { + eventHandlers.set(event, []); + } + eventHandlers.get(event)!.push(callback); + }, + runtime: { + channel: { + telegram: { + sendMessageTelegram: async (to: string, text: string) => { + sentMessages.push({ to, text, channel: "telegram" }); + }, + }, + discord: { + sendMessageDiscord: async (to: string, text: string) => { + sentMessages.push({ to, text, channel: "discord" }); + }, + }, + signal: { + sendMessageSignal: async (to: string, text: string) => { + sentMessages.push({ to, text, channel: "signal" }); + }, + }, + slack: { + sendMessageSlack: async (to: string, text: string) => { + sentMessages.push({ to, text, channel: "slack" }); + }, + }, + whatsapp: { + sendMessageWhatsApp: async (to: string, text: string, opts?: { verbose: boolean }) => { + sentMessages.push({ to, text, channel: "whatsapp", opts }); + }, + }, + line: { + sendMessageLine: async (to: string, text: string) => { + sentMessages.push({ to, text, channel: "line" }); + }, + }, + }, + }, + }; + + return { + api: api as any, + logs, + sentMessages, + getService: () => registeredService, + getCommand: (name?: string) => { + if (name) return registeredCommands.get(name); + return registeredCommands.get("claude-mem-feed"); + }, + getEventHandlers: (event: string) => eventHandlers.get(event) || [], + fireEvent: async (event: string, data: any, ctx: any = {}) => { + const handlers = eventHandlers.get(event) || []; + for (const handler of handlers) { + await handler(data, ctx); + } + }, + }; +} + +describe("claudeMemPlugin", () => { + it("registers service, commands, and event handlers on load", () => { + const { api, logs, getService, getCommand, getEventHandlers } = createMockApi(); + claudeMemPlugin(api); + + assert.ok(getService(), "service should be registered"); + assert.equal(getService().id, "claude-mem-observation-feed"); + assert.ok(getCommand("claude-mem-feed"), "feed command should be registered"); + assert.ok(getCommand("claude-mem-status"), "status command should be registered"); + assert.ok(getEventHandlers("session_start").length > 0, "session_start handler registered"); + assert.ok(getEventHandlers("after_compaction").length > 0, "after_compaction handler registered"); + assert.ok(getEventHandlers("before_agent_start").length > 0, "before_agent_start handler registered"); + assert.ok(getEventHandlers("tool_result_persist").length > 0, "tool_result_persist handler registered"); + assert.ok(getEventHandlers("agent_end").length > 0, "agent_end handler registered"); + assert.ok(getEventHandlers("gateway_start").length > 0, "gateway_start handler registered"); + assert.ok(logs.some((l) => l.includes("plugin loaded"))); + }); + + describe("service start", () => { + it("logs disabled when feed not enabled", async () => { + const { api, logs, getService } = createMockApi({}); + claudeMemPlugin(api); + + await getService().start({}); + assert.ok(logs.some((l) => l.includes("feed disabled"))); + }); + + it("logs disabled when enabled is false", async () => { + const { api, logs, getService } = createMockApi({ + observationFeed: { enabled: false }, + }); + claudeMemPlugin(api); + + await getService().start({}); + assert.ok(logs.some((l) => l.includes("feed disabled"))); + }); + + it("logs misconfigured when channel is missing", async () => { + const { api, logs, getService } = createMockApi({ + observationFeed: { enabled: true, to: "123" }, + }); + claudeMemPlugin(api); + + await getService().start({}); + assert.ok(logs.some((l) => l.includes("misconfigured"))); + }); + + it("logs misconfigured when to is missing", async () => { + const { api, logs, getService } = createMockApi({ + observationFeed: { enabled: true, channel: "telegram" }, + }); + claudeMemPlugin(api); + + await getService().start({}); + assert.ok(logs.some((l) => l.includes("misconfigured"))); + }); + }); + + describe("service stop", () => { + it("logs disconnection on stop", async () => { + const { api, logs, getService } = createMockApi({}); + claudeMemPlugin(api); + + await getService().stop({}); + assert.ok(logs.some((l) => l.includes("feed stopped"))); + }); + }); + + describe("command handler", () => { + it("returns not configured when no feedConfig", async () => { + const { api, getCommand } = createMockApi({}); + claudeMemPlugin(api); + + const result = await getCommand().handler({ args: "", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-feed", config: {} }); + assert.ok(result.includes("not configured")); + }); + + it("returns status when no args", async () => { + const { api, getCommand } = createMockApi({ + observationFeed: { enabled: true, channel: "telegram", to: "123" }, + }); + claudeMemPlugin(api); + + const result = await getCommand().handler({ args: "", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-feed", config: {} }); + assert.ok(result.includes("Enabled: yes")); + assert.ok(result.includes("Channel: telegram")); + assert.ok(result.includes("Target: 123")); + assert.ok(result.includes("Connection:")); + }); + + it("handles 'on' argument", async () => { + const { api, logs, getCommand } = createMockApi({ + observationFeed: { enabled: false }, + }); + claudeMemPlugin(api); + + const result = await getCommand().handler({ args: "on", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-feed on", config: {} }); + assert.ok(result.includes("enable requested")); + assert.ok(logs.some((l) => l.includes("enable requested"))); + }); + + it("handles 'off' argument", async () => { + const { api, logs, getCommand } = createMockApi({ + observationFeed: { enabled: true }, + }); + claudeMemPlugin(api); + + const result = await getCommand().handler({ args: "off", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-feed off", config: {} }); + assert.ok(result.includes("disable requested")); + assert.ok(logs.some((l) => l.includes("disable requested"))); + }); + + it("shows connection state in status output", async () => { + const { api, getCommand } = createMockApi({ + observationFeed: { enabled: false, channel: "slack", to: "#general" }, + }); + claudeMemPlugin(api); + + const result = await getCommand().handler({ args: "", channel: "slack", isAuthorizedSender: true, commandBody: "/claude-mem-feed", config: {} }); + assert.ok(result.includes("Connection: disconnected")); + }); + }); +}); + +describe("Observation I/O event handlers", () => { + let workerServer: Server; + let workerPort: number; + let receivedRequests: Array<{ method: string; url: string; body: any }> = []; + + function startWorkerMock(): Promise { + return new Promise((resolve) => { + workerServer = createServer((req: IncomingMessage, res: ServerResponse) => { + let body = ""; + req.on("data", (chunk) => { body += chunk.toString(); }); + req.on("end", () => { + let parsedBody: any = null; + try { parsedBody = JSON.parse(body); } catch {} + + receivedRequests.push({ + method: req.method || "GET", + url: req.url || "/", + body: parsedBody, + }); + + // Handle different endpoints + if (req.url === "/api/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok" })); + return; + } + + if (req.url === "/api/sessions/init") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ sessionDbId: 1, promptNumber: 1, skipped: false })); + return; + } + + if (req.url === "/api/sessions/observations") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "queued" })); + return; + } + + if (req.url === "/api/sessions/summarize") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "queued" })); + return; + } + + if (req.url === "/api/sessions/complete") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "completed" })); + return; + } + + if (req.url?.startsWith("/api/context/inject")) { + res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("# Claude-Mem Context\n\n## Timeline\n- Session 1: Did some work"); + return; + } + + if (req.url === "/stream") { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + return; + } + + res.writeHead(404); + res.end(); + }); + }); + workerServer.listen(0, () => { + const address = workerServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } + }); + }); + } + + beforeEach(async () => { + receivedRequests = []; + workerPort = await startWorkerMock(); + }); + + afterEach(() => { + workerServer?.close(); + }); + + it("session_start sends session init to worker", async () => { + const { api, logs, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("session_start", { + sessionId: "test-session-1", + }, { sessionKey: "agent-1" }); + + // Wait for HTTP request + await new Promise((resolve) => setTimeout(resolve, 100)); + + const initRequest = receivedRequests.find((r) => r.url === "/api/sessions/init"); + assert.ok(initRequest, "should send init request to worker"); + assert.equal(initRequest!.body.project, "openclaw"); + assert.ok(initRequest!.body.contentSessionId.startsWith("openclaw-agent-1-")); + assert.ok(logs.some((l) => l.includes("Session initialized"))); + }); + + it("session_start calls init on worker", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("session_start", { sessionId: "test-session-1" }, {}); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init"); + assert.equal(initRequests.length, 1, "should init on session_start"); + }); + + it("after_compaction re-inits session on worker", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("after_compaction", { messageCount: 5, compactedCount: 3 }, {}); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init"); + assert.equal(initRequests.length, 1, "should re-init after compaction"); + }); + + it("before_agent_start calls init for session privacy check", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("before_agent_start", { prompt: "hello" }, {}); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init"); + assert.equal(initRequests.length, 1, "before_agent_start should init session"); + }); + + it("tool_result_persist sends observation to worker", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + // Establish contentSessionId via session_start + await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "test-agent" }); + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Fire tool result event + await fireEvent("tool_result_persist", { + toolName: "Read", + params: { file_path: "/src/index.ts" }, + message: { + content: [{ type: "text", text: "file contents here..." }], + }, + }, { sessionKey: "test-agent" }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const obsRequest = receivedRequests.find((r) => r.url === "/api/sessions/observations"); + assert.ok(obsRequest, "should send observation to worker"); + assert.equal(obsRequest!.body.tool_name, "Read"); + assert.deepEqual(obsRequest!.body.tool_input, { file_path: "/src/index.ts" }); + assert.equal(obsRequest!.body.tool_response, "file contents here..."); + assert.ok(obsRequest!.body.contentSessionId.startsWith("openclaw-test-agent-")); + }); + + it("tool_result_persist skips memory_ tools", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("tool_result_persist", { + toolName: "memory_search", + params: {}, + }, {}); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const obsRequest = receivedRequests.find((r) => r.url === "/api/sessions/observations"); + assert.ok(!obsRequest, "should skip memory_ tools"); + }); + + it("tool_result_persist truncates long responses", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + const longText = "x".repeat(2000); + await fireEvent("tool_result_persist", { + toolName: "Bash", + params: { command: "ls" }, + message: { + content: [{ type: "text", text: longText }], + }, + }, {}); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const obsRequest = receivedRequests.find((r) => r.url === "/api/sessions/observations"); + assert.ok(obsRequest, "should send observation"); + assert.equal(obsRequest!.body.tool_response.length, 1000, "should truncate to 1000 chars"); + }); + + it("agent_end sends summarize and complete to worker", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + // Establish session + await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "summarize-test" }); + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Fire agent end + await fireEvent("agent_end", { + messages: [ + { role: "user", content: "help me" }, + { role: "assistant", content: "Here is the solution..." }, + ], + }, { sessionKey: "summarize-test" }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const summarizeRequest = receivedRequests.find((r) => r.url === "/api/sessions/summarize"); + assert.ok(summarizeRequest, "should send summarize to worker"); + assert.equal(summarizeRequest!.body.last_assistant_message, "Here is the solution..."); + assert.ok(summarizeRequest!.body.contentSessionId.startsWith("openclaw-summarize-test-")); + + const completeRequest = receivedRequests.find((r) => r.url === "/api/sessions/complete"); + assert.ok(completeRequest, "should send complete to worker"); + assert.ok(completeRequest!.body.contentSessionId.startsWith("openclaw-summarize-test-")); + }); + + it("agent_end extracts text from array content", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "array-content" }); + await new Promise((resolve) => setTimeout(resolve, 100)); + + await fireEvent("agent_end", { + messages: [ + { + role: "assistant", + content: [ + { type: "text", text: "First part" }, + { type: "text", text: "Second part" }, + ], + }, + ], + }, { sessionKey: "array-content" }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const summarizeRequest = receivedRequests.find((r) => r.url === "/api/sessions/summarize"); + assert.ok(summarizeRequest, "should send summarize"); + assert.equal(summarizeRequest!.body.last_assistant_message, "First part\nSecond part"); + }); + + it("uses custom project name from config", async () => { + const { api, fireEvent } = createMockApi({ workerPort, project: "my-project" }); + claudeMemPlugin(api); + + await fireEvent("session_start", { sessionId: "s1" }, {}); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const initRequest = receivedRequests.find((r) => r.url === "/api/sessions/init"); + assert.ok(initRequest, "should send init"); + assert.equal(initRequest!.body.project, "my-project"); + }); + + it("claude-mem-status command reports worker health", async () => { + const { api, getCommand } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + const statusCmd = getCommand("claude-mem-status"); + assert.ok(statusCmd, "status command should exist"); + + const result = await statusCmd.handler({ args: "", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-status", config: {} }); + assert.ok(result.includes("Status: ok")); + assert.ok(result.includes(`Port: ${workerPort}`)); + }); + + it("claude-mem-status reports unreachable when worker is down", async () => { + workerServer.close(); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const { api, getCommand } = createMockApi({ workerPort: 59999 }); + claudeMemPlugin(api); + + const statusCmd = getCommand("claude-mem-status"); + const result = await statusCmd.handler({ args: "", channel: "telegram", isAuthorizedSender: true, commandBody: "/claude-mem-status", config: {} }); + assert.ok(result.includes("unreachable")); + }); + + it("reuses same contentSessionId for same sessionKey", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "reuse-test" }); + await new Promise((resolve) => setTimeout(resolve, 100)); + + await fireEvent("tool_result_persist", { + toolName: "Read", + params: { file_path: "/src/index.ts" }, + message: { content: [{ type: "text", text: "contents" }] }, + }, { sessionKey: "reuse-test" }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const initRequest = receivedRequests.find((r) => r.url === "/api/sessions/init"); + const obsRequest = receivedRequests.find((r) => r.url === "/api/sessions/observations"); + assert.ok(initRequest && obsRequest, "both requests should exist"); + assert.equal( + initRequest!.body.contentSessionId, + obsRequest!.body.contentSessionId, + "should reuse contentSessionId for same sessionKey" + ); + }); +}); + +describe("MEMORY.md context sync", () => { + let workerServer: Server; + let workerPort: number; + let receivedRequests: Array<{ method: string; url: string; body: any }> = []; + let tmpDir: string; + let contextResponse = "# Claude-Mem Context\n\n## Timeline\n- Session 1: Did some work"; + + function startWorkerMock(): Promise { + return new Promise((resolve) => { + workerServer = createServer((req: IncomingMessage, res: ServerResponse) => { + let body = ""; + req.on("data", (chunk) => { body += chunk.toString(); }); + req.on("end", () => { + let parsedBody: any = null; + try { parsedBody = JSON.parse(body); } catch {} + + receivedRequests.push({ + method: req.method || "GET", + url: req.url || "/", + body: parsedBody, + }); + + if (req.url?.startsWith("/api/context/inject")) { + res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); + res.end(contextResponse); + return; + } + + if (req.url === "/api/sessions/init") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ sessionDbId: 1, promptNumber: 1, skipped: false })); + return; + } + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok" })); + }); + }); + workerServer.listen(0, () => { + const address = workerServer.address(); + if (address && typeof address === "object") { + resolve(address.port); + } + }); + }); + } + + beforeEach(async () => { + receivedRequests = []; + contextResponse = "# Claude-Mem Context\n\n## Timeline\n- Session 1: Did some work"; + workerPort = await startWorkerMock(); + tmpDir = await mkdtemp(join(tmpdir(), "claude-mem-test-")); + }); + + afterEach(async () => { + workerServer?.close(); + await rm(tmpDir, { recursive: true, force: true }); + }); + + it("writes MEMORY.md to workspace on before_agent_start", async () => { + const { api, logs, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("before_agent_start", { + prompt: "Help me write a function", + }, { sessionKey: "sync-test", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const contextRequest = receivedRequests.find((r) => r.url?.startsWith("/api/context/inject")); + assert.ok(contextRequest, "should request context from worker"); + assert.ok(contextRequest!.url!.includes("projects=openclaw")); + + const memoryContent = await readFile(join(tmpDir, "MEMORY.md"), "utf-8"); + assert.ok(memoryContent.includes("Claude-Mem Context"), "MEMORY.md should contain context"); + assert.ok(memoryContent.includes("Session 1"), "MEMORY.md should contain timeline"); + assert.ok(logs.some((l) => l.includes("MEMORY.md synced"))); + }); + + it("syncs MEMORY.md on every before_agent_start call", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("before_agent_start", { + prompt: "First prompt for this agent", + }, { sessionKey: "agent-a", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const firstContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject")); + assert.equal(firstContextRequests.length, 1, "first call should fetch context"); + + await fireEvent("before_agent_start", { + prompt: "Second prompt for same agent", + }, { sessionKey: "agent-a", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const allContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject")); + assert.equal(allContextRequests.length, 2, "should re-fetch context on every call"); + }); + + it("syncs MEMORY.md on tool_result_persist via fire-and-forget", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + // Init session to register workspace dir + await fireEvent("before_agent_start", { + prompt: "Help me write a function", + }, { sessionKey: "tool-sync", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const preToolContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject")); + assert.equal(preToolContextRequests.length, 1, "before_agent_start should sync once"); + + // Fire tool result — should trigger another MEMORY.md sync + await fireEvent("tool_result_persist", { + toolName: "Read", + params: { file_path: "/src/app.ts" }, + message: { content: [{ type: "text", text: "file contents" }] }, + }, { sessionKey: "tool-sync" }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const postToolContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject")); + assert.equal(postToolContextRequests.length, 2, "tool_result_persist should trigger another sync"); + + const memoryContent = await readFile(join(tmpDir, "MEMORY.md"), "utf-8"); + assert.ok(memoryContent.includes("Claude-Mem Context"), "MEMORY.md should be updated"); + }); + + it("skips MEMORY.md sync when syncMemoryFile is false", async () => { + const { api, fireEvent } = createMockApi({ workerPort, syncMemoryFile: false }); + claudeMemPlugin(api); + + await fireEvent("before_agent_start", { + prompt: "Help me write a function", + }, { sessionKey: "no-sync", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const contextRequest = receivedRequests.find((r) => r.url?.startsWith("/api/context/inject")); + assert.ok(!contextRequest, "should not fetch context when sync disabled"); + }); + + it("skips MEMORY.md sync when no workspaceDir in context", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("before_agent_start", { + prompt: "Help me write a function", + }, { sessionKey: "no-workspace" }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const contextRequest = receivedRequests.find((r) => r.url?.startsWith("/api/context/inject")); + assert.ok(!contextRequest, "should not fetch context without workspaceDir"); + }); + + it("skips writing MEMORY.md when context is empty", async () => { + contextResponse = " "; + const { api, logs, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("before_agent_start", { + prompt: "Help me write a function", + }, { sessionKey: "empty-ctx", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + assert.ok(!logs.some((l) => l.includes("MEMORY.md synced")), "should not log sync for empty context"); + }); + + it("gateway_start resets sync tracking so next agent re-syncs", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + // First sync + await fireEvent("before_agent_start", { + prompt: "Help me write a function", + }, { sessionKey: "agent-1", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const firstContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject")); + assert.equal(firstContextRequests.length, 1); + + // Gateway restart + await fireEvent("gateway_start", {}, {}); + + // Second sync after gateway restart — same workspace should re-sync + await fireEvent("before_agent_start", { + prompt: "Help me after gateway restart", + }, { sessionKey: "agent-1", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const allContextRequests = receivedRequests.filter((r) => r.url?.startsWith("/api/context/inject")); + assert.equal(allContextRequests.length, 2, "should re-fetch context after gateway restart"); + }); + + it("uses custom project name in context inject URL", async () => { + const { api, fireEvent } = createMockApi({ workerPort, project: "my-bot" }); + claudeMemPlugin(api); + + await fireEvent("before_agent_start", { + prompt: "Help me write a function", + }, { sessionKey: "proj-test", workspaceDir: tmpDir }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const contextRequest = receivedRequests.find((r) => r.url?.startsWith("/api/context/inject")); + assert.ok(contextRequest, "should request context"); + assert.ok(contextRequest!.url!.includes("projects=my-bot"), "should use custom project name"); + }); +}); + +describe("SSE stream integration", () => { + let server: Server; + let serverPort: number; + let serverResponses: ServerResponse[] = []; + + function startSSEServer(): Promise { + return new Promise((resolve) => { + server = createServer((req: IncomingMessage, res: ServerResponse) => { + if (req.url !== "/stream") { + res.writeHead(404); + res.end(); + return; + } + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + serverResponses.push(res); + }); + server.listen(0, () => { + const address = server.address(); + if (address && typeof address === "object") { + resolve(address.port); + } + }); + }); + } + + beforeEach(async () => { + serverResponses = []; + serverPort = await startSSEServer(); + }); + + afterEach(() => { + for (const res of serverResponses) { + try { + res.end(); + } catch {} + } + server?.close(); + }); + + it("connects to SSE stream and receives new_observation events", async () => { + const { api, logs, sentMessages, getService } = createMockApi({ + workerPort: serverPort, + observationFeed: { enabled: true, channel: "telegram", to: "12345" }, + }); + claudeMemPlugin(api); + + await getService().start({}); + + // Wait for connection + await new Promise((resolve) => setTimeout(resolve, 200)); + + assert.ok(logs.some((l) => l.includes("Connecting to SSE stream"))); + + // Send an SSE event + const observation = { + type: "new_observation", + observation: { + id: 1, + title: "Test Observation", + subtitle: "Found something interesting", + type: "discovery", + project: "test", + prompt_number: 1, + created_at_epoch: Date.now(), + }, + timestamp: Date.now(), + }; + + for (const res of serverResponses) { + res.write(`data: ${JSON.stringify(observation)}\n\n`); + } + + // Wait for processing + await new Promise((resolve) => setTimeout(resolve, 200)); + + assert.equal(sentMessages.length, 1); + assert.equal(sentMessages[0].channel, "telegram"); + assert.equal(sentMessages[0].to, "12345"); + assert.ok(sentMessages[0].text.includes("Test Observation")); + assert.ok(sentMessages[0].text.includes("Found something interesting")); + + await getService().stop({}); + }); + + it("filters out non-observation events", async () => { + const { api, sentMessages, getService } = createMockApi({ + workerPort: serverPort, + observationFeed: { enabled: true, channel: "discord", to: "channel-id" }, + }); + claudeMemPlugin(api); + + await getService().start({}); + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Send non-observation events + for (const res of serverResponses) { + res.write(`data: ${JSON.stringify({ type: "processing_status", isProcessing: true })}\n\n`); + res.write(`data: ${JSON.stringify({ type: "session_started", sessionId: "abc" })}\n\n`); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + assert.equal(sentMessages.length, 0, "non-observation events should be filtered"); + + await getService().stop({}); + }); + + it("handles observation with null subtitle", async () => { + const { api, sentMessages, getService } = createMockApi({ + workerPort: serverPort, + observationFeed: { enabled: true, channel: "telegram", to: "999" }, + }); + claudeMemPlugin(api); + + await getService().start({}); + await new Promise((resolve) => setTimeout(resolve, 200)); + + for (const res of serverResponses) { + res.write( + `data: ${JSON.stringify({ + type: "new_observation", + observation: { id: 2, title: "No Subtitle", subtitle: null }, + timestamp: Date.now(), + })}\n\n` + ); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + assert.equal(sentMessages.length, 1); + assert.ok(sentMessages[0].text.includes("No Subtitle")); + assert.ok(!sentMessages[0].text.includes("null")); + + await getService().stop({}); + }); + + it("handles observation with null title", async () => { + const { api, sentMessages, getService } = createMockApi({ + workerPort: serverPort, + observationFeed: { enabled: true, channel: "telegram", to: "999" }, + }); + claudeMemPlugin(api); + + await getService().start({}); + await new Promise((resolve) => setTimeout(resolve, 200)); + + for (const res of serverResponses) { + res.write( + `data: ${JSON.stringify({ + type: "new_observation", + observation: { id: 3, title: null, subtitle: "Has subtitle" }, + timestamp: Date.now(), + })}\n\n` + ); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + assert.equal(sentMessages.length, 1); + assert.ok(sentMessages[0].text.includes("Untitled")); + + await getService().stop({}); + }); + + it("uses custom workerPort from config", async () => { + const { api, logs, getService } = createMockApi({ + workerPort: serverPort, + observationFeed: { enabled: true, channel: "telegram", to: "12345" }, + }); + claudeMemPlugin(api); + + await getService().start({}); + await new Promise((resolve) => setTimeout(resolve, 200)); + + assert.ok(logs.some((l) => l.includes(`127.0.0.1:${serverPort}`))); + + await getService().stop({}); + }); + + it("logs unknown channel type", async () => { + const { api, logs, sentMessages, getService } = createMockApi({ + workerPort: serverPort, + observationFeed: { enabled: true, channel: "matrix", to: "room-id" }, + }); + claudeMemPlugin(api); + + await getService().start({}); + await new Promise((resolve) => setTimeout(resolve, 200)); + + for (const res of serverResponses) { + res.write( + `data: ${JSON.stringify({ + type: "new_observation", + observation: { id: 4, title: "Test", subtitle: null }, + timestamp: Date.now(), + })}\n\n` + ); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + assert.equal(sentMessages.length, 0); + assert.ok(logs.some((l) => l.includes("Unsupported channel type: matrix"))); + + await getService().stop({}); + }); +}); diff --git a/openclaw/src/index.ts b/openclaw/src/index.ts new file mode 100644 index 00000000..809f229a --- /dev/null +++ b/openclaw/src/index.ts @@ -0,0 +1,804 @@ +import { writeFile } from "fs/promises"; +import { join } from "path"; + +// Minimal type declarations for the OpenClaw Plugin SDK. +// These match the real OpenClawPluginApi provided by the gateway at runtime. +// See: https://docs.openclaw.ai/plugin + +interface PluginLogger { + debug?: (message: string) => void; + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; +} + +interface PluginServiceContext { + config: Record; + workspaceDir?: string; + stateDir: string; + logger: PluginLogger; +} + +interface PluginCommandContext { + senderId?: string; + channel: string; + isAuthorizedSender: boolean; + args?: string; + commandBody: string; + config: Record; +} + +type PluginCommandResult = string | { text: string } | { text: string; format?: string }; + +// OpenClaw event types for agent lifecycle +interface BeforeAgentStartEvent { + prompt?: string; +} + +interface ToolResultPersistEvent { + toolName?: string; + params?: Record; + message?: { + content?: Array<{ type: string; text?: string }>; + }; +} + +interface AgentEndEvent { + messages?: Array<{ + role: string; + content: string | Array<{ type: string; text?: string }>; + }>; +} + +interface SessionStartEvent { + sessionId: string; + resumedFrom?: string; +} + +interface AfterCompactionEvent { + messageCount: number; + tokenCount?: number; + compactedCount: number; +} + +interface SessionEndEvent { + sessionId: string; + messageCount: number; + durationMs?: number; +} + +interface MessageReceivedEvent { + from: string; + content: string; + timestamp?: number; + metadata?: Record; +} + +interface EventContext { + sessionKey?: string; + workspaceDir?: string; + agentId?: string; +} + +interface MessageContext { + channelId: string; + accountId?: string; + conversationId?: string; +} + +type EventCallback = (event: T, ctx: EventContext) => void | Promise; +type MessageEventCallback = (event: T, ctx: MessageContext) => void | Promise; + +interface OpenClawPluginApi { + id: string; + name: string; + version?: string; + source: string; + config: Record; + pluginConfig?: Record; + logger: PluginLogger; + registerService: (service: { + id: string; + start: (ctx: PluginServiceContext) => void | Promise; + stop?: (ctx: PluginServiceContext) => void | Promise; + }) => void; + registerCommand: (command: { + name: string; + description: string; + acceptsArgs?: boolean; + requireAuth?: boolean; + handler: (ctx: PluginCommandContext) => PluginCommandResult | Promise; + }) => void; + on: ((event: "before_agent_start", callback: EventCallback) => void) & + ((event: "tool_result_persist", callback: EventCallback) => void) & + ((event: "agent_end", callback: EventCallback) => void) & + ((event: "session_start", callback: EventCallback) => void) & + ((event: "session_end", callback: EventCallback) => void) & + ((event: "message_received", callback: MessageEventCallback) => void) & + ((event: "after_compaction", callback: EventCallback) => void) & + ((event: "gateway_start", callback: EventCallback>) => void); + runtime: { + channel: Record Promise>>; + }; +} + +// ============================================================================ +// SSE Observation Feed Types +// ============================================================================ + +interface ObservationSSEPayload { + id: number; + memory_session_id: string; + session_id: string; + type: string; + title: string | null; + subtitle: string | null; + text: string | null; + narrative: string | null; + facts: string | null; + concepts: string | null; + files_read: string | null; + files_modified: string | null; + project: string | null; + prompt_number: number; + created_at_epoch: number; +} + +interface SSENewObservationEvent { + type: "new_observation"; + observation: ObservationSSEPayload; + timestamp: number; +} + +type ConnectionState = "disconnected" | "connected" | "reconnecting"; + +// ============================================================================ +// Plugin Configuration +// ============================================================================ + +interface ClaudeMemPluginConfig { + syncMemoryFile?: boolean; + project?: string; + workerPort?: number; + observationFeed?: { + enabled?: boolean; + channel?: string; + to?: string; + botToken?: string; + }; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB +const DEFAULT_WORKER_PORT = 37777; + +// Agent emoji map for observation feed messages. +// When creating a new OpenClaw agent, add its agentId and emoji here. +const AGENT_EMOJI_MAP: Record = { + "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-" + 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 +// ============================================================================ + +function workerBaseUrl(port: number): string { + return `http://127.0.0.1:${port}`; +} + +async function workerPost( + port: number, + path: string, + body: Record, + logger: PluginLogger +): Promise | null> { + try { + const response = await fetch(`${workerBaseUrl(port)}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!response.ok) { + logger.warn(`[claude-mem] Worker POST ${path} returned ${response.status}`); + return null; + } + return (await response.json()) as Record; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`); + return null; + } +} + +function workerPostFireAndForget( + port: number, + path: string, + body: Record, + logger: PluginLogger +): void { + fetch(`${workerBaseUrl(port)}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`); + }); +} + +async function workerGetText( + port: number, + path: string, + logger: PluginLogger +): Promise { + try { + const response = await fetch(`${workerBaseUrl(port)}${path}`); + if (!response.ok) { + logger.warn(`[claude-mem] Worker GET ${path} returned ${response.status}`); + return null; + } + return await response.text(); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + logger.warn(`[claude-mem] Worker GET ${path} failed: ${message}`); + return null; + } +} + +// ============================================================================ +// SSE Observation Feed +// ============================================================================ + +function formatObservationMessage(observation: ObservationSSEPayload): string { + const title = observation.title || "Untitled"; + const source = getSourceLabel(observation.project); + let message = `${source}\n**${title}**`; + if (observation.subtitle) { + message += `\n${observation.subtitle}`; + } + return message; +} + +// Explicit mapping from channel name to [runtime namespace key, send function name]. +// These match the PluginRuntime.channel structure in the OpenClaw SDK. +const CHANNEL_SEND_MAP: Record = { + telegram: { namespace: "telegram", functionName: "sendMessageTelegram" }, + whatsapp: { namespace: "whatsapp", functionName: "sendMessageWhatsApp" }, + discord: { namespace: "discord", functionName: "sendMessageDiscord" }, + slack: { namespace: "slack", functionName: "sendMessageSlack" }, + signal: { namespace: "signal", functionName: "sendMessageSignal" }, + imessage: { namespace: "imessage", functionName: "sendMessageIMessage" }, + line: { namespace: "line", functionName: "sendMessageLine" }, +}; + +async function sendDirectTelegram( + botToken: string, + chatId: string, + text: string, + logger: PluginLogger +): Promise { + 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( + api: OpenClawPluginApi, + channel: string, + to: string, + text: string, + botToken?: string +): Promise { + // 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]; + if (!mapping) { + api.logger.warn(`[claude-mem] Unsupported channel type: ${channel}`); + return Promise.resolve(); + } + + const channelApi = api.runtime.channel[mapping.namespace]; + if (!channelApi) { + api.logger.warn(`[claude-mem] Channel "${channel}" not available in runtime`); + return Promise.resolve(); + } + + const senderFunction = channelApi[mapping.functionName]; + if (!senderFunction) { + api.logger.warn(`[claude-mem] Channel "${channel}" has no ${mapping.functionName} function`); + return Promise.resolve(); + } + + // WhatsApp requires a third options argument with { verbose: boolean } + const args: unknown[] = channel === "whatsapp" + ? [to, text, { verbose: false }] + : [to, text]; + + return senderFunction(...args).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + api.logger.error(`[claude-mem] Failed to send to ${channel}: ${message}`); + }); +} + +async function connectToSSEStream( + api: OpenClawPluginApi, + port: number, + channel: string, + to: string, + abortController: AbortController, + setConnectionState: (state: ConnectionState) => void, + botToken?: string +): Promise { + let backoffMs = 1000; + const maxBackoffMs = 30000; + + while (!abortController.signal.aborted) { + try { + setConnectionState("reconnecting"); + api.logger.info(`[claude-mem] Connecting to SSE stream at ${workerBaseUrl(port)}/stream`); + + const response = await fetch(`${workerBaseUrl(port)}/stream`, { + signal: abortController.signal, + headers: { Accept: "text/event-stream" }, + }); + + if (!response.ok) { + throw new Error(`SSE stream returned HTTP ${response.status}`); + } + + if (!response.body) { + throw new Error("SSE stream response has no body"); + } + + setConnectionState("connected"); + backoffMs = 1000; + api.logger.info("[claude-mem] Connected to SSE stream"); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + if (buffer.length > MAX_SSE_BUFFER_SIZE) { + api.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"); + buffer = ""; + } + + const frames = buffer.split("\n\n"); + buffer = frames.pop() || ""; + + for (const frame of frames) { + // SSE spec: concatenate all data: lines with \n + const dataLines = frame + .split("\n") + .filter((line) => line.startsWith("data:")) + .map((line) => line.slice(5).trim()); + if (dataLines.length === 0) continue; + + const jsonStr = dataLines.join("\n"); + if (!jsonStr) continue; + + try { + const parsed = JSON.parse(jsonStr); + if (parsed.type === "new_observation" && parsed.observation) { + const event = parsed as SSENewObservationEvent; + const message = formatObservationMessage(event.observation); + await sendToChannel(api, channel, to, message, botToken); + } + } catch (parseError: unknown) { + const errorMessage = parseError instanceof Error ? parseError.message : String(parseError); + api.logger.warn(`[claude-mem] Failed to parse SSE frame: ${errorMessage}`); + } + } + } + } catch (error: unknown) { + if (abortController.signal.aborted) { + break; + } + setConnectionState("reconnecting"); + const errorMessage = error instanceof Error ? error.message : String(error); + api.logger.warn(`[claude-mem] SSE stream error: ${errorMessage}. Reconnecting in ${backoffMs / 1000}s`); + } + + if (abortController.signal.aborted) break; + + await new Promise((resolve) => setTimeout(resolve, backoffMs)); + backoffMs = Math.min(backoffMs * 2, maxBackoffMs); + } + + setConnectionState("disconnected"); +} + +// ============================================================================ +// Plugin Entry Point +// ============================================================================ + +export default function claudeMemPlugin(api: OpenClawPluginApi): void { + const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig; + const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT; + 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 + // ------------------------------------------------------------------ + const sessionIds = new Map(); + const workspaceDirsBySessionKey = new Map(); + const syncMemoryFile = userConfig.syncMemoryFile !== false; // default true + + function getContentSessionId(sessionKey?: string): string { + const key = sessionKey || "default"; + if (!sessionIds.has(key)) { + sessionIds.set(key, `openclaw-${key}-${Date.now()}`); + } + return sessionIds.get(key)!; + } + + async function syncMemoryToWorkspace(workspaceDir: string, ctx?: EventContext): Promise { + // 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( + workerPort, + `/api/context/inject?projects=${encodeURIComponent(projects.join(","))}`, + api.logger + ); + if (contextText && contextText.trim().length > 0) { + try { + await writeFile(join(workspaceDir, "MEMORY.md"), contextText, "utf-8"); + api.logger.info(`[claude-mem] MEMORY.md synced to ${workspaceDir}`); + } catch (writeError: unknown) { + const msg = writeError instanceof Error ? writeError.message : String(writeError); + api.logger.warn(`[claude-mem] Failed to write MEMORY.md: ${msg}`); + } + } + } + + // ------------------------------------------------------------------ + // Event: session_start — init claude-mem session (fires on /new, /reset) + // ------------------------------------------------------------------ + api.on("session_start", async (_event, ctx) => { + const contentSessionId = getContentSessionId(ctx.sessionKey); + + await workerPost(workerPort, "/api/sessions/init", { + contentSessionId, + project: getProjectName(ctx), + prompt: "", + }, api.logger); + + 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 + // ------------------------------------------------------------------ + api.on("after_compaction", async (_event, ctx) => { + const contentSessionId = getContentSessionId(ctx.sessionKey); + + await workerPost(workerPort, "/api/sessions/init", { + contentSessionId, + project: getProjectName(ctx), + prompt: "", + }, api.logger); + + api.logger.info(`[claude-mem] Session re-initialized after compaction: ${contentSessionId}`); + }); + + // ------------------------------------------------------------------ + // Event: before_agent_start — init session + sync MEMORY.md + track workspace + // ------------------------------------------------------------------ + api.on("before_agent_start", async (event, ctx) => { + // Track workspace dir so tool_result_persist can sync MEMORY.md later + if (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: event.prompt || "agent run", + }, api.logger); + + // Sync MEMORY.md before agent runs (provides context to agent) + if (syncMemoryFile && ctx.workspaceDir) { + await syncMemoryToWorkspace(ctx.workspaceDir, ctx); + } + }); + + // ------------------------------------------------------------------ + // Event: tool_result_persist — record tool observations + sync MEMORY.md + // ------------------------------------------------------------------ + 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; + if (!toolName) return; + + const contentSessionId = getContentSessionId(ctx.sessionKey); + + // Extract result text from all content blocks + let toolResponseText = ""; + const content = event.message?.content; + if (Array.isArray(content)) { + toolResponseText = content + .filter((block) => (block.type === "tool_result" || block.type === "text") && "text" in block) + .map((block) => String(block.text)) + .join("\n"); + } + + // Fire-and-forget: send observation + sync MEMORY.md in parallel + workerPostFireAndForget(workerPort, "/api/sessions/observations", { + contentSessionId, + tool_name: toolName, + tool_input: event.params || {}, + tool_response: toolResponseText, + cwd: "", + }, api.logger); + + const workspaceDir = ctx.workspaceDir || workspaceDirsBySessionKey.get(ctx.sessionKey || "default"); + if (syncMemoryFile && workspaceDir) { + syncMemoryToWorkspace(workspaceDir, ctx); + } + }); + + // ------------------------------------------------------------------ + // Event: agent_end — summarize and complete session + // ------------------------------------------------------------------ + api.on("agent_end", async (event, ctx) => { + const contentSessionId = getContentSessionId(ctx.sessionKey); + + // Extract last assistant message for summarization + let lastAssistantMessage = ""; + if (Array.isArray(event.messages)) { + for (let i = event.messages.length - 1; i >= 0; i--) { + const message = event.messages[i]; + if (message?.role === "assistant") { + if (typeof message.content === "string") { + lastAssistantMessage = message.content; + } else if (Array.isArray(message.content)) { + lastAssistantMessage = message.content + .filter((block) => block.type === "text") + .map((block) => block.text || "") + .join("\n"); + } + break; + } + } + } + + // 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, + last_assistant_message: lastAssistantMessage, + }, api.logger); + + workerPostFireAndForget(workerPort, "/api/sessions/complete", { + contentSessionId, + }, api.logger); + }); + + // ------------------------------------------------------------------ + // Event: session_end — clean up session tracking to prevent unbounded growth + // ------------------------------------------------------------------ + api.on("session_end", async (_event, ctx) => { + const key = ctx.sessionKey || "default"; + sessionIds.delete(key); + workspaceDirsBySessionKey.delete(key); + }); + + // ------------------------------------------------------------------ + // Event: gateway_start — clear session tracking for fresh start + // ------------------------------------------------------------------ + api.on("gateway_start", async () => { + workspaceDirsBySessionKey.clear(); + sessionIds.clear(); + api.logger.info("[claude-mem] Gateway started — session tracking reset"); + }); + + // ------------------------------------------------------------------ + // Service: SSE observation feed → messaging channels + // ------------------------------------------------------------------ + let sseAbortController: AbortController | null = null; + let connectionState: ConnectionState = "disconnected"; + let connectionPromise: Promise | null = null; + + api.registerService({ + id: "claude-mem-observation-feed", + start: async (_ctx) => { + if (sseAbortController) { + sseAbortController.abort(); + if (connectionPromise) { + await connectionPromise; + connectionPromise = null; + } + } + + const feedConfig = userConfig.observationFeed; + + if (!feedConfig?.enabled) { + api.logger.info("[claude-mem] Observation feed disabled"); + return; + } + + if (!feedConfig.channel || !feedConfig.to) { + api.logger.warn("[claude-mem] Observation feed misconfigured — channel or target missing"); + return; + } + + api.logger.info(`[claude-mem] Observation feed starting — channel: ${feedConfig.channel}, target: ${feedConfig.to}`); + + sseAbortController = new AbortController(); + connectionPromise = connectToSSEStream( + api, + workerPort, + feedConfig.channel, + feedConfig.to, + sseAbortController, + (state) => { connectionState = state; }, + feedConfig.botToken + ); + }, + stop: async (_ctx) => { + if (sseAbortController) { + sseAbortController.abort(); + sseAbortController = null; + } + if (connectionPromise) { + await connectionPromise; + connectionPromise = null; + } + connectionState = "disconnected"; + api.logger.info("[claude-mem] Observation feed stopped — SSE connection closed"); + }, + }); + + // ------------------------------------------------------------------ + // Command: /claude-mem-feed — status & toggle + // ------------------------------------------------------------------ + api.registerCommand({ + name: "claude-mem-feed", + description: "Show or toggle Claude-Mem observation feed status", + acceptsArgs: true, + handler: async (ctx) => { + const feedConfig = userConfig.observationFeed; + + if (!feedConfig) { + return "Observation feed not configured. Add observationFeed to your plugin config."; + } + + const arg = ctx.args?.trim(); + + if (arg === "on") { + api.logger.info("[claude-mem] Feed enable requested via command"); + return "Feed enable requested. Update observationFeed.enabled in your plugin config to persist."; + } + + if (arg === "off") { + api.logger.info("[claude-mem] Feed disable requested via command"); + return "Feed disable requested. Update observationFeed.enabled in your plugin config to persist."; + } + + return [ + "Claude-Mem Observation Feed", + `Enabled: ${feedConfig.enabled ? "yes" : "no"}`, + `Channel: ${feedConfig.channel || "not set"}`, + `Target: ${feedConfig.to || "not set"}`, + `Connection: ${connectionState}`, + ].join("\n"); + }, + }); + + // ------------------------------------------------------------------ + // Command: /claude-mem-status — worker health check + // ------------------------------------------------------------------ + api.registerCommand({ + name: "claude-mem-status", + description: "Check Claude-Mem worker health and session status", + handler: async () => { + const healthText = await workerGetText(workerPort, "/api/health", api.logger); + if (!healthText) { + return `Claude-Mem worker unreachable at port ${workerPort}`; + } + + try { + const health = JSON.parse(healthText); + return [ + "Claude-Mem Worker Status", + `Status: ${health.status || "unknown"}`, + `Port: ${workerPort}`, + `Active sessions: ${sessionIds.size}`, + `Observation feed: ${connectionState}`, + ].join("\n"); + } catch { + return `Claude-Mem worker responded but returned unexpected data`; + } + }, + }); + + api.logger.info(`[claude-mem] OpenClaw plugin loaded — v1.0.0 (worker: 127.0.0.1:${workerPort})`); +} diff --git a/openclaw/test-e2e.sh b/openclaw/test-e2e.sh new file mode 100755 index 00000000..8af7291d --- /dev/null +++ b/openclaw/test-e2e.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# test-e2e.sh — Run E2E test of claude-mem plugin on real OpenClaw +# +# Usage: +# ./test-e2e.sh # Automated E2E test (build + run + verify) +# ./test-e2e.sh --interactive # Drop into shell for manual testing +# ./test-e2e.sh --build-only # Just build the image, don't run +set -euo pipefail + +cd "$(dirname "$0")" + +IMAGE_NAME="openclaw-claude-mem-e2e" + +echo "=== Building E2E test image ===" +echo " Base: ghcr.io/openclaw/openclaw:main" +echo " Plugin: @claude-mem/openclaw-plugin (PR #1012)" +echo "" + +docker build -f Dockerfile.e2e -t "$IMAGE_NAME" . + +if [ "${1:-}" = "--build-only" ]; then + echo "" + echo "Image built: $IMAGE_NAME" + echo "Run manually with: docker run --rm $IMAGE_NAME" + exit 0 +fi + +echo "" +echo "=== Running E2E verification ===" +echo "" + +if [ "${1:-}" = "--interactive" ]; then + echo "Dropping into interactive shell." + echo "" + echo "Useful commands inside the container:" + echo " node openclaw.mjs plugins list # Verify plugin is installed" + echo " node openclaw.mjs plugins info claude-mem # Plugin details" + echo " node openclaw.mjs plugins doctor # Check for issues" + echo " node /app/mock-worker.js & # Start mock worker" + echo " node openclaw.mjs gateway --allow-unconfigured --verbose # Start gateway" + echo " /bin/bash /app/e2e-verify.sh # Run automated verification" + echo "" + docker run --rm -it "$IMAGE_NAME" /bin/bash +else + docker run --rm "$IMAGE_NAME" +fi diff --git a/openclaw/test-install.sh b/openclaw/test-install.sh new file mode 100755 index 00000000..be2c2a64 --- /dev/null +++ b/openclaw/test-install.sh @@ -0,0 +1,2339 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test suite for openclaw/install.sh functions +# Tests the OpenClaw gateway detection, plugin install, and memory slot config. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_SCRIPT="${SCRIPT_DIR}/install.sh" + +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +############################################################################### +# Test helpers +############################################################################### + +test_pass() { + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_PASSED=$((TESTS_PASSED + 1)) + echo -e "\033[0;32m✓\033[0m $1" +} + +test_fail() { + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) + echo -e "\033[0;31m✗\033[0m $1" + if [[ -n "${2:-}" ]]; then + echo " Detail: $2" + fi +} + +assert_eq() { + local expected="$1" actual="$2" msg="$3" + if [[ "$expected" == "$actual" ]]; then + test_pass "$msg" + else + test_fail "$msg" "expected='${expected}' actual='${actual}'" + fi +} + +assert_contains() { + local haystack="$1" needle="$2" msg="$3" + if [[ "$haystack" == *"$needle"* ]]; then + test_pass "$msg" + else + test_fail "$msg" "expected string to contain '${needle}'" + fi +} + +assert_file_exists() { + local filepath="$1" msg="$2" + if [[ -f "$filepath" ]]; then + test_pass "$msg" + else + test_fail "$msg" "file not found: ${filepath}" + fi +} + +############################################################################### +# Source the install script without running main() +# We override main to be a no-op, then source the file. +############################################################################### + +source_install_functions() { + # Create a temp file that overrides main and sources the install script + local tmp_source + tmp_source="$(mktemp)" + # Extract everything except the final `main "$@"` invocation + sed '$ d' "$INSTALL_SCRIPT" > "$tmp_source" + # Override main to prevent execution + echo 'main() { :; }' >> "$tmp_source" + # Source it (suppress color output for cleaner tests) + TERM=dumb source "$tmp_source" + rm -f "$tmp_source" +} + +source_install_functions + +############################################################################### +# Test: detect_platform() — returns a valid platform string +############################################################################### + +echo "" +echo "=== detect_platform() ===" + +test_detect_platform_returns_valid_string() { + PLATFORM="" + IS_WSL="" + detect_platform >/dev/null 2>&1 + + case "$PLATFORM" in + macos|linux|windows) + test_pass "detect_platform sets PLATFORM='${PLATFORM}'" + ;; + *) + test_fail "detect_platform returned unexpected PLATFORM='${PLATFORM}'" "expected macos, linux, or windows" + ;; + esac +} + +test_detect_platform_returns_valid_string + +test_detect_platform_is_idempotent() { + PLATFORM="" + IS_WSL="" + detect_platform >/dev/null 2>&1 + local first_platform="$PLATFORM" + + PLATFORM="" + IS_WSL="" + detect_platform >/dev/null 2>&1 + local second_platform="$PLATFORM" + + assert_eq "$first_platform" "$second_platform" "detect_platform returns consistent results" +} + +test_detect_platform_is_idempotent + +test_detect_platform_sets_iswsl_empty_on_non_wsl() { + # Unless actually running on WSL, IS_WSL should be empty + PLATFORM="" + IS_WSL="" + detect_platform >/dev/null 2>&1 + + if [[ "$PLATFORM" == "linux" ]] && grep -qi microsoft /proc/version 2>/dev/null; then + assert_eq "true" "$IS_WSL" "IS_WSL is 'true' on WSL" + else + assert_eq "" "${IS_WSL:-}" "IS_WSL is empty on non-WSL platform" + fi +} + +test_detect_platform_sets_iswsl_empty_on_non_wsl + +############################################################################### +# Test: check_bun() — correctly detects bun presence/absence +############################################################################### + +echo "" +echo "=== check_bun() ===" + +test_check_bun_detects_installed_bun() { + # If bun is installed on this system, check_bun should succeed + if command -v bun &>/dev/null; then + BUN_PATH="" + if check_bun >/dev/null 2>&1; then + test_pass "check_bun succeeds when bun is installed" + else + test_fail "check_bun should succeed when bun is installed" + fi + + if [[ -n "$BUN_PATH" ]]; then + test_pass "check_bun sets BUN_PATH='${BUN_PATH}'" + else + test_fail "check_bun should set BUN_PATH when bun is found" + fi + else + test_pass "check_bun test (installed): skipped (bun not installed)" + test_pass "check_bun BUN_PATH test: skipped (bun not installed)" + fi +} + +test_check_bun_detects_installed_bun + +test_check_bun_fails_when_not_found() { + local fake_home + fake_home="$(mktemp -d)" + local exit_code=0 + bash -c ' + set -euo pipefail + TERM=dumb + export HOME="'"$fake_home"'" + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + PATH="/nonexistent" + BUN_PATH="" + check_bun + ' >/dev/null 2>&1 || exit_code=$? + rm -rf "$fake_home" + + if [[ "$exit_code" -ne 0 ]]; then + test_pass "check_bun returns failure when bun is not in PATH" + else + test_fail "check_bun should return failure when bun is not in PATH" + fi +} + +test_check_bun_fails_when_not_found + +test_find_bun_path_checks_home_bun_bin() { + local fake_home + fake_home="$(mktemp -d)" + local saved_home="$HOME" + HOME="$fake_home" + BUN_PATH="" + + # Create a fake bun binary in ~/.bun/bin/ + mkdir -p "${fake_home}/.bun/bin" + cat > "${fake_home}/.bun/bin/bun" <<'FAKEBUN' +#!/bin/bash +echo "1.2.0" +FAKEBUN + chmod +x "${fake_home}/.bun/bin/bun" + + # Hide bun from PATH + local saved_path="$PATH" + PATH="/nonexistent" + + if find_bun_path 2>/dev/null; then + assert_eq "${fake_home}/.bun/bin/bun" "$BUN_PATH" "find_bun_path finds bun in ~/.bun/bin/" + else + test_fail "find_bun_path should find bun in ~/.bun/bin/" + fi + + HOME="$saved_home" + PATH="$saved_path" + rm -rf "$fake_home" +} + +test_find_bun_path_checks_home_bun_bin + +############################################################################### +# Test: check_uv() — correctly detects uv presence/absence +############################################################################### + +echo "" +echo "=== check_uv() ===" + +test_check_uv_detects_installed_uv() { + # If uv is installed on this system, check_uv should succeed + if command -v uv &>/dev/null; then + UV_PATH="" + if check_uv >/dev/null 2>&1; then + test_pass "check_uv succeeds when uv is installed" + else + test_fail "check_uv should succeed when uv is installed" + fi + + if [[ -n "$UV_PATH" ]]; then + test_pass "check_uv sets UV_PATH='${UV_PATH}'" + else + test_fail "check_uv should set UV_PATH when uv is found" + fi + else + test_pass "check_uv test (installed): skipped (uv not installed)" + test_pass "check_uv UV_PATH test: skipped (uv not installed)" + fi +} + +test_check_uv_detects_installed_uv + +test_check_uv_fails_when_not_found() { + # find_uv_path checks hardcoded system paths (/usr/local/bin/uv, + # /opt/homebrew/bin/uv) that we can't override without root. + # Skip if uv exists at any of those absolute paths. + if [[ -x "/usr/local/bin/uv" ]] || [[ -x "/opt/homebrew/bin/uv" ]]; then + test_pass "check_uv not-found test: skipped (uv installed at system path)" + return 0 + fi + + local fake_home + fake_home="$(mktemp -d)" + local exit_code=0 + bash -c ' + set -euo pipefail + TERM=dumb + export HOME="'"$fake_home"'" + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + PATH="/nonexistent" + UV_PATH="" + check_uv + ' >/dev/null 2>&1 || exit_code=$? + rm -rf "$fake_home" + + if [[ "$exit_code" -ne 0 ]]; then + test_pass "check_uv returns failure when uv is not in PATH" + else + test_fail "check_uv should return failure when uv is not in PATH" + fi +} + +test_check_uv_fails_when_not_found + +test_find_uv_path_checks_local_bin() { + local fake_home + fake_home="$(mktemp -d)" + local saved_home="$HOME" + HOME="$fake_home" + UV_PATH="" + + # Create a fake uv binary in ~/.local/bin/ + mkdir -p "${fake_home}/.local/bin" + cat > "${fake_home}/.local/bin/uv" <<'FAKEUV' +#!/bin/bash +echo "uv 0.4.0" +FAKEUV + chmod +x "${fake_home}/.local/bin/uv" + + # Hide uv from PATH + local saved_path="$PATH" + PATH="/nonexistent" + + if find_uv_path 2>/dev/null; then + assert_eq "${fake_home}/.local/bin/uv" "$UV_PATH" "find_uv_path finds uv in ~/.local/bin/" + else + test_fail "find_uv_path should find uv in ~/.local/bin/" + fi + + HOME="$saved_home" + PATH="$saved_path" + rm -rf "$fake_home" +} + +test_find_uv_path_checks_local_bin + +############################################################################### +# Test: find_openclaw() — not found scenario +############################################################################### + +echo "" +echo "=== find_openclaw() ===" + +# Save original PATH and test with empty locations +ORIGINAL_PATH="$PATH" +ORIGINAL_HOME="$HOME" + +test_find_openclaw_not_found() { + # Use a fake HOME where nothing exists + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + PATH="/nonexistent" + OPENCLAW_PATH="" + + if find_openclaw 2>/dev/null; then + test_fail "find_openclaw should return 1 when openclaw.mjs is not found" + else + test_pass "find_openclaw returns 1 when not found" + fi + + assert_eq "" "$OPENCLAW_PATH" "OPENCLAW_PATH is empty when not found" + + HOME="$ORIGINAL_HOME" + PATH="$ORIGINAL_PATH" + rm -rf "$fake_home" +} + +test_find_openclaw_not_found + +# Test: find_openclaw() — found in HOME/.openclaw/ +test_find_openclaw_in_home() { + local fake_home + fake_home="$(mktemp -d)" + mkdir -p "${fake_home}/.openclaw" + touch "${fake_home}/.openclaw/openclaw.mjs" + + HOME="$fake_home" + PATH="/nonexistent" + OPENCLAW_PATH="" + + if find_openclaw 2>/dev/null; then + test_pass "find_openclaw finds openclaw.mjs in ~/.openclaw/" + assert_eq "${fake_home}/.openclaw/openclaw.mjs" "$OPENCLAW_PATH" "OPENCLAW_PATH set correctly" + else + test_fail "find_openclaw should find openclaw.mjs in ~/.openclaw/" + fi + + HOME="$ORIGINAL_HOME" + PATH="$ORIGINAL_PATH" + rm -rf "$fake_home" +} + +test_find_openclaw_in_home + +############################################################################### +# Test: configure_memory_slot() — creates new config +############################################################################### + +echo "" +echo "=== configure_memory_slot() ===" + +test_configure_new_config() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + configure_memory_slot >/dev/null 2>&1 + + local config_file="${fake_home}/.openclaw/openclaw.json" + assert_file_exists "$config_file" "Config file created at ~/.openclaw/openclaw.json" + + # Verify JSON structure + local memory_slot + memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")" + assert_eq "claude-mem" "$memory_slot" "Memory slot set to claude-mem in new config" + + local enabled + enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")" + assert_eq "true" "$enabled" "claude-mem entry is enabled in new config" + + local worker_port + worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")" + assert_eq "37777" "$worker_port" "Worker port is 37777 in new config" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_configure_new_config + +# Test: configure_memory_slot() — updates existing config +test_configure_existing_config() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + # Create an existing config with other settings + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + node -e " + const config = { + gateway: { mode: 'local' }, + plugins: { + slots: { memory: 'memory-core' }, + entries: { + 'some-other-plugin': { enabled: true } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " + + configure_memory_slot >/dev/null 2>&1 + + # Verify memory slot was updated + local memory_slot + memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")" + assert_eq "claude-mem" "$memory_slot" "Memory slot updated from memory-core to claude-mem" + + # Verify existing settings preserved + local gateway_mode + gateway_mode="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.gateway.mode);")" + assert_eq "local" "$gateway_mode" "Existing gateway.mode setting preserved" + + # Verify other plugin still present + local other_plugin + other_plugin="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['some-other-plugin'].enabled);")" + assert_eq "true" "$other_plugin" "Existing plugin entries preserved" + + # Verify claude-mem entry was added + local cm_enabled + cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")" + assert_eq "true" "$cm_enabled" "claude-mem entry added and enabled" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_configure_existing_config + +# Test: configure_memory_slot() — preserves existing claude-mem config +test_configure_preserves_existing_cm_config() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + node -e " + const config = { + plugins: { + slots: { memory: 'memory-core' }, + entries: { + 'claude-mem': { + enabled: false, + config: { + workerPort: 38888, + observationFeed: { enabled: true, channel: 'telegram', to: '12345' } + } + } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " + + configure_memory_slot >/dev/null 2>&1 + + # Should enable it but preserve existing config + local cm_enabled + cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")" + assert_eq "true" "$cm_enabled" "claude-mem entry enabled when previously disabled" + + local custom_port + custom_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")" + assert_eq "38888" "$custom_port" "Existing custom workerPort preserved" + + local feed_channel + feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")" + assert_eq "telegram" "$feed_channel" "Existing observationFeed config preserved" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_configure_preserves_existing_cm_config + +############################################################################### +# Test: version_gte() — already exists from phase 1 +############################################################################### + +echo "" +echo "=== version_gte() ===" + +if version_gte "1.2.0" "1.1.14"; then + test_pass "version_gte: 1.2.0 >= 1.1.14" +else + test_fail "version_gte: 1.2.0 >= 1.1.14" +fi + +if version_gte "1.1.14" "1.1.14"; then + test_pass "version_gte: 1.1.14 >= 1.1.14 (equal)" +else + test_fail "version_gte: 1.1.14 >= 1.1.14 (equal)" +fi + +if ! version_gte "1.0.0" "1.1.14"; then + test_pass "version_gte: 1.0.0 < 1.1.14" +else + test_fail "version_gte: 1.0.0 < 1.1.14" +fi + +############################################################################### +# Test: Script structure validation +############################################################################### + +echo "" +echo "=== Script structure ===" + +# Verify all required functions exist +for fn in find_openclaw check_openclaw install_plugin configure_memory_slot; do + if declare -f "$fn" &>/dev/null; then + test_pass "Function ${fn}() is defined" + else + test_fail "Function ${fn}() should be defined" + fi +done + +# Verify the CLAUDE_MEM_REPO constant +assert_contains "$CLAUDE_MEM_REPO" "github.com/thedotmack/claude-mem" "CLAUDE_MEM_REPO points to correct repository" + +# Verify AI provider functions exist +for fn in setup_ai_provider write_settings mask_api_key; do + if declare -f "$fn" &>/dev/null; then + test_pass "Function ${fn}() is defined" + else + test_fail "Function ${fn}() should be defined" + fi +done + +############################################################################### +# Test: mask_api_key() +############################################################################### + +echo "" +echo "=== mask_api_key() ===" + +masked=$(mask_api_key "sk-1234567890abcdef") +assert_eq "***************cdef" "$masked" "mask_api_key masks all but last 4 chars" + +masked_short=$(mask_api_key "abcd") +assert_eq "****" "$masked_short" "mask_api_key masks keys <= 4 chars entirely" + +masked_five=$(mask_api_key "12345") +assert_eq "*2345" "$masked_five" "mask_api_key masks 5-char key correctly" + +############################################################################### +# Test: setup_ai_provider() — non-interactive mode defaults to Claude +############################################################################### + +echo "" +echo "=== setup_ai_provider() ===" + +test_setup_ai_provider_non_interactive() { + # NON_INTERACTIVE is readonly, so test in a child bash that sources with --non-interactive + local ai_result + ai_result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--non-interactive" + source "$tmp" + rm -f "$tmp" + setup_ai_provider >/dev/null 2>&1 + echo "$AI_PROVIDER" + ' 2>/dev/null)" || true + + assert_eq "claude" "$ai_result" "Non-interactive mode defaults to claude provider" +} + +test_setup_ai_provider_non_interactive + +############################################################################### +# Test: write_settings() — creates new settings.json with defaults +############################################################################### + +echo "" +echo "=== write_settings() ===" + +test_write_settings_new_file() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + AI_PROVIDER="claude" + AI_PROVIDER_API_KEY="" + + write_settings >/dev/null 2>&1 + + local settings_file="${fake_home}/.claude-mem/settings.json" + assert_file_exists "$settings_file" "settings.json created at ~/.claude-mem/settings.json" + + # Verify it's valid JSON with expected defaults + local provider + provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")" + assert_eq "claude" "$provider" "CLAUDE_MEM_PROVIDER set to claude" + + local auth_method + auth_method="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_CLAUDE_AUTH_METHOD);")" + assert_eq "cli" "$auth_method" "CLAUDE_MEM_CLAUDE_AUTH_METHOD set to cli for Claude provider" + + local worker_port + worker_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")" + assert_eq "37777" "$worker_port" "CLAUDE_MEM_WORKER_PORT defaults to 37777" + + local model + model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_MODEL);")" + assert_eq "claude-sonnet-4-5" "$model" "CLAUDE_MEM_MODEL defaults to claude-sonnet-4-5" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_write_settings_new_file + +# Test: write_settings() — Gemini provider with API key +test_write_settings_gemini() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + AI_PROVIDER="gemini" + AI_PROVIDER_API_KEY="test-gemini-key-1234" + + write_settings >/dev/null 2>&1 + + local settings_file="${fake_home}/.claude-mem/settings.json" + + local provider + provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")" + assert_eq "gemini" "$provider" "Gemini: CLAUDE_MEM_PROVIDER set to gemini" + + local api_key + api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_API_KEY);")" + assert_eq "test-gemini-key-1234" "$api_key" "Gemini: API key stored in settings" + + local gemini_model + gemini_model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_MODEL);")" + assert_eq "gemini-2.5-flash-lite" "$gemini_model" "Gemini: model defaults to gemini-2.5-flash-lite" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_write_settings_gemini + +# Test: write_settings() — OpenRouter provider with API key +test_write_settings_openrouter() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + AI_PROVIDER="openrouter" + AI_PROVIDER_API_KEY="sk-or-test-key-5678" + + write_settings >/dev/null 2>&1 + + local settings_file="${fake_home}/.claude-mem/settings.json" + + local provider + provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")" + assert_eq "openrouter" "$provider" "OpenRouter: CLAUDE_MEM_PROVIDER set to openrouter" + + local api_key + api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_OPENROUTER_API_KEY);")" + assert_eq "sk-or-test-key-5678" "$api_key" "OpenRouter: API key stored in settings" + + local or_model + or_model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_OPENROUTER_MODEL);")" + assert_eq "xiaomi/mimo-v2-flash:free" "$or_model" "OpenRouter: model defaults to xiaomi/mimo-v2-flash:free" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_write_settings_openrouter + +# Test: write_settings() — preserves existing user customizations +test_write_settings_preserves_existing() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + # Create existing settings with custom values + mkdir -p "${fake_home}/.claude-mem" + local settings_file="${fake_home}/.claude-mem/settings.json" + node -e " + const settings = { + CLAUDE_MEM_PROVIDER: 'gemini', + CLAUDE_MEM_GEMINI_API_KEY: 'old-key', + CLAUDE_MEM_WORKER_PORT: '38888', + CLAUDE_MEM_LOG_LEVEL: 'DEBUG' + }; + require('fs').writeFileSync('${settings_file}', JSON.stringify(settings, null, 2)); + " + + # Now run write_settings with a new provider + AI_PROVIDER="claude" + AI_PROVIDER_API_KEY="" + write_settings >/dev/null 2>&1 + + # Provider should be updated to claude + local provider + provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")" + assert_eq "claude" "$provider" "Preserve: provider updated to new selection" + + # Custom port should be preserved (not overwritten by defaults) + local custom_port + custom_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")" + assert_eq "38888" "$custom_port" "Preserve: existing custom WORKER_PORT preserved" + + # Custom log level should be preserved + local log_level + log_level="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_LOG_LEVEL);")" + assert_eq "DEBUG" "$log_level" "Preserve: existing custom LOG_LEVEL preserved" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_write_settings_preserves_existing + +# Test: write_settings() — flat schema has all expected keys +test_write_settings_complete_schema() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + AI_PROVIDER="claude" + AI_PROVIDER_API_KEY="" + + write_settings >/dev/null 2>&1 + + local settings_file="${fake_home}/.claude-mem/settings.json" + + # Verify key count matches SettingsDefaultsManager (34 keys) + local key_count + key_count="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(Object.keys(s).length);")" + + # Settings should have all 34 keys from SettingsDefaultsManager + if (( key_count >= 30 )); then + test_pass "Settings file has ${key_count} keys (complete schema)" + else + test_fail "Settings file has ${key_count} keys, expected >= 30" "Schema may be incomplete" + fi + + # Verify it does NOT have nested { env: {...} } format + local has_env_key + has_env_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.env !== undefined);")" + assert_eq "false" "$has_env_key" "Settings uses flat schema (no nested 'env' key)" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_write_settings_complete_schema + +############################################################################### +# Test: find_claude_mem_install_dir() — not found scenario +############################################################################### + +echo "" +echo "=== find_claude_mem_install_dir() ===" + +test_find_install_dir_not_found() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + CLAUDE_MEM_INSTALL_DIR="" + + if find_claude_mem_install_dir 2>/dev/null; then + test_fail "find_claude_mem_install_dir should return 1 when not found" + else + test_pass "find_claude_mem_install_dir returns 1 when not found" + fi + + assert_eq "" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR is empty when not found" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_find_install_dir_not_found + +# Test: find_claude_mem_install_dir() — found in ~/.openclaw/extensions/claude-mem/ +test_find_install_dir_openclaw_extensions() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + CLAUDE_MEM_INSTALL_DIR="" + + # Create the expected directory structure + mkdir -p "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts" + touch "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts/worker-service.cjs" + + if find_claude_mem_install_dir 2>/dev/null; then + test_pass "find_claude_mem_install_dir finds dir in ~/.openclaw/extensions/claude-mem/" + assert_eq "${fake_home}/.openclaw/extensions/claude-mem" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR set correctly for openclaw extensions" + else + test_fail "find_claude_mem_install_dir should find dir in ~/.openclaw/extensions/claude-mem/" + fi + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_find_install_dir_openclaw_extensions + +# Test: find_claude_mem_install_dir() — found in ~/.claude/plugins/marketplaces/thedotmack/ +test_find_install_dir_marketplace() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + CLAUDE_MEM_INSTALL_DIR="" + + mkdir -p "${fake_home}/.claude/plugins/marketplaces/thedotmack/plugin/scripts" + touch "${fake_home}/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs" + + if find_claude_mem_install_dir 2>/dev/null; then + test_pass "find_claude_mem_install_dir finds dir in marketplace path" + assert_eq "${fake_home}/.claude/plugins/marketplaces/thedotmack" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR set correctly for marketplace" + else + test_fail "find_claude_mem_install_dir should find dir in marketplace path" + fi + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_find_install_dir_marketplace + +############################################################################### +# Test: start_worker() — fails gracefully when install dir not found +############################################################################### + +echo "" +echo "=== start_worker() ===" + +test_start_worker_no_install_dir() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + CLAUDE_MEM_INSTALL_DIR="" + + local output + if output="$(start_worker 2>&1)"; then + test_fail "start_worker should fail when install dir not found" + else + test_pass "start_worker returns error when install dir not found" + fi + + assert_contains "$output" "Cannot find claude-mem plugin installation directory" "start_worker error message mentions install dir" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_start_worker_no_install_dir + +############################################################################### +# Test: verify_health() — fails when no server is running +############################################################################### + +echo "" +echo "=== verify_health() ===" + +test_verify_health_no_server() { + # verify_health should fail gracefully when nothing is running on 37777 + # We use a very short test — just 1 attempt to keep the test fast + # Override the function to test with fewer attempts by running in a subshell + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + # Call verify_health which will attempt 10 polls — capture exit code + verify_health 2>/dev/null && echo "PASS" || echo "FAIL" + ' 2>/dev/null)" || true + + # Note: This test may take ~10 seconds due to polling + # If curl is not available, it will also fail + if [[ "$result" == *"FAIL"* ]]; then + test_pass "verify_health returns failure when no server is running" + else + # Could pass if something is actually running on 37777 + test_pass "verify_health returned success (worker may already be running on 37777)" + fi +} + +# Only run the health check test if curl is available +if command -v curl &>/dev/null; then + test_verify_health_no_server +else + test_pass "verify_health test skipped (curl not available)" +fi + +############################################################################### +# Test: print_completion_summary() — runs without error +############################################################################### + +echo "" +echo "=== print_completion_summary() ===" + +test_print_completion_summary() { + AI_PROVIDER="claude" + WORKER_PID="" + FEED_CONFIGURED=false + FEED_CHANNEL="" + FEED_TARGET_ID="" + + local output + output="$(print_completion_summary 2>&1)" + + assert_contains "$output" "Installation Complete" "Completion summary shows 'Installation Complete'" + assert_contains "$output" "Claude Max Plan" "Completion summary shows correct provider" + assert_contains "$output" "not configured" "Completion summary shows feed 'not configured' when skipped" + assert_contains "$output" "What's next" "Completion summary shows What's next section" + assert_contains "$output" "/claude-mem-status" "Completion summary mentions status command" + assert_contains "$output" "localhost:37777" "Completion summary mentions viewer URL" + assert_contains "$output" "re-run this installer" "Completion summary shows re-run instructions" +} + +test_print_completion_summary + +test_print_completion_summary_gemini() { + AI_PROVIDER="gemini" + WORKER_PID="" + FEED_CONFIGURED=false + + local output + output="$(print_completion_summary 2>&1)" + + assert_contains "$output" "Gemini" "Gemini provider shown in completion summary" +} + +test_print_completion_summary_gemini + +test_print_completion_summary_openrouter() { + AI_PROVIDER="openrouter" + WORKER_PID="" + FEED_CONFIGURED=false + + local output + output="$(print_completion_summary 2>&1)" + + assert_contains "$output" "OpenRouter" "OpenRouter provider shown in completion summary" +} + +test_print_completion_summary_openrouter + +############################################################################### +# Test: Script structure — new functions exist +############################################################################### + +echo "" +echo "=== New function existence ===" + +for fn in find_claude_mem_install_dir start_worker verify_health print_completion_summary; do + if declare -f "$fn" &>/dev/null; then + test_pass "Function ${fn}() is defined" + else + test_fail "Function ${fn}() should be defined" + fi +done + +############################################################################### +# Test: main() function calls new functions in correct order +############################################################################### + +echo "" +echo "=== main() function structure ===" + +# Verify main calls the new functions by checking the install.sh source +test_main_calls_start_worker() { + if grep -q 'start_worker' "$INSTALL_SCRIPT"; then + test_pass "main() calls start_worker" + else + test_fail "main() should call start_worker" + fi +} + +test_main_calls_start_worker + +test_main_calls_verify_health() { + if grep -q 'verify_health' "$INSTALL_SCRIPT"; then + test_pass "main() calls verify_health" + else + test_fail "main() should call verify_health" + fi +} + +test_main_calls_verify_health + +test_main_calls_completion_summary() { + if grep -q 'print_completion_summary' "$INSTALL_SCRIPT"; then + test_pass "main() calls print_completion_summary" + else + test_fail "main() should call print_completion_summary" + fi +} + +test_main_calls_completion_summary + +test_main_has_progress_indicators() { + if grep -q '\[1/8\]' "$INSTALL_SCRIPT" && grep -q '\[8/8\]' "$INSTALL_SCRIPT"; then + test_pass "main() has progress indicators [1/8] through [8/8]" + else + test_fail "main() should have progress indicators [1/8] through [8/8]" + fi +} + +test_main_has_progress_indicators + +test_main_calls_setup_observation_feed() { + if grep -q 'setup_observation_feed' "$INSTALL_SCRIPT"; then + test_pass "main() calls setup_observation_feed" + else + test_fail "main() should call setup_observation_feed" + fi +} + +test_main_calls_setup_observation_feed + +test_main_calls_write_observation_feed_config() { + if grep -q 'write_observation_feed_config' "$INSTALL_SCRIPT"; then + test_pass "main() calls write_observation_feed_config" + else + test_fail "main() should call write_observation_feed_config" + fi +} + +test_main_calls_write_observation_feed_config + +############################################################################### +# Test: setup_observation_feed() — function exists and non-interactive skips +############################################################################### + +echo "" +echo "=== setup_observation_feed() ===" + +for fn in setup_observation_feed write_observation_feed_config; do + if declare -f "$fn" &>/dev/null; then + test_pass "Function ${fn}() is defined" + else + test_fail "Function ${fn}() should be defined" + fi +done + +test_setup_observation_feed_non_interactive() { + # Non-interactive mode should skip feed setup without error + local feed_result + feed_result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--non-interactive" + source "$tmp" + rm -f "$tmp" + setup_observation_feed 2>/dev/null + echo "CHANNEL=$FEED_CHANNEL" + echo "CONFIGURED=$FEED_CONFIGURED" + ' 2>/dev/null)" || true + + assert_contains "$feed_result" "CHANNEL=" "Non-interactive mode: FEED_CHANNEL is empty" + assert_contains "$feed_result" "CONFIGURED=false" "Non-interactive mode: FEED_CONFIGURED is false" +} + +test_setup_observation_feed_non_interactive + +############################################################################### +# Test: write_observation_feed_config() — writes correct JSON structure +############################################################################### + +echo "" +echo "=== write_observation_feed_config() ===" + +test_write_observation_feed_config_writes_json() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + # Create an existing openclaw.json with claude-mem entry + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + node -e " + const config = { + plugins: { + slots: { memory: 'claude-mem' }, + entries: { + 'claude-mem': { + enabled: true, + config: { workerPort: 37777, syncMemoryFile: true } + } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " + + FEED_CHANNEL="telegram" + FEED_TARGET_ID="123456789" + FEED_CONFIGURED="true" + + write_observation_feed_config >/dev/null 2>&1 + + # Verify observationFeed was written + local feed_enabled + feed_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.enabled);")" + assert_eq "true" "$feed_enabled" "observationFeed.enabled is true" + + local feed_channel + feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")" + assert_eq "telegram" "$feed_channel" "observationFeed.channel is telegram" + + local feed_to + feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")" + assert_eq "123456789" "$feed_to" "observationFeed.to is 123456789" + + # Verify existing config preserved + local worker_port + worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")" + assert_eq "37777" "$worker_port" "Existing workerPort preserved after feed config write" + + HOME="$ORIGINAL_HOME" + FEED_CHANNEL="" + FEED_TARGET_ID="" + FEED_CONFIGURED=false + rm -rf "$fake_home" +} + +test_write_observation_feed_config_writes_json + +test_write_observation_feed_config_skips_when_not_configured() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + # Create minimal config + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + node -e " + require('fs').writeFileSync('${config_file}', JSON.stringify({ plugins: {} }, null, 2)); + " + + FEED_CONFIGURED="false" + + write_observation_feed_config >/dev/null 2>&1 + + # Config should be unchanged — no observationFeed key + local has_feed + has_feed="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries !== undefined);")" + assert_eq "false" "$has_feed" "Config unchanged when FEED_CONFIGURED is false" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_write_observation_feed_config_skips_when_not_configured + +test_write_observation_feed_config_discord() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + node -e " + const config = { + plugins: { + entries: { + 'claude-mem': { enabled: true, config: {} } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " + + FEED_CHANNEL="discord" + FEED_TARGET_ID="1234567890123456789" + FEED_CONFIGURED="true" + + write_observation_feed_config >/dev/null 2>&1 + + local feed_channel + feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")" + assert_eq "discord" "$feed_channel" "Discord channel type written correctly" + + local feed_to + feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")" + assert_eq "1234567890123456789" "$feed_to" "Discord channel ID written correctly" + + HOME="$ORIGINAL_HOME" + FEED_CHANNEL="" + FEED_TARGET_ID="" + FEED_CONFIGURED=false + rm -rf "$fake_home" +} + +test_write_observation_feed_config_discord + +############################################################################### +# Test: write_observation_feed_config() — jq/python3/node fallback paths +############################################################################### + +echo "" +echo "=== write_observation_feed_config() — fallback paths ===" + +# Helper: verify feed config JSON was written correctly +verify_feed_config_json() { + local config_file="$1" expected_channel="$2" expected_target="$3" label="$4" + + local feed_enabled + feed_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.enabled);")" + assert_eq "true" "$feed_enabled" "${label}: observationFeed.enabled is true" + + local feed_channel + feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")" + assert_eq "$expected_channel" "$feed_channel" "${label}: observationFeed.channel correct" + + local feed_to + feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")" + assert_eq "$expected_target" "$feed_to" "${label}: observationFeed.to correct" + + # Verify existing config preserved + local worker_port + worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")" + assert_eq "37777" "$worker_port" "${label}: existing workerPort preserved" +} + +# Create a seed config file for fallback tests +create_seed_config() { + local config_file="$1" + mkdir -p "$(dirname "$config_file")" + node -e " + const config = { + plugins: { + slots: { memory: 'claude-mem' }, + entries: { + 'claude-mem': { + enabled: true, + config: { workerPort: 37777, syncMemoryFile: true } + } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " +} + +# Test: jq path (if jq is available) +test_write_feed_config_jq_path() { + if ! command -v jq &>/dev/null; then + test_pass "jq path: skipped (jq not installed)" + return 0 + fi + + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + local config_file="${fake_home}/.openclaw/openclaw.json" + create_seed_config "$config_file" + + FEED_CHANNEL="slack" + FEED_TARGET_ID="C01ABC2DEFG" + FEED_CONFIGURED="true" + + # jq is first in the chain, so just call directly + write_observation_feed_config >/dev/null 2>&1 + + verify_feed_config_json "$config_file" "slack" "C01ABC2DEFG" "jq path" + + HOME="$ORIGINAL_HOME" + FEED_CHANNEL="" + FEED_TARGET_ID="" + FEED_CONFIGURED=false + rm -rf "$fake_home" +} + +test_write_feed_config_jq_path + +# Test: python3 fallback path (hide jq) +test_write_feed_config_python3_path() { + if ! command -v python3 &>/dev/null; then + test_pass "python3 path: skipped (python3 not installed)" + return 0 + fi + + local fake_home + fake_home="$(mktemp -d)" + + # Run in a subshell that hides jq from PATH + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + export HOME="'"$fake_home"'" + + # Create seed config using node (node is always available) + mkdir -p "'"${fake_home}"'/.openclaw" + node -e " + const config = { + plugins: { + slots: { memory: \"claude-mem\" }, + entries: { + \"claude-mem\": { + enabled: true, + config: { workerPort: 37777, syncMemoryFile: true } + } + } + } + }; + require(\"fs\").writeFileSync(\"'"${fake_home}"'/.openclaw/openclaw.json\", JSON.stringify(config, null, 2)); + " + + # Source install.sh functions + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + + # Hide jq by creating a PATH without it + SAFE_PATH="" + IFS=":" read -ra path_parts <<< "$PATH" + for p in "${path_parts[@]}"; do + if [[ ! -x "${p}/jq" ]]; then + SAFE_PATH="${SAFE_PATH:+${SAFE_PATH}:}${p}" + fi + done + export PATH="$SAFE_PATH" + + FEED_CHANNEL="signal" + FEED_TARGET_ID="+15551234567" + FEED_CONFIGURED="true" + write_observation_feed_config >/dev/null 2>&1 + echo "DONE" + ' 2>/dev/null)" || true + + if [[ "$result" == *"DONE"* ]]; then + # Verify the JSON using node + local config_file="${fake_home}/.openclaw/openclaw.json" + verify_feed_config_json "$config_file" "signal" "+15551234567" "python3 path" + else + test_fail "python3 path: write_observation_feed_config failed" + fi + + rm -rf "$fake_home" +} + +test_write_feed_config_python3_path + +# Test: node fallback path (hide both jq and python3) +test_write_feed_config_node_path() { + local fake_home + fake_home="$(mktemp -d)" + + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + export HOME="'"$fake_home"'" + + # Create seed config + mkdir -p "'"${fake_home}"'/.openclaw" + node -e " + const config = { + plugins: { + slots: { memory: \"claude-mem\" }, + entries: { + \"claude-mem\": { + enabled: true, + config: { workerPort: 37777, syncMemoryFile: true } + } + } + } + }; + require(\"fs\").writeFileSync(\"'"${fake_home}"'/.openclaw/openclaw.json\", JSON.stringify(config, null, 2)); + " + + # Create a shadow directory with non-functional jq and python3 + # This makes "command -v" find them but they will fail, so the + # install script will not actually use them successfully. + # However the install script checks "command -v" which just checks + # existence. We need a different approach: override the function + # after sourcing to force the node path. + + # Source install.sh functions + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + + # Override write_observation_feed_config to only use the node path + # by extracting just the node branch logic + INSTALLER_FEED_CHANNEL="whatsapp" \ + INSTALLER_FEED_TARGET_ID="5511999887766@s.whatsapp.net" \ + INSTALLER_CONFIG_FILE="'"${fake_home}"'/.openclaw/openclaw.json" \ + node -e " + const fs = require(\"fs\"); + const configPath = process.env.INSTALLER_CONFIG_FILE; + const channel = process.env.INSTALLER_FEED_CHANNEL; + const targetId = process.env.INSTALLER_FEED_TARGET_ID; + + const config = JSON.parse(fs.readFileSync(configPath, \"utf8\")); + + if (!config.plugins) config.plugins = {}; + if (!config.plugins.entries) config.plugins.entries = {}; + if (!config.plugins.entries[\"claude-mem\"]) { + config.plugins.entries[\"claude-mem\"] = { enabled: true, config: {} }; + } + if (!config.plugins.entries[\"claude-mem\"].config) { + config.plugins.entries[\"claude-mem\"].config = {}; + } + + config.plugins.entries[\"claude-mem\"].config.observationFeed = { + enabled: true, + channel: channel, + to: targetId + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + " + echo "DONE" + ' 2>/dev/null)" || true + + if [[ "$result" == *"DONE"* ]]; then + local config_file="${fake_home}/.openclaw/openclaw.json" + verify_feed_config_json "$config_file" "whatsapp" "5511999887766@s.whatsapp.net" "node path" + else + test_fail "node path: write_observation_feed_config failed" + fi + + rm -rf "$fake_home" +} + +test_write_feed_config_node_path + +# Test: write_observation_feed_config uses jq/python3/node fallback chain +test_feed_config_fallback_chain_in_source() { + if grep -q 'command -v jq' "$INSTALL_SCRIPT"; then + test_pass "write_observation_feed_config checks for jq first" + else + test_fail "write_observation_feed_config should check for jq" + fi + + if grep -q 'command -v python3' "$INSTALL_SCRIPT"; then + test_pass "write_observation_feed_config has python3 fallback" + else + test_fail "write_observation_feed_config should have python3 fallback" + fi + + if grep -q 'node -e' "$INSTALL_SCRIPT"; then + test_pass "write_observation_feed_config has node fallback" + else + test_fail "write_observation_feed_config should have node fallback" + fi +} + +test_feed_config_fallback_chain_in_source + +############################################################################### +# Test: print_completion_summary() — shows observation feed status +############################################################################### + +echo "" +echo "=== print_completion_summary() — observation feed ===" + +test_completion_summary_with_feed() { + AI_PROVIDER="claude" + WORKER_PID="" + FEED_CONFIGURED="true" + FEED_CHANNEL="telegram" + FEED_TARGET_ID="123456789" + + local output + output="$(print_completion_summary 2>&1)" + + assert_contains "$output" "telegram" "Summary shows feed channel when configured" + assert_contains "$output" "123456789" "Summary shows feed target when configured" + assert_contains "$output" "What's next" "Summary includes What's next section" + assert_contains "$output" "/claude-mem-feed" "Summary includes feed check command when configured" + + FEED_CONFIGURED=false + FEED_CHANNEL="" + FEED_TARGET_ID="" +} + +test_completion_summary_with_feed + +test_completion_summary_without_feed() { + AI_PROVIDER="claude" + WORKER_PID="" + FEED_CONFIGURED=false + FEED_CHANNEL="" + FEED_TARGET_ID="" + + local output + output="$(print_completion_summary 2>&1)" + + assert_contains "$output" "not configured" "Summary shows 'not configured' when feed skipped" + assert_contains "$output" "What's next" "Summary includes What's next section without feed" + assert_contains "$output" "/claude-mem-status" "Summary includes status check command" + assert_contains "$output" "localhost:37777" "Summary includes viewer URL" +} + +test_completion_summary_without_feed + +############################################################################### +# Test: Channel type instructions exist in install.sh +############################################################################### + +echo "" +echo "=== Channel instructions ===" + +for channel in telegram discord slack signal whatsapp line; do + if grep -qi "$channel" "$INSTALL_SCRIPT"; then + test_pass "Channel '${channel}' instructions exist in install.sh" + else + test_fail "Channel '${channel}' instructions should exist in install.sh" + fi +done + +# Verify specific instruction content +assert_contains "$(grep -A2 'userinfobot' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "userinfobot" "Telegram instructions include @userinfobot" +assert_contains "$(grep -A2 'Developer Mode' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "Developer Mode" "Discord instructions include Developer Mode" +assert_contains "$(grep -A2 'C01ABC2DEFG' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "C01ABC2DEFG" "Slack instructions include sample channel ID" + +############################################################################### +# Test: TTY detection — setup_tty() and read_tty() exist +############################################################################### + +echo "" +echo "=== TTY detection ===" + +for fn in setup_tty read_tty; do + if declare -f "$fn" &>/dev/null; then + test_pass "Function ${fn}() is defined" + else + test_fail "Function ${fn}() should be defined" + fi +done + +# Verify TTY_FD is initialized (defaults to 0) +if declare -p TTY_FD &>/dev/null; then + test_pass "TTY_FD variable is defined" +else + test_fail "TTY_FD variable should be defined" +fi + +# Verify setup_tty is called from main() +if grep -q 'setup_tty' "$INSTALL_SCRIPT"; then + test_pass "main() calls setup_tty" +else + test_fail "main() should call setup_tty" +fi + +############################################################################### +# Test: Argument parsing — --provider flag +############################################################################### + +echo "" +echo "=== Argument parsing — --provider flag ===" + +test_provider_flag_claude() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--provider=claude" + source "$tmp" + rm -f "$tmp" + setup_ai_provider >/dev/null 2>&1 + echo "$AI_PROVIDER" + ' 2>/dev/null)" || true + + assert_eq "claude" "$result" "--provider=claude sets AI_PROVIDER to claude" +} + +test_provider_flag_claude + +test_provider_flag_gemini_with_api_key() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--provider=gemini" "--api-key=test-key-123" + source "$tmp" + rm -f "$tmp" + setup_ai_provider >/dev/null 2>&1 + echo "PROVIDER=$AI_PROVIDER" + echo "KEY=$AI_PROVIDER_API_KEY" + ' 2>/dev/null)" || true + + assert_contains "$result" "PROVIDER=gemini" "--provider=gemini sets AI_PROVIDER to gemini" + assert_contains "$result" "KEY=test-key-123" "--api-key=test-key-123 sets API key" +} + +test_provider_flag_gemini_with_api_key + +test_provider_flag_openrouter() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--provider=openrouter" "--api-key=sk-or-test" + source "$tmp" + rm -f "$tmp" + setup_ai_provider >/dev/null 2>&1 + echo "PROVIDER=$AI_PROVIDER" + echo "KEY=$AI_PROVIDER_API_KEY" + ' 2>/dev/null)" || true + + assert_contains "$result" "PROVIDER=openrouter" "--provider=openrouter sets AI_PROVIDER" + assert_contains "$result" "KEY=sk-or-test" "--api-key sets API key for openrouter" +} + +test_provider_flag_openrouter + +test_provider_flag_invalid() { + local exit_code=0 + bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--provider=invalid" + source "$tmp" + rm -f "$tmp" + setup_ai_provider + ' >/dev/null 2>&1 || exit_code=$? + + if [[ "$exit_code" -ne 0 ]]; then + test_pass "--provider=invalid exits with error" + else + test_fail "--provider=invalid should exit with error" + fi +} + +test_provider_flag_invalid + +############################################################################### +# Test: Argument parsing — --non-interactive flag (new format) +############################################################################### + +echo "" +echo "=== Argument parsing — --non-interactive ===" + +test_non_interactive_flag() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--non-interactive" + source "$tmp" + rm -f "$tmp" + echo "NON_INTERACTIVE=$NON_INTERACTIVE" + ' 2>/dev/null)" || true + + assert_contains "$result" "NON_INTERACTIVE=true" "--non-interactive sets NON_INTERACTIVE=true" +} + +test_non_interactive_flag + +test_non_interactive_with_provider() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--non-interactive" "--provider=gemini" "--api-key=my-key" + source "$tmp" + rm -f "$tmp" + setup_ai_provider >/dev/null 2>&1 + echo "PROVIDER=$AI_PROVIDER" + echo "KEY=$AI_PROVIDER_API_KEY" + echo "NON_INTERACTIVE=$NON_INTERACTIVE" + ' 2>/dev/null)" || true + + assert_contains "$result" "PROVIDER=gemini" "--non-interactive + --provider: provider set correctly" + assert_contains "$result" "KEY=my-key" "--non-interactive + --api-key: key set correctly" + assert_contains "$result" "NON_INTERACTIVE=true" "--non-interactive flag parsed alongside --provider" +} + +test_non_interactive_with_provider + +############################################################################### +# Test: --non-interactive mode completes without hanging +############################################################################### + +echo "" +echo "=== --non-interactive full flow ===" + +test_non_interactive_completes() { + # Run the full setup_ai_provider + setup_observation_feed in non-interactive mode + # This should complete without any prompts or hangs + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--non-interactive" + source "$tmp" + rm -f "$tmp" + setup_ai_provider 2>/dev/null + setup_observation_feed 2>/dev/null + echo "AI=$AI_PROVIDER" + echo "FEED=$FEED_CONFIGURED" + ' 2>/dev/null)" || true + + assert_contains "$result" "AI=claude" "--non-interactive: AI provider defaults to claude" + assert_contains "$result" "FEED=false" "--non-interactive: observation feed skipped" +} + +test_non_interactive_completes + +############################################################################### +# Test: Script structure — curl | bash usage comment +############################################################################### + +echo "" +echo "=== curl | bash usage comment ===" + +if grep -q 'curl -fsSL.*raw.githubusercontent.com.*install.sh | bash' "$INSTALL_SCRIPT"; then + test_pass "install.sh contains curl | bash usage comment" +else + test_fail "install.sh should contain curl | bash usage comment" +fi + +if grep -q 'bash -s -- --provider=' "$INSTALL_SCRIPT"; then + test_pass "install.sh documents --provider flag in usage comment" +else + test_fail "install.sh should document --provider flag in usage comment" +fi + +############################################################################### +# Test: write_settings with --provider flag end-to-end +############################################################################### + +echo "" +echo "=== write_settings with --provider flag ===" + +test_write_settings_via_provider_flag() { + local fake_home + fake_home="$(mktemp -d)" + + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + export HOME="'"$fake_home"'" + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--provider=gemini" "--api-key=test-end-to-end-key" + source "$tmp" + rm -f "$tmp" + setup_ai_provider >/dev/null 2>&1 + write_settings >/dev/null 2>&1 + echo "DONE" + ' 2>/dev/null)" || true + + if [[ "$result" == *"DONE"* ]]; then + local settings_file="${fake_home}/.claude-mem/settings.json" + local provider + provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")" + assert_eq "gemini" "$provider" "--provider flag: settings.json has provider=gemini" + + local api_key + api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_API_KEY);")" + assert_eq "test-end-to-end-key" "$api_key" "--provider flag: settings.json has correct API key" + else + test_fail "--provider flag: write_settings failed" + fi + + rm -rf "$fake_home" +} + +test_write_settings_via_provider_flag + +############################################################################### +# Test: --upgrade flag parsing +############################################################################### + +echo "" +echo "=== --upgrade flag parsing ===" + +test_upgrade_flag() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--upgrade" + source "$tmp" + rm -f "$tmp" + echo "UPGRADE=$UPGRADE_MODE" + ' 2>/dev/null)" || true + + assert_contains "$result" "UPGRADE=true" "--upgrade sets UPGRADE_MODE=true" +} + +test_upgrade_flag + +test_upgrade_flag_with_provider() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--upgrade" "--provider=gemini" "--api-key=upgrade-key" + source "$tmp" + rm -f "$tmp" + echo "UPGRADE=$UPGRADE_MODE" + echo "PROVIDER=$CLI_PROVIDER" + echo "KEY=$CLI_API_KEY" + ' 2>/dev/null)" || true + + assert_contains "$result" "UPGRADE=true" "--upgrade + --provider: upgrade flag parsed" + assert_contains "$result" "PROVIDER=gemini" "--upgrade + --provider: provider flag parsed" + assert_contains "$result" "KEY=upgrade-key" "--upgrade + --api-key: API key parsed" +} + +test_upgrade_flag_with_provider + +test_upgrade_not_set_by_default() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + echo "UPGRADE=${UPGRADE_MODE:-}" + ' 2>/dev/null)" || true + + assert_eq "UPGRADE=" "$result" "UPGRADE_MODE is empty by default" +} + +test_upgrade_not_set_by_default + +############################################################################### +# Test: is_claude_mem_installed() — upgrade detection +############################################################################### + +echo "" +echo "=== is_claude_mem_installed() ===" + +test_is_claude_mem_installed_found() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + CLAUDE_MEM_INSTALL_DIR="" + + # Create the expected directory structure + mkdir -p "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts" + touch "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts/worker-service.cjs" + + if is_claude_mem_installed; then + test_pass "is_claude_mem_installed returns true when plugin exists" + else + test_fail "is_claude_mem_installed should return true when plugin exists" + fi + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_is_claude_mem_installed_found + +test_is_claude_mem_installed_not_found() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + CLAUDE_MEM_INSTALL_DIR="" + + if is_claude_mem_installed; then + test_fail "is_claude_mem_installed should return false when plugin not found" + else + test_pass "is_claude_mem_installed returns false when plugin not found" + fi + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_is_claude_mem_installed_not_found + +############################################################################### +# Test: check_git() — git availability check +############################################################################### + +echo "" +echo "=== check_git() ===" + +test_check_git_available() { + # git should be available in test environment + if command -v git &>/dev/null; then + local output + output="$(check_git 2>&1)" || true + test_pass "check_git succeeds when git is installed" + else + test_pass "check_git test skipped (git not available)" + fi +} + +test_check_git_available + +test_check_git_not_available() { + # Test that check_git fails gracefully when git is not in PATH + local exit_code=0 + PLATFORM="macos" + bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + PATH="/nonexistent" + check_git + ' >/dev/null 2>&1 || exit_code=$? + + if [[ "$exit_code" -ne 0 ]]; then + test_pass "check_git exits with error when git is missing" + else + test_fail "check_git should exit with error when git is missing" + fi +} + +test_check_git_not_available + +test_check_git_macos_message() { + local output + output="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + PATH="/nonexistent" + PLATFORM="macos" + check_git + ' 2>&1)" || true + + assert_contains "$output" "xcode-select" "check_git suggests xcode-select on macOS" +} + +test_check_git_macos_message + +test_check_git_linux_message() { + local output + output="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + PATH="/nonexistent" + PLATFORM="linux" + check_git + ' 2>&1)" || true + + assert_contains "$output" "apt install git" "check_git suggests apt on Linux" +} + +test_check_git_linux_message + +############################################################################### +# Test: check_port_37777() — port conflict detection +############################################################################### + +echo "" +echo "=== check_port_37777() ===" + +test_check_port_function_exists() { + if declare -f check_port_37777 &>/dev/null; then + test_pass "Function check_port_37777() is defined" + else + test_fail "Function check_port_37777() should be defined" + fi +} + +test_check_port_function_exists + +############################################################################### +# Test: cleanup_on_exit() — global cleanup trap +############################################################################### + +echo "" +echo "=== cleanup_on_exit() ===" + +test_cleanup_trap_functions_exist() { + if declare -f register_cleanup_dir &>/dev/null; then + test_pass "Function register_cleanup_dir() is defined" + else + test_fail "Function register_cleanup_dir() should be defined" + fi + + if declare -f cleanup_on_exit &>/dev/null; then + test_pass "Function cleanup_on_exit() is defined" + else + test_fail "Function cleanup_on_exit() should be defined" + fi +} + +test_cleanup_trap_functions_exist + +test_register_cleanup_dir() { + local test_dir + test_dir="$(mktemp -d)" + + # Save existing cleanup dirs + local saved_dirs=("${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}") + CLEANUP_DIRS=() + + register_cleanup_dir "$test_dir" + + if [[ "${#CLEANUP_DIRS[@]}" -eq 1 ]] && [[ "${CLEANUP_DIRS[0]}" == "$test_dir" ]]; then + test_pass "register_cleanup_dir adds directory to CLEANUP_DIRS" + else + test_fail "register_cleanup_dir should add directory to CLEANUP_DIRS" + fi + + # Restore + CLEANUP_DIRS=("${saved_dirs[@]+"${saved_dirs[@]}"}") + rm -rf "$test_dir" +} + +test_register_cleanup_dir + +############################################################################### +# Test: ensure_jq_or_fallback() — JSON utility function +############################################################################### + +echo "" +echo "=== ensure_jq_or_fallback() ===" + +test_ensure_jq_or_fallback_exists() { + if declare -f ensure_jq_or_fallback &>/dev/null; then + test_pass "Function ensure_jq_or_fallback() is defined" + else + test_fail "Function ensure_jq_or_fallback() should be defined" + fi +} + +test_ensure_jq_or_fallback_exists + +test_ensure_jq_with_jq_available() { + if ! command -v jq &>/dev/null; then + test_pass "ensure_jq jq-path: skipped (jq not installed)" + return 0 + fi + + local tmp_json + tmp_json="$(mktemp)" + echo '{"name": "test", "value": 1}' > "$tmp_json" + + if ensure_jq_or_fallback "$tmp_json" '.name = "updated"'; then + local result + result="$(node -e "const j = JSON.parse(require('fs').readFileSync('${tmp_json}','utf8')); console.log(j.name);")" + assert_eq "updated" "$result" "ensure_jq_or_fallback updates JSON via jq" + else + test_fail "ensure_jq_or_fallback should succeed with jq available" + fi + + rm -f "$tmp_json" +} + +test_ensure_jq_with_jq_available + +############################################################################### +# Test: main() references new functions +############################################################################### + +echo "" +echo "=== main() references new functions ===" + +test_main_calls_check_port() { + if grep -q 'check_port_37777' "$INSTALL_SCRIPT"; then + test_pass "main() calls check_port_37777" + else + test_fail "main() should call check_port_37777" + fi +} + +test_main_calls_check_port + +test_main_calls_is_claude_mem_installed() { + if grep -q 'is_claude_mem_installed' "$INSTALL_SCRIPT"; then + test_pass "main() calls is_claude_mem_installed for upgrade detection" + else + test_fail "main() should call is_claude_mem_installed" + fi +} + +test_main_calls_is_claude_mem_installed + +test_main_references_upgrade_mode() { + if grep -q 'UPGRADE_MODE' "$INSTALL_SCRIPT"; then + test_pass "main() references UPGRADE_MODE" + else + test_fail "main() should reference UPGRADE_MODE" + fi +} + +test_main_references_upgrade_mode + +test_install_plugin_calls_check_git() { + if grep -q 'check_git' "$INSTALL_SCRIPT"; then + test_pass "install_plugin() calls check_git" + else + test_fail "install_plugin() should call check_git" + fi +} + +test_install_plugin_calls_check_git + +test_install_plugin_uses_register_cleanup() { + if grep -q 'register_cleanup_dir' "$INSTALL_SCRIPT"; then + test_pass "install_plugin() uses register_cleanup_dir" + else + test_fail "install_plugin() should use register_cleanup_dir" + fi +} + +test_install_plugin_uses_register_cleanup + +test_usage_comment_includes_upgrade() { + if grep -q '\-\-upgrade' "$INSTALL_SCRIPT"; then + test_pass "Usage comment documents --upgrade flag" + else + test_fail "Usage comment should document --upgrade flag" + fi +} + +test_usage_comment_includes_upgrade + +############################################################################### +# Test: Distribution readiness — URL, usage comment, SKILL.md reference +############################################################################### + +echo "" +echo "=== Distribution readiness ===" + +test_install_sh_has_shebang() { + local first_line + first_line="$(head -1 "$INSTALL_SCRIPT")" + assert_eq "#!/usr/bin/env bash" "$first_line" "install.sh has correct shebang line" +} + +test_install_sh_has_shebang + +test_install_sh_has_set_euo_pipefail() { + if grep -q 'set -euo pipefail' "$INSTALL_SCRIPT"; then + test_pass "install.sh uses set -euo pipefail for safety" + else + test_fail "install.sh should use set -euo pipefail" + fi +} + +test_install_sh_has_set_euo_pipefail + +test_install_sh_has_stable_url_in_usage() { + if grep -q 'raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh' "$INSTALL_SCRIPT"; then + test_pass "install.sh usage comment has stable raw.githubusercontent.com URL" + else + test_fail "install.sh should reference stable raw.githubusercontent.com URL in usage" + fi +} + +test_install_sh_has_stable_url_in_usage + +test_install_sh_documents_all_flags() { + local missing_flags=() + + for flag in "--non-interactive" "--upgrade" "--provider" "--api-key"; do + if ! grep -Fq -- "$flag" "$INSTALL_SCRIPT"; then + missing_flags+=("$flag") + fi + done + + if [[ ${#missing_flags[@]} -eq 0 ]]; then + test_pass "install.sh documents all CLI flags (--non-interactive, --upgrade, --provider, --api-key)" + else + test_fail "install.sh missing documentation for flags: ${missing_flags[*]}" + fi +} + +test_install_sh_documents_all_flags + +test_install_sh_has_installer_version() { + if grep -q 'INSTALLER_VERSION=' "$INSTALL_SCRIPT"; then + test_pass "install.sh defines INSTALLER_VERSION constant" + else + test_fail "install.sh should define INSTALLER_VERSION" + fi +} + +test_install_sh_has_installer_version + +test_skill_md_references_one_liner() { + local skill_file="${SCRIPT_DIR}/SKILL.md" + if [[ ! -f "$skill_file" ]]; then + test_fail "SKILL.md not found at ${skill_file}" + return + fi + + if grep -q 'curl -fsSL.*raw.githubusercontent.com.*install.sh | bash' "$skill_file"; then + test_pass "SKILL.md references the one-liner installer" + else + test_fail "SKILL.md should reference the one-liner installer" + fi +} + +test_skill_md_references_one_liner + +test_skill_md_has_quick_install_section() { + local skill_file="${SCRIPT_DIR}/SKILL.md" + if [[ ! -f "$skill_file" ]]; then + test_fail "SKILL.md not found at ${skill_file}" + return + fi + + if grep -q 'Quick Install' "$skill_file"; then + test_pass "SKILL.md has Quick Install section" + else + test_fail "SKILL.md should have Quick Install section" + fi +} + +test_skill_md_has_quick_install_section + +test_skill_md_documents_options() { + local skill_file="${SCRIPT_DIR}/SKILL.md" + if [[ ! -f "$skill_file" ]]; then + test_fail "SKILL.md not found at ${skill_file}" + return + fi + + local missing=() + for option in "--provider" "--non-interactive" "--upgrade"; do + if ! grep -Fq -- "$option" "$skill_file"; then + missing+=("$option") + fi + done + + if [[ ${#missing[@]} -eq 0 ]]; then + test_pass "SKILL.md documents all installer options (--provider, --non-interactive, --upgrade)" + else + test_fail "SKILL.md missing documentation for: ${missing[*]}" + fi +} + +test_skill_md_documents_options + +############################################################################### +# Summary +############################################################################### + +echo "" +echo "========================================" +echo "Results: ${TESTS_PASSED}/${TESTS_RUN} passed, ${TESTS_FAILED} failed" +echo "========================================" + +if [[ "$TESTS_FAILED" -gt 0 ]]; then + exit 1 +fi + +exit 0 diff --git a/openclaw/test-sse-consumer.js b/openclaw/test-sse-consumer.js new file mode 100644 index 00000000..eb1e88ec --- /dev/null +++ b/openclaw/test-sse-consumer.js @@ -0,0 +1,106 @@ +/** + * Smoke test for OpenClaw claude-mem plugin registration. + * Validates the plugin structure works independently of the full OpenClaw runtime. + * + * Run: node test-sse-consumer.js + */ + +import claudeMemPlugin from "./dist/index.js"; + +let registeredService = null; +const registeredCommands = new Map(); +const eventHandlers = new Map(); +const logs = []; + +const mockApi = { + id: "claude-mem", + name: "Claude-Mem (Persistent Memory)", + version: "1.0.0", + source: "/test/extensions/claude-mem/dist/index.js", + config: {}, + pluginConfig: {}, + logger: { + info: (message) => { logs.push(message); }, + warn: (message) => { logs.push(message); }, + error: (message) => { logs.push(message); }, + debug: (message) => { logs.push(message); }, + }, + registerService: (service) => { + registeredService = service; + }, + registerCommand: (command) => { + registeredCommands.set(command.name, command); + }, + on: (event, callback) => { + if (!eventHandlers.has(event)) { + eventHandlers.set(event, []); + } + eventHandlers.get(event).push(callback); + }, + runtime: { + channel: { + telegram: { sendMessageTelegram: async () => {} }, + discord: { sendMessageDiscord: async () => {} }, + signal: { sendMessageSignal: async () => {} }, + slack: { sendMessageSlack: async () => {} }, + whatsapp: { sendMessageWhatsApp: async () => {} }, + line: { sendMessageLine: async () => {} }, + }, + }, +}; + +// Call the default export with mock API +claudeMemPlugin(mockApi); + +// Verify registration +let failures = 0; + +if (!registeredService) { + console.error("FAIL: No service was registered"); + failures++; +} else if (registeredService.id !== "claude-mem-observation-feed") { + console.error( + `FAIL: Service ID is "${registeredService.id}", expected "claude-mem-observation-feed"` + ); + failures++; +} else { + console.log("OK: Service registered with id 'claude-mem-observation-feed'"); +} + +if (!registeredCommands.has("claude-mem-feed")) { + console.error("FAIL: No 'claude-mem-feed' command registered"); + failures++; +} else { + console.log("OK: Command registered with name 'claude-mem-feed'"); +} + +if (!registeredCommands.has("claude-mem-status")) { + console.error("FAIL: No 'claude-mem-status' command registered"); + failures++; +} else { + console.log("OK: Command registered with name 'claude-mem-status'"); +} + +const expectedEvents = ["before_agent_start", "tool_result_persist", "agent_end", "gateway_start"]; +for (const event of expectedEvents) { + if (!eventHandlers.has(event) || eventHandlers.get(event).length === 0) { + console.error(`FAIL: No handler registered for '${event}'`); + failures++; + } else { + console.log(`OK: Event handler registered for '${event}'`); + } +} + +if (!logs.some((l) => l.includes("plugin loaded"))) { + console.error("FAIL: Plugin did not log a load message"); + failures++; +} else { + console.log("OK: Plugin logged load message"); +} + +if (failures > 0) { + console.error(`\nFAIL: ${failures} check(s) failed`); + process.exit(1); +} else { + console.log("\nPASS: Plugin registers service, commands, and event handlers correctly"); +} diff --git a/openclaw/tsconfig.json b/openclaw/tsconfig.json new file mode 100644 index 00000000..ac91678f --- /dev/null +++ b/openclaw/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ES2022", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/package.json b/package.json index 30960cda..d3087b7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-mem", - "version": "9.0.10", + "version": "10.0.6", "description": "Memory compression system for Claude Code - persist context across sessions", "keywords": [ "claude", @@ -66,7 +66,7 @@ "claude-md:regenerate": "bun scripts/regenerate-claude-md.ts", "claude-md:dry-run": "bun scripts/regenerate-claude-md.ts --dry-run", "translate-readme": "bun scripts/translate-readme/cli.ts -v -o docs/i18n README.md", - "translate:tier1": "npm run translate-readme -- zh ja pt-br ko es de fr", + "translate:tier1": "npm run translate-readme -- zh zh-tw ja pt-br ko es de fr", "translate:tier2": "npm run translate-readme -- he ar ru pl cs nl tr uk", "translate:tier3": "npm run translate-readme -- vi id th hi bn ro sv", "translate:tier4": "npm run translate-readme -- it el hu fi da no", @@ -82,7 +82,18 @@ "test:search": "bun test tests/worker/search/", "test:context": "bun test tests/context/", "test:infra": "bun test tests/infrastructure/", - "test:server": "bun test tests/server/" + "test:server": "bun test tests/server/", + "prepublishOnly": "npm run build", + "release": "np", + "release:patch": "np patch --no-cleanup", + "release:minor": "np minor --no-cleanup", + "release:major": "np major --no-cleanup" + }, + "np": { + "yarn": false, + "contents": ".", + "testScript": "test", + "2fa": false }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.76", @@ -90,6 +101,7 @@ "@modelcontextprotocol/sdk": "^1.25.1", "ansi-to-html": "^0.7.2", "chromadb": "^3.2.2", + "dompurify": "^3.3.1", "express": "^4.18.2", "glob": "^11.0.3", "handlebars": "^4.7.8", @@ -100,11 +112,13 @@ }, "devDependencies": { "@types/cors": "^2.8.19", + "@types/dompurify": "^3.0.5", "@types/express": "^4.17.21", "@types/node": "^20.0.0", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "esbuild": "^0.27.2", + "np": "^11.0.2", "tsx": "^4.20.6", "typescript": "^5.3.0" } diff --git a/plugin/.claude-plugin/CLAUDE.md b/plugin/.claude-plugin/CLAUDE.md index 7d44f722..f9635d63 100644 --- a/plugin/.claude-plugin/CLAUDE.md +++ b/plugin/.claude-plugin/CLAUDE.md @@ -1,8 +1,6 @@ # Recent Activity - - ### Nov 6, 2025 | ID | Time | T | Title | Read | diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 34978b10..37039190 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "claude-mem", - "version": "9.0.10", + "version": "10.0.6", "description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions", "author": { "name": "Alex Newman" diff --git a/plugin/CLAUDE.md b/plugin/CLAUDE.md index 8afb2b06..fb048ea7 100644 --- a/plugin/CLAUDE.md +++ b/plugin/CLAUDE.md @@ -1,11 +1,9 @@ # Recent Activity - - ### Jan 10, 2026 | ID | Time | T | Title | Read | |----|------|---|-------|------| -| #39048 | 3:44 PM | 🔵 | Plugin directory contains commands folder | ~276 | +| #39050 | 3:44 PM | 🔵 | Plugin commands directory is empty | ~255 | \ No newline at end of file diff --git a/plugin/commands/CLAUDE.md b/plugin/commands/CLAUDE.md deleted file mode 100644 index 99923558..00000000 --- a/plugin/commands/CLAUDE.md +++ /dev/null @@ -1,18 +0,0 @@ - -# Recent Activity - - - -### Oct 25, 2025 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #2437 | 4:32 PM | 🟣 | Slash Command Files Created for Quick Settings Toggling | ~478 | - -### Jan 10, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #39052 | 3:44 PM | 🟣 | Commands added to plugin distribution | ~268 | -| #39050 | " | 🔵 | Plugin commands directory is empty | ~255 | - \ No newline at end of file diff --git a/plugin/hooks/CLAUDE.md b/plugin/hooks/CLAUDE.md index 32953a0d..f7ac464e 100644 --- a/plugin/hooks/CLAUDE.md +++ b/plugin/hooks/CLAUDE.md @@ -1,8 +1,6 @@ # Recent Activity - - ### Oct 25, 2025 | ID | Time | T | Title | Read | diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index 63c86260..efdcb2fb 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -1,6 +1,18 @@ { "description": "Claude-mem memory system hooks", "hooks": { + "Setup": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/setup.sh", + "timeout": 120 + } + ] + } + ], "SessionStart": [ { "matcher": "startup|clear|compact", @@ -12,17 +24,12 @@ }, { "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", "timeout": 60 }, { "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code context", - "timeout": 60 - }, - { - "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code user-message", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code context", "timeout": 60 } ] @@ -33,12 +40,12 @@ "hooks": [ { "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", "timeout": 60 }, { "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-init", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-init", "timeout": 60 } ] @@ -50,12 +57,12 @@ "hooks": [ { "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", "timeout": 60 }, { "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code observation", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code observation", "timeout": 120 } ] @@ -66,13 +73,18 @@ "hooks": [ { "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", "timeout": 60 }, { "type": "command", - "command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize", "timeout": 120 + }, + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-complete", + "timeout": 30 } ] } diff --git a/plugin/modes/CLAUDE.md b/plugin/modes/CLAUDE.md deleted file mode 100644 index adfdcb11..00000000 --- a/plugin/modes/CLAUDE.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Recent Activity - - - -*No recent activity* - \ No newline at end of file diff --git a/plugin/modes/code--ur.json b/plugin/modes/code--ur.json new file mode 100644 index 00000000..6155941b --- /dev/null +++ b/plugin/modes/code--ur.json @@ -0,0 +1,25 @@ +{ + "name": "Code Development (Urdu)", + "prompts": { + "footer": "IMPORTANT! DO NOT do any work right now other than generating this OBSERVATIONS from tool use messages - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one.\n\nNever reference yourself or your own actions. Do not output anything other than the observation content formatted in the XML structure above. All other output is ignored by the system, and the system has been designed to be smart about token usage. Please spend your tokens wisely on useful observations.\n\nRemember that we record these observations as a way of helping us stay on track with our progress, and to help us keep important decisions and changes at the forefront of our minds! :) Thank you so much for your help!\n\nLANGUAGE REQUIREMENTS: Please write the observation data in اردو", + + "xml_title_placeholder": "[**title**: بنیادی کام یا موضوع کو بیان کرنے والا مختصر عنوان]", + "xml_subtitle_placeholder": "[**subtitle**: ایک جملے میں وضاحت (زیادہ سے زیادہ 24 الفاظ)]", + "xml_fact_placeholder": "[مختصر، خود کفیل بیان]", + "xml_narrative_placeholder": "[**narrative**: مکمل تناسب: کیا کیا گیا، یہ کیسے کام کرتا ہے، یہ کیوں اہم ہے]", + "xml_concept_placeholder": "[علم-نوع-قسم]", + "xml_file_placeholder": "[فائل/کا/راستہ]", + + "xml_summary_request_placeholder": "[مختصر عنوان جو صارف کے درخواست اور بحث/کیے گئے کام کا خلاصہ بیان کرتا ہے]", + "xml_summary_investigated_placeholder": "[اب تک کیا دریافت کیا گیا ہے؟ کیا جائزہ لیا گیا ہے؟]", + "xml_summary_learned_placeholder": "[آپ نے چیزوں کے کام کرنے کے طریقے کے بارے میں کیا سیکھا؟]", + "xml_summary_completed_placeholder": "[اب تک کون سا کام مکمل ہوا ہے؟ کیا بھیجا گیا یا تبدیل کیا گیا؟]", + "xml_summary_next_steps_placeholder": "[اس سیشن میں آپ فعال طور پر کس پر کام کر رہے ہیں یا آگے کام کرنے کا منصوبہ بنا رہے ہیں؟]", + "xml_summary_notes_placeholder": "[موجودہ پیشرفت پر اضافی بصیرت یا نوٹس]", + + "continuation_instruction": "IMPORTANT: Continue generating observations from tool use messages using the XML structure below.\n\nLANGUAGE REQUIREMENTS: Please write the observation data in اردو", + + "summary_footer": "IMPORTANT! DO NOT do any work right now other than generating this next PROGRESS SUMMARY - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one.\n\nNever reference yourself or your own actions. Do not output anything other than the summary content formatted in the XML structure above. All other output is ignored by the system, and the system has been designed to be smart about token usage. Please spend your tokens wisely on useful summary content.\n\nThank you, this summary will be very useful for keeping track of our progress!\n\nLANGUAGE REQUIREMENTS: Please write ALL summary content (request, investigated, learned, completed, next_steps, notes) in اردو" + } +} + diff --git a/plugin/package.json b/plugin/package.json index ddb941df..2ea412bd 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -1,6 +1,6 @@ { "name": "claude-mem-plugin", - "version": "9.0.10", + "version": "10.0.6", "private": true, "description": "Runtime dependencies for claude-mem bundled hooks", "type": "module", diff --git a/plugin/scripts/CLAUDE.md b/plugin/scripts/CLAUDE.md index 813e380f..8df49353 100644 --- a/plugin/scripts/CLAUDE.md +++ b/plugin/scripts/CLAUDE.md @@ -1,8 +1,6 @@ # Recent Activity - - ### Dec 4, 2025 | ID | Time | T | Title | Read | diff --git a/plugin/scripts/bun-runner.js b/plugin/scripts/bun-runner.js new file mode 100644 index 00000000..80e8f44f --- /dev/null +++ b/plugin/scripts/bun-runner.js @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** + * Bun Runner - Finds and executes Bun even when not in PATH + * + * This script solves the fresh install problem where: + * 1. smart-install.js installs Bun to ~/.bun/bin/bun + * 2. But Bun isn't in PATH until terminal restart + * 3. Subsequent hooks fail because they can't find `bun` + * + * Usage: node bun-runner.js