Compare commits

..

9 Commits

Author SHA1 Message Date
Alex Newman 8f1a260d96 chore: bump version to 9.0.15
Includes PR #745 isolated credentials fix - prevents API key hijacking
from random project .env files by using centralized credentials from
~/.claude-mem/.env

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:21:01 -05:00
Alex Newman a69613b4e0 MAESTRO: Complete PR #745 auth method verification task
Verified auth method logging works correctly after merge:
- Rebuilt and synced local code (v9.0.14 release predated merge)
- Restarted worker with PR #745 EnvManager code
- Confirmed log shows: authMethod=Claude Code CLI (subscription billing)
- Verified getAuthMethodDescription() correctly detects no API key

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:17:35 -05:00
Alex Newman e7cae825bd MAESTRO: Mark PR #745 merge task complete
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:13:41 -05:00
Alex Newman 486570d2b8 Merge branch 'fix/isolated-credentials-733' into main
Fixes API key hijacking issue (#733) via centralized credential management.

- Add EnvManager.ts for isolated environment building
- SDKAgent passes isolated env to SDK query() to prevent API key pollution
- GeminiAgent and OpenRouterAgent use getCredential() helper
- Add CLAUDE_MEM_CLAUDE_AUTH_METHOD setting

Reviewed-by: bayanoj330-dev
Closes #745, Closes #733
2026-02-04 20:13:07 -05:00
Alex Newman ce576db0dc MAESTRO: Fix console usage in EnvManager.ts and verify build/tests pass
- Replaced console.warn/error with logger.warn/error calls per project standards
- Test suite enforces no console.* in background services (logs are invisible)
- Build verified: worker-service, mcp-server, context-generator, viewer UI all built
- All 797 tests pass (0 fail)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:12:27 -05:00
Alex Newman 53f6f57420 MAESTRO: Complete EnvManager.ts security and correctness review
Reviewed PR #745 EnvManager implementation:
- Security: Credentials isolated in ~/.claude-mem/.env, not process.env
- Correctness: Properly integrates with SDKAgent, GeminiAgent, OpenRouterAgent
- Code quality: Well-documented, type-safe, cross-platform compatible

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:10:45 -05:00
Alex Newman 814d2f6c03 MAESTRO: Mark PR #745 rebase task complete
- Resolved 4 conflicts during rebase onto main
- Merged zombie process cleanup (main) with isolated credentials (PR)
- SDKAgent.ts now has both spawnClaudeCodeProcess and env options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:10:00 -05:00
bigphoot 006ff40175 fix: use centralized credentials from ~/.claude-mem/.env to prevent API key hijacking (#733)
This fixes Issue #733 where claude-mem would incorrectly use ANTHROPIC_API_KEY from
random project .env files instead of the user's configured Claude Code CLI subscription.

Root cause: The SDK's `query()` function inherits from `process.env` when no `env`
option is passed. When users work in projects with their own .env files containing
API keys, the SDK would discover and use those keys, billing the wrong account.

Solution: Centralized credential management via ~/.claude-mem/.env

Changes:
- Add EnvManager.ts: Centralized credential storage and isolated env builder
- SDKAgent: Pass isolated env to SDK query() that only includes credentials from
  ~/.claude-mem/.env, not random keys from process.env inheritance
- GeminiAgent/OpenRouterAgent: Use getCredential() instead of process.env fallback
- SettingsDefaultsManager: Add CLAUDE_MEM_CLAUDE_AUTH_METHOD setting ('cli' | 'api')

How it works:
1. buildIsolatedEnv() creates a clean environment with only essential system vars
   (PATH, HOME, etc.) and credentials explicitly configured in ~/.claude-mem/.env
2. SDK subprocess runs with this isolated env, never seeing random API keys
3. If no ANTHROPIC_API_KEY is in ~/.claude-mem/.env, Claude Code CLI billing is used
4. Same pattern applied to Gemini/OpenRouter agents for consistency

This ensures claude-mem always uses the user's intended billing method, regardless
of what .env files exist in their working directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:09:41 -05:00
Alex Newman aedee33ca9 docs: update CHANGELOG.md for v9.0.14 2026-02-04 19:58:33 -05:00
17 changed files with 670 additions and 157 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "9.0.14",
"version": "9.0.15",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
@@ -0,0 +1,82 @@
# Phase 01: Merge PR #745 - Isolated Credentials
**PR:** https://github.com/thedotmack/claude-mem/pull/745
**Branch:** `fix/isolated-credentials-733`
**Status:** Has conflicts, needs rebase
**Review:** Approved by bayanoj330-dev
**Priority:** HIGH - Foundation for credential isolation, required by PR #847
## Summary
Fixes API key hijacking issue (#733) where SDK would use `ANTHROPIC_API_KEY` from random project `.env` files instead of Claude Code CLI subscription billing.
**Root Cause:** The SDK's `query()` function inherits from `process.env` when no `env` option is passed.
**Solution:** Centralized credential management via `~/.claude-mem/.env` with `EnvManager.ts`.
## Files Changed
| File | Change |
|------|--------|
| `src/shared/EnvManager.ts` | NEW: Centralized credential storage and isolated env builder |
| `src/services/worker/SDKAgent.ts` | Pass isolated env to SDK `query()` |
| `src/services/worker/GeminiAgent.ts` | Use `getCredential()` instead of `process.env` |
| `src/services/worker/OpenRouterAgent.ts` | Use `getCredential()` instead of `process.env` |
| `src/shared/SettingsDefaultsManager.ts` | Add `CLAUDE_MEM_CLAUDE_AUTH_METHOD` setting |
## Dependencies
- **None** - This is a foundation PR
## Tasks
- [x] Checkout PR branch `fix/isolated-credentials-733` and rebase onto main to resolve conflicts
- ✓ Resolved 4 conflicts (3 build artifacts, 1 source file)
- ✓ Merged both main's zombie process cleanup and PR's isolated credentials into SDKAgent.ts
- ✓ Commit 006ff401 now sits on top of main (aedee33c)
- [x] Review `EnvManager.ts` implementation for security and correctness
-**Security Assessment - PASS**:
- Credentials stored in user-private location (`~/.claude-mem/.env`) with standard file permissions
- `buildIsolatedEnv()` explicitly excludes `process.env` credentials, preventing Issue #733
- Only whitelisted essential system vars (PATH, HOME, NODE_ENV, etc.) are passed to subprocesses
- Quote stripping in `.env` parser handles both single and double quotes correctly
- No credential logging - keys are never written to logs
-**Correctness Assessment - PASS**:
- `loadClaudeMemEnv()` gracefully returns empty object if `.env` doesn't exist (enables CLI billing fallback)
- `saveClaudeMemEnv()` preserves existing keys and creates directory if needed
- `getCredential()` used correctly by GeminiAgent and OpenRouterAgent
- SDKAgent passes `isolatedEnv` to SDK query() options, blocking random API key pollution
- Auth method description properly reflects whether CLI billing or explicit API key is used
-**Code Quality - GOOD**:
- Well-documented with JSDoc comments explaining Issue #733 fix
- Type-safe with `ClaudeMemEnv` interface
- Essential vars list covers cross-platform needs (Windows, Linux, macOS)
- [x] Verify build succeeds after rebase
- ✓ Build completed successfully: worker-service (1788KB), mcp-server (332KB), context-generator (61KB), viewer UI
- [x] Run test suite to ensure no regressions
- ✓ Fixed console.log/console.error usage in EnvManager.ts (replaced with logger calls per project standards)
- ✓ All 797 tests pass (0 fail, 3 skip)
- [x] Merge PR #745 to main with admin override if needed
- ✓ Merged with `--no-ff` to preserve commit history
- ✓ Commit 486570d2 on main includes all 4 PR commits
- ✓ GitHub branch protection bypassed with admin privileges
- ✓ PR #745 auto-closed by GitHub upon detecting commits in main
- ✓ Build verified successful after merge
- [x] Verify auth method shows "Claude Code CLI (subscription billing)" in logs after merge
- ✓ Rebuilt and synced local code (v9.0.14 release predated PR merge, so needed fresh build)
- ✓ Restarted worker with PR #745 code
- ✓ Confirmed log output: `authMethod=Claude Code CLI (subscription billing)`
- ✓ Verified `getAuthMethodDescription()` correctly detects no API key in `~/.claude-mem/.env`
## Verification
```bash
# After merge, check logs for correct auth method
grep -i "authMethod" ~/.claude-mem/logs/*.log | tail -5
```
## Notes
- This PR creates the `EnvManager.ts` module that PR #847 depends on
- The isolated env approach ensures SDK subprocess never sees random API keys from parent process
- If no `ANTHROPIC_API_KEY` is in `~/.claude-mem/.env`, Claude Code CLI billing is used (default)
@@ -0,0 +1,57 @@
# Phase 02: Merge PR #820 - Health Check Endpoint Fix
**PR:** https://github.com/thedotmack/claude-mem/pull/820
**Branch:** `fix/health-check-endpoint-811`
**Status:** Has conflicts, needs rebase
**Review:** Approved by bayanoj330-dev
**Priority:** HIGH - Fixes 15-second timeout issue affecting all users
## Summary
Fixes the "Worker did not become ready within 15 seconds" timeout issue by changing health check functions from `/api/readiness` to `/api/health`.
**Root Cause:** `isWorkerHealthy()` and `waitForHealth()` were using `/api/readiness` which returns 503 until full initialization completes (including MCP connection which can take 5+ minutes). Hooks only have 15 seconds timeout.
**Solution:** Use `/api/health` (liveness check) which returns 200 as soon as HTTP server is listening.
## Files Changed
| File | Change |
|------|--------|
| `src/shared/worker-utils.ts` | Change `/api/readiness``/api/health` in `isWorkerHealthy()` |
| `src/services/infrastructure/HealthMonitor.ts` | Change `/api/readiness``/api/health` in `waitForHealth()` |
| `tests/infrastructure/health-monitor.test.ts` | Update test to expect `/api/health` |
## Dependencies
- **None** - Independent fix
## Fixes Issues
- #811
- #772
- #729
## Tasks
- [ ] Checkout PR branch `fix/health-check-endpoint-811` and rebase onto main to resolve conflicts
- [ ] Review the endpoint change logic in `worker-utils.ts` and `HealthMonitor.ts`
- [ ] Verify build succeeds after rebase
- [ ] Run health monitor tests: `npm test -- tests/infrastructure/health-monitor.test.ts`
- [ ] Merge PR #820 to main
- [ ] Manual verification: Kill worker and start fresh session - should not see 15-second timeout
## Verification
```bash
# After merge, verify hooks work during MCP initialization
# Start a fresh session and observe logs
tail -f ~/.claude-mem/logs/worker.log | grep -i "health"
```
## Notes
- This is a quick fix with minimal code changes
- The `/api/health` endpoint returns 200 as soon as Express is listening
- Background initialization continues after health check passes
- Related to PR #774 which had the same fix but has merge conflicts
@@ -0,0 +1,70 @@
# Phase 03: Merge PR #827 - Bun Runner for Fresh Install
**PR:** https://github.com/thedotmack/claude-mem/pull/827
**Branch:** `fix/fresh-install-bun-path-818`
**Status:** Has conflicts, needs rebase
**Review:** Approved by bayanoj330-dev
**Priority:** MEDIUM - Fixes fresh installation issues
## Summary
Fixes the fresh install issue where worker fails to start because Bun isn't in PATH yet after `smart-install.js` installs it.
**Root Cause:** On fresh installations:
1. `smart-install.js` installs Bun to `~/.bun/bin/bun`
2. Bun isn't in current shell's PATH until terminal restart
3. Hooks try to run `bun ...` directly and fail
4. Worker never starts, database never created
**Solution:** Introduce `bun-runner.js` - a Node.js script that finds Bun in common install locations (not just PATH) and runs commands with it.
## Files Changed
| File | Change |
|------|--------|
| `plugin/scripts/bun-runner.js` | NEW: Script to find and run Bun |
| `plugin/hooks/hooks.json` | Use `node bun-runner.js` instead of direct `bun` calls |
## Dependencies
- **None** - Independent fix
## Fixes Issues
- #818
## Bun Search Locations
The bun-runner checks these locations in order:
- 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\bin\bun.exe` with fallback
## Tasks
- [ ] Checkout PR branch `fix/fresh-install-bun-path-818` and rebase onto main to resolve conflicts
- [ ] Review `bun-runner.js` for correctness across platforms
- [ ] Verify hooks.json uses correct `node bun-runner.js` pattern
- [ ] Verify build succeeds after rebase
- [ ] Merge PR #827 to main
- [ ] Test on fresh install (uninstall claude-mem, reinstall) to verify Bun is found
## Verification
```bash
# After merge, verify bun-runner finds Bun
node plugin/scripts/bun-runner.js --version
# Check hooks.json uses bun-runner
grep -i "bun-runner" plugin/hooks/hooks.json
```
## Notes
- This is a surgical fix that doesn't change core functionality
- All hooks now go through the Node.js bun-runner script
- Cross-platform: Linux, macOS, Windows
- The bun-runner approach is more robust than relying on PATH
+23 -8
View File
@@ -2,6 +2,29 @@
All notable changes to claude-mem.
## [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
@@ -1323,11 +1346,3 @@ This release improves session efficiency by reducing the token overhead of MCP t
🤖 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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "9.0.14",
"version": "9.0.15",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "9.0.14",
"version": "9.0.15",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem-plugin",
"version": "9.0.14",
"version": "9.0.15",
"private": true,
"description": "Runtime dependencies for claude-mem bundled hooks",
"type": "module",
+5 -5
View File
@@ -1,12 +1,12 @@
"use strict";var _t=Object.create;var k=Object.defineProperty;var Et=Object.getOwnPropertyDescriptor;var gt=Object.getOwnPropertyNames;var Tt=Object.getPrototypeOf,ft=Object.prototype.hasOwnProperty;var St=(r,e)=>{for(var t in e)k(r,t,{get:e[t],enumerable:!0})},se=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of gt(e))!ft.call(r,n)&&n!==t&&k(r,n,{get:()=>e[n],enumerable:!(s=Et(e,n))||s.enumerable});return r};var v=(r,e,t)=>(t=r!=null?_t(Tt(r)):{},se(e||!r||!r.__esModule?k(t,"default",{value:r,enumerable:!0}):t,r)),ht=r=>se(k({},"__esModule",{value:!0}),r);var kt={};St(kt,{generateContext:()=>te});module.exports=ht(kt);var mt=v(require("path"),1),ut=require("os"),lt=require("fs");var _e=require("bun:sqlite");var h=require("path"),de=require("os"),pe=require("fs");var ce=require("url");var N=require("fs"),$=require("path"),oe=require("os");var re="bugfix,feature,refactor,discovery,decision,change",ne="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var y=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:(0,$.join)((0,oe.homedir)(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:re,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:ne,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(e){return this.DEFAULTS[e]}static getInt(e){let t=this.get(e);return parseInt(t,10)}static getBool(e){return this.get(e)==="true"}static loadFromFile(e){try{if(!(0,N.existsSync)(e)){let i=this.getAllDefaults();try{let a=(0,$.dirname)(e);(0,N.existsSync)(a)||(0,N.mkdirSync)(a,{recursive:!0}),(0,N.writeFileSync)(e,JSON.stringify(i,null,2),"utf-8"),console.log("[SETTINGS] Created settings file with defaults:",e)}catch(a){console.warn("[SETTINGS] Failed to create settings file, using in-memory defaults:",e,a)}return i}let t=(0,N.readFileSync)(e,"utf-8"),s=JSON.parse(t),n=s;if(s.env&&typeof s.env=="object"){n=s.env;try{(0,N.writeFileSync)(e,JSON.stringify(n,null,2),"utf-8"),console.log("[SETTINGS] Migrated settings file from nested to flat schema:",e)}catch(i){console.warn("[SETTINGS] Failed to auto-migrate settings file:",e,i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(t){return console.warn("[SETTINGS] Failed to load settings, using defaults:",e,t),this.getAllDefaults()}}};var I=require("fs"),M=require("path"),ae=require("os"),G=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(G||{}),ie=(0,M.join)((0,ae.homedir)(),".claude-mem"),H=class{level=null;useColor;logFilePath=null;logFileInitialized=!1;constructor(){this.useColor=process.stdout.isTTY??!1}ensureLogFileInitialized(){if(!this.logFileInitialized){this.logFileInitialized=!0;try{let e=(0,M.join)(ie,"logs");(0,I.existsSync)(e)||(0,I.mkdirSync)(e,{recursive:!0});let t=new Date().toISOString().split("T")[0];this.logFilePath=(0,M.join)(e,`claude-mem-${t}.log`)}catch(e){console.error("[LOGGER] Failed to initialize log file:",e),this.logFilePath=null}}}getLevel(){if(this.level===null)try{let e=(0,M.join)(ie,"settings.json");if((0,I.existsSync)(e)){let t=(0,I.readFileSync)(e,"utf-8"),n=(JSON.parse(t).CLAUDE_MEM_LOG_LEVEL||"INFO").toUpperCase();this.level=G[n]??1}else this.level=1}catch{this.level=1}return this.level}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.getLevel()===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;let s=t;if(typeof t=="string")try{s=JSON.parse(t)}catch{s=t}if(e==="Bash"&&s.command)return`${e}(${s.command})`;if(s.file_path)return`${e}(${s.file_path})`;if(s.notebook_path)return`${e}(${s.notebook_path})`;if(e==="Glob"&&s.pattern)return`${e}(${s.pattern})`;if(e==="Grep"&&s.pattern)return`${e}(${s.pattern})`;if(s.url)return`${e}(${s.url})`;if(s.query)return`${e}(${s.query})`;if(e==="Task"){if(s.subagent_type)return`${e}(${s.subagent_type})`;if(s.description)return`${e}(${s.description})`}return e==="Skill"&&s.skill?`${e}(${s.skill})`:e==="LSP"&&s.operation?`${e}(${s.operation})`:e}formatTimestamp(e){let t=e.getFullYear(),s=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0"),o=String(e.getHours()).padStart(2,"0"),i=String(e.getMinutes()).padStart(2,"0"),a=String(e.getSeconds()).padStart(2,"0"),d=String(e.getMilliseconds()).padStart(3,"0");return`${t}-${s}-${n} ${o}:${i}:${a}.${d}`}log(e,t,s,n,o){if(e<this.getLevel())return;this.ensureLogFileInitialized();let i=this.formatTimestamp(new Date),a=G[e].padEnd(5),d=t.padEnd(6),c="";n?.correlationId?c=`[${n.correlationId}] `:n?.sessionId&&(c=`[session-${n.sessionId}] `);let u="";o!=null&&(o instanceof Error?u=this.getLevel()===0?`
"use strict";var _t=Object.create;var k=Object.defineProperty;var Et=Object.getOwnPropertyDescriptor;var gt=Object.getOwnPropertyNames;var Tt=Object.getPrototypeOf,ft=Object.prototype.hasOwnProperty;var St=(r,e)=>{for(var t in e)k(r,t,{get:e[t],enumerable:!0})},se=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of gt(e))!ft.call(r,n)&&n!==t&&k(r,n,{get:()=>e[n],enumerable:!(s=Et(e,n))||s.enumerable});return r};var v=(r,e,t)=>(t=r!=null?_t(Tt(r)):{},se(e||!r||!r.__esModule?k(t,"default",{value:r,enumerable:!0}):t,r)),ht=r=>se(k({},"__esModule",{value:!0}),r);var kt={};St(kt,{generateContext:()=>te});module.exports=ht(kt);var mt=v(require("path"),1),ut=require("os"),lt=require("fs");var _e=require("bun:sqlite");var h=require("path"),de=require("os"),pe=require("fs");var ce=require("url");var N=require("fs"),$=require("path"),oe=require("os");var re="bugfix,feature,refactor,discovery,decision,change",ne="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var y=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_CLAUDE_AUTH_METHOD:"cli",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:(0,$.join)((0,oe.homedir)(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:re,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:ne,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(e){return this.DEFAULTS[e]}static getInt(e){let t=this.get(e);return parseInt(t,10)}static getBool(e){return this.get(e)==="true"}static loadFromFile(e){try{if(!(0,N.existsSync)(e)){let i=this.getAllDefaults();try{let a=(0,$.dirname)(e);(0,N.existsSync)(a)||(0,N.mkdirSync)(a,{recursive:!0}),(0,N.writeFileSync)(e,JSON.stringify(i,null,2),"utf-8"),console.log("[SETTINGS] Created settings file with defaults:",e)}catch(a){console.warn("[SETTINGS] Failed to create settings file, using in-memory defaults:",e,a)}return i}let t=(0,N.readFileSync)(e,"utf-8"),s=JSON.parse(t),n=s;if(s.env&&typeof s.env=="object"){n=s.env;try{(0,N.writeFileSync)(e,JSON.stringify(n,null,2),"utf-8"),console.log("[SETTINGS] Migrated settings file from nested to flat schema:",e)}catch(i){console.warn("[SETTINGS] Failed to auto-migrate settings file:",e,i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(t){return console.warn("[SETTINGS] Failed to load settings, using defaults:",e,t),this.getAllDefaults()}}};var I=require("fs"),M=require("path"),ae=require("os"),H=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(H||{}),ie=(0,M.join)((0,ae.homedir)(),".claude-mem"),G=class{level=null;useColor;logFilePath=null;logFileInitialized=!1;constructor(){this.useColor=process.stdout.isTTY??!1}ensureLogFileInitialized(){if(!this.logFileInitialized){this.logFileInitialized=!0;try{let e=(0,M.join)(ie,"logs");(0,I.existsSync)(e)||(0,I.mkdirSync)(e,{recursive:!0});let t=new Date().toISOString().split("T")[0];this.logFilePath=(0,M.join)(e,`claude-mem-${t}.log`)}catch(e){console.error("[LOGGER] Failed to initialize log file:",e),this.logFilePath=null}}}getLevel(){if(this.level===null)try{let e=(0,M.join)(ie,"settings.json");if((0,I.existsSync)(e)){let t=(0,I.readFileSync)(e,"utf-8"),n=(JSON.parse(t).CLAUDE_MEM_LOG_LEVEL||"INFO").toUpperCase();this.level=H[n]??1}else this.level=1}catch{this.level=1}return this.level}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.getLevel()===0?`${e.message}
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;let s=t;if(typeof t=="string")try{s=JSON.parse(t)}catch{s=t}if(e==="Bash"&&s.command)return`${e}(${s.command})`;if(s.file_path)return`${e}(${s.file_path})`;if(s.notebook_path)return`${e}(${s.notebook_path})`;if(e==="Glob"&&s.pattern)return`${e}(${s.pattern})`;if(e==="Grep"&&s.pattern)return`${e}(${s.pattern})`;if(s.url)return`${e}(${s.url})`;if(s.query)return`${e}(${s.query})`;if(e==="Task"){if(s.subagent_type)return`${e}(${s.subagent_type})`;if(s.description)return`${e}(${s.description})`}return e==="Skill"&&s.skill?`${e}(${s.skill})`:e==="LSP"&&s.operation?`${e}(${s.operation})`:e}formatTimestamp(e){let t=e.getFullYear(),s=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0"),o=String(e.getHours()).padStart(2,"0"),i=String(e.getMinutes()).padStart(2,"0"),a=String(e.getSeconds()).padStart(2,"0"),d=String(e.getMilliseconds()).padStart(3,"0");return`${t}-${s}-${n} ${o}:${i}:${a}.${d}`}log(e,t,s,n,o){if(e<this.getLevel())return;this.ensureLogFileInitialized();let i=this.formatTimestamp(new Date),a=H[e].padEnd(5),d=t.padEnd(6),c="";n?.correlationId?c=`[${n.correlationId}] `:n?.sessionId&&(c=`[session-${n.sessionId}] `);let u="";o!=null&&(o instanceof Error?u=this.getLevel()===0?`
${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?u=`
`+JSON.stringify(o,null,2):u=" "+this.formatData(o));let l="";if(n){let{sessionId:E,memorySessionId:T,correlationId:b,..._}=n;Object.keys(_).length>0&&(l=` {${Object.entries(_).map(([f,C])=>`${f}=${C}`).join(", ")}}`)}let g=`[${i}] [${a}] [${d}] ${c}${s}${l}${u}`;if(this.logFilePath)try{(0,I.appendFileSync)(this.logFilePath,g+`
`,"utf8")}catch(E){process.stderr.write(`[LOGGER] Failed to write to log file: ${E}
`)}else process.stderr.write(g+`
`)}debug(e,t,s,n){this.log(0,e,t,s,n)}info(e,t,s,n){this.log(1,e,t,s,n)}warn(e,t,s,n){this.log(2,e,t,s,n)}error(e,t,s,n){this.log(3,e,t,s,n)}dataIn(e,t,s,n){this.info(e,`\u2192 ${t}`,s,n)}dataOut(e,t,s,n){this.info(e,`\u2190 ${t}`,s,n)}success(e,t,s,n){this.info(e,`\u2713 ${t}`,s,n)}failure(e,t,s,n){this.error(e,`\u2717 ${t}`,s,n)}timing(e,t,s,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${s}ms`})}happyPathError(e,t,s,n,o=""){let c=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),u=c?`${c[1].split("/").pop()}:${c[2]}`:"unknown",l={...s,location:u};return this.warn(e,`[HAPPY-PATH] ${t}`,l,n),o}},m=new H;var Ct={};function bt(){return typeof __dirname<"u"?__dirname:(0,h.dirname)((0,ce.fileURLToPath)(Ct.url))}var Ot=bt(),R=y.get("CLAUDE_MEM_DATA_DIR"),W=process.env.CLAUDE_CONFIG_DIR||(0,h.join)((0,de.homedir)(),".claude"),Ht=(0,h.join)(R,"archives"),Wt=(0,h.join)(R,"logs"),Yt=(0,h.join)(R,"trash"),Vt=(0,h.join)(R,"backups"),qt=(0,h.join)(R,"modes"),Kt=(0,h.join)(R,"settings.json"),me=(0,h.join)(R,"claude-mem.db"),Jt=(0,h.join)(R,"vector-db"),zt=(0,h.join)(R,"observer-sessions"),Qt=(0,h.join)(W,"settings.json"),Zt=(0,h.join)(W,"commands"),es=(0,h.join)(W,"CLAUDE.md");function ue(r){(0,pe.mkdirSync)(r,{recursive:!0})}function le(){return(0,h.join)(Ot,"..")}var U=class{db;constructor(e=me){e!==":memory:"&&ue(R),this.db=new _e.Database(e),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn(),this.createPendingMessagesTable(),this.renameSessionIdColumns(),this.repairSessionIdColumnRename(),this.addFailedAtEpochColumn()}initializeSchema(){this.db.run(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),u=c?`${c[1].split("/").pop()}:${c[2]}`:"unknown",l={...s,location:u};return this.warn(e,`[HAPPY-PATH] ${t}`,l,n),o}},m=new G;var Ct={};function bt(){return typeof __dirname<"u"?__dirname:(0,h.dirname)((0,ce.fileURLToPath)(Ct.url))}var Ot=bt(),R=y.get("CLAUDE_MEM_DATA_DIR"),W=process.env.CLAUDE_CONFIG_DIR||(0,h.join)((0,de.homedir)(),".claude"),Gt=(0,h.join)(R,"archives"),Wt=(0,h.join)(R,"logs"),Yt=(0,h.join)(R,"trash"),Vt=(0,h.join)(R,"backups"),qt=(0,h.join)(R,"modes"),Kt=(0,h.join)(R,"settings.json"),me=(0,h.join)(R,"claude-mem.db"),Jt=(0,h.join)(R,"vector-db"),zt=(0,h.join)(R,"observer-sessions"),Qt=(0,h.join)(W,"settings.json"),Zt=(0,h.join)(W,"commands"),es=(0,h.join)(W,"CLAUDE.md");function ue(r){(0,pe.mkdirSync)(r,{recursive:!0})}function le(){return(0,h.join)(Ot,"..")}var U=class{db;constructor(e=me){e!==":memory:"&&ue(R),this.db=new _e.Database(e),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn(),this.createPendingMessagesTable(),this.renameSessionIdColumns(),this.repairSessionIdColumnRename(),this.addFailedAtEpochColumn()}initializeSchema(){this.db.run(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -534,11 +534,11 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?u=`
`).all(...e,t.sessionCount+V)}function Nt(r){return r.replace(/\//g,"-")}function It(r){try{if(!(0,F.existsSync)(r))return{userMessage:"",assistantMessage:""};let e=(0,F.readFileSync)(r,"utf-8").trim();if(!e)return{userMessage:"",assistantMessage:""};let t=e.split(`
`).filter(n=>n.trim()),s="";for(let n=t.length-1;n>=0;n--)try{let o=t[n];if(!o.includes('"type":"assistant"'))continue;let i=JSON.parse(o);if(i.type==="assistant"&&i.message?.content&&Array.isArray(i.message.content)){let a="";for(let d of i.message.content)d.type==="text"&&(a+=d.text);if(a=a.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,"").trim(),a){s=a;break}}}catch(o){m.debug("PARSER","Skipping malformed transcript line",{lineIndex:n},o);continue}return{userMessage:"",assistantMessage:s}}catch(e){return m.failure("WORKER","Failed to extract prior messages from transcript",{transcriptPath:r},e),{userMessage:"",assistantMessage:""}}}function Q(r,e,t,s){if(!e.showLastMessage||r.length===0)return{userMessage:"",assistantMessage:""};let n=r.find(d=>d.memory_session_id!==t);if(!n)return{userMessage:"",assistantMessage:""};let o=n.memory_session_id,i=Nt(s),a=he.default.join((0,be.homedir)(),".claude","projects",i,`${o}.jsonl`);return It(a)}function Re(r,e){let t=e[0]?.id;return r.map((s,n)=>{let o=n===0?null:e[n+1];return{...s,displayEpoch:o?o.created_at_epoch:s.created_at_epoch,displayTime:o?o.created_at:s.created_at,shouldShowLink:s.id!==t}})}function Z(r,e){let t=[...r.map(s=>({type:"observation",data:s})),...e.map(s=>({type:"summary",data:s}))];return t.sort((s,n)=>{let o=s.type==="observation"?s.data.created_at_epoch:s.data.displayEpoch,i=n.type==="observation"?n.data.created_at_epoch:n.data.displayEpoch;return o-i}),t}function Ne(r,e){return new Set(r.slice(0,e).map(t=>t.id))}function Ie(){let r=new Date,e=r.toLocaleDateString("en-CA"),t=r.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0}).toLowerCase().replace(" ",""),s=r.toLocaleTimeString("en-US",{timeZoneName:"short"}).split(" ").pop();return`${e} ${t} ${s}`}function ye(r){return[`# [${r}] recent context, ${Ie()}`,""]}function Ae(){return[`**Legend:** session-request | ${O.getInstance().getActiveMode().observation_types.map(t=>`${t.emoji} ${t.id}`).join(" | ")}`,""]}function ve(){return["**Column Key**:","- **Read**: Tokens to read this observation (cost to learn it now)","- **Work**: Tokens spent on work that produced this record ( research, building, deciding)",""]}function Me(){return["**Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.","","When you need implementation details, rationale, or debugging context:","- Use MCP tools (search, get_observations) to fetch full observations on-demand","- Critical types ( bugfix, decision) often need detailed fetching","- Trust this index over re-reading code for past decisions and learnings",""]}function Le(r,e){let t=[];if(t.push("**Context Economics**:"),t.push(`- Loading: ${r.totalObservations} observations (${r.totalReadTokens.toLocaleString()} tokens to read)`),t.push(`- Work investment: ${r.totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions`),r.totalDiscoveryTokens>0&&(e.showSavingsAmount||e.showSavingsPercent)){let s="- Your savings: ";e.showSavingsAmount&&e.showSavingsPercent?s+=`${r.savings.toLocaleString()} tokens (${r.savingsPercent}% reduction from reuse)`:e.showSavingsAmount?s+=`${r.savings.toLocaleString()} tokens`:s+=`${r.savingsPercent}% reduction from reuse`,t.push(s)}return t.push(""),t}function De(r){return[`### ${r}`,""]}function xe(r){return[`**${r}**`,"| ID | Time | T | Title | Read | Work |","|----|------|---|-------|------|------|"]}function ke(r,e,t){let s=r.title||"Untitled",n=O.getInstance().getTypeIcon(r.type),{readTokens:o,discoveryDisplay:i}=A(r,t),a=t.showReadTokens?`~${o}`:"",d=t.showWorkTokens?i:"";return`| #${r.id} | ${e||'"'} | ${n} | ${s} | ${a} | ${d} |`}function $e(r,e,t,s){let n=[],o=r.title||"Untitled",i=O.getInstance().getTypeIcon(r.type),{readTokens:a,discoveryDisplay:d}=A(r,s);n.push(`**#${r.id}** ${e||'"'} ${i} **${o}**`),t&&(n.push(""),n.push(t),n.push(""));let c=[];return s.showReadTokens&&c.push(`Read: ~${a}`),s.showWorkTokens&&c.push(`Work: ${d}`),c.length>0&&n.push(c.join(", ")),n.push(""),n}function Ue(r,e){let t=`${r.request||"Session started"} (${e})`;return[`**#S${r.id}** ${t}`,""]}function D(r,e){return e?[`**${r}**: ${e}`,""]:[]}function we(r){return r.assistantMessage?["","---","","**Previously**","",`A: ${r.assistantMessage}`,""]:[]}function Pe(r,e){return["",`Access ${Math.round(r/1e3)}k tokens of past research & decisions for just ${e.toLocaleString()}t. Use MCP search tools to access memories by ID.`]}function Fe(r){return`# [${r}] recent context, ${Ie()}
No previous sessions found for this project yet.`}function je(){let r=new Date,e=r.toLocaleDateString("en-CA"),t=r.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0}).toLowerCase().replace(" ",""),s=r.toLocaleTimeString("en-US",{timeZoneName:"short"}).split(" ").pop();return`${e} ${t} ${s}`}function Xe(r){return["",`${p.bright}${p.cyan}[${r}] recent context, ${je()}${p.reset}`,`${p.gray}${"\u2500".repeat(60)}${p.reset}`,""]}function Be(){let e=O.getInstance().getActiveMode().observation_types.map(t=>`${t.emoji} ${t.id}`).join(" | ");return[`${p.dim}Legend: session-request | ${e}${p.reset}`,""]}function Ge(){return[`${p.bright}Column Key${p.reset}`,`${p.dim} Read: Tokens to read this observation (cost to learn it now)${p.reset}`,`${p.dim} Work: Tokens spent on work that produced this record ( research, building, deciding)${p.reset}`,""]}function He(){return[`${p.dim}Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${p.reset}`,"",`${p.dim}When you need implementation details, rationale, or debugging context:${p.reset}`,`${p.dim} - Use MCP tools (search, get_observations) to fetch full observations on-demand${p.reset}`,`${p.dim} - Critical types ( bugfix, decision) often need detailed fetching${p.reset}`,`${p.dim} - Trust this index over re-reading code for past decisions and learnings${p.reset}`,""]}function We(r,e){let t=[];if(t.push(`${p.bright}${p.cyan}Context Economics${p.reset}`),t.push(`${p.dim} Loading: ${r.totalObservations} observations (${r.totalReadTokens.toLocaleString()} tokens to read)${p.reset}`),t.push(`${p.dim} Work investment: ${r.totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions${p.reset}`),r.totalDiscoveryTokens>0&&(e.showSavingsAmount||e.showSavingsPercent)){let s=" Your savings: ";e.showSavingsAmount&&e.showSavingsPercent?s+=`${r.savings.toLocaleString()} tokens (${r.savingsPercent}% reduction from reuse)`:e.showSavingsAmount?s+=`${r.savings.toLocaleString()} tokens`:s+=`${r.savingsPercent}% reduction from reuse`,t.push(`${p.green}${s}${p.reset}`)}return t.push(""),t}function Ye(r){return[`${p.bright}${p.cyan}${r}${p.reset}`,""]}function Ve(r){return[`${p.dim}${r}${p.reset}`]}function qe(r,e,t,s){let n=r.title||"Untitled",o=O.getInstance().getTypeIcon(r.type),{readTokens:i,discoveryTokens:a,workEmoji:d}=A(r,s),c=t?`${p.dim}${e}${p.reset}`:" ".repeat(e.length),u=s.showReadTokens&&i>0?`${p.dim}(~${i}t)${p.reset}`:"",l=s.showWorkTokens&&a>0?`${p.dim}(${d} ${a.toLocaleString()}t)${p.reset}`:"";return` ${p.dim}#${r.id}${p.reset} ${c} ${o} ${n} ${u} ${l}`}function Ke(r,e,t,s,n){let o=[],i=r.title||"Untitled",a=O.getInstance().getTypeIcon(r.type),{readTokens:d,discoveryTokens:c,workEmoji:u}=A(r,n),l=t?`${p.dim}${e}${p.reset}`:" ".repeat(e.length),g=n.showReadTokens&&d>0?`${p.dim}(~${d}t)${p.reset}`:"",E=n.showWorkTokens&&c>0?`${p.dim}(${u} ${c.toLocaleString()}t)${p.reset}`:"";return o.push(` ${p.dim}#${r.id}${p.reset} ${l} ${a} ${p.bright}${i}${p.reset}`),s&&o.push(` ${p.dim}${s}${p.reset}`),(g||E)&&o.push(` ${g} ${E}`),o.push(""),o}function Je(r,e){let t=`${r.request||"Session started"} (${e})`;return[`${p.yellow}#S${r.id}${p.reset} ${t}`,""]}function x(r,e,t){return e?[`${t}${r}:${p.reset} ${e}`,""]:[]}function ze(r){return r.assistantMessage?["","---","",`${p.bright}${p.magenta}Previously${p.reset}`,"",`${p.dim}A: ${r.assistantMessage}${p.reset}`,""]:[]}function Qe(r,e){let t=Math.round(r/1e3);return["",`${p.dim}Access ${t}k tokens of past research & decisions for just ${e.toLocaleString()}t. Use MCP search tools to access memories by ID.${p.reset}`]}function Ze(r){return`
No previous sessions found for this project yet.`}function je(){let r=new Date,e=r.toLocaleDateString("en-CA"),t=r.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0}).toLowerCase().replace(" ",""),s=r.toLocaleTimeString("en-US",{timeZoneName:"short"}).split(" ").pop();return`${e} ${t} ${s}`}function Xe(r){return["",`${p.bright}${p.cyan}[${r}] recent context, ${je()}${p.reset}`,`${p.gray}${"\u2500".repeat(60)}${p.reset}`,""]}function Be(){let e=O.getInstance().getActiveMode().observation_types.map(t=>`${t.emoji} ${t.id}`).join(" | ");return[`${p.dim}Legend: session-request | ${e}${p.reset}`,""]}function He(){return[`${p.bright}Column Key${p.reset}`,`${p.dim} Read: Tokens to read this observation (cost to learn it now)${p.reset}`,`${p.dim} Work: Tokens spent on work that produced this record ( research, building, deciding)${p.reset}`,""]}function Ge(){return[`${p.dim}Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${p.reset}`,"",`${p.dim}When you need implementation details, rationale, or debugging context:${p.reset}`,`${p.dim} - Use MCP tools (search, get_observations) to fetch full observations on-demand${p.reset}`,`${p.dim} - Critical types ( bugfix, decision) often need detailed fetching${p.reset}`,`${p.dim} - Trust this index over re-reading code for past decisions and learnings${p.reset}`,""]}function We(r,e){let t=[];if(t.push(`${p.bright}${p.cyan}Context Economics${p.reset}`),t.push(`${p.dim} Loading: ${r.totalObservations} observations (${r.totalReadTokens.toLocaleString()} tokens to read)${p.reset}`),t.push(`${p.dim} Work investment: ${r.totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions${p.reset}`),r.totalDiscoveryTokens>0&&(e.showSavingsAmount||e.showSavingsPercent)){let s=" Your savings: ";e.showSavingsAmount&&e.showSavingsPercent?s+=`${r.savings.toLocaleString()} tokens (${r.savingsPercent}% reduction from reuse)`:e.showSavingsAmount?s+=`${r.savings.toLocaleString()} tokens`:s+=`${r.savingsPercent}% reduction from reuse`,t.push(`${p.green}${s}${p.reset}`)}return t.push(""),t}function Ye(r){return[`${p.bright}${p.cyan}${r}${p.reset}`,""]}function Ve(r){return[`${p.dim}${r}${p.reset}`]}function qe(r,e,t,s){let n=r.title||"Untitled",o=O.getInstance().getTypeIcon(r.type),{readTokens:i,discoveryTokens:a,workEmoji:d}=A(r,s),c=t?`${p.dim}${e}${p.reset}`:" ".repeat(e.length),u=s.showReadTokens&&i>0?`${p.dim}(~${i}t)${p.reset}`:"",l=s.showWorkTokens&&a>0?`${p.dim}(${d} ${a.toLocaleString()}t)${p.reset}`:"";return` ${p.dim}#${r.id}${p.reset} ${c} ${o} ${n} ${u} ${l}`}function Ke(r,e,t,s,n){let o=[],i=r.title||"Untitled",a=O.getInstance().getTypeIcon(r.type),{readTokens:d,discoveryTokens:c,workEmoji:u}=A(r,n),l=t?`${p.dim}${e}${p.reset}`:" ".repeat(e.length),g=n.showReadTokens&&d>0?`${p.dim}(~${d}t)${p.reset}`:"",E=n.showWorkTokens&&c>0?`${p.dim}(${u} ${c.toLocaleString()}t)${p.reset}`:"";return o.push(` ${p.dim}#${r.id}${p.reset} ${l} ${a} ${p.bright}${i}${p.reset}`),s&&o.push(` ${p.dim}${s}${p.reset}`),(g||E)&&o.push(` ${g} ${E}`),o.push(""),o}function Je(r,e){let t=`${r.request||"Session started"} (${e})`;return[`${p.yellow}#S${r.id}${p.reset} ${t}`,""]}function x(r,e,t){return e?[`${t}${r}:${p.reset} ${e}`,""]:[]}function ze(r){return r.assistantMessage?["","---","",`${p.bright}${p.magenta}Previously${p.reset}`,"",`${p.dim}A: ${r.assistantMessage}${p.reset}`,""]:[]}function Qe(r,e){let t=Math.round(r/1e3);return["",`${p.dim}Access ${t}k tokens of past research & decisions for just ${e.toLocaleString()}t. Use MCP search tools to access memories by ID.${p.reset}`]}function Ze(r){return`
${p.bright}${p.cyan}[${r}] recent context, ${je()}${p.reset}
${p.gray}${"\u2500".repeat(60)}${p.reset}
${p.dim}No previous sessions found for this project yet.${p.reset}
`}function et(r,e,t,s){let n=[];return s?n.push(...Xe(r)):n.push(...ye(r)),s?n.push(...Be()):n.push(...Ae()),s?n.push(...Ge()):n.push(...ve()),s?n.push(...He()):n.push(...Me()),P(t)&&(s?n.push(...We(e,t)):n.push(...Le(e,t))),n}var ee=v(require("path"),1);function B(r){if(!r)return[];try{let e=JSON.parse(r);return Array.isArray(e)?e:[]}catch(e){return m.debug("PARSER","Failed to parse JSON array, using empty fallback",{preview:r?.substring(0,50)},e),[]}}function st(r){return new Date(r).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function rt(r){return new Date(r).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function nt(r){return new Date(r).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function tt(r,e){return ee.default.isAbsolute(r)?ee.default.relative(e,r):r}function ot(r,e,t){let s=B(r);if(s.length>0)return tt(s[0],e);if(t){let n=B(t);if(n.length>0)return tt(n[0],e)}return"General"}function yt(r){let e=new Map;for(let s of r){let n=s.type==="observation"?s.data.created_at:s.data.displayTime,o=nt(n);e.has(o)||e.set(o,[]),e.get(o).push(s)}let t=Array.from(e.entries()).sort((s,n)=>{let o=new Date(s[0]).getTime(),i=new Date(n[0]).getTime();return o-i});return new Map(t)}function At(r,e){return e.fullObservationField==="narrative"?r.narrative:r.facts?B(r.facts).join(`
`}function et(r,e,t,s){let n=[];return s?n.push(...Xe(r)):n.push(...ye(r)),s?n.push(...Be()):n.push(...Ae()),s?n.push(...He()):n.push(...ve()),s?n.push(...Ge()):n.push(...Me()),P(t)&&(s?n.push(...We(e,t)):n.push(...Le(e,t))),n}var ee=v(require("path"),1);function B(r){if(!r)return[];try{let e=JSON.parse(r);return Array.isArray(e)?e:[]}catch(e){return m.debug("PARSER","Failed to parse JSON array, using empty fallback",{preview:r?.substring(0,50)},e),[]}}function st(r){return new Date(r).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function rt(r){return new Date(r).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function nt(r){return new Date(r).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function tt(r,e){return ee.default.isAbsolute(r)?ee.default.relative(e,r):r}function ot(r,e,t){let s=B(r);if(s.length>0)return tt(s[0],e);if(t){let n=B(t);if(n.length>0)return tt(n[0],e)}return"General"}function yt(r){let e=new Map;for(let s of r){let n=s.type==="observation"?s.data.created_at:s.data.displayTime,o=nt(n);e.has(o)||e.set(o,[]),e.get(o).push(s)}let t=Array.from(e.entries()).sort((s,n)=>{let o=new Date(s[0]).getTime(),i=new Date(n[0]).getTime();return o-i});return new Map(t)}function At(r,e){return e.fullObservationField==="narrative"?r.narrative:r.facts?B(r.facts).join(`
`):null}function vt(r,e,t,s,n,o){let i=[];o?i.push(...Ye(r)):i.push(...De(r));let a=null,d="",c=!1;for(let u of e)if(u.type==="summary"){c&&(i.push(""),c=!1,a=null,d="");let l=u.data,g=st(l.displayTime);o?i.push(...Je(l,g)):i.push(...Ue(l,g))}else{let l=u.data,g=ot(l.files_modified,n,l.files_read),E=rt(l.created_at),T=E!==d,b=T?E:"";d=E;let _=t.has(l.id);if(g!==a&&(c&&i.push(""),o?i.push(...Ve(g)):i.push(...xe(g)),a=g,c=!0),_){let S=At(l,s);o?i.push(...Ke(l,E,T,S,s)):(c&&!o&&(i.push(""),c=!1),i.push(...$e(l,b,S,s)),a=null)}else o?i.push(qe(l,E,T,s)):i.push(ke(l,b,s))}return c&&i.push(""),i}function it(r,e,t,s,n){let o=[],i=yt(r);for(let[a,d]of i)o.push(...vt(a,d,e,t,s,n));return o}function at(r,e,t){return!(!r.showLastSummary||!e||!!!(e.investigated||e.learned||e.completed||e.next_steps)||t&&e.created_at_epoch<=t.created_at_epoch)}function dt(r,e){let t=[];return e?(t.push(...x("Investigated",r.investigated,p.blue)),t.push(...x("Learned",r.learned,p.yellow)),t.push(...x("Completed",r.completed,p.green)),t.push(...x("Next Steps",r.next_steps,p.magenta))):(t.push(...D("Investigated",r.investigated)),t.push(...D("Learned",r.learned)),t.push(...D("Completed",r.completed)),t.push(...D("Next Steps",r.next_steps))),t}function pt(r,e){return e?ze(r):we(r)}function ct(r,e,t){return!P(e)||r.totalDiscoveryTokens<=0||r.savings<=0?[]:t?Qe(r.totalDiscoveryTokens,r.totalReadTokens):Pe(r.totalDiscoveryTokens,r.totalReadTokens)}var Mt=mt.default.join((0,ut.homedir)(),".claude","plugins","marketplaces","thedotmack","plugin",".install-version");function Lt(){try{return new U}catch(r){if(r.code==="ERR_DLOPEN_FAILED"){try{(0,lt.unlinkSync)(Mt)}catch(e){m.debug("SYSTEM","Marker file cleanup failed (may not exist)",{},e)}return m.error("SYSTEM","Native module rebuild needed - restart Claude Code to auto-fix"),null}throw r}}function Dt(r,e){return e?Ze(r):Fe(r)}function xt(r,e,t,s,n,o,i){let a=[],d=K(e);a.push(...et(r,d,s,i));let c=t.slice(0,s.sessionCount),u=Re(c,t),l=Z(e,u),g=Ne(e,s.fullObservationCount);a.push(...it(l,g,s,n,i));let E=t[0],T=e[0];at(s,E,T)&&a.push(...dt(E,i));let b=Q(e,s,o,n);return a.push(...pt(b,i)),a.push(...ct(d,s,i)),a.join(`
`).trimEnd()}async function te(r,e=!1){let t=Y(),s=r?.cwd??process.cwd(),n=ge(s),o=r?.projects||[n],i=Lt();if(!i)return"";try{let a=o.length>1?Oe(i,o,t):J(i,n,t),d=o.length>1?Ce(i,o,t):z(i,n,t);return a.length===0&&d.length===0?Dt(n,e):xt(n,a,d,t,s,r?.session_id,e)}finally{i.close()}}0&&(module.exports={generateContext});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-6
View File
@@ -1,9 +1,3 @@
<claude-mem-context>
# Recent Activity
### Dec 26, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32964 | 10:53 PM | 🔵 | No CSS or SCSS files found in entire src/ui directory | ~225 |
</claude-mem-context>
+7 -3
View File
@@ -17,6 +17,7 @@ import { SessionManager } from './SessionManager.js';
import { logger } from '../../utils/logger.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { getCredential } from '../../shared/EnvManager.js';
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
import { ModeManager } from '../domain/ModeManager.js';
import {
@@ -367,13 +368,15 @@ export class GeminiAgent {
/**
* Get Gemini configuration from settings or environment
* Issue #733: Uses centralized ~/.claude-mem/.env for credentials, not random project .env files
*/
private getGeminiConfig(): { apiKey: string; model: GeminiModel; rateLimitingEnabled: boolean } {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
// API key: check settings first, then environment variable
const apiKey = settings.CLAUDE_MEM_GEMINI_API_KEY || process.env.GEMINI_API_KEY || '';
// API key: check settings first, then centralized claude-mem .env (NOT process.env)
// This prevents Issue #733 where random project .env files could interfere
const apiKey = settings.CLAUDE_MEM_GEMINI_API_KEY || getCredential('GEMINI_API_KEY') || '';
// Model: from settings or default, with validation
const defaultModel: GeminiModel = 'gemini-2.5-flash';
@@ -407,11 +410,12 @@ export class GeminiAgent {
/**
* Check if Gemini is available (has API key configured)
* Issue #733: Uses centralized ~/.claude-mem/.env, not random project .env files
*/
export function isGeminiAvailable(): boolean {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
return !!(settings.CLAUDE_MEM_GEMINI_API_KEY || process.env.GEMINI_API_KEY);
return !!(settings.CLAUDE_MEM_GEMINI_API_KEY || getCredential('GEMINI_API_KEY'));
}
/**
+7 -3
View File
@@ -17,6 +17,7 @@ import { logger } from '../../utils/logger.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import { getCredential } from '../../shared/EnvManager.js';
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
import { ModeManager } from '../domain/ModeManager.js';
import {
@@ -409,13 +410,15 @@ export class OpenRouterAgent {
/**
* Get OpenRouter configuration from settings or environment
* Issue #733: Uses centralized ~/.claude-mem/.env for credentials, not random project .env files
*/
private getOpenRouterConfig(): { apiKey: string; model: string; siteUrl?: string; appName?: string } {
const settingsPath = USER_SETTINGS_PATH;
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
// API key: check settings first, then environment variable
const apiKey = settings.CLAUDE_MEM_OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY || '';
// API key: check settings first, then centralized claude-mem .env (NOT process.env)
// This prevents Issue #733 where random project .env files could interfere
const apiKey = settings.CLAUDE_MEM_OPENROUTER_API_KEY || getCredential('OPENROUTER_API_KEY') || '';
// Model: from settings or default
const model = settings.CLAUDE_MEM_OPENROUTER_MODEL || 'xiaomi/mimo-v2-flash:free';
@@ -430,11 +433,12 @@ export class OpenRouterAgent {
/**
* Check if OpenRouter is available (has API key configured)
* Issue #733: Uses centralized ~/.claude-mem/.env, not random project .env files
*/
export function isOpenRouterAvailable(): boolean {
const settingsPath = USER_SETTINGS_PATH;
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
return !!(settings.CLAUDE_MEM_OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY);
return !!(settings.CLAUDE_MEM_OPENROUTER_API_KEY || getCredential('OPENROUTER_API_KEY'));
}
/**
+12 -2
View File
@@ -17,6 +17,7 @@ import { logger } from '../../utils/logger.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH, OBSERVER_SESSIONS_DIR, ensureDir } from '../../shared/paths.js';
import { buildIsolatedEnv, getAuthMethodDescription } from '../../shared/EnvManager.js';
import type { ActiveSession, SDKUserMessage } from '../worker-types.js';
import { ModeManager } from '../domain/ModeManager.js';
import { processAgentResponse, type WorkerRef } from './agents/index.js';
@@ -76,13 +77,20 @@ export class SDKAgent {
// NEVER use contentSessionId for resume - that would inject messages into the user's transcript!
const hasRealMemorySessionId = !!session.memorySessionId;
// Build isolated environment from ~/.claude-mem/.env
// This prevents Issue #733: random ANTHROPIC_API_KEY from project .env files
// being used instead of the configured auth method (CLI subscription or explicit API key)
const isolatedEnv = buildIsolatedEnv();
const authMethod = getAuthMethodDescription();
logger.info('SDK', 'Starting SDK query', {
sessionDbId: session.sessionDbId,
contentSessionId: session.contentSessionId,
memorySessionId: session.memorySessionId,
hasRealMemorySessionId,
resume_parameter: hasRealMemorySessionId ? session.memorySessionId : '(none - fresh start)',
lastPromptNumber: session.lastPromptNumber
lastPromptNumber: session.lastPromptNumber,
authMethod
});
// Debug-level alignment logs for detailed tracing
@@ -103,6 +111,7 @@ export class SDKAgent {
// Use custom spawn to capture PIDs for zombie process cleanup (Issue #737)
// Use dedicated cwd to isolate observer sessions from user's `claude --resume` list
ensureDir(OBSERVER_SESSIONS_DIR);
// CRITICAL: Pass isolated env to prevent Issue #733 (API key pollution from project .env files)
const queryResult = query({
prompt: messageGenerator,
options: {
@@ -118,7 +127,8 @@ export class SDKAgent {
abortController: session.abortController,
pathToClaudeCodeExecutable: claudePath,
// Custom spawn function captures PIDs to fix zombie process accumulation
spawnClaudeCodeProcess: createPidCapturingSpawn(session.sessionDbId)
spawnClaudeCodeProcess: createPidCapturingSpawn(session.sessionDbId),
env: isolatedEnv // Use isolated credentials from ~/.claude-mem/.env, not process.env
}
});
+274
View File
@@ -0,0 +1,274 @@
/**
* EnvManager - Centralized environment variable management for claude-mem
*
* Provides isolated credential storage in ~/.claude-mem/.env
* This ensures claude-mem uses its own configured credentials,
* not random ANTHROPIC_API_KEY values from project .env files.
*
* Issue #733: SDK was auto-discovering API keys from user's shell environment,
* causing memory operations to bill personal API accounts instead of CLI subscription.
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import { logger } from '../utils/logger.js';
// Path to claude-mem's centralized .env file
const DATA_DIR = join(homedir(), '.claude-mem');
export const ENV_FILE_PATH = join(DATA_DIR, '.env');
// Essential system environment variables that subprocesses need to function
const ESSENTIAL_SYSTEM_VARS = [
'PATH',
'HOME',
'USER',
'SHELL',
'TMPDIR',
'TMP',
'TEMP',
'LANG',
'LC_ALL',
'LC_CTYPE',
// Node.js specific
'NODE_ENV',
'NODE_PATH',
// Platform specific
'SYSTEMROOT', // Windows
'WINDIR', // Windows
'PROGRAMFILES', // Windows
'APPDATA', // Windows
'LOCALAPPDATA', // Windows
'XDG_RUNTIME_DIR', // Linux
'XDG_CONFIG_HOME', // Linux
'XDG_DATA_HOME', // Linux
// Claude Code specific (not credentials)
'CLAUDE_CONFIG_DIR',
'CLAUDE_CODE_DEBUG_LOGS_DIR',
];
// Credential keys that claude-mem manages
export const MANAGED_CREDENTIAL_KEYS = [
'ANTHROPIC_API_KEY',
'GEMINI_API_KEY',
'OPENROUTER_API_KEY',
];
export interface ClaudeMemEnv {
// Credentials (optional - empty means use CLI billing for Claude)
ANTHROPIC_API_KEY?: string;
GEMINI_API_KEY?: string;
OPENROUTER_API_KEY?: string;
}
/**
* Parse a .env file content into key-value pairs
*/
function parseEnvFile(content: string): Record<string, string> {
const result: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith('#')) continue;
// Parse KEY=value format
const eqIndex = trimmed.indexOf('=');
if (eqIndex === -1) continue;
const key = trimmed.slice(0, eqIndex).trim();
let value = trimmed.slice(eqIndex + 1).trim();
// Remove surrounding quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
if (key) {
result[key] = value;
}
}
return result;
}
/**
* Serialize key-value pairs to .env file format
*/
function serializeEnvFile(env: Record<string, string>): string {
const lines: string[] = [
'# claude-mem credentials',
'# This file stores API keys for claude-mem memory agent',
'# Edit this file or use claude-mem settings to configure',
'',
];
for (const [key, value] of Object.entries(env)) {
if (value) {
// Quote values that contain spaces or special characters
const needsQuotes = /[\s#=]/.test(value);
lines.push(`${key}=${needsQuotes ? `"${value}"` : value}`);
}
}
return lines.join('\n') + '\n';
}
/**
* Load credentials from ~/.claude-mem/.env
* Returns empty object if file doesn't exist (means use CLI billing)
*/
export function loadClaudeMemEnv(): ClaudeMemEnv {
if (!existsSync(ENV_FILE_PATH)) {
return {};
}
try {
const content = readFileSync(ENV_FILE_PATH, 'utf-8');
const parsed = parseEnvFile(content);
// Only return managed credential keys
const result: ClaudeMemEnv = {};
if (parsed.ANTHROPIC_API_KEY) result.ANTHROPIC_API_KEY = parsed.ANTHROPIC_API_KEY;
if (parsed.GEMINI_API_KEY) result.GEMINI_API_KEY = parsed.GEMINI_API_KEY;
if (parsed.OPENROUTER_API_KEY) result.OPENROUTER_API_KEY = parsed.OPENROUTER_API_KEY;
return result;
} catch (error) {
logger.warn('ENV', 'Failed to load .env file', { path: ENV_FILE_PATH }, error as Error);
return {};
}
}
/**
* Save credentials to ~/.claude-mem/.env
*/
export function saveClaudeMemEnv(env: ClaudeMemEnv): void {
try {
// Ensure directory exists
if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true });
}
// Load existing to preserve any extra keys
const existing = existsSync(ENV_FILE_PATH)
? parseEnvFile(readFileSync(ENV_FILE_PATH, 'utf-8'))
: {};
// Update with new values
const updated: Record<string, string> = { ...existing };
// Only update managed keys
if (env.ANTHROPIC_API_KEY !== undefined) {
if (env.ANTHROPIC_API_KEY) {
updated.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
} else {
delete updated.ANTHROPIC_API_KEY;
}
}
if (env.GEMINI_API_KEY !== undefined) {
if (env.GEMINI_API_KEY) {
updated.GEMINI_API_KEY = env.GEMINI_API_KEY;
} else {
delete updated.GEMINI_API_KEY;
}
}
if (env.OPENROUTER_API_KEY !== undefined) {
if (env.OPENROUTER_API_KEY) {
updated.OPENROUTER_API_KEY = env.OPENROUTER_API_KEY;
} else {
delete updated.OPENROUTER_API_KEY;
}
}
writeFileSync(ENV_FILE_PATH, serializeEnvFile(updated), 'utf-8');
} catch (error) {
logger.error('ENV', 'Failed to save .env file', { path: ENV_FILE_PATH }, error as Error);
throw error;
}
}
/**
* Build a clean, isolated environment for spawning SDK subprocesses
*
* This is the key function that prevents Issue #733:
* - Includes only essential system variables (PATH, HOME, etc.)
* - Adds credentials ONLY from claude-mem's .env file
* - Does NOT inherit random ANTHROPIC_API_KEY from user's shell
*
* @param includeCredentials - Whether to include API keys (default: true)
*/
export function buildIsolatedEnv(includeCredentials: boolean = true): Record<string, string> {
const isolatedEnv: Record<string, string> = {};
// 1. Copy essential system variables from current process
for (const key of ESSENTIAL_SYSTEM_VARS) {
const value = process.env[key];
if (value !== undefined) {
isolatedEnv[key] = value;
}
}
// 2. Add SDK entrypoint marker
isolatedEnv.CLAUDE_CODE_ENTRYPOINT = 'sdk-ts';
// 3. Add credentials from claude-mem's .env file (NOT from process.env)
if (includeCredentials) {
const credentials = loadClaudeMemEnv();
// Only add ANTHROPIC_API_KEY if explicitly configured in claude-mem
// If not configured, CLI billing will be used (via pathToClaudeCodeExecutable)
if (credentials.ANTHROPIC_API_KEY) {
isolatedEnv.ANTHROPIC_API_KEY = credentials.ANTHROPIC_API_KEY;
}
// Note: GEMINI_API_KEY and OPENROUTER_API_KEY are handled by their respective agents
if (credentials.GEMINI_API_KEY) {
isolatedEnv.GEMINI_API_KEY = credentials.GEMINI_API_KEY;
}
if (credentials.OPENROUTER_API_KEY) {
isolatedEnv.OPENROUTER_API_KEY = credentials.OPENROUTER_API_KEY;
}
}
return isolatedEnv;
}
/**
* Get a specific credential from claude-mem's .env
* Returns undefined if not set (which means use default/CLI billing)
*/
export function getCredential(key: keyof ClaudeMemEnv): string | undefined {
const env = loadClaudeMemEnv();
return env[key];
}
/**
* Set a specific credential in claude-mem's .env
* Pass empty string to remove the credential
*/
export function setCredential(key: keyof ClaudeMemEnv, value: string): void {
const env = loadClaudeMemEnv();
env[key] = value || undefined;
saveClaudeMemEnv(env);
}
/**
* Check if claude-mem has an Anthropic API key configured
* If false, it means CLI billing should be used
*/
export function hasAnthropicApiKey(): boolean {
const env = loadClaudeMemEnv();
return !!env.ANTHROPIC_API_KEY;
}
/**
* Get auth method description for logging
*/
export function getAuthMethodDescription(): string {
if (hasAnthropicApiKey()) {
return 'API key (from ~/.claude-mem/.env)';
}
return 'Claude Code CLI (subscription billing)';
}
+2
View File
@@ -20,6 +20,7 @@ export interface SettingsDefaults {
CLAUDE_MEM_SKIP_TOOLS: string;
// AI Provider Configuration
CLAUDE_MEM_PROVIDER: string; // 'claude' | 'gemini' | 'openrouter'
CLAUDE_MEM_CLAUDE_AUTH_METHOD: string; // 'cli' | 'api' - how Claude provider authenticates
CLAUDE_MEM_GEMINI_API_KEY: string;
CLAUDE_MEM_GEMINI_MODEL: string; // 'gemini-2.5-flash-lite' | 'gemini-2.5-flash' | 'gemini-3-flash'
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: string; // 'true' | 'false' - enable rate limiting for free tier
@@ -64,6 +65,7 @@ export class SettingsDefaultsManager {
CLAUDE_MEM_SKIP_TOOLS: 'ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion',
// AI Provider Configuration
CLAUDE_MEM_PROVIDER: 'claude', // Default to Claude
CLAUDE_MEM_CLAUDE_AUTH_METHOD: 'cli', // Default to CLI subscription billing (not API key)
CLAUDE_MEM_GEMINI_API_KEY: '', // Empty by default, can be set via UI or env
CLAUDE_MEM_GEMINI_MODEL: 'gemini-2.5-flash-lite', // Default Gemini model (highest free tier RPM)
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: 'true', // Rate limiting ON by default for free tier users