Skip to content

Commit af9d484

Browse files
feat(diffmem): add DiffMem integration for long-term user memory
- Add DiffMemClient for communicating with DiffMem server API - Add MCP tool handlers: diffmem_get_user_context, diffmem_store_learning, diffmem_search, diffmem_status - Add session hooks for auto-fetch on start and sync on end - Add UnifiedContextAssembler with token budget management - Add PrivacyFilter with strict/standard/permissive modes - Update initialization script with --with-diffmem flag - Add npm scripts: diffmem:start, diffmem:setup Two-layer architecture: - DiffMem: long-term user knowledge (preferences, expertise, patterns) - StackMemory: session/task context (frames, decisions, tool calls) Token budget allocation: 20% user knowledge, 70% task context, 10% system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9159158 commit af9d484

14 files changed

Lines changed: 2600 additions & 1 deletion

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,7 @@ service-account.json
115115
# Claude settings with secrets
116116
.claude/settings.local.json
117117
external/
118+
119+
# DiffMem subpackage (cloned repo)
120+
packages/diffmem/
121+
.env.diffmem

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
"daemons:stop": "node scripts/claude-sm-autostart.js stop",
8080
"daemon:session": "node dist/daemon/session-daemon.js",
8181
"daemon:session:start": "node dist/daemon/session-daemon.js --session-id",
82+
"diffmem:start": "PYTHONPATH=packages/diffmem/src packages/diffmem/.venv/bin/uvicorn diffmem.server:app --host 127.0.0.1 --port 8000",
83+
"diffmem:setup": "cd packages/diffmem && python3 -m venv .venv && .venv/bin/pip install -r requirements-server.txt -e .",
8284
"daemon:start": "node dist/daemon/unified-daemon.js",
8385
"daemon:stop": "node dist/cli/index.js daemon stop",
8486
"daemon:status": "node dist/cli/index.js daemon status",

scripts/initialize.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import {
1313
import { join } from 'path';
1414
import { execSync } from 'child_process';
1515
import chalk from 'chalk';
16+
17+
// Parse CLI args for --with-diffmem flag
18+
const enableDiffMem = process.argv.includes('--with-diffmem');
1619
// Type-safe environment variable access
1720
function getEnv(key: string, defaultValue?: string): string {
1821
const value = process.env[key];
@@ -125,14 +128,93 @@ try {
125128
console.log(chalk.yellow('⚠') + ' Build failed - run npm run build manually');
126129
}
127130

131+
// 7. Optional: Initialize DiffMem for long-term user memory
132+
if (enableDiffMem) {
133+
console.log(chalk.blue('\n🧠 Setting up DiffMem integration...\n'));
134+
135+
const diffmemDir = join(stackDir, 'diffmem');
136+
const diffmemStorageDir = join(diffmemDir, 'storage');
137+
const diffmemWorktreesDir = join(diffmemDir, 'worktrees');
138+
139+
// Create DiffMem directories
140+
mkdirSync(diffmemStorageDir, { recursive: true });
141+
mkdirSync(diffmemWorktreesDir, { recursive: true });
142+
143+
// Initialize git repo for storage
144+
if (!existsSync(join(diffmemStorageDir, '.git'))) {
145+
try {
146+
execSync('git init', { cwd: diffmemStorageDir, stdio: 'pipe' });
147+
execSync('git commit --allow-empty -m "Initialize DiffMem storage"', {
148+
cwd: diffmemStorageDir,
149+
stdio: 'pipe',
150+
});
151+
console.log(chalk.green('✓') + ' Initialized DiffMem storage repository');
152+
} catch {
153+
console.log(
154+
chalk.yellow('⚠') + ' Failed to initialize git repo for DiffMem'
155+
);
156+
}
157+
}
158+
159+
// Create DiffMem config
160+
const diffmemConfigPath = join(diffmemDir, 'config.json');
161+
const diffmemConfig = {
162+
enabled: true,
163+
endpoint: 'http://localhost:8000',
164+
userId: process.env['USER'] || 'default',
165+
storagePath: diffmemStorageDir,
166+
worktreePath: diffmemWorktreesDir,
167+
};
168+
writeFileSync(diffmemConfigPath, JSON.stringify(diffmemConfig, null, 2));
169+
console.log(chalk.green('✓') + ' Created DiffMem configuration');
170+
171+
// Add DiffMem env vars to .env file
172+
const envPath = join(projectRoot, '.env');
173+
const diffmemEnvVars = `
174+
# DiffMem Configuration (Long-term User Memory)
175+
DIFFMEM_ENABLED=true
176+
DIFFMEM_ENDPOINT=http://localhost:8000
177+
DIFFMEM_USER_ID=${process.env['USER'] || 'default'}
178+
DIFFMEM_STORAGE_PATH=${diffmemStorageDir}
179+
DIFFMEM_WORKTREE_PATH=${diffmemWorktreesDir}
180+
`;
181+
182+
if (existsSync(envPath)) {
183+
const envContent = readFileSync(envPath, 'utf-8');
184+
if (!envContent.includes('DIFFMEM_')) {
185+
appendFileSync(envPath, diffmemEnvVars);
186+
console.log(chalk.green('✓') + ' Added DiffMem configuration to .env');
187+
}
188+
} else {
189+
writeFileSync(envPath, diffmemEnvVars.trim() + '\n');
190+
console.log(chalk.green('✓') + ' Created .env with DiffMem configuration');
191+
}
192+
193+
console.log(chalk.green('✓') + ' DiffMem integration configured');
194+
console.log(
195+
chalk.gray(
196+
' Note: DiffMem server must be running for user memory features'
197+
)
198+
);
199+
}
200+
128201
console.log(chalk.green.bold('\n✅ StackMemory initialized successfully!\n'));
129202
console.log(chalk.gray('Next steps:'));
130203
console.log(chalk.gray('1. Add the MCP configuration above to Claude Code'));
131204
console.log(chalk.gray('2. Restart Claude Code'));
132205
console.log(chalk.gray('3. Start using context tracking!'));
206+
if (enableDiffMem) {
207+
console.log(chalk.gray('4. Start DiffMem server: npm run diffmem:start'));
208+
}
133209
console.log(chalk.gray('\nUseful commands:'));
134210
console.log(
135211
chalk.cyan(' npm run mcp:dev') + ' - Start MCP server in dev mode'
136212
);
137213
console.log(chalk.cyan(' npm run status') + ' - Check StackMemory status');
138-
console.log(chalk.cyan(' npm run analyze') + ' - Analyze context usage\n');
214+
console.log(chalk.cyan(' npm run analyze') + ' - Analyze context usage');
215+
if (enableDiffMem) {
216+
console.log(
217+
chalk.cyan(' npm run diffmem:start') + ' - Start DiffMem server'
218+
);
219+
}
220+
console.log();

src/core/retrieval/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
export * from './types.js';
77
export * from './summary-generator.js';
88
export * from './llm-context-retrieval.js';
9+
export * from './privacy-filter.js';
10+
export * from './unified-context-assembler.js';
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* Privacy Filter for Unified Context Assembly
3+
* Filters sensitive patterns from content before including in context
4+
*/
5+
6+
import { SENSITIVE_PATTERNS } from '../security/input-sanitizer.js';
7+
8+
export type PrivacyMode = 'strict' | 'standard' | 'permissive';
9+
10+
export interface PrivacyFilterConfig {
11+
mode: PrivacyMode;
12+
}
13+
14+
export interface FilterResult {
15+
filtered: string;
16+
redactedCount: number;
17+
}
18+
19+
/**
20+
* Additional patterns for privacy filtering beyond security patterns
21+
* Organized by strictness level
22+
*/
23+
const PRIVACY_PATTERNS: Record<PrivacyMode, RegExp[]> = {
24+
// Permissive: Only critical secrets
25+
permissive: [],
26+
27+
// Standard: Secrets + PII basics
28+
standard: [
29+
// Email addresses
30+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
31+
// Phone numbers (various formats)
32+
/\b(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b/g,
33+
// SSN-like patterns
34+
/\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g,
35+
// IP addresses (v4)
36+
/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g,
37+
],
38+
39+
// Strict: All standard + additional PII
40+
strict: [
41+
// Credit card-like numbers (13-19 digits, optionally separated)
42+
/\b(?:\d{4}[-.\s]?){3,4}\d{1,4}\b/g,
43+
// Date of birth patterns (various formats)
44+
/\b(?:0?[1-9]|1[0-2])[-/](?:0?[1-9]|[12][0-9]|3[01])[-/](?:19|20)\d{2}\b/g,
45+
// AWS-style keys
46+
/\b(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}\b/g,
47+
// Private key markers
48+
/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
49+
// JWT tokens
50+
/\beyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g,
51+
// URLs with credentials
52+
/(?:https?|ftp):\/\/[^\s:@]+:[^\s:@]+@[^\s]+/gi,
53+
// MAC addresses
54+
/\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g,
55+
// UUID-like patterns that might be sensitive
56+
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,
57+
],
58+
};
59+
60+
/**
61+
* Privacy Filter class for filtering sensitive content
62+
*/
63+
export class PrivacyFilter {
64+
private config: PrivacyFilterConfig;
65+
private patterns: RegExp[];
66+
67+
constructor(config: PrivacyFilterConfig) {
68+
this.config = config;
69+
this.patterns = this.buildPatternList();
70+
}
71+
72+
/**
73+
* Build the complete list of patterns based on privacy mode
74+
*/
75+
private buildPatternList(): RegExp[] {
76+
const patterns: RegExp[] = [];
77+
78+
// Always include security patterns (API keys, tokens, etc.)
79+
patterns.push(...SENSITIVE_PATTERNS);
80+
81+
// Add mode-specific patterns
82+
switch (this.config.mode) {
83+
case 'strict':
84+
patterns.push(...PRIVACY_PATTERNS.strict);
85+
patterns.push(...PRIVACY_PATTERNS.standard);
86+
break;
87+
case 'standard':
88+
patterns.push(...PRIVACY_PATTERNS.standard);
89+
break;
90+
case 'permissive':
91+
// Only security patterns (already added above)
92+
break;
93+
}
94+
95+
return patterns;
96+
}
97+
98+
/**
99+
* Filter sensitive patterns from content
100+
* @param content The content to filter
101+
* @returns Filtered content and count of redactions
102+
*/
103+
filter(content: string): FilterResult {
104+
if (!content) {
105+
return { filtered: '', redactedCount: 0 };
106+
}
107+
108+
let filtered = content;
109+
let redactedCount = 0;
110+
111+
for (const pattern of this.patterns) {
112+
// Reset regex state for global patterns
113+
pattern.lastIndex = 0;
114+
115+
// Count matches before replacing
116+
const matches = content.match(pattern);
117+
if (matches) {
118+
redactedCount += matches.length;
119+
}
120+
121+
// Replace sensitive content
122+
filtered = filtered.replace(pattern, '[REDACTED]');
123+
}
124+
125+
return { filtered, redactedCount };
126+
}
127+
128+
/**
129+
* Check if content contains sensitive data without modifying it
130+
* @param content The content to check
131+
* @returns True if sensitive data is detected
132+
*/
133+
containsSensitive(content: string): boolean {
134+
if (!content) return false;
135+
136+
for (const pattern of this.patterns) {
137+
pattern.lastIndex = 0;
138+
if (pattern.test(content)) {
139+
return true;
140+
}
141+
}
142+
143+
return false;
144+
}
145+
146+
/**
147+
* Get the current privacy mode
148+
*/
149+
getMode(): PrivacyMode {
150+
return this.config.mode;
151+
}
152+
153+
/**
154+
* Update the privacy mode and rebuild patterns
155+
*/
156+
setMode(mode: PrivacyMode): void {
157+
this.config.mode = mode;
158+
this.patterns = this.buildPatternList();
159+
}
160+
161+
/**
162+
* Get the count of active patterns
163+
*/
164+
getPatternCount(): number {
165+
return this.patterns.length;
166+
}
167+
}
168+
169+
/**
170+
* Create a privacy filter with default config
171+
*/
172+
export function createPrivacyFilter(
173+
mode: PrivacyMode = 'standard'
174+
): PrivacyFilter {
175+
return new PrivacyFilter({ mode });
176+
}

0 commit comments

Comments
 (0)