shieldcortex 3.4.37 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -10
- package/dist/api/routes/memories.js +18 -3
- package/dist/cortex/cli.js +44 -1
- package/dist/cortex/store.d.ts +23 -0
- package/dist/cortex/store.js +43 -0
- package/dist/database/init.js +11 -1
- package/dist/index.js +15 -1
- package/dist/lib.d.ts +11 -0
- package/dist/lib.js +7 -0
- package/dist/memory/consolidate.d.ts +14 -0
- package/dist/memory/consolidate.js +73 -0
- package/dist/memory/rerank.d.ts +31 -0
- package/dist/memory/rerank.js +88 -0
- package/dist/memory/save-filter.d.ts +19 -0
- package/dist/memory/save-filter.js +60 -0
- package/dist/memory/staleness.d.ts +27 -0
- package/dist/memory/staleness.js +68 -0
- package/dist/memory/store.js +13 -3
- package/dist/memory/types.d.ts +12 -0
- package/dist/memory/types.js +9 -0
- package/dist/tools/recall.js +9 -0
- package/dist/tools/remember.d.ts +6 -0
- package/dist/tools/remember.js +15 -0
- package/package.json +1 -1
- package/plugins/openclaw/dist/index.js +72 -7
- package/plugins/openclaw/dist/intercept-ingest.js +18 -0
- package/plugins/openclaw/dist/interceptor.js +280 -0
- package/plugins/openclaw/dist/openclaw.plugin.json +42 -3
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Your AI agent forgets useful context, stores untrusted context, and then confide
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npm install -g shieldcortex
|
|
18
|
-
shieldcortex
|
|
18
|
+
shieldcortex quickstart
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
> [!NOTE]
|
|
@@ -109,11 +109,12 @@ Blocked content goes to quarantine for review — nothing is silently dropped.
|
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
111
|
npm install -g shieldcortex
|
|
112
|
-
shieldcortex
|
|
113
|
-
# restart your editor — done
|
|
112
|
+
shieldcortex quickstart
|
|
114
113
|
```
|
|
115
114
|
|
|
116
|
-
|
|
115
|
+
`quickstart` scans your machine and auto-detects which agent tools are installed — **Claude Code, OpenClaw, VS Code, Cursor, and Codex** — then configures ShieldCortex for all of them in one go. One command, everything detected, no per-tool setup steps.
|
|
116
|
+
|
|
117
|
+
> If you want to configure a single tool manually, use `shieldcortex install` instead. It registers the MCP server and session hooks for whichever agent is in the current working directory.
|
|
117
118
|
|
|
118
119
|
Verify everything works:
|
|
119
120
|
|
|
@@ -128,12 +129,6 @@ shieldcortex doctor
|
|
|
128
129
|
✅ API server: running (port 3001)
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
Fastest guided setup:
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
shieldcortex quickstart
|
|
135
|
-
```
|
|
136
|
-
|
|
137
132
|
## 💳 Licensing and Trial
|
|
138
133
|
|
|
139
134
|
ShieldCortex has three distinct states:
|
|
@@ -401,6 +396,43 @@ Scans every prompt and response as they flow through OpenClaw:
|
|
|
401
396
|
- 📤 **Outbound extraction** — architectural decisions and learnings detected in assistant responses are auto-saved to memory
|
|
402
397
|
- 📋 **Audit trail** — all scans logged to `~/.shieldcortex/audit/` with full threat details
|
|
403
398
|
|
|
399
|
+
### Tool Call Interceptor — Active Memory Firewall
|
|
400
|
+
|
|
401
|
+
Requires **OpenClaw v2026.3.28+**. Previous versions fall back to passive logging.
|
|
402
|
+
|
|
403
|
+
The plugin now watches `remember` and `mcp__memory__remember` tool calls and can **block them before they execute**. Content passes through the full 6-layer defence pipeline, and the outcome depends on severity:
|
|
404
|
+
|
|
405
|
+
| Severity | Action | If pipeline fails |
|
|
406
|
+
|---|---|---|
|
|
407
|
+
| Low | Log | Allow |
|
|
408
|
+
| Medium | Warn | Allow |
|
|
409
|
+
| High | Require user approval | Deny |
|
|
410
|
+
| Critical | Require user approval | Deny |
|
|
411
|
+
|
|
412
|
+
Denied calls are cached (exact-match, session-scoped, 2-hour TTL) so the same poisoned content does not re-prompt. Approval prompts are rate-limited to 5 per minute.
|
|
413
|
+
|
|
414
|
+
Configure via `~/.shieldcortex/config.json`:
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{
|
|
418
|
+
"interceptor": {
|
|
419
|
+
"enabled": true,
|
|
420
|
+
"severityActions": {
|
|
421
|
+
"low": "log",
|
|
422
|
+
"medium": "warn",
|
|
423
|
+
"high": "require_approval",
|
|
424
|
+
"critical": "require_approval"
|
|
425
|
+
},
|
|
426
|
+
"failurePolicy": {
|
|
427
|
+
"low": "allow",
|
|
428
|
+
"medium": "allow",
|
|
429
|
+
"high": "deny",
|
|
430
|
+
"critical": "deny"
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
404
436
|
> [!TIP]
|
|
405
437
|
> Auto-extraction is **off by default** to respect OpenClaw's native memory system. Enable it when you want both:
|
|
406
438
|
> ```bash
|
|
@@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
|
4
4
|
import { getDatabase } from '../../database/init.js';
|
|
5
5
|
import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateMemory, mergeMemories, promoteMemory, createMemoryLink, rowToMemory, enrichMemory, } from '../../memory/store.js';
|
|
6
6
|
import { calculateDecayedScore } from '../../memory/decay.js';
|
|
7
|
-
import { consolidate, findDuplicateMemoryPairs, formatContextSummary, generateContextSummary, } from '../../memory/consolidate.js';
|
|
7
|
+
import { consolidate, findDuplicateMemoryPairs, formatContextSummary, generateContextSummary, consolidateMemories, } from '../../memory/consolidate.js';
|
|
8
8
|
import { getActivationStats, getActiveMemories } from '../../memory/activation.js';
|
|
9
9
|
import { detectContradictions, getContradictionsFor } from '../../memory/contradiction.js';
|
|
10
10
|
import { emitConsolidation } from '../events.js';
|
|
@@ -495,7 +495,7 @@ export function registerMemoryRoutes(app, deps) {
|
|
|
495
495
|
enforceAmber: true,
|
|
496
496
|
}), (req, res) => {
|
|
497
497
|
try {
|
|
498
|
-
const { title, content, type, category, project, tags, salience } = req.body;
|
|
498
|
+
const { title, content, type, category, project, tags, salience, memoryPurpose, memoryScope } = req.body;
|
|
499
499
|
if (!title || !content) {
|
|
500
500
|
return res.status(400).json({ error: 'Title and content required' });
|
|
501
501
|
}
|
|
@@ -507,6 +507,8 @@ export function registerMemoryRoutes(app, deps) {
|
|
|
507
507
|
project,
|
|
508
508
|
tags: tags || [],
|
|
509
509
|
salience,
|
|
510
|
+
memoryPurpose: memoryPurpose || undefined,
|
|
511
|
+
memoryScope: memoryScope || undefined,
|
|
510
512
|
});
|
|
511
513
|
res.status(201).json(memory);
|
|
512
514
|
}
|
|
@@ -974,7 +976,7 @@ export function registerMemoryRoutes(app, deps) {
|
|
|
974
976
|
}), (req, res) => {
|
|
975
977
|
try {
|
|
976
978
|
const id = parseInt(req.params.id, 10);
|
|
977
|
-
const { title, content, category, tags, importance, status, pinned, reviewedBy, cloudExcluded, scope, project } = req.body;
|
|
979
|
+
const { title, content, category, tags, importance, status, pinned, reviewedBy, cloudExcluded, scope, project, memoryPurpose, memoryScope } = req.body;
|
|
978
980
|
if (title !== undefined && (typeof title !== 'string' || title.trim().length === 0)) {
|
|
979
981
|
return res.status(400).json({ error: 'Title must be a non-empty string' });
|
|
980
982
|
}
|
|
@@ -1127,4 +1129,17 @@ export function registerMemoryRoutes(app, deps) {
|
|
|
1127
1129
|
res.status(500).json({ error: error.message });
|
|
1128
1130
|
}
|
|
1129
1131
|
});
|
|
1132
|
+
// v4.0.0: Dream Mode — comprehensive memory consolidation
|
|
1133
|
+
app.post('/api/consolidate', requireNotLocked, (_req, res) => {
|
|
1134
|
+
try {
|
|
1135
|
+
const result = consolidateMemories();
|
|
1136
|
+
res.json({
|
|
1137
|
+
success: true,
|
|
1138
|
+
...result,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
res.status(500).json({ error: error.message });
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1130
1145
|
}
|
package/dist/cortex/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Pro tier feature: systematic mistake learning + pre-flight checks.
|
|
4
4
|
*/
|
|
5
5
|
import { requireFeature } from '../license/gate.js';
|
|
6
|
-
import { capture, preflight, review, graduate, search, loadMistakes, } from './store.js';
|
|
6
|
+
import { capture, preflight, review, graduate, search, loadMistakes, captureConfirmation, searchConfirmations, loadConfirmations, } from './store.js';
|
|
7
7
|
const SEVERITY_ICON = {
|
|
8
8
|
critical: '🔴',
|
|
9
9
|
high: '🟠',
|
|
@@ -26,11 +26,14 @@ function printUsage() {
|
|
|
26
26
|
list Browse mistake log
|
|
27
27
|
stats Summary counts and trends
|
|
28
28
|
search Full-text search across all entries
|
|
29
|
+
confirm Log a positive confirmation (what worked well)
|
|
30
|
+
confirmations List or search confirmations
|
|
29
31
|
|
|
30
32
|
Examples:
|
|
31
33
|
shieldcortex cortex capture --category code --what "Guessed URLs" --why "Didn't use API" --rule "Always fetch handles from API"
|
|
32
34
|
shieldcortex cortex preflight --task "deploy to Fly.io"
|
|
33
35
|
shieldcortex cortex review
|
|
36
|
+
shieldcortex cortex confirm --category code --what "Used TypeScript strict mode" --why-worked "Caught type errors early" --when-repeat "Always on new TS projects"
|
|
34
37
|
shieldcortex cortex stats
|
|
35
38
|
`);
|
|
36
39
|
}
|
|
@@ -184,6 +187,46 @@ export async function handleCortexCommand(args) {
|
|
|
184
187
|
}
|
|
185
188
|
break;
|
|
186
189
|
}
|
|
190
|
+
case 'confirm': {
|
|
191
|
+
if (!opts.category || !opts.what || !opts['why-worked'] || !opts['when-repeat']) {
|
|
192
|
+
console.error('Required: --category, --what, --why-worked, --when-repeat');
|
|
193
|
+
console.error(`Categories: ${VALID_CATEGORIES.join(', ')}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (!VALID_CATEGORIES.includes(opts.category)) {
|
|
197
|
+
console.error(`Invalid category. Valid: ${VALID_CATEGORIES.join(', ')}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
const entry = captureConfirmation({
|
|
201
|
+
category: opts.category,
|
|
202
|
+
what: opts.what,
|
|
203
|
+
whyItWorked: opts['why-worked'],
|
|
204
|
+
whenToRepeat: opts['when-repeat'],
|
|
205
|
+
tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : [],
|
|
206
|
+
agent: opts.agent,
|
|
207
|
+
taskContext: opts.context,
|
|
208
|
+
});
|
|
209
|
+
console.log(`✅ Captured confirmation #${entry.id}: ${entry.what.slice(0, 60)}`);
|
|
210
|
+
console.log(` Why it worked: ${entry.whyItWorked}`);
|
|
211
|
+
console.log(` When to repeat: ${entry.whenToRepeat}`);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case 'confirmations': {
|
|
215
|
+
const query = opts.query;
|
|
216
|
+
const confirmations = query ? searchConfirmations(query) : loadConfirmations();
|
|
217
|
+
if (confirmations.length === 0) {
|
|
218
|
+
console.log(query ? `No confirmations matching '${query}'` : 'No confirmations logged yet.');
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.log(`🎯 ${confirmations.length} confirmation(s)${query ? ` matching '${query}'` : ''}:\n`);
|
|
222
|
+
for (const c of confirmations) {
|
|
223
|
+
console.log(` ✅ #${c.id} [${c.category}] ${c.what}`);
|
|
224
|
+
console.log(` Why: ${c.whyItWorked}`);
|
|
225
|
+
console.log(` Repeat: ${c.whenToRepeat}\n`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
187
230
|
default:
|
|
188
231
|
printUsage();
|
|
189
232
|
}
|
package/dist/cortex/store.d.ts
CHANGED
|
@@ -50,3 +50,26 @@ export declare function preflight(taskDescription: string): PreflightMatch[];
|
|
|
50
50
|
export declare function review(): ReviewStats;
|
|
51
51
|
export declare function graduate(): number;
|
|
52
52
|
export declare function search(query: string): Mistake[];
|
|
53
|
+
export interface Confirmation {
|
|
54
|
+
id: number;
|
|
55
|
+
timestamp: string;
|
|
56
|
+
category: MistakeCategory;
|
|
57
|
+
what: string;
|
|
58
|
+
whyItWorked: string;
|
|
59
|
+
whenToRepeat: string;
|
|
60
|
+
tags: string[];
|
|
61
|
+
agent?: string;
|
|
62
|
+
taskContext?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function loadConfirmations(): Confirmation[];
|
|
65
|
+
export declare function saveConfirmations(confirmations: Confirmation[]): void;
|
|
66
|
+
export declare function captureConfirmation(opts: {
|
|
67
|
+
category: MistakeCategory;
|
|
68
|
+
what: string;
|
|
69
|
+
whyItWorked: string;
|
|
70
|
+
whenToRepeat: string;
|
|
71
|
+
tags?: string[];
|
|
72
|
+
agent?: string;
|
|
73
|
+
taskContext?: string;
|
|
74
|
+
}): Confirmation;
|
|
75
|
+
export declare function searchConfirmations(query: string): Confirmation[];
|
package/dist/cortex/store.js
CHANGED
|
@@ -157,3 +157,46 @@ export function search(query) {
|
|
|
157
157
|
return text.includes(q);
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
|
+
function getConfirmationsFile() {
|
|
161
|
+
return join(getDataDir(), 'confirmations.json');
|
|
162
|
+
}
|
|
163
|
+
export function loadConfirmations() {
|
|
164
|
+
const file = getConfirmationsFile();
|
|
165
|
+
if (!existsSync(file))
|
|
166
|
+
return [];
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function saveConfirmations(confirmations) {
|
|
175
|
+
const file = getConfirmationsFile();
|
|
176
|
+
writeFileSync(file, JSON.stringify(confirmations, null, 2) + '\n', { mode: 0o600 });
|
|
177
|
+
}
|
|
178
|
+
export function captureConfirmation(opts) {
|
|
179
|
+
const confirmations = loadConfirmations();
|
|
180
|
+
const entry = {
|
|
181
|
+
id: confirmations.length + 1,
|
|
182
|
+
timestamp: new Date().toISOString(),
|
|
183
|
+
category: opts.category,
|
|
184
|
+
what: opts.what,
|
|
185
|
+
whyItWorked: opts.whyItWorked,
|
|
186
|
+
whenToRepeat: opts.whenToRepeat,
|
|
187
|
+
tags: opts.tags || [],
|
|
188
|
+
agent: opts.agent,
|
|
189
|
+
taskContext: opts.taskContext,
|
|
190
|
+
};
|
|
191
|
+
confirmations.push(entry);
|
|
192
|
+
saveConfirmations(confirmations);
|
|
193
|
+
return entry;
|
|
194
|
+
}
|
|
195
|
+
export function searchConfirmations(query) {
|
|
196
|
+
const confirmations = loadConfirmations();
|
|
197
|
+
const lower = query.toLowerCase();
|
|
198
|
+
return confirmations.filter(c => c.what.toLowerCase().includes(lower) ||
|
|
199
|
+
c.whyItWorked.toLowerCase().includes(lower) ||
|
|
200
|
+
c.whenToRepeat.toLowerCase().includes(lower) ||
|
|
201
|
+
c.tags.some(t => t.toLowerCase().includes(lower)));
|
|
202
|
+
}
|
package/dist/database/init.js
CHANGED
|
@@ -784,6 +784,14 @@ function runMigrations(database) {
|
|
|
784
784
|
if (!columnNames.has('graph_extraction_version')) {
|
|
785
785
|
database.exec('ALTER TABLE memories ADD COLUMN graph_extraction_version INTEGER DEFAULT 0');
|
|
786
786
|
}
|
|
787
|
+
// Migration: v4.0.0 — memory_purpose for type taxonomy
|
|
788
|
+
if (!columnNames.has('memory_purpose')) {
|
|
789
|
+
database.exec("ALTER TABLE memories ADD COLUMN memory_purpose TEXT DEFAULT 'project'");
|
|
790
|
+
}
|
|
791
|
+
// Migration: v4.0.0 — memory_scope for private/team visibility
|
|
792
|
+
if (!columnNames.has('memory_scope')) {
|
|
793
|
+
database.exec("ALTER TABLE memories ADD COLUMN memory_scope TEXT DEFAULT 'private'");
|
|
794
|
+
}
|
|
787
795
|
// Migration: Pro feature tables (custom_patterns, iron_dome_policies, firewall_rules)
|
|
788
796
|
try {
|
|
789
797
|
database.exec(`
|
|
@@ -1054,7 +1062,9 @@ function getInlineSchema() {
|
|
|
1054
1062
|
trust_score REAL DEFAULT 1.0,
|
|
1055
1063
|
sensitivity_level TEXT DEFAULT 'INTERNAL',
|
|
1056
1064
|
source TEXT DEFAULT 'user:direct',
|
|
1057
|
-
graph_extraction_version INTEGER DEFAULT 0
|
|
1065
|
+
graph_extraction_version INTEGER DEFAULT 0,
|
|
1066
|
+
memory_purpose TEXT DEFAULT 'project',
|
|
1067
|
+
memory_scope TEXT DEFAULT 'private'
|
|
1058
1068
|
);
|
|
1059
1069
|
|
|
1060
1070
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
package/dist/index.js
CHANGED
|
@@ -749,6 +749,20 @@ ${bold}DOCS${reset}
|
|
|
749
749
|
console.log(`\n${bold}Summary:${reset} ${filesToScan.length} scanned, ${threatCount} with threats\n`);
|
|
750
750
|
process.exit(threatCount > 0 ? 1 : 0);
|
|
751
751
|
}
|
|
752
|
+
// Handle "consolidate" subcommand (v4.0.0 — Dream Mode)
|
|
753
|
+
if (process.argv[2] === 'consolidate') {
|
|
754
|
+
const { initDatabase } = await import('./database/init.js');
|
|
755
|
+
initDatabase();
|
|
756
|
+
const { consolidateMemories } = await import('./memory/consolidate.js');
|
|
757
|
+
console.log('🧠 Starting Dream Mode consolidation...');
|
|
758
|
+
const result = consolidateMemories();
|
|
759
|
+
console.log(`✅ Consolidation complete:`);
|
|
760
|
+
console.log(` Near-duplicates merged: ${result.nearDuplicatesMerged}`);
|
|
761
|
+
console.log(` Archival candidates: ${result.archivalCandidates}`);
|
|
762
|
+
console.log(` Contradictions found: ${result.contradictionsDetected}`);
|
|
763
|
+
console.log(` Total processed: ${result.totalProcessed}`);
|
|
764
|
+
process.exit(0);
|
|
765
|
+
}
|
|
752
766
|
// Handle "cortex" subcommand (Pro tier — mistake learning)
|
|
753
767
|
if (process.argv[2] === 'cortex') {
|
|
754
768
|
const { handleCortexCommand } = await import('./cortex/cli.js');
|
|
@@ -760,7 +774,7 @@ ${bold}DOCS${reset}
|
|
|
760
774
|
'doctor', 'quickstart', 'setup', 'install', 'migrate', 'uninstall', 'hook',
|
|
761
775
|
'openclaw', 'clawdbot', 'copilot', 'codex', 'service', 'config', 'status',
|
|
762
776
|
'graph', 'license', 'licence', 'audit', 'iron-dome', 'scan', 'cloud',
|
|
763
|
-
'scan-skill', 'scan-skills', 'dashboard', 'api', 'worker', 'stats', 'cortex',
|
|
777
|
+
'scan-skill', 'scan-skills', 'dashboard', 'api', 'worker', 'stats', 'cortex', 'consolidate',
|
|
764
778
|
]);
|
|
765
779
|
const arg = process.argv[2];
|
|
766
780
|
if (arg && !arg.startsWith('-') && !knownCommands.has(arg)) {
|
package/dist/lib.d.ts
CHANGED
|
@@ -38,3 +38,14 @@ export type { AuditFinding, AuditSeverity } from './audit/types.js';
|
|
|
38
38
|
export { getLicense, getLicenseTier, isFeatureEnabled, requireFeature, listFeatures, FeatureGatedError, verifyLicenseKey, TIER_RANK, } from './license/index.js';
|
|
39
39
|
export type { LicenseTier, LicenseInfo, GatedFeature, } from './license/index.js';
|
|
40
40
|
export declare const version: string;
|
|
41
|
+
export { memoryAgeDays, memoryAge, memoryFreshnessScore, memoryFreshnessWarning, appendStalenessWarning, } from './memory/staleness.js';
|
|
42
|
+
export { rerankResults, buildRerankPrompt, parseRerankResponse, DEFAULT_RERANK_CONFIG, } from './memory/rerank.js';
|
|
43
|
+
export type { RerankConfig } from './memory/rerank.js';
|
|
44
|
+
export { consolidateMemories, } from './memory/consolidate.js';
|
|
45
|
+
export type { DreamModeResult } from './memory/consolidate.js';
|
|
46
|
+
export { shouldFilterMemory, DEFAULT_SAVE_FILTER_CONFIG, } from './memory/save-filter.js';
|
|
47
|
+
export type { SaveFilterConfig, SaveFilterResult } from './memory/save-filter.js';
|
|
48
|
+
export { captureConfirmation, searchConfirmations, loadConfirmations, } from './cortex/store.js';
|
|
49
|
+
export type { Confirmation } from './cortex/store.js';
|
|
50
|
+
export type { MemoryPurpose, MemoryScope, } from './memory/types.js';
|
|
51
|
+
export { VALID_MEMORY_PURPOSES, VALID_MEMORY_SCOPES, isValidMemoryPurpose, isValidMemoryScope, } from './memory/types.js';
|
package/dist/lib.js
CHANGED
|
@@ -45,3 +45,10 @@ import { createRequire } from 'module';
|
|
|
45
45
|
const require = createRequire(import.meta.url);
|
|
46
46
|
const pkg = require('../package.json');
|
|
47
47
|
export const version = pkg.version;
|
|
48
|
+
// ── v4.0.0: New Modules ────────────────────────────────────────
|
|
49
|
+
export { memoryAgeDays, memoryAge, memoryFreshnessScore, memoryFreshnessWarning, appendStalenessWarning, } from './memory/staleness.js';
|
|
50
|
+
export { rerankResults, buildRerankPrompt, parseRerankResponse, DEFAULT_RERANK_CONFIG, } from './memory/rerank.js';
|
|
51
|
+
export { consolidateMemories, } from './memory/consolidate.js';
|
|
52
|
+
export { shouldFilterMemory, DEFAULT_SAVE_FILTER_CONFIG, } from './memory/save-filter.js';
|
|
53
|
+
export { captureConfirmation, searchConfirmations, loadConfirmations, } from './cortex/store.js';
|
|
54
|
+
export { VALID_MEMORY_PURPOSES, VALID_MEMORY_SCOPES, isValidMemoryPurpose, isValidMemoryScope, } from './memory/types.js';
|
|
@@ -132,3 +132,17 @@ export declare function fullCleanup(config?: MemoryConfig): {
|
|
|
132
132
|
merged: number;
|
|
133
133
|
quarantineExpired: number;
|
|
134
134
|
};
|
|
135
|
+
export interface DreamModeResult {
|
|
136
|
+
nearDuplicatesMerged: number;
|
|
137
|
+
archivalCandidates: number;
|
|
138
|
+
contradictionsDetected: number;
|
|
139
|
+
totalProcessed: number;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* "Dream Mode" — comprehensive memory consolidation.
|
|
143
|
+
*
|
|
144
|
+
* 1. Find near-duplicates by embedding similarity >0.9, merge them
|
|
145
|
+
* 2. Flag memories >30 days old with no access as archival candidates
|
|
146
|
+
* 3. Detect and flag contradictions
|
|
147
|
+
*/
|
|
148
|
+
export declare function consolidateMemories(): DreamModeResult;
|
|
@@ -763,3 +763,76 @@ export function fullCleanup(config = DEFAULT_CONFIG) {
|
|
|
763
763
|
}
|
|
764
764
|
return { consolidation, vacuumed, merged, quarantineExpired };
|
|
765
765
|
}
|
|
766
|
+
/**
|
|
767
|
+
* "Dream Mode" — comprehensive memory consolidation.
|
|
768
|
+
*
|
|
769
|
+
* 1. Find near-duplicates by embedding similarity >0.9, merge them
|
|
770
|
+
* 2. Flag memories >30 days old with no access as archival candidates
|
|
771
|
+
* 3. Detect and flag contradictions
|
|
772
|
+
*/
|
|
773
|
+
export function consolidateMemories() {
|
|
774
|
+
const db = getDatabase();
|
|
775
|
+
let nearDuplicatesMerged = 0;
|
|
776
|
+
let archivalCandidates = 0;
|
|
777
|
+
let contradictionsDetected = 0;
|
|
778
|
+
// Step 1: Find and merge near-duplicates
|
|
779
|
+
const duplicatePairs = findDuplicateMemoryPairs({ limit: 100 });
|
|
780
|
+
for (const pair of duplicatePairs) {
|
|
781
|
+
try {
|
|
782
|
+
const memA = pair.memoryA;
|
|
783
|
+
const memB = pair.memoryB;
|
|
784
|
+
if (!memA || !memB)
|
|
785
|
+
continue;
|
|
786
|
+
// Keep the recommended one, delete the other
|
|
787
|
+
const removedId = pair.recommendedKeepId === memA.id ? memB.id : memA.id;
|
|
788
|
+
const keptId = pair.recommendedKeepId;
|
|
789
|
+
// Merge tags from both
|
|
790
|
+
const kept = keptId === memA.id ? memA : memB;
|
|
791
|
+
const removed = keptId === memA.id ? memB : memA;
|
|
792
|
+
const mergedTags = [...new Set([...kept.tags, ...removed.tags])];
|
|
793
|
+
db.prepare('UPDATE memories SET tags = ? WHERE id = ?')
|
|
794
|
+
.run(JSON.stringify(mergedTags), keptId);
|
|
795
|
+
deleteMemory(removedId);
|
|
796
|
+
nearDuplicatesMerged++;
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
// Skip problematic pairs
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// Step 2: Flag stale memories for archival (>30 days, no access)
|
|
803
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 86_400_000).toISOString();
|
|
804
|
+
const staleRows = db.prepare(`
|
|
805
|
+
SELECT id FROM memories
|
|
806
|
+
WHERE status = 'active'
|
|
807
|
+
AND last_accessed < ?
|
|
808
|
+
AND access_count <= 1
|
|
809
|
+
AND pinned = 0
|
|
810
|
+
`).all(thirtyDaysAgo);
|
|
811
|
+
for (const row of staleRows) {
|
|
812
|
+
try {
|
|
813
|
+
db.prepare("UPDATE memories SET status = 'archived', updated_at = CURRENT_TIMESTAMP WHERE id = ?")
|
|
814
|
+
.run(row.id);
|
|
815
|
+
archivalCandidates++;
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
// Skip
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// Step 3: Detect contradictions
|
|
822
|
+
try {
|
|
823
|
+
const contradictions = detectContradictions();
|
|
824
|
+
if (contradictions.length > 0) {
|
|
825
|
+
contradictionsDetected = linkContradictions(contradictions);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
// Contradiction detection may fail on empty/small DBs
|
|
830
|
+
}
|
|
831
|
+
const totalProcessed = duplicatePairs.length + staleRows.length;
|
|
832
|
+
return {
|
|
833
|
+
nearDuplicatesMerged,
|
|
834
|
+
archivalCandidates,
|
|
835
|
+
contradictionsDetected,
|
|
836
|
+
totalProcessed,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-Powered Memory Reranking — v4.0.0
|
|
3
|
+
*
|
|
4
|
+
* Optional hybrid recall: after embedding-based retrieval returns top-N candidates,
|
|
5
|
+
* send them to an LLM to rerank by relevance to the query.
|
|
6
|
+
*/
|
|
7
|
+
import type { SearchResult } from './types.js';
|
|
8
|
+
export interface RerankConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
model: string;
|
|
11
|
+
maxCandidates: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_RERANK_CONFIG: RerankConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Build the reranking prompt for the LLM.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildRerankPrompt(query: string, candidates: {
|
|
18
|
+
id: number;
|
|
19
|
+
title: string;
|
|
20
|
+
content: string;
|
|
21
|
+
}[]): string;
|
|
22
|
+
/**
|
|
23
|
+
* Parse the LLM response to extract the reranked order.
|
|
24
|
+
* Returns array of memory IDs in relevance order.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseRerankResponse(response: string): number[];
|
|
27
|
+
/**
|
|
28
|
+
* Rerank search results using an LLM.
|
|
29
|
+
* This is a framework function — the actual LLM call is delegated to a caller-provided function.
|
|
30
|
+
*/
|
|
31
|
+
export declare function rerankResults(query: string, results: SearchResult[], llmCall: (prompt: string) => Promise<string>, config?: RerankConfig): Promise<SearchResult[]>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-Powered Memory Reranking — v4.0.0
|
|
3
|
+
*
|
|
4
|
+
* Optional hybrid recall: after embedding-based retrieval returns top-N candidates,
|
|
5
|
+
* send them to an LLM to rerank by relevance to the query.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_RERANK_CONFIG = {
|
|
8
|
+
enabled: false,
|
|
9
|
+
model: 'sonnet',
|
|
10
|
+
maxCandidates: 20,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Build the reranking prompt for the LLM.
|
|
14
|
+
*/
|
|
15
|
+
export function buildRerankPrompt(query, candidates) {
|
|
16
|
+
const candidateList = candidates
|
|
17
|
+
.map((c, i) => `[${i + 1}] ID=${c.id} | Title: ${c.title}\n${c.content.slice(0, 200)}`)
|
|
18
|
+
.join('\n\n');
|
|
19
|
+
return `Given the query: "${query}"
|
|
20
|
+
|
|
21
|
+
Rank the following memory candidates by relevance to the query. Return ONLY a JSON array of memory IDs in order of relevance (most relevant first).
|
|
22
|
+
|
|
23
|
+
Candidates:
|
|
24
|
+
${candidateList}
|
|
25
|
+
|
|
26
|
+
Response format: [id1, id2, id3, ...]`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse the LLM response to extract the reranked order.
|
|
30
|
+
* Returns array of memory IDs in relevance order.
|
|
31
|
+
*/
|
|
32
|
+
export function parseRerankResponse(response) {
|
|
33
|
+
try {
|
|
34
|
+
// Try to extract JSON array from the response
|
|
35
|
+
const match = response.match(/\[[\d,\s]+\]/);
|
|
36
|
+
if (match) {
|
|
37
|
+
return JSON.parse(match[0]);
|
|
38
|
+
}
|
|
39
|
+
// Fallback: extract numbers in order
|
|
40
|
+
const numbers = response.match(/\d+/g);
|
|
41
|
+
return numbers ? numbers.map(Number) : [];
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Rerank search results using an LLM.
|
|
49
|
+
* This is a framework function — the actual LLM call is delegated to a caller-provided function.
|
|
50
|
+
*/
|
|
51
|
+
export async function rerankResults(query, results, llmCall, config = DEFAULT_RERANK_CONFIG) {
|
|
52
|
+
if (!config.enabled || results.length <= 1) {
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
const candidates = results.slice(0, config.maxCandidates).map(r => ({
|
|
56
|
+
id: r.memory.id,
|
|
57
|
+
title: r.memory.title,
|
|
58
|
+
content: r.memory.content,
|
|
59
|
+
}));
|
|
60
|
+
const prompt = buildRerankPrompt(query, candidates);
|
|
61
|
+
try {
|
|
62
|
+
const response = await llmCall(prompt);
|
|
63
|
+
const rankedIds = parseRerankResponse(response);
|
|
64
|
+
if (rankedIds.length === 0) {
|
|
65
|
+
return results; // Fallback to original order
|
|
66
|
+
}
|
|
67
|
+
// Build a map for O(1) lookup
|
|
68
|
+
const resultMap = new Map(results.map(r => [r.memory.id, r]));
|
|
69
|
+
const reranked = [];
|
|
70
|
+
// Add results in LLM-determined order
|
|
71
|
+
for (const id of rankedIds) {
|
|
72
|
+
const result = resultMap.get(id);
|
|
73
|
+
if (result) {
|
|
74
|
+
reranked.push(result);
|
|
75
|
+
resultMap.delete(id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Append any results not mentioned by the LLM
|
|
79
|
+
for (const remaining of resultMap.values()) {
|
|
80
|
+
reranked.push(remaining);
|
|
81
|
+
}
|
|
82
|
+
return reranked;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// If LLM reranking fails, return original order
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Save Filter — v4.0.0
|
|
3
|
+
*
|
|
4
|
+
* Prevents saving derivable information that can be found in
|
|
5
|
+
* code, git history, or file system.
|
|
6
|
+
*/
|
|
7
|
+
export interface SaveFilterConfig {
|
|
8
|
+
filterDerivable: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const DEFAULT_SAVE_FILTER_CONFIG: SaveFilterConfig;
|
|
11
|
+
export interface SaveFilterResult {
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
reason?: string;
|
|
14
|
+
warning?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check whether a memory should be filtered (not saved).
|
|
18
|
+
*/
|
|
19
|
+
export declare function shouldFilterMemory(title: string, content: string, config?: SaveFilterConfig): SaveFilterResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Save Filter — v4.0.0
|
|
3
|
+
*
|
|
4
|
+
* Prevents saving derivable information that can be found in
|
|
5
|
+
* code, git history, or file system.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_SAVE_FILTER_CONFIG = {
|
|
8
|
+
filterDerivable: true,
|
|
9
|
+
};
|
|
10
|
+
// Patterns that suggest derivable content
|
|
11
|
+
const DERIVABLE_PATTERNS = [
|
|
12
|
+
// File paths that can be found with grep/find
|
|
13
|
+
{ pattern: /^(?:the )?file (?:is )?(?:at |located at )?[\/~][\w\/.@-]+$/i, reason: 'File paths are discoverable via file system' },
|
|
14
|
+
// Git commit references
|
|
15
|
+
{ pattern: /^(?:commit |git )(?:hash |sha )?[0-9a-f]{7,40}/i, reason: 'Git history is queryable with git log' },
|
|
16
|
+
// Simple grep-able patterns
|
|
17
|
+
{ pattern: /^(?:the )?(?:function|class|method|variable|const|let|var) ['"`]?\w+['"`]? (?:is |exists )?(?:in|at) /i, reason: 'Code symbols are discoverable via grep/search' },
|
|
18
|
+
// Package version info
|
|
19
|
+
{ pattern: /^(?:the )?(?:version|pkg) (?:of |is )?\w+(?:@| is )[\d.]+/i, reason: 'Package versions are in package.json/lock files' },
|
|
20
|
+
// Environment variable values
|
|
21
|
+
{ pattern: /^(?:the )?(?:env(?:ironment)? )?(?:var(?:iable)? )?[A-Z_]{3,}(?:\s*=\s*|\s+is\s+)/i, reason: 'Environment variables should not be stored in memory (security risk)' },
|
|
22
|
+
];
|
|
23
|
+
// Content characteristics that suggest derivable info
|
|
24
|
+
const DERIVABLE_SIGNALS = [
|
|
25
|
+
{ test: (c) => /^(?:import|require|from)\s/m.test(c), reason: 'Import statements are visible in source code' },
|
|
26
|
+
{ test: (c) => c.split('\n').length <= 2 && /^\s*(?:cd |ls |cat |grep |find |git )\s/m.test(c), reason: 'Shell commands are not valuable to memorize' },
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Check whether a memory should be filtered (not saved).
|
|
30
|
+
*/
|
|
31
|
+
export function shouldFilterMemory(title, content, config = DEFAULT_SAVE_FILTER_CONFIG) {
|
|
32
|
+
if (!config.filterDerivable) {
|
|
33
|
+
return { allowed: true };
|
|
34
|
+
}
|
|
35
|
+
const combined = `${title}\n${content}`.trim();
|
|
36
|
+
// Check against derivable patterns
|
|
37
|
+
for (const { pattern, reason } of DERIVABLE_PATTERNS) {
|
|
38
|
+
if (pattern.test(title) || pattern.test(content)) {
|
|
39
|
+
return { allowed: false, reason };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Check content signals
|
|
43
|
+
for (const { test, reason } of DERIVABLE_SIGNALS) {
|
|
44
|
+
if (test(combined)) {
|
|
45
|
+
return { allowed: false, reason, warning: `Filtered: ${reason}` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Heuristic: very short content with file path patterns = likely derivable
|
|
49
|
+
if (combined.length < 100) {
|
|
50
|
+
const pathCount = (combined.match(/[\/~][\w\/.@-]{5,}/g) || []).length;
|
|
51
|
+
if (pathCount > 0 && combined.replace(/[\/~][\w\/.@-]+/g, '').trim().length < 30) {
|
|
52
|
+
return {
|
|
53
|
+
allowed: false,
|
|
54
|
+
reason: 'Content appears to be primarily file paths (discoverable via file system)',
|
|
55
|
+
warning: 'Memory content is mostly file paths — these are discoverable without memory',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { allowed: true };
|
|
60
|
+
}
|