memorix 1.4.5 → 1.5.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 (75) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +5 -2
  4. package/dist/cli/index.js +1470 -1014
  5. package/dist/index.js +1279 -989
  6. package/dist/{maintenance-jobs-B1-W0ssz.d.ts → maintenance-jobs-DVRF-46r.d.ts} +1 -1
  7. package/dist/maintenance-runner.d.ts +1 -1
  8. package/dist/maintenance-runner.js +1769 -830
  9. package/dist/media-audio-runner.js +1774 -836
  10. package/dist/media-video-runner.js +1769 -831
  11. package/dist/memcode-runtime/CHANGELOG.md +78 -0
  12. package/dist/sdk.js +1271 -988
  13. package/dist/types.d.ts +6 -0
  14. package/dist/vector-backfill-runner.d.ts +1 -1
  15. package/dist/vector-backfill-runner.js +1765 -827
  16. package/docs/1.4.5-AGENT-AUTO-MEMORY-RESEARCH.md +95 -0
  17. package/docs/API_REFERENCE.md +23 -4
  18. package/docs/CONFIGURATION.md +29 -3
  19. package/docs/SETUP.md +5 -3
  20. package/package.json +1 -1
  21. package/plugins/antigravity/memorix/mcp_config.json +3 -1
  22. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  23. package/plugins/claude/memorix/.mcp.json +1 -1
  24. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  25. package/plugins/codebuddy/memorix/.mcp.json +1 -1
  26. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  27. package/plugins/codex/memorix/.mcp.json +1 -1
  28. package/plugins/copilot/memorix/.mcp.json +1 -1
  29. package/plugins/copilot/memorix/plugin.json +1 -1
  30. package/plugins/gemini/memorix/gemini-extension.json +4 -2
  31. package/plugins/omp/memorix/package.json +1 -1
  32. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  33. package/plugins/openclaw/memorix/.mcp.json +3 -1
  34. package/plugins/pi/memorix/package.json +1 -1
  35. package/server.json +2 -2
  36. package/src/cli/commands/dashboard.ts +14 -3
  37. package/src/cli/commands/ingest-commit.ts +47 -3
  38. package/src/cli/commands/memory.ts +17 -4
  39. package/src/cli/commands/purge.ts +120 -0
  40. package/src/cli/commands/serve-http.ts +39 -5
  41. package/src/cli/commands/setup.ts +3 -1
  42. package/src/cli/index.ts +3 -804
  43. package/src/codegraph/auto-context.ts +82 -1
  44. package/src/config/yaml-loader.ts +3 -8
  45. package/src/config.ts +23 -7
  46. package/src/evaluation/session-replay.ts +98 -0
  47. package/src/git/extractor.ts +5 -3
  48. package/src/hooks/installers/index.ts +15 -0
  49. package/src/knowledge/workset.ts +58 -0
  50. package/src/memory/long-term.ts +97 -2
  51. package/src/runtime/control-plane-maintenance.ts +1 -0
  52. package/src/runtime/isolated-maintenance.ts +1 -0
  53. package/src/runtime/lifecycle.ts +19 -0
  54. package/src/runtime/maintenance-jobs.ts +1 -0
  55. package/src/runtime/project-maintenance.ts +8 -0
  56. package/src/search/surfaced-registry.ts +62 -0
  57. package/src/server.ts +74 -14
  58. package/src/store/orama-store.ts +17 -1
  59. package/src/types.ts +6 -0
  60. package/src/cli/commands/audit-list.ts +0 -89
  61. package/src/cli/commands/purge-all-memory.ts +0 -85
  62. package/src/cli/commands/purge-project-memory.ts +0 -83
  63. package/src/cli/commands/uninstall-project-artifacts.ts +0 -86
  64. package/src/cli/tui/Sidebar.tsx +0 -179
  65. package/src/cli/tui/interactive-commands.ts +0 -261
  66. package/src/cli/tui/markdown-render.tsx +0 -371
  67. package/src/cli/tui/use-mouse.ts +0 -157
  68. package/src/cli/tui/useNavigation.ts +0 -56
  69. package/src/orchestrate/context-compact.ts +0 -188
  70. package/src/team/file-locks.ts +0 -173
  71. package/src/team/index.ts +0 -23
  72. package/src/team/messages.ts +0 -203
  73. package/src/team/persistence.ts +0 -96
  74. package/src/team/registry.ts +0 -158
  75. package/src/team/tasks.ts +0 -211
package/src/server.ts CHANGED
@@ -52,6 +52,7 @@ import type { FormationConfig, SearchHit, FormedMemory, FormationStage, Formatio
52
52
  import { parseFormationTimeoutMs } from './server/formation-timeout.js';
53
53
  import { withTimeout } from './timeout.js';
54
54
  import { sanitizeCredentials } from './memory/secret-filter.js';
55
+ import { getSurfacedIds, recordSurfacedIds } from './search/surfaced-registry.js';
55
56
  import {
56
57
  createProjectBindingController,
57
58
  type ProjectBindingController,
@@ -508,6 +509,12 @@ export async function createMemorixServer(
508
509
  kind: 'consolidation',
509
510
  dedupeKey: 'startup-consolidation',
510
511
  });
512
+ lifecycle.enqueueLongTermMaintenance({
513
+ dataDir: projectDir,
514
+ projectId: project.id,
515
+ source: 'startup',
516
+ queue,
517
+ });
511
518
  }
512
519
 
513
520
  // This check is only a small SQLite metadata query. The actual directory
@@ -1152,7 +1159,7 @@ export async function createMemorixServer(
1152
1159
  let longTermNote = '';
1153
1160
  if (longTerm) {
1154
1161
  try {
1155
- const { promoteObservationToLongTermMemory } = await import('./memory/long-term.js');
1162
+ const { promoteObservationToLongTermMemory, maybeAutoQualifyLongTermMemory } = await import('./memory/long-term.js');
1156
1163
  const promoted = await promoteObservationToLongTermMemory({
1157
1164
  dataDir: projectDir,
1158
1165
  observation: obs,
@@ -1166,7 +1173,28 @@ export async function createMemorixServer(
1166
1173
  ...(reader.isTeamMember ? { isTeamMember: true } : {}),
1167
1174
  },
1168
1175
  });
1169
- longTermNote = `\nLong-term candidate: ${promoted.memory.id} (${promoted.memory.kind}/${promoted.memory.scope}). Review it with the CLI before it is delivered automatically.`;
1176
+ // An explicit store call carries its own source evidence: qualify
1177
+ // on the spot instead of waiting for a manual CLI review.
1178
+ const autoQualified = await maybeAutoQualifyLongTermMemory({
1179
+ dataDir: projectDir,
1180
+ id: promoted.memory.id,
1181
+ sourceDetail: 'explicit',
1182
+ });
1183
+ if (autoQualified) {
1184
+ // A fresh explicit record retires stale same-title records on the
1185
+ // durable rule leg instead of waiting for the next session start.
1186
+ try {
1187
+ const { enqueueLongTermMaintenance } = await import('./runtime/lifecycle.js');
1188
+ enqueueLongTermMaintenance({
1189
+ dataDir: projectDir,
1190
+ projectId: project.id,
1191
+ source: 'long-term:' + promoted.memory.id,
1192
+ });
1193
+ } catch { /* maintenance is optional; the record already qualified */ }
1194
+ }
1195
+ longTermNote = autoQualified
1196
+ ? `\nLong-term memory: ${promoted.memory.id} (${promoted.memory.kind}/${promoted.memory.scope}, auto-qualified) and now enters briefs as durable context. Audit with \`memorix memory long-term list\`.`
1197
+ : `\nLong-term candidate: ${promoted.memory.id} (${promoted.memory.kind}/${promoted.memory.scope}). Review it with the CLI before it is delivered automatically.`;
1170
1198
  } catch (longTermError) {
1171
1199
  longTermNote = `\n[WARN] Observation stored, but long-term candidate was not created: ${longTermError instanceof Error ? longTermError.message : 'unknown error'}`;
1172
1200
  }
@@ -1394,6 +1422,17 @@ export async function createMemorixServer(
1394
1422
  const boundary = requireExplicitAutopilotExpansion('memorix_search', purpose);
1395
1423
  if (boundary) return boundary;
1396
1424
  }
1425
+
1426
+ // Session dedup scope: prefer the active session id, fall back to the
1427
+ // bound project. Best-effort bookkeeping — losing it only costs one
1428
+ // redundant result row, never data.
1429
+ let surfacedKey = `project:${project.id}`;
1430
+ try {
1431
+ const { getActiveSession } = await import('./memory/session.js');
1432
+ const active = await getActiveSession(projectDir, project.id);
1433
+ if (active) surfacedKey = `session:${active.id}`;
1434
+ } catch { /* fall back to the project key */ }
1435
+
1397
1436
  return withFreshIndex(async () => {
1398
1437
 
1399
1438
  const safeLimit = limit != null ? coerceNumber(limit, 20) : undefined;
@@ -1415,6 +1454,7 @@ export async function createMemorixServer(
1415
1454
  source: source as 'agent' | 'git' | 'manual' | undefined,
1416
1455
  quality: quality as 'fast' | 'balanced' | 'thorough',
1417
1456
  reader: getObservationReader(scope === 'global' ? 'global' : 'project'),
1457
+ surfacedIds: getSurfacedIds(surfacedKey),
1418
1458
  });
1419
1459
 
1420
1460
  let result;
@@ -1436,6 +1476,10 @@ export async function createMemorixServer(
1436
1476
  throw error;
1437
1477
  }
1438
1478
 
1479
+ // These rows were just shown to the model — remember them so later
1480
+ // searches in the same session demote instead of re-showing.
1481
+ recordSurfacedIds(surfacedKey, result.entries.map(entry => entry.id));
1482
+
1439
1483
  const activeBoundary = getActiveAutopilotRetrievalBoundary();
1440
1484
  if (
1441
1485
  scope !== 'global'
@@ -4998,6 +5042,12 @@ export async function createMemorixServer(
4998
5042
  return undefined;
4999
5043
  }
5000
5044
 
5045
+ // The advertised schema matches what the operator actually enabled: a
5046
+ // closed gate means the action does not exist for the agent, not that it
5047
+ // exists and throws. The handler keeps its own runtime checks as well.
5048
+ const mediaGenerationEnabled = process.env.MEMORIX_MCP_MEDIA_GENERATION === '1';
5049
+ const mediaTranscriptionEnabled = process.env.MEMORIX_MCP_MEDIA_TRANSCRIPTION === '1';
5050
+ const mediaGenerationOrTranscription = mediaGenerationEnabled || mediaTranscriptionEnabled;
5001
5051
  server.registerTool(
5002
5052
  'memorix_media',
5003
5053
  {
@@ -5008,7 +5058,11 @@ export async function createMemorixServer(
5008
5058
  'Use the CLI for destructive removal, quota cleanup, and direct generation. ' +
5009
5059
  'MCP generation is disabled by default and requires MEMORIX_MCP_MEDIA_GENERATION=1 after the operator reviews provider billing.',
5010
5060
  inputSchema: {
5011
- action: z.enum(['import', 'attach', 'list', 'show', 'derive-pdf', 'derive-audio', 'generate-image', 'generate-video', 'status', 'cancel']),
5061
+ action: z.enum([
5062
+ 'import', 'attach', 'list', 'show', 'derive-pdf', 'status', 'cancel',
5063
+ ...(mediaTranscriptionEnabled ? ['derive-audio'] : []),
5064
+ ...(mediaGenerationEnabled ? ['generate-image', 'generate-video'] : []),
5065
+ ] as [string, ...string[]]),
5012
5066
  path: z.string().optional().describe('Explicit local image/audio/video/PDF path for import.'),
5013
5067
  assetId: z.string().optional().describe('Controlled MediaAsset ID for attach/show.'),
5014
5068
  jobId: z.string().optional().describe('Durable media job ID for status.'),
@@ -5016,20 +5070,26 @@ export async function createMemorixServer(
5016
5070
  limit: z.number().int().min(1).max(100).optional().describe('Maximum assets to list.'),
5017
5071
  title: z.string().optional().describe('Observation title when attaching generated/imported output.'),
5018
5072
  narrative: z.string().optional().describe('Short retrieval text when attaching an asset.'),
5019
- prompt: z.string().optional().describe('Explicit MiniMax generation prompt.'),
5020
- image: z.string().optional().describe('Base64-encoded reference image for image-to-image generation.'),
5021
- model: z.string().max(160).optional().describe('Provider model for the selected media action.'),
5022
- provider: z.enum(['openai', 'groq']).optional().describe('Explicit audio transcription provider.'),
5023
- language: z.string().max(32).optional().describe('Optional ISO-639-1 hint for audio transcription.'),
5024
- region: z.enum(['global', 'cn']).optional().describe('MiniMax deployment region.'),
5025
- n: z.number().int().min(1).max(4).optional().describe('Image output count.'),
5026
- ratio: z.enum(['adaptive', '1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9']).optional().describe('Image or video aspect ratio.'),
5027
- width: z.number().int().min(1).max(8192).optional().describe('Requested image width.'),
5028
- height: z.number().int().min(1).max(8192).optional().describe('Requested image height.'),
5029
- duration: z.union([z.literal(5), z.literal(10)]).optional().describe('MiniMax video duration in seconds.'),
5030
5073
  maxPages: z.number().int().min(1).max(100).optional().describe('Bounded PDF extraction page limit.'),
5031
5074
  maxChars: z.number().int().min(1).max(60_000).optional().describe('Bounded PDF extraction character limit.'),
5032
5075
  attach: z.boolean().optional().describe('Attach generated/imported output to normal project memory explicitly.'),
5076
+ ...(mediaGenerationOrTranscription ? {
5077
+ model: z.string().max(160).optional().describe('Provider model for the selected media action.'),
5078
+ prompt: z.string().optional().describe('Explicit prompt for generation or transcription.'),
5079
+ } : {}),
5080
+ ...(mediaTranscriptionEnabled ? {
5081
+ provider: z.enum(['openai', 'groq']).optional().describe('Explicit audio transcription provider.'),
5082
+ language: z.string().max(32).optional().describe('Optional ISO-639-1 hint for audio transcription.'),
5083
+ } : {}),
5084
+ ...(mediaGenerationEnabled ? {
5085
+ image: z.string().optional().describe('Base64-encoded reference image for image-to-image generation.'),
5086
+ region: z.enum(['global', 'cn']).optional().describe('MiniMax deployment region.'),
5087
+ n: z.number().int().min(1).max(4).optional().describe('Image output count.'),
5088
+ ratio: z.enum(['adaptive', '1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9']).optional().describe('Image or video aspect ratio.'),
5089
+ width: z.number().int().min(1).max(8192).optional().describe('Requested image width.'),
5090
+ height: z.number().int().min(1).max(8192).optional().describe('Requested image height.'),
5091
+ duration: z.union([z.literal(5), z.literal(10)]).optional().describe('MiniMax video duration in seconds.'),
5092
+ } : {}),
5033
5093
  },
5034
5094
  },
5035
5095
  async ({ action, path: assetPath, assetId, jobId, kind, limit, title, narrative, prompt, image, model, provider, language, region, n, ratio, width, height, duration, maxPages, maxChars, attach }) => {
@@ -587,7 +587,9 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
587
587
 
588
588
  // Orama's vector/hybrid search can leak cross-project hits even when `where`
589
589
  // is present, so always keep enough headroom for a deterministic post-filter.
590
- const requestLimit = projectIds
590
+ // Session dedup demotes already-surfaced entries after ranking, so it needs
591
+ // the same headroom to let unseen candidates rise into the final limit.
592
+ const requestLimit = projectIds || (options.surfacedIds && options.surfacedIds.length > 0)
591
593
  ? (options.limit ?? 20) * 3
592
594
  : (options.limit ?? 20);
593
595
 
@@ -902,6 +904,20 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
902
904
  intermediate.sort((a, b) => b.score - a.score);
903
905
  }
904
906
 
907
+ // ── Session dedup (demote, never remove) ────────────────────────
908
+ // Entries already surfaced earlier in the same session are demoted so
909
+ // unseen evidence can rise into the final limit. A demoted entry is never
910
+ // removed: when it is the strong match for a new question, it still wins
911
+ // on score and answers that turn.
912
+ if (options.surfacedIds && options.surfacedIds.length > 0) {
913
+ const SURFACED_PENALTY = 0.5;
914
+ const surfaced = new Set(options.surfacedIds);
915
+ intermediate = intermediate.map(entry =>
916
+ surfaced.has(entry.id) ? { ...entry, score: (entry.score ?? 0) * SURFACED_PENALTY } : entry,
917
+ );
918
+ intermediate.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
919
+ }
920
+
905
921
  // Temporal filtering: since/until date range
906
922
  if (options.since) {
907
923
  const sinceDate = new Date(options.since).getTime();
package/src/types.ts CHANGED
@@ -317,6 +317,12 @@ export interface SearchOptions {
317
317
  trackAccess?: boolean;
318
318
  /** Internal reader context for agent-facing visibility filtering. */
319
319
  reader?: ObservationReader;
320
+ /**
321
+ * Observation ids already surfaced earlier in the same session. Surfaced
322
+ * entries are demoted (never removed) so unseen evidence can rise into the
323
+ * result limit; a strong match still answers a new question.
324
+ */
325
+ surfacedIds?: number[];
320
326
  /**
321
327
  * `fast` stays fully local, `balanced` (default) permits embeddings, and
322
328
  * `thorough` explicitly permits optional LLM query rewrite and reranking.
@@ -1,89 +0,0 @@
1
- /**
2
- * CLI Command: memorix audit list
3
- *
4
- * List all files written by Memorix (audit trail).
5
- */
6
-
7
- import { defineCommand } from 'citty';
8
- import * as p from '@clack/prompts';
9
-
10
- export default defineCommand({
11
- meta: {
12
- name: 'list',
13
- description: 'List Memorix-written files (audit trail)',
14
- },
15
- args: {
16
- project: {
17
- type: 'string',
18
- description: 'Filter by project (optional)',
19
- required: false,
20
- },
21
- },
22
- run: async ({ args }) => {
23
- const { getAllAuditEntries, getProjectId } = await import('../../audit/index.js');
24
- const os = await import('node:os');
25
- let cwd: string;
26
- try {
27
- cwd = process.cwd();
28
- } catch {
29
- cwd = os.homedir();
30
- console.log(`[WARN] Could not access current directory, using home: ${cwd}`);
31
- }
32
-
33
- p.intro('Memorix Audit Trail');
34
-
35
- const allEntries = await getAllAuditEntries();
36
-
37
- if (allEntries.length === 0) {
38
- console.log('No Memorix-written files found.');
39
- p.outro('Audit trail is empty.');
40
- return;
41
- }
42
-
43
- // Filter by project if specified
44
- let entries = allEntries;
45
- if (args.project) {
46
- const targetProjectId = getProjectId(args.project);
47
- entries = allEntries.filter((e) => e.projectId === targetProjectId);
48
- }
49
-
50
- if (entries.length === 0) {
51
- console.log('No Memorix-written files found for this project.');
52
- p.outro('Audit trail is empty for this project.');
53
- return;
54
- }
55
-
56
- // Group by project
57
- const byProject: Record<string, Array<{ entry: import('../../audit/index.js').AuditEntry }>> = {};
58
- for (const { projectId, entry } of entries) {
59
- if (!byProject[projectId]) {
60
- byProject[projectId] = [];
61
- }
62
- byProject[projectId].push({ entry });
63
- }
64
-
65
- // Display by project
66
- console.log('');
67
- for (const [projectId, items] of Object.entries(byProject)) {
68
- console.log(`Project: ${projectId}`);
69
- console.log('');
70
-
71
- for (const { entry } of items) {
72
- const icon = entry.type === 'hook' ? '[HOOK]' : '[TASK]';
73
- const agent = entry.agent ? ` (${entry.agent})` : '';
74
- console.log(`${icon} ${entry.path}${agent}`);
75
- console.log(` Created: ${entry.createdAt}`);
76
- console.log('');
77
- }
78
- }
79
-
80
- // Summary
81
- const hookCount = entries.filter((e) => e.entry.type === 'hook').length;
82
- const ruleCount = entries.filter((e) => e.entry.type === 'rule').length;
83
- const projectCount = new Set(entries.map((e) => e.projectId)).size;
84
-
85
- p.note(`${projectCount} projects, ${hookCount} hooks, ${ruleCount} rules`);
86
-
87
- p.outro('Audit trail complete.');
88
- },
89
- });
@@ -1,85 +0,0 @@
1
- /**
2
- * CLI Command: memorix purge all-memory
3
- *
4
- * Delete ALL memories (dangerous operation).
5
- */
6
-
7
- import { defineCommand } from 'citty';
8
- import * as p from '@clack/prompts';
9
-
10
- export default defineCommand({
11
- meta: {
12
- name: 'all-memory',
13
- description: 'Delete ALL memories (dangerous)',
14
- },
15
- args: {},
16
- run: async ({}) => {
17
- p.intro('[WARN] Purge All Memory');
18
-
19
- // Show warning
20
- console.log('');
21
- console.log('[WARN] WARNING: This will delete ALL memories!');
22
- console.log('');
23
- console.log('This will remove:');
24
- console.log(' - All observations from all projects');
25
- console.log(' - All entities and relations');
26
- console.log(' - All embeddings');
27
- console.log('');
28
- console.log('This action CANNOT be undone.');
29
- console.log('');
30
- console.log('Project artifacts (hook files) will be preserved.');
31
- console.log('');
32
-
33
- // First confirmation
34
- const confirmed1 = await p.confirm({
35
- message: 'Are you sure you want to delete ALL memories?',
36
- });
37
-
38
- if (p.isCancel(confirmed1) || !confirmed1) {
39
- p.outro('Purge cancelled.');
40
- return;
41
- }
42
-
43
- // Second confirmation (type "DELETE" to confirm)
44
- const confirmed2 = await p.text({
45
- message: 'Type "DELETE" to confirm:',
46
- validate: (value) => {
47
- if (value !== 'DELETE') {
48
- return 'Please type "DELETE" to confirm';
49
- }
50
- return undefined;
51
- },
52
- });
53
-
54
- if (p.isCancel(confirmed2) || confirmed2 !== 'DELETE') {
55
- p.outro('Purge cancelled.');
56
- return;
57
- }
58
-
59
- // Delete all memories
60
- try {
61
- const {
62
- getAllObservations,
63
- resolveObservations,
64
- } = await import('../../memory/observations.js');
65
- const { withFreshIndex } = await import('../../memory/freshness.js');
66
- const observations = await withFreshIndex(() => getAllObservations());
67
- const ids = observations.map((o) => o.id);
68
-
69
- if (ids.length === 0) {
70
- console.log('No observations found.');
71
- p.outro('No memories to purge.');
72
- return;
73
- }
74
-
75
- const result = await resolveObservations(ids, 'archived');
76
-
77
- console.log(`[OK] Archived ${result.resolved.length} observations`);
78
-
79
- p.outro(`All memory purged. ${result.resolved.length} observations archived.`);
80
- } catch (err) {
81
- console.error(`[ERROR] Failed to purge all memory: ${err}`);
82
- p.outro('Purge failed.');
83
- }
84
- },
85
- });
@@ -1,83 +0,0 @@
1
- /**
2
- * CLI Command: memorix purge project-memory
3
- *
4
- * Delete memories for the current project only.
5
- */
6
-
7
- import { defineCommand } from 'citty';
8
- import * as p from '@clack/prompts';
9
-
10
- export default defineCommand({
11
- meta: {
12
- name: 'project-memory',
13
- description: 'Delete memories for current project',
14
- },
15
- args: {},
16
- run: async ({}) => {
17
- const os = await import('node:os');
18
- let cwd: string;
19
- try { cwd = process.cwd(); } catch { cwd = os.homedir(); }
20
-
21
- p.intro('Purge Project Memory');
22
-
23
- // Show what will be deleted
24
- console.log('');
25
- console.log('Will delete memories for:');
26
- console.log(` Project: ${cwd}`);
27
- console.log('');
28
- console.log('This will remove all observations, entities, and relations');
29
- console.log('associated with this project from the memory database.');
30
- console.log('');
31
- console.log('Project artifacts (hook files) will be preserved.');
32
- console.log('');
33
-
34
- // Confirm
35
- const confirmed = await p.confirm({
36
- message: 'Continue purge?',
37
- });
38
-
39
- if (p.isCancel(confirmed) || !confirmed) {
40
- p.outro('Purge cancelled.');
41
- return;
42
- }
43
-
44
- // Get project ID
45
- const projectId = getProjectId(cwd);
46
-
47
- // Delete project memories
48
- try {
49
- const {
50
- getProjectObservations,
51
- resolveObservations,
52
- } = await import('../../memory/observations.js');
53
- const { withFreshIndex } = await import('../../memory/freshness.js');
54
- const observations = await withFreshIndex(() => getProjectObservations(projectId));
55
- const ids = observations.map((o) => o.id);
56
-
57
- if (ids.length === 0) {
58
- console.log('No observations found for this project.');
59
- p.outro('No memories to purge.');
60
- return;
61
- }
62
-
63
- const result = await resolveObservations(ids, 'archived');
64
-
65
- console.log(`[OK] Archived ${result.resolved.length} observations for project`);
66
-
67
- p.outro(`Project memory purged. ${result.resolved.length} observations archived.`);
68
- } catch (err) {
69
- console.error(`[ERROR] Failed to purge project memory: ${err}`);
70
- p.outro('Purge failed.');
71
- }
72
- },
73
- });
74
-
75
- function getProjectId(projectRoot: string): string {
76
- try {
77
- const { detectProject } = require('../project/detector.js');
78
- const project = detectProject(projectRoot);
79
- if (project) return project.id;
80
- } catch { /* fallback below */ }
81
- const normalized = projectRoot.replace(/\\/g, '/');
82
- return `untracked/${normalized}`;
83
- }
@@ -1,86 +0,0 @@
1
- /**
2
- * CLI Command: memorix uninstall project-artifacts
3
- *
4
- * Uninstall project-level hook files only (preserve memory data).
5
- */
6
-
7
- import { defineCommand } from 'citty';
8
- import * as p from '@clack/prompts';
9
-
10
- export default defineCommand({
11
- meta: {
12
- name: 'project-artifacts',
13
- description: 'Uninstall project hook files (preserve memory data)',
14
- },
15
- args: {
16
- agent: {
17
- type: 'string',
18
- description: 'Target agent (all if omitted)',
19
- required: false,
20
- },
21
- },
22
- run: async ({ args }) => {
23
- const { detectInstalledAgents, uninstallHooks } = await import('../../hooks/installers/index.js');
24
- const os = await import('node:os');
25
- let cwd: string;
26
- try { cwd = process.cwd(); } catch { cwd = os.homedir(); }
27
-
28
- p.intro('Uninstall Project Artifacts');
29
-
30
- let agents: string[];
31
- if (args.agent) {
32
- agents = [args.agent];
33
- } else {
34
- agents = await detectInstalledAgents();
35
- }
36
-
37
- if (agents.length === 0) {
38
- console.log('No supported agents detected.');
39
- return;
40
- }
41
-
42
- // Show what will be uninstalled
43
- console.log('');
44
- console.log('Will uninstall hooks for:');
45
- for (const agent of agents) {
46
- console.log(` - ${agent}`);
47
- }
48
- console.log('');
49
- console.log('This will remove hook files from the project:');
50
- console.log(' - .claude/settings.local.json');
51
- console.log(' - .windsurf/hooks.json');
52
- console.log(' - .cursor/hooks.json');
53
- console.log(' - .github/hooks/memorix.json');
54
- console.log(' - .cursor/rules/memorix.mdc');
55
- console.log(' - etc.');
56
- console.log('');
57
- console.log('Memory data will be preserved.');
58
- console.log('');
59
-
60
- // Confirm
61
- const confirmed = await p.confirm({
62
- message: 'Continue uninstall?',
63
- });
64
-
65
- if (p.isCancel(confirmed) || !confirmed) {
66
- p.outro('Uninstall cancelled.');
67
- return;
68
- }
69
-
70
- // Uninstall
71
- for (const agent of agents) {
72
- const ok = await uninstallHooks(
73
- agent as import('../../hooks/types.js').AgentName,
74
- cwd,
75
- false, // project-level
76
- );
77
- if (ok) {
78
- console.log(`[OK] ${agent}: hooks removed`);
79
- } else {
80
- console.log(`[SKIP] ${agent}: no hooks found`);
81
- }
82
- }
83
-
84
- p.outro('Project artifacts uninstalled. Memory data preserved.');
85
- },
86
- });