claude-memory-explorer 0.1.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 (80) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +177 -0
  4. package/dist/cli/commands/dedupe.d.ts +1 -0
  5. package/dist/cli/commands/dedupe.js +187 -0
  6. package/dist/cli/commands/dedupe.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +1 -0
  8. package/dist/cli/commands/doctor.js +177 -0
  9. package/dist/cli/commands/doctor.js.map +1 -0
  10. package/dist/cli/commands/lint.d.ts +1 -0
  11. package/dist/cli/commands/lint.js +139 -0
  12. package/dist/cli/commands/lint.js.map +1 -0
  13. package/dist/cli/commands/list.d.ts +1 -0
  14. package/dist/cli/commands/list.js +97 -0
  15. package/dist/cli/commands/list.js.map +1 -0
  16. package/dist/cli/commands/mcp.d.ts +1 -0
  17. package/dist/cli/commands/mcp.js +105 -0
  18. package/dist/cli/commands/mcp.js.map +1 -0
  19. package/dist/cli/commands/merge.d.ts +1 -0
  20. package/dist/cli/commands/merge.js +111 -0
  21. package/dist/cli/commands/merge.js.map +1 -0
  22. package/dist/cli/commands/promote.d.ts +1 -0
  23. package/dist/cli/commands/promote.js +157 -0
  24. package/dist/cli/commands/promote.js.map +1 -0
  25. package/dist/cli/commands/tui.d.ts +1 -0
  26. package/dist/cli/commands/tui.js +60 -0
  27. package/dist/cli/commands/tui.js.map +1 -0
  28. package/dist/cli/commands/undo.d.ts +1 -0
  29. package/dist/cli/commands/undo.js +157 -0
  30. package/dist/cli/commands/undo.js.map +1 -0
  31. package/dist/cli/index.d.ts +2 -0
  32. package/dist/cli/index.js +85 -0
  33. package/dist/cli/index.js.map +1 -0
  34. package/dist/cli/tui/App.d.ts +8 -0
  35. package/dist/cli/tui/App.js +333 -0
  36. package/dist/cli/tui/App.js.map +1 -0
  37. package/dist/core/apply.d.ts +27 -0
  38. package/dist/core/apply.js +191 -0
  39. package/dist/core/apply.js.map +1 -0
  40. package/dist/core/claudeMd.d.ts +27 -0
  41. package/dist/core/claudeMd.js +103 -0
  42. package/dist/core/claudeMd.js.map +1 -0
  43. package/dist/core/dedupe.d.ts +78 -0
  44. package/dist/core/dedupe.js +212 -0
  45. package/dist/core/dedupe.js.map +1 -0
  46. package/dist/core/doctor.d.ts +35 -0
  47. package/dist/core/doctor.js +106 -0
  48. package/dist/core/doctor.js.map +1 -0
  49. package/dist/core/journal.d.ts +31 -0
  50. package/dist/core/journal.js +64 -0
  51. package/dist/core/journal.js.map +1 -0
  52. package/dist/core/lint.d.ts +26 -0
  53. package/dist/core/lint.js +254 -0
  54. package/dist/core/lint.js.map +1 -0
  55. package/dist/core/memoryIndex.d.ts +42 -0
  56. package/dist/core/memoryIndex.js +81 -0
  57. package/dist/core/memoryIndex.js.map +1 -0
  58. package/dist/core/merge.d.ts +19 -0
  59. package/dist/core/merge.js +58 -0
  60. package/dist/core/merge.js.map +1 -0
  61. package/dist/core/parse.d.ts +2 -0
  62. package/dist/core/parse.js +84 -0
  63. package/dist/core/parse.js.map +1 -0
  64. package/dist/core/plan.d.ts +34 -0
  65. package/dist/core/plan.js +85 -0
  66. package/dist/core/plan.js.map +1 -0
  67. package/dist/core/promote.d.ts +29 -0
  68. package/dist/core/promote.js +103 -0
  69. package/dist/core/promote.js.map +1 -0
  70. package/dist/core/scan.d.ts +16 -0
  71. package/dist/core/scan.js +81 -0
  72. package/dist/core/scan.js.map +1 -0
  73. package/dist/core/types.d.ts +36 -0
  74. package/dist/core/types.js +4 -0
  75. package/dist/core/types.js.map +1 -0
  76. package/dist/mcp/server.d.ts +13 -0
  77. package/dist/mcp/server.js +211 -0
  78. package/dist/mcp/server.js.map +1 -0
  79. package/package.json +71 -0
  80. package/skills/curate-memory/SKILL.md +60 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/core/journal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { type ScanOptions } from "./scan.js";
2
+ export declare const INDEX_LINE_CAP = 200;
3
+ export declare const INDEX_BYTE_CAP: number;
4
+ export type Severity = "error" | "warn" | "info";
5
+ export interface LintIssue {
6
+ severity: Severity;
7
+ code: string;
8
+ projectSlug: string;
9
+ /** Absolute file path the issue is about, when applicable. */
10
+ filePath?: string;
11
+ message: string;
12
+ /** Optional remediation hint. */
13
+ hint?: string;
14
+ }
15
+ export interface LintOptions extends ScanOptions {
16
+ /** Restrict to projects whose slug includes this substring. */
17
+ projectFilter?: string;
18
+ }
19
+ export interface LintResult {
20
+ issues: LintIssue[];
21
+ /** Number of projects considered (post-filter). */
22
+ projectsScanned: number;
23
+ /** Number of memory files considered. */
24
+ filesScanned: number;
25
+ }
26
+ export declare function lintAll(opts?: LintOptions): LintResult;
@@ -0,0 +1,254 @@
1
+ // Lint rules over the memory tree.
2
+ //
3
+ // Each rule produces zero or more `LintIssue`s with a stable `code`. The CLI
4
+ // (or MCP) is free to filter by code or severity. We never mutate anything
5
+ // here — lint is read-only.
6
+ //
7
+ // Severity scale:
8
+ // error — confirmed problem (data loss, unusable file)
9
+ // warn — likely problem (broken link, missing index when topic files exist)
10
+ // info — informational (untyped file in memory dir, etc.)
11
+ import { existsSync, readdirSync, statSync } from "node:fs";
12
+ import { join, isAbsolute, resolve } from "node:path";
13
+ import { scanAll } from "./scan.js";
14
+ import { parseMemoryIndex, isInlineContentStyle } from "./memoryIndex.js";
15
+ // Documented limits — content past either is silently dropped at session start.
16
+ // Source: https://code.claude.com/docs/en/memory
17
+ export const INDEX_LINE_CAP = 200;
18
+ export const INDEX_BYTE_CAP = 25 * 1024; // 25 KB
19
+ function readIndexSafely(memoryDir) {
20
+ const idxPath = join(memoryDir, "MEMORY.md");
21
+ if (!existsSync(idxPath))
22
+ return { index: null, readError: null };
23
+ try {
24
+ return { index: parseMemoryIndex(idxPath), readError: null };
25
+ }
26
+ catch (err) {
27
+ return {
28
+ index: null,
29
+ readError: err instanceof Error ? err.message : String(err),
30
+ };
31
+ }
32
+ }
33
+ function resolveLinkTarget(memoryDir, link) {
34
+ if (isAbsolute(link))
35
+ return link;
36
+ return resolve(memoryDir, link);
37
+ }
38
+ // Rule: MEMORY.md exceeds one of the documented caps. Content past the cap is
39
+ // silently dropped, so this is severity: error.
40
+ function lintIndexBloat(project, index) {
41
+ const issues = [];
42
+ const idxPath = join(project.path, "memory", "MEMORY.md");
43
+ if (index.lineCount > INDEX_LINE_CAP) {
44
+ issues.push({
45
+ severity: "error",
46
+ code: "index-line-cap",
47
+ projectSlug: project.slug,
48
+ filePath: idxPath,
49
+ message: `MEMORY.md is ${index.lineCount} lines; harness loads only the first ${INDEX_LINE_CAP}. ${index.lineCount - INDEX_LINE_CAP} lines are silently dropped.`,
50
+ hint: "Move detailed notes into separate topic files and reference them from MEMORY.md.",
51
+ });
52
+ }
53
+ if (index.byteSize > INDEX_BYTE_CAP) {
54
+ issues.push({
55
+ severity: "error",
56
+ code: "index-byte-cap",
57
+ projectSlug: project.slug,
58
+ filePath: idxPath,
59
+ message: `MEMORY.md is ${index.byteSize} bytes; harness loads only the first ${INDEX_BYTE_CAP} (25 KB).`,
60
+ hint: "Move detailed notes into separate topic files and reference them from MEMORY.md.",
61
+ });
62
+ }
63
+ return issues;
64
+ }
65
+ // Rule: a markdown link in MEMORY.md points at a path that doesn't exist.
66
+ // Only check links that look like file references (end in .md or have no
67
+ // scheme like http: — those are external).
68
+ function lintDanglingRefs(project, index) {
69
+ const issues = [];
70
+ const memoryDir = join(project.path, "memory");
71
+ const idxPath = join(memoryDir, "MEMORY.md");
72
+ for (const link of index.links) {
73
+ // Skip external URLs and anchor-only links.
74
+ if (/^[a-z][a-z0-9+.-]*:/.test(link.target))
75
+ continue;
76
+ if (link.target.startsWith("#"))
77
+ continue;
78
+ if (!link.target.endsWith(".md"))
79
+ continue;
80
+ const resolved = resolveLinkTarget(memoryDir, link.target);
81
+ if (!existsSync(resolved)) {
82
+ issues.push({
83
+ severity: "warn",
84
+ code: "dangling-ref",
85
+ projectSlug: project.slug,
86
+ filePath: idxPath,
87
+ message: `MEMORY.md:${link.line} links to "${link.target}" but no such file exists.`,
88
+ hint: "Either create the file or remove the index entry.",
89
+ });
90
+ }
91
+ }
92
+ return issues;
93
+ }
94
+ // Rule: a topic file exists in memory/ but isn't referenced from MEMORY.md.
95
+ // Only flag if MEMORY.md is using the pointer-style format — when it's inline
96
+ // (no .md links at all), assume the user opted out of pointer indexing.
97
+ function lintOrphans(project, index) {
98
+ if (isInlineContentStyle(index))
99
+ return [];
100
+ const issues = [];
101
+ const memoryDir = join(project.path, "memory");
102
+ const referenced = new Set();
103
+ for (const link of index.links) {
104
+ if (link.target.endsWith(".md")) {
105
+ referenced.add(resolveLinkTarget(memoryDir, link.target));
106
+ }
107
+ }
108
+ for (const m of project.memories) {
109
+ if (!referenced.has(m.filePath)) {
110
+ issues.push({
111
+ severity: "info",
112
+ code: "orphan-file",
113
+ projectSlug: project.slug,
114
+ filePath: m.filePath,
115
+ message: `${m.fileName} is not referenced from MEMORY.md.`,
116
+ hint: "Add a `- [Title](" + m.fileName + ")` line to MEMORY.md, or delete the file if it's stale.",
117
+ });
118
+ }
119
+ }
120
+ return issues;
121
+ }
122
+ // Rule: memory dir has topic files but no MEMORY.md index at all.
123
+ function lintMissingIndex(project) {
124
+ if (project.hasIndex)
125
+ return [];
126
+ if (project.memories.length === 0)
127
+ return []; // Empty memory dir is fine.
128
+ return [
129
+ {
130
+ severity: "warn",
131
+ code: "missing-index",
132
+ projectSlug: project.slug,
133
+ filePath: join(project.path, "memory"),
134
+ message: `Project has ${project.memories.length} memory file(s) but no MEMORY.md index. Topic files load on demand but the session has no overview.`,
135
+ hint: "Create a MEMORY.md with one bullet per topic file.",
136
+ },
137
+ ];
138
+ }
139
+ // Rule: a markdown file in the memory dir has no recognized type (no
140
+ // frontmatter type, no filename prefix). Informational — these are usually
141
+ // human-written docs (ARCHITECTURE.md, etc.) that won't be touched by the
142
+ // auto-memory system but are valid markdown.
143
+ function lintUntypedFiles(project) {
144
+ const issues = [];
145
+ for (const m of project.memories) {
146
+ if (m.type !== "untyped")
147
+ continue;
148
+ issues.push({
149
+ severity: "info",
150
+ code: "untyped-file",
151
+ projectSlug: project.slug,
152
+ filePath: m.filePath,
153
+ message: `${m.fileName} has no frontmatter type and no recognized prefix; not loaded as auto-memory.`,
154
+ hint: "If this is human-written docs, that's fine. If it should be auto-memory, " +
155
+ "add a frontmatter block with `type: user|feedback|project|reference`.",
156
+ });
157
+ }
158
+ return issues;
159
+ }
160
+ // Rule: a memory file failed to parse during scan. We detect this by re-trying
161
+ // the parse from disk for any file that the scan would have skipped. The scan
162
+ // already wrote a warning; we promote it to an `error` lint issue here.
163
+ function lintBrokenYaml(memoryDir, projectSlug, parsedFiles) {
164
+ // Build a set of paths the scan successfully parsed.
165
+ const parsed = new Set(parsedFiles.map((m) => m.filePath));
166
+ const issues = [];
167
+ // Re-list the dir and check for .md files (excluding MEMORY.md) that aren't
168
+ // in the parsed set.
169
+ let entries;
170
+ try {
171
+ entries = readdirSync(memoryDir);
172
+ }
173
+ catch {
174
+ return issues;
175
+ }
176
+ for (const name of entries) {
177
+ if (!name.endsWith(".md"))
178
+ continue;
179
+ if (name === "MEMORY.md")
180
+ continue;
181
+ const full = join(memoryDir, name);
182
+ if (parsed.has(full))
183
+ continue;
184
+ // Verify it's actually a file (not a directory — the dir-as-file edge case
185
+ // from the scan tests). If it's a regular file, this is a real parse fail.
186
+ let isFile = false;
187
+ try {
188
+ isFile = statSync(full).isFile();
189
+ }
190
+ catch {
191
+ // ignore
192
+ }
193
+ if (isFile) {
194
+ issues.push({
195
+ severity: "error",
196
+ code: "broken-yaml",
197
+ projectSlug,
198
+ filePath: full,
199
+ message: `${name} could not be parsed (likely malformed YAML frontmatter). Claude can't load this memory.`,
200
+ hint: "Open the frontmatter (between the `---` lines). Common cause: a value with unescaped quotes (e.g. `name: \"X\" in incidents`) or a colon inside an unquoted string. Wrap the whole value in single quotes to fix.",
201
+ });
202
+ }
203
+ else {
204
+ issues.push({
205
+ severity: "error",
206
+ code: "unreadable-file",
207
+ projectSlug,
208
+ filePath: full,
209
+ message: `${name} exists but is not a regular file.`,
210
+ hint: "Inspect manually — something unexpected is at this path.",
211
+ });
212
+ }
213
+ }
214
+ return issues;
215
+ }
216
+ export function lintAll(opts = {}) {
217
+ const projects = scanAll(opts).filter((p) => !opts.projectFilter || p.slug.includes(opts.projectFilter));
218
+ const issues = [];
219
+ let fileCount = 0;
220
+ for (const project of projects) {
221
+ fileCount += project.memories.length;
222
+ const memoryDir = join(project.path, "memory");
223
+ const { index, readError } = readIndexSafely(memoryDir);
224
+ if (readError) {
225
+ issues.push({
226
+ severity: "error",
227
+ code: "index-unreadable",
228
+ projectSlug: project.slug,
229
+ filePath: join(memoryDir, "MEMORY.md"),
230
+ message: `MEMORY.md exists but could not be read: ${readError}`,
231
+ });
232
+ }
233
+ if (index) {
234
+ issues.push(...lintIndexBloat(project, index));
235
+ issues.push(...lintDanglingRefs(project, index));
236
+ issues.push(...lintOrphans(project, index));
237
+ }
238
+ issues.push(...lintMissingIndex(project));
239
+ issues.push(...lintUntypedFiles(project));
240
+ issues.push(...lintBrokenYaml(memoryDir, project.slug, project.memories));
241
+ }
242
+ // Stable ordering: by project, then severity (error > warn > info), then code.
243
+ const SEV_ORDER = { error: 0, warn: 1, info: 2 };
244
+ issues.sort((a, b) => {
245
+ if (a.projectSlug !== b.projectSlug)
246
+ return a.projectSlug.localeCompare(b.projectSlug);
247
+ if (SEV_ORDER[a.severity] !== SEV_ORDER[b.severity]) {
248
+ return SEV_ORDER[a.severity] - SEV_ORDER[b.severity];
249
+ }
250
+ return a.code.localeCompare(b.code);
251
+ });
252
+ return { issues, projectsScanned: projects.length, filesScanned: fileCount };
253
+ }
254
+ //# sourceMappingURL=lint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint.js","sourceRoot":"","sources":["../../src/core/lint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ export interface IndexLink {
2
+ /** Display text from `[title](...)`. */
3
+ title: string;
4
+ /** The link target, e.g. `user_git.md` or `./feedback.md`. */
5
+ target: string;
6
+ /** 1-indexed line where the link was found. */
7
+ line: number;
8
+ }
9
+ export interface ParsedIndex {
10
+ /** Full file body. */
11
+ rawText: string;
12
+ /** Number of lines (count of `\n` + 1, like wc -l + 1 for non-trailing-newline files). */
13
+ lineCount: number;
14
+ /** Byte size on disk. */
15
+ byteSize: number;
16
+ /** Last modified time. */
17
+ mtimeMs: number;
18
+ /** Every markdown link found in the file, in document order. */
19
+ links: IndexLink[];
20
+ }
21
+ export declare function parseMemoryIndex(filePath: string): ParsedIndex;
22
+ /**
23
+ * Heuristic: an index is "inline-content style" when there are no markdown
24
+ * links pointing at sibling `.md` files. This avoids false-positive orphan
25
+ * warnings when the user is using the inline style on purpose.
26
+ */
27
+ export declare function isInlineContentStyle(index: ParsedIndex): boolean;
28
+ /**
29
+ * Remove every line in a MEMORY.md that references the given filename via a
30
+ * markdown link. Used by Phase 6 merge/promote when a topic file is deleted —
31
+ * we don't want to leave dangling pointers in the index.
32
+ *
33
+ * Conservative: only removes lines that contain the link `(<fileName>)`. If
34
+ * the line has unrelated content alongside, the whole line is still removed
35
+ * (the documented MEMORY.md style is "one bullet per topic file"). If the
36
+ * user has hand-edited their index with mixed content, they should review the
37
+ * dry-run diff before --apply.
38
+ */
39
+ export declare function removeIndexEntry(content: string, fileName: string): {
40
+ content: string;
41
+ removedLines: number;
42
+ };
@@ -0,0 +1,81 @@
1
+ // Parses a `MEMORY.md` index file.
2
+ //
3
+ // Two real-world styles exist:
4
+ //
5
+ // 1. **Pointer-style** (the documented shape) — short lines, each a markdown
6
+ // link to a topic file:
7
+ // - [User Git Rules](user_git_rules.md)
8
+ // - [Feedback Testing](feedback_testing_style.md)
9
+ //
10
+ // 2. **Inline-content style** (what the harness actually writes most of the
11
+ // time) — full body text with headings and bullets, no links to sibling
12
+ // topic files.
13
+ //
14
+ // We don't pick one as "right" — we just record what's there and let the lint
15
+ // rules decide what to flag.
16
+ import { readFileSync, statSync } from "node:fs";
17
+ const MD_LINK_RE = /\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
18
+ export function parseMemoryIndex(filePath) {
19
+ const raw = readFileSync(filePath, "utf8");
20
+ const stats = statSync(filePath);
21
+ // Line count: number of \n characters + 1 (last line w/o newline still counts).
22
+ // Empty file → 1 line of 0 chars (matches Node's String.split("\n").length).
23
+ const lineCount = raw.length === 0 ? 0 : raw.split("\n").length;
24
+ const links = [];
25
+ const lines = raw.split("\n");
26
+ for (let i = 0; i < lines.length; i++) {
27
+ const line = lines[i];
28
+ MD_LINK_RE.lastIndex = 0;
29
+ let m;
30
+ while ((m = MD_LINK_RE.exec(line)) !== null) {
31
+ links.push({ title: m[1], target: m[2], line: i + 1 });
32
+ }
33
+ }
34
+ return {
35
+ rawText: raw,
36
+ lineCount,
37
+ byteSize: Buffer.byteLength(raw, "utf8"),
38
+ mtimeMs: stats.mtimeMs,
39
+ links,
40
+ };
41
+ }
42
+ /**
43
+ * Heuristic: an index is "inline-content style" when there are no markdown
44
+ * links pointing at sibling `.md` files. This avoids false-positive orphan
45
+ * warnings when the user is using the inline style on purpose.
46
+ */
47
+ export function isInlineContentStyle(index) {
48
+ return !index.links.some((l) => l.target.endsWith(".md"));
49
+ }
50
+ /**
51
+ * Remove every line in a MEMORY.md that references the given filename via a
52
+ * markdown link. Used by Phase 6 merge/promote when a topic file is deleted —
53
+ * we don't want to leave dangling pointers in the index.
54
+ *
55
+ * Conservative: only removes lines that contain the link `(<fileName>)`. If
56
+ * the line has unrelated content alongside, the whole line is still removed
57
+ * (the documented MEMORY.md style is "one bullet per topic file"). If the
58
+ * user has hand-edited their index with mixed content, they should review the
59
+ * dry-run diff before --apply.
60
+ */
61
+ export function removeIndexEntry(content, fileName) {
62
+ const lines = content.split("\n");
63
+ const out = [];
64
+ let removed = 0;
65
+ // Match patterns: `(./fileName)` or `(fileName)` — anchored to a closing
66
+ // paren so we don't accidentally match a substring of a longer name.
67
+ const variants = [
68
+ `(${fileName})`,
69
+ `(./${fileName})`,
70
+ ];
71
+ for (const line of lines) {
72
+ const hit = variants.some((v) => line.includes(v));
73
+ if (hit) {
74
+ removed++;
75
+ continue;
76
+ }
77
+ out.push(line);
78
+ }
79
+ return { content: out.join("\n"), removedLines: removed };
80
+ }
81
+ //# sourceMappingURL=memoryIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memoryIndex.js","sourceRoot":"","sources":["../../src/core/memoryIndex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { type Plan } from "./plan.js";
2
+ import type { DuplicateCluster } from "./dedupe.js";
3
+ export interface MergePlanOptions {
4
+ /** Override which member id is treated as the keep target. */
5
+ keepId?: string;
6
+ /** When true, MEMORY.md indexes are not modified. */
7
+ skipIndexUpdate?: boolean;
8
+ }
9
+ export interface MergeSummary {
10
+ clusterId: string;
11
+ keepId: string;
12
+ deletedIds: string[];
13
+ indexesUpdated: string[];
14
+ }
15
+ export interface MergePlanResult {
16
+ plan: Plan;
17
+ summary: MergeSummary;
18
+ }
19
+ export declare function buildMergePlan(cluster: DuplicateCluster, opts?: MergePlanOptions): MergePlanResult;
@@ -0,0 +1,58 @@
1
+ // Build a Plan that merges a duplicate cluster down to its representative.
2
+ //
3
+ // Inputs: a DuplicateCluster from Phase 4's `findDuplicates`.
4
+ // Outputs: a Plan that, when applied:
5
+ // 1. Deletes every non-representative member.
6
+ // 2. For each affected project, removes the deleted file's pointer from
7
+ // MEMORY.md if present.
8
+ //
9
+ // The representative file is left untouched. Phase 5's applyPlan captures
10
+ // inverses, so every merge is fully undoable via `memex undo`.
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { planBuilder } from "./plan.js";
14
+ import { removeIndexEntry } from "./memoryIndex.js";
15
+ export function buildMergePlan(cluster, opts = {}) {
16
+ const keepId = opts.keepId ?? cluster.representative.id;
17
+ const keep = cluster.members.find((m) => m.id === keepId);
18
+ if (!keep) {
19
+ throw new Error(`cluster ${cluster.id} has no member with id "${keepId}"; available: ${cluster.members
20
+ .map((m) => m.id)
21
+ .join(", ")}`);
22
+ }
23
+ const toDelete = cluster.members.filter((m) => m.id !== keepId);
24
+ const builder = planBuilder(`merge:${cluster.id}`, `Keep ${keepId}; delete ${toDelete.length} duplicate(s)` +
25
+ (opts.skipIndexUpdate ? "" : " and clean MEMORY.md references"));
26
+ const indexesUpdated = [];
27
+ for (const m of toDelete) {
28
+ builder.delete(m.filePath);
29
+ if (opts.skipIndexUpdate)
30
+ continue;
31
+ const memoryDir = dirname(m.filePath);
32
+ const indexPath = join(memoryDir, "MEMORY.md");
33
+ if (!existsSync(indexPath))
34
+ continue;
35
+ try {
36
+ const current = readFileSync(indexPath, "utf8");
37
+ const { content, removedLines } = removeIndexEntry(current, m.filePath.split("/").pop());
38
+ if (removedLines > 0) {
39
+ builder.write(indexPath, content);
40
+ indexesUpdated.push(indexPath);
41
+ }
42
+ }
43
+ catch {
44
+ // If we can't read the index, leave it alone — lint will surface
45
+ // any resulting dangling reference.
46
+ }
47
+ }
48
+ return {
49
+ plan: builder.build(),
50
+ summary: {
51
+ clusterId: cluster.id,
52
+ keepId,
53
+ deletedIds: toDelete.map((m) => m.id),
54
+ indexesUpdated,
55
+ },
56
+ };
57
+ }
58
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../../src/core/merge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { type Memory } from "./types.js";
2
+ export declare function parseMemoryFile(filePath: string, projectSlug: string): Memory;
@@ -0,0 +1,84 @@
1
+ // Parses a single memory markdown file into a Memory record.
2
+ //
3
+ // Tolerates three shapes seen in the wild:
4
+ // 1. Full frontmatter with `type:` (the canonical auto-memory format)
5
+ // 2. Frontmatter with unknown extra fields (e.g. `originSessionId:` written
6
+ // by some harness versions) — we preserve them all in `frontmatter`.
7
+ // 3. Plain markdown with no frontmatter at all (humans drop ARCHITECTURE.md
8
+ // files into memory dirs). We classify these as "untyped".
9
+ import { readFileSync, statSync } from "node:fs";
10
+ import { basename } from "node:path";
11
+ import matter from "gray-matter";
12
+ import { MEMORY_TYPES } from "./types.js";
13
+ const FILE_PREFIX_TO_TYPE = [
14
+ ["user_", "user"],
15
+ ["feedback_", "feedback"],
16
+ ["project_", "project"],
17
+ ["reference_", "reference"],
18
+ ];
19
+ function detectTypeFromFilename(fileName) {
20
+ const lower = fileName.toLowerCase();
21
+ for (const [prefix, type] of FILE_PREFIX_TO_TYPE) {
22
+ if (lower.startsWith(prefix))
23
+ return type;
24
+ }
25
+ return null;
26
+ }
27
+ function asString(v) {
28
+ if (typeof v !== "string")
29
+ return null;
30
+ const trimmed = v.trim();
31
+ return trimmed.length > 0 ? trimmed : null;
32
+ }
33
+ function isValidType(v) {
34
+ return MEMORY_TYPES.includes(v);
35
+ }
36
+ export function parseMemoryFile(filePath, projectSlug) {
37
+ const raw = readFileSync(filePath, "utf8");
38
+ const stats = statSync(filePath);
39
+ const fileName = basename(filePath);
40
+ // gray-matter returns { content: <full>, data: {} } when there's no
41
+ // frontmatter at all, so this works for both shapes.
42
+ //
43
+ // Crucially: pass { cache: false }. gray-matter's default cache is keyed
44
+ // on file content and can return a successful parse on the *second* call
45
+ // for content that THREW on the first call — turning broken-YAML detection
46
+ // into a coin flip. The cache buys nothing here (we read each file once
47
+ // per scan) and corrupts results across multiple scans in one process.
48
+ // gray-matter's TS types don't expose `cache`, but the option is honored at
49
+ // runtime (and documented in the README). Cast to bypass the type gap.
50
+ const parsed = matter(raw, { cache: false });
51
+ const fm = (parsed.data ?? {});
52
+ const fmTypeRaw = asString(fm.type)?.toLowerCase();
53
+ let type;
54
+ let typeSource;
55
+ if (fmTypeRaw && isValidType(fmTypeRaw)) {
56
+ type = fmTypeRaw;
57
+ typeSource = "frontmatter";
58
+ }
59
+ else {
60
+ const fromName = detectTypeFromFilename(fileName);
61
+ if (fromName) {
62
+ type = fromName;
63
+ typeSource = "filename-prefix";
64
+ }
65
+ else {
66
+ type = "untyped";
67
+ typeSource = "unknown";
68
+ }
69
+ }
70
+ return {
71
+ id: `${projectSlug}/${fileName}`,
72
+ projectSlug,
73
+ filePath,
74
+ fileName,
75
+ type,
76
+ typeSource,
77
+ name: asString(fm.name),
78
+ description: asString(fm.description),
79
+ body: parsed.content.replace(/^\n+/, "").replace(/\n+$/, ""),
80
+ frontmatter: fm,
81
+ mtimeMs: stats.mtimeMs,
82
+ };
83
+ }
84
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/core/parse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ export type OperationKind = "write" | "delete" | "ensure-dir";
2
+ export type Operation = {
3
+ kind: "write";
4
+ /** Absolute path of the target file. */
5
+ path: string;
6
+ /** Full new contents (UTF-8). */
7
+ content: string;
8
+ } | {
9
+ kind: "delete";
10
+ path: string;
11
+ } | {
12
+ kind: "ensure-dir";
13
+ /** Directory to mkdir -p. */
14
+ path: string;
15
+ };
16
+ export interface Plan {
17
+ /** Stable id, used as journal filename. */
18
+ id: string;
19
+ /** Short label visible in `memex undo --list`. */
20
+ label: string;
21
+ /** Human-readable summary printed before --apply confirmation. */
22
+ description: string;
23
+ createdAt: string;
24
+ ops: Operation[];
25
+ }
26
+ export interface PlanBuilder {
27
+ build(): Plan;
28
+ write(path: string, content: string): PlanBuilder;
29
+ delete(path: string): PlanBuilder;
30
+ ensureDir(path: string): PlanBuilder;
31
+ }
32
+ export declare function planBuilder(label: string, description?: string): PlanBuilder;
33
+ /** Pretty-print a plan for `--dry-run` output. */
34
+ export declare function describePlan(plan: Plan): string;