shieldcortex 3.3.0 → 3.4.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.
Files changed (40) hide show
  1. package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
  2. package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
  3. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
  4. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
  5. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  6. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  8. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
  11. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +1 -1
  12. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  13. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  14. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  15. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
  19. package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +2 -2
  20. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  21. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +2 -2
  22. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  23. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +1 -1
  24. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  25. package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  26. package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
  27. package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
  28. package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
  29. package/dashboard/.next/standalone/dashboard/.next/static/chunks/ffdceca8b5f407c5.js +9 -0
  30. package/dist/api/routes/memories.js +24 -2
  31. package/dist/memory/consolidate.d.ts +13 -0
  32. package/dist/memory/consolidate.js +83 -72
  33. package/dist/memory/store.d.ts +3 -0
  34. package/dist/memory/store.js +98 -1
  35. package/dist/setup/codex.js +55 -18
  36. package/package.json +1 -1
  37. package/dashboard/.next/standalone/dashboard/.next/static/chunks/9cb86821c1107fd6.js +0 -9
  38. /package/dashboard/.next/standalone/dashboard/.next/static/{CS0CaiptEOR_bYt33J6A9 → 6FufdsuScTToJQBReS4H-}/_buildManifest.js +0 -0
  39. /package/dashboard/.next/standalone/dashboard/.next/static/{CS0CaiptEOR_bYt33J6A9 → 6FufdsuScTToJQBReS4H-}/_clientMiddlewareManifest.json +0 -0
  40. /package/dashboard/.next/standalone/dashboard/.next/static/{CS0CaiptEOR_bYt33J6A9 → 6FufdsuScTToJQBReS4H-}/_ssgManifest.js +0 -0
@@ -2,9 +2,9 @@ import { homedir } from 'os';
2
2
  import { join } from 'path';
3
3
  import { existsSync, readdirSync, readFileSync } from 'fs';
4
4
  import { getDatabase } from '../../database/init.js';
5
- import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateMemory, promoteMemory, createMemoryLink, rowToMemory, enrichMemory, } from '../../memory/store.js';
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, formatContextSummary, generateContextSummary, } from '../../memory/consolidate.js';
7
+ import { consolidate, findDuplicateMemoryPairs, formatContextSummary, generateContextSummary, } 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';
@@ -383,6 +383,7 @@ export function registerMemoryRoutes(app, requireNotLocked) {
383
383
  minScore: 0.4,
384
384
  limit,
385
385
  });
386
+ const duplicates = findDuplicateMemoryPairs({ project, limit });
386
387
  res.json({
387
388
  summary: {
388
389
  stale: stale.length,
@@ -391,6 +392,7 @@ export function registerMemoryRoutes(app, requireNotLocked) {
391
392
  noisyAutoExtracted: noisyAutoExtracted.length,
392
393
  projectless: projectless.length,
393
394
  contradictions: contradictions.length,
395
+ duplicates: duplicates.length,
394
396
  },
395
397
  openClaw: {
396
398
  total: openClawSummary.total ?? 0,
@@ -405,6 +407,7 @@ export function registerMemoryRoutes(app, requireNotLocked) {
405
407
  lowTrust: lowTrust.map(rowToMemory),
406
408
  noisyAutoExtracted: noisyAutoExtracted.map(rowToMemory),
407
409
  projectless: projectless.map(rowToMemory),
410
+ duplicates,
408
411
  contradictions: contradictions.map((item) => ({
409
412
  memoryA: item.memoryA,
410
413
  memoryB: item.memoryB,
@@ -419,6 +422,25 @@ export function registerMemoryRoutes(app, requireNotLocked) {
419
422
  res.status(500).json({ error: error.message });
420
423
  }
421
424
  });
425
+ app.post('/api/memories/merge', requireNotLocked, (req, res) => {
426
+ try {
427
+ const { keptId, removedId, reviewedBy } = req.body;
428
+ if (!Number.isInteger(keptId) || !Number.isInteger(removedId)) {
429
+ return res.status(400).json({ error: 'keptId and removedId are required integers' });
430
+ }
431
+ if (keptId === removedId) {
432
+ return res.status(400).json({ error: 'keptId and removedId must be different' });
433
+ }
434
+ const merged = mergeMemories(keptId, removedId, { reviewedBy });
435
+ if (!merged) {
436
+ return res.status(404).json({ error: 'Unable to merge memories' });
437
+ }
438
+ res.json(merged);
439
+ }
440
+ catch (error) {
441
+ res.status(500).json({ error: error.message });
442
+ }
443
+ });
422
444
  app.get('/api/memories/:id', requireNotLocked, (req, res) => {
423
445
  try {
424
446
  const id = parseInt(req.params.id, 10);
@@ -13,6 +13,19 @@ import { Memory, MemoryConfig, ConsolidationResult, ContextSummary } from './typ
13
13
  * This is like the brain's sleep consolidation - should be run periodically
14
14
  */
15
15
  export declare function consolidate(config?: MemoryConfig): ConsolidationResult;
16
+ export interface DuplicateMemoryPair {
17
+ memoryA: Memory;
18
+ memoryB: Memory;
19
+ recommendedKeepId: number;
20
+ similarity: string;
21
+ titleSimilarity: number;
22
+ contentOverlap: number;
23
+ sharedWords: number;
24
+ }
25
+ export declare function findDuplicateMemoryPairs(options?: {
26
+ project?: string;
27
+ limit?: number;
28
+ }): DuplicateMemoryPair[];
16
29
  /**
17
30
  * Find memories with very similar titles/content and merge them.
18
31
  * Keeps the newer one, appends unique content from the older one.
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { getDatabase, withTransaction } from '../database/init.js';
11
11
  import { DEFAULT_CONFIG, } from './types.js';
12
- import { getMemoriesByType, getRecentMemories, promoteMemory, deleteMemory, searchMemories, getMemoryStats, updateDecayScores, addMemory, } from './store.js';
12
+ import { getMemoriesByType, getRecentMemories, promoteMemory, deleteMemory, searchMemories, getMemoryStats, updateDecayScores, addMemory, rowToMemory, } from './store.js';
13
13
  import { processDecay, } from './decay.js';
14
14
  import { detectContradictions, linkContradictions, } from './contradiction.js';
15
15
  import { jaccardSimilarity } from './similarity.js';
@@ -191,6 +191,67 @@ function sharedWordCount(a, b) {
191
191
  }
192
192
  return count;
193
193
  }
194
+ export function findDuplicateMemoryPairs(options) {
195
+ const db = getDatabase();
196
+ const limit = options?.limit ?? 20;
197
+ const rows = options?.project
198
+ ? db.prepare("SELECT * FROM memories WHERE type = 'long_term' AND project = ? ORDER BY created_at ASC").all(options.project)
199
+ : db.prepare("SELECT * FROM memories WHERE type = 'long_term' ORDER BY created_at ASC").all();
200
+ const groups = new Map();
201
+ for (const mem of rows) {
202
+ const cat = mem.category || 'note';
203
+ if (!groups.has(cat))
204
+ groups.set(cat, []);
205
+ groups.get(cat).push(mem);
206
+ }
207
+ const pairs = [];
208
+ const seen = new Set();
209
+ for (const [, group] of groups) {
210
+ if (group.length < 2)
211
+ continue;
212
+ for (let i = 0; i < group.length && pairs.length < limit; i++) {
213
+ const memA = group[i];
214
+ for (let j = i + 1; j < group.length && pairs.length < limit; j++) {
215
+ const memB = group[j];
216
+ const idA = memA.id;
217
+ const idB = memB.id;
218
+ const pairKey = `${Math.min(idA, idB)}:${Math.max(idA, idB)}`;
219
+ if (seen.has(pairKey))
220
+ continue;
221
+ const titleA = memA.title || '';
222
+ const titleB = memB.title || '';
223
+ const contentA = memA.content || '';
224
+ const contentB = memB.content || '';
225
+ const titleSimilarity = levenshteinSimilarity(titleA.toLowerCase(), titleB.toLowerCase());
226
+ const sharedWords = sharedWordCount(titleA, titleB);
227
+ const titlesMatch = titleSimilarity > 0.7 || sharedWords >= 3 || titleA.toLowerCase() === titleB.toLowerCase();
228
+ if (!titlesMatch)
229
+ continue;
230
+ const contentOverlap = wordOverlap(contentA, contentB);
231
+ if (contentOverlap <= 0.5)
232
+ continue;
233
+ const salienceA = memA.salience || 0;
234
+ const salienceB = memB.salience || 0;
235
+ const createdA = new Date(memA.created_at || 0).getTime();
236
+ const createdB = new Date(memB.created_at || 0).getTime();
237
+ const recommendedKeepId = salienceA > salienceB || (salienceA === salienceB && createdA >= createdB)
238
+ ? idA
239
+ : idB;
240
+ pairs.push({
241
+ memoryA: rowToMemory(memA),
242
+ memoryB: rowToMemory(memB),
243
+ recommendedKeepId,
244
+ similarity: `title=${titleSimilarity.toFixed(2)}, content=${contentOverlap.toFixed(2)}`,
245
+ titleSimilarity,
246
+ contentOverlap,
247
+ sharedWords,
248
+ });
249
+ seen.add(pairKey);
250
+ }
251
+ }
252
+ }
253
+ return pairs;
254
+ }
194
255
  /**
195
256
  * Find memories with very similar titles/content and merge them.
196
257
  * Keeps the newer one, appends unique content from the older one.
@@ -200,81 +261,31 @@ export function deduplicateMemories(options) {
200
261
  return withTransaction(() => {
201
262
  const db = getDatabase();
202
263
  const pairs = [];
203
- // Query all LTM memories
204
- const ltmMemories = db.prepare("SELECT * FROM memories WHERE type = 'long_term' ORDER BY created_at ASC").all();
205
- // Group by category
206
- const groups = new Map();
207
- for (const mem of ltmMemories) {
208
- const cat = mem.category || 'note';
209
- if (!groups.has(cat))
210
- groups.set(cat, []);
211
- groups.get(cat).push(mem);
212
- }
213
264
  const removed = new Set();
214
- for (const [, group] of groups) {
215
- if (group.length < 2)
265
+ for (const pair of findDuplicateMemoryPairs()) {
266
+ if (removed.has(pair.memoryA.id) || removed.has(pair.memoryB.id))
216
267
  continue;
217
- for (let i = 0; i < group.length; i++) {
218
- const memA = group[i];
219
- if (removed.has(memA.id))
220
- continue;
221
- for (let j = i + 1; j < group.length; j++) {
222
- const memB = group[j];
223
- if (removed.has(memB.id))
224
- continue;
225
- const titleA = memA.title || '';
226
- const titleB = memB.title || '';
227
- const contentA = memA.content || '';
228
- const contentB = memB.content || '';
229
- // Check title similarity: Levenshtein > 0.7 OR 3+ shared words
230
- const titleSim = levenshteinSimilarity(titleA.toLowerCase(), titleB.toLowerCase());
231
- const titleSharedWords = sharedWordCount(titleA, titleB);
232
- const titlesMatch = titleSim > 0.7 || titleSharedWords >= 3;
233
- if (!titlesMatch)
234
- continue;
235
- // Check content overlap > 50%
236
- const contentOverlap = wordOverlap(contentA, contentB);
237
- if (contentOverlap <= 0.5)
238
- continue;
239
- // They are duplicates — keep the one with higher salience (or newer if equal)
240
- const salienceA = memA.salience || 0;
241
- const salienceB = memB.salience || 0;
242
- const createdA = new Date(memA.created_at || 0).getTime();
243
- const createdB = new Date(memB.created_at || 0).getTime();
244
- let kept;
245
- let discarded;
246
- if (salienceA > salienceB || (salienceA === salienceB && createdA >= createdB)) {
247
- kept = memA;
248
- discarded = memB;
249
- }
250
- else {
251
- kept = memB;
252
- discarded = memA;
253
- }
254
- const similarity = `title=${titleSim.toFixed(2)}, content=${contentOverlap.toFixed(2)}`;
255
- if (!dryRun) {
256
- // Append unique sentences from discarded to kept
257
- const keptContent = kept.content || '';
258
- const discardedContent = discarded.content || '';
259
- const keptSentences = new Set(keptContent.split(/[.!?\n]+/).map(s => s.trim().toLowerCase()).filter(s => s.length > 0));
260
- const uniqueSentences = discardedContent
261
- .split(/[.!?\n]+/)
262
- .map(s => s.trim())
263
- .filter(s => s.length > 0 && !keptSentences.has(s.toLowerCase()));
264
- if (uniqueSentences.length > 0) {
265
- const mergedContent = keptContent + '\n\nMerged from duplicate:\n' + uniqueSentences.join('. ') + '.';
266
- db.prepare('UPDATE memories SET content = ? WHERE id = ?').run(mergedContent, kept.id);
267
- }
268
- deleteMemory(discarded.id);
269
- }
270
- removed.add(discarded.id);
271
- pairs.push({
272
- kept: kept.id,
273
- removed: discarded.id,
274
- similarity,
275
- });
268
+ const removedId = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryB.id : pair.memoryA.id;
269
+ if (!dryRun) {
270
+ const kept = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryA : pair.memoryB;
271
+ const discarded = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryB : pair.memoryA;
272
+ const keptSentences = new Set(kept.content.split(/[.!?\n]+/).map(s => s.trim().toLowerCase()).filter(s => s.length > 0));
273
+ const uniqueSentences = discarded.content
274
+ .split(/[.!?\n]+/)
275
+ .map(s => s.trim())
276
+ .filter(s => s.length > 0 && !keptSentences.has(s.toLowerCase()));
277
+ if (uniqueSentences.length > 0) {
278
+ const mergedContent = kept.content + '\n\nMerged from duplicate:\n' + uniqueSentences.join('. ') + '.';
279
+ db.prepare('UPDATE memories SET content = ? WHERE id = ?').run(mergedContent, kept.id);
276
280
  }
281
+ deleteMemory(removedId);
277
282
  }
283
+ removed.add(removedId);
284
+ pairs.push({
285
+ kept: pair.recommendedKeepId,
286
+ removed: removedId,
287
+ similarity: pair.similarity,
288
+ });
278
289
  }
279
290
  return { merged: pairs.length, pairs };
280
291
  });
@@ -42,6 +42,9 @@ export declare function getMemoryById(id: number, source?: DefenceSource): Memor
42
42
  * Update a memory
43
43
  */
44
44
  export declare function updateMemory(id: number, updates: Partial<MemoryInput>): Memory | null;
45
+ export declare function mergeMemories(keptId: number, removedId: number, options?: {
46
+ reviewedBy?: string | null;
47
+ }): Memory | null;
45
48
  /**
46
49
  * Delete a memory
47
50
  */
@@ -5,7 +5,7 @@
5
5
  * Handles storage, retrieval, and management of memories.
6
6
  */
7
7
  import { randomUUID } from 'crypto';
8
- import { getDatabase, isDatabaseInitialized } from '../database/init.js';
8
+ import { getDatabase, isDatabaseInitialized, withTransaction } from '../database/init.js';
9
9
  import { DEFAULT_CONFIG, } from './types.js';
10
10
  import { calculateSalience, suggestCategory, extractTags, } from './salience.js';
11
11
  import { calculateDecayedScore, calculateReinforcementBoost, calculatePriority, } from './decay.js';
@@ -655,6 +655,103 @@ export function updateMemory(id, updates) {
655
655
  }
656
656
  return updatedMemory;
657
657
  }
658
+ function uniqueSentencesFrom(source, existing) {
659
+ const existingSentences = new Set(existing
660
+ .split(/[.!?\n]+/)
661
+ .map((sentence) => sentence.trim().toLowerCase())
662
+ .filter(Boolean));
663
+ return source
664
+ .split(/[.!?\n]+/)
665
+ .map((sentence) => sentence.trim())
666
+ .filter((sentence) => sentence.length > 0 && !existingSentences.has(sentence.toLowerCase()));
667
+ }
668
+ export function mergeMemories(keptId, removedId, options) {
669
+ if (keptId === removedId)
670
+ return getMemoryById(keptId);
671
+ return withTransaction(() => {
672
+ const db = getDatabase();
673
+ const kept = getMemoryById(keptId);
674
+ const removed = getMemoryById(removedId);
675
+ if (!kept || !removed)
676
+ return null;
677
+ const mergedSnippets = uniqueSentencesFrom(removed.content, kept.content);
678
+ const mergedContent = mergedSnippets.length > 0
679
+ ? `${kept.content}\n\nMerged from duplicate (${removed.title}):\n${mergedSnippets.join('. ')}.`
680
+ : kept.content;
681
+ const mergedTags = Array.from(new Set([...(kept.tags ?? []), ...(removed.tags ?? [])]));
682
+ const mergedFrom = Array.isArray(kept.metadata?.mergedFrom)
683
+ ? [...kept.metadata.mergedFrom]
684
+ : [];
685
+ mergedFrom.push({
686
+ id: removed.id,
687
+ uuid: removed.uuid,
688
+ title: removed.title,
689
+ mergedAt: new Date().toISOString(),
690
+ });
691
+ const mergedMetadata = {
692
+ ...kept.metadata,
693
+ mergedFrom,
694
+ mergedFromCount: mergedFrom.length,
695
+ };
696
+ const mergedStatus = kept.status === 'canonical' || removed.status === 'canonical'
697
+ ? 'canonical'
698
+ : kept.status === 'active' || removed.status === 'active'
699
+ ? 'active'
700
+ : kept.status;
701
+ const mergedPinned = kept.pinned || removed.pinned || mergedStatus === 'canonical';
702
+ const mergedCloudExcluded = kept.cloudExcluded || removed.cloudExcluded;
703
+ const mergedScope = kept.scope === 'global' || removed.scope === 'global' ? 'global' : 'project';
704
+ const mergedProject = kept.project ?? removed.project ?? null;
705
+ const mergedTransferable = kept.transferable || removed.transferable;
706
+ const mergedSalience = Math.max(kept.salience, removed.salience);
707
+ const mergedTrustScore = Math.max(kept.trustScore ?? 0, removed.trustScore ?? 0);
708
+ const mergedAccessCount = kept.accessCount + removed.accessCount;
709
+ const mergedLastAccessed = new Date(Math.max(new Date(kept.lastAccessed).getTime(), new Date(removed.lastAccessed).getTime())).toISOString();
710
+ const mergedReviewedBy = options?.reviewedBy ?? kept.reviewedBy ?? 'review-merge';
711
+ const mergedSensitivity = kept.sensitivityLevel === 'SECRET' || removed.sensitivityLevel === 'SECRET'
712
+ ? 'SECRET'
713
+ : kept.sensitivityLevel === 'CONFIDENTIAL' || removed.sensitivityLevel === 'CONFIDENTIAL'
714
+ ? 'CONFIDENTIAL'
715
+ : kept.sensitivityLevel ?? removed.sensitivityLevel ?? 'INTERNAL';
716
+ db.prepare(`
717
+ UPDATE memories
718
+ SET content = ?,
719
+ tags = ?,
720
+ salience = ?,
721
+ project = ?,
722
+ metadata = ?,
723
+ scope = ?,
724
+ transferable = ?,
725
+ status = ?,
726
+ pinned = ?,
727
+ reviewed_by = ?,
728
+ reviewed_at = ?,
729
+ trust_score = ?,
730
+ sensitivity_level = ?,
731
+ cloud_excluded = ?,
732
+ access_count = ?,
733
+ last_accessed = ?,
734
+ updated_at = CURRENT_TIMESTAMP
735
+ WHERE id = ?
736
+ `).run(mergedContent, JSON.stringify(mergedTags), mergedSalience, mergedProject, JSON.stringify(mergedMetadata), mergedScope, mergedTransferable ? 1 : 0, mergedStatus, mergedPinned ? 1 : 0, mergedReviewedBy, new Date().toISOString(), mergedTrustScore, mergedSensitivity, mergedCloudExcluded ? 1 : 0, mergedAccessCount, mergedLastAccessed, kept.id);
737
+ const updatedMemory = getMemoryById(kept.id);
738
+ try {
739
+ const extraction = extractFromMemory(updatedMemory.title, updatedMemory.content, updatedMemory.category);
740
+ replaceMemoryGraph(updatedMemory.id, extraction);
741
+ }
742
+ catch (e) {
743
+ console.error('[shieldcortex] Entity extraction refresh failed after merge:', e);
744
+ }
745
+ emitMemoryUpdated(updatedMemory);
746
+ persistEvent('memory_updated', { memory: updatedMemory, mergedFromId: removed.id });
747
+ if (isFeatureEnabled('cloud_sync')) {
748
+ syncMemoryUpsertToCloud(updatedMemory);
749
+ syncGraphForMemoryToCloud(updatedMemory.id);
750
+ }
751
+ deleteMemory(removed.id);
752
+ return updatedMemory;
753
+ });
754
+ }
658
755
  /**
659
756
  * Delete a memory
660
757
  */
@@ -13,6 +13,9 @@ const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
14
  const SERVER_NAME = 'shieldcortex-memory';
15
15
  const MCP_ENTRY = path.resolve(__dirname, '..', 'index.js');
16
+ function escapeRegExp(value) {
17
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ }
16
19
  function getCodexConfigPath() {
17
20
  return path.join(os.homedir(), '.codex', 'config.toml');
18
21
  }
@@ -38,32 +41,66 @@ function buildServerBlock() {
38
41
  '',
39
42
  ].join('\n');
40
43
  }
44
+ function parseSections(content) {
45
+ const lines = content.split(/\r?\n/);
46
+ const sections = [];
47
+ let current = { header: null, lines: [] };
48
+ for (const line of lines) {
49
+ const headerMatch = line.match(/^\[([^\]]+)\]\s*$/);
50
+ if (headerMatch) {
51
+ sections.push(current);
52
+ current = { header: headerMatch[1], lines: [line] };
53
+ continue;
54
+ }
55
+ current.lines.push(line);
56
+ }
57
+ sections.push(current);
58
+ return sections;
59
+ }
60
+ function normalizeContent(content) {
61
+ const trimmed = content.replace(/\n{3,}/g, '\n\n').trimEnd();
62
+ return trimmed.length > 0 ? `${trimmed}\n` : '';
63
+ }
41
64
  function hasServerBlock(content) {
42
- return new RegExp(`^\\[mcp_servers\\.${SERVER_NAME}\\]`, 'm').test(content);
65
+ return new RegExp(`^\\[mcp_servers\\.${escapeRegExp(SERVER_NAME)}\\]\\r?$`, 'm').test(content);
43
66
  }
44
67
  function upsertServerBlock(content) {
45
- const block = buildServerBlock();
46
- const sectionPattern = new RegExp(`^\\[mcp_servers\\.${SERVER_NAME}\\]\\n[\\s\\S]*?(?=^\\[|\\Z)`, 'm');
47
- if (sectionPattern.test(content)) {
48
- const nextContent = content.replace(sectionPattern, block);
49
- return { content: nextContent, changed: nextContent !== content };
68
+ const block = buildServerBlock().trimEnd();
69
+ const sections = parseSections(content);
70
+ const output = [];
71
+ let insertionIndex = -1;
72
+ let found = false;
73
+ for (const section of sections) {
74
+ if (section.header === `mcp_servers.${SERVER_NAME}`) {
75
+ if (insertionIndex === -1)
76
+ insertionIndex = output.length;
77
+ found = true;
78
+ continue;
79
+ }
80
+ const rendered = section.lines.join('\n').trimEnd();
81
+ if (rendered.length > 0)
82
+ output.push(rendered);
83
+ }
84
+ if (insertionIndex === -1)
85
+ insertionIndex = output.length;
86
+ output.splice(insertionIndex, 0, block);
87
+ const normalized = normalizeContent(output.join('\n\n'));
88
+ if (found) {
89
+ return { content: normalized, changed: normalized !== content };
50
90
  }
51
- const trimmed = content.trimEnd();
52
- return {
53
- content: trimmed.length > 0 ? `${trimmed}\n\n${block}` : block,
54
- changed: true,
55
- };
91
+ return { content: normalized, changed: normalized !== content };
56
92
  }
57
93
  function removeServerBlock(content) {
58
- const sectionPattern = new RegExp(`^\\[mcp_servers\\.${SERVER_NAME}\\]\\n[\\s\\S]*?(?=^\\[|\\Z)`, 'm');
59
- if (!sectionPattern.test(content)) {
94
+ const sections = parseSections(content);
95
+ const filtered = sections.filter((section) => section.header !== `mcp_servers.${SERVER_NAME}`);
96
+ if (filtered.length === sections.length) {
60
97
  return { content, changed: false };
61
98
  }
62
- const nextContent = content
63
- .replace(sectionPattern, '')
64
- .replace(/\n{3,}/g, '\n\n')
65
- .trimEnd();
66
- return { content: nextContent.length > 0 ? `${nextContent}\n` : '', changed: true };
99
+ const normalized = normalizeContent(filtered
100
+ .map((section) => section.lines.join('\n').trimEnd())
101
+ .filter(Boolean)
102
+ .join('\n\n'));
103
+ return { content: normalized, changed: true };
67
104
  }
68
105
  export async function installCodex() {
69
106
  if (!fs.existsSync(MCP_ENTRY)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shieldcortex",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Trustworthy memory and security for AI agents. Recall debugging, review queue, OpenClaw session capture, and memory poisoning defence for Claude Code, Codex, OpenClaw, LangChain, and MCP agents.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",