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.
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/cli/commands/dedupe.d.ts +1 -0
- package/dist/cli/commands/dedupe.js +187 -0
- package/dist/cli/commands/dedupe.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +177 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/lint.d.ts +1 -0
- package/dist/cli/commands/lint.js +139 -0
- package/dist/cli/commands/lint.js.map +1 -0
- package/dist/cli/commands/list.d.ts +1 -0
- package/dist/cli/commands/list.js +97 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +1 -0
- package/dist/cli/commands/mcp.js +105 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/merge.d.ts +1 -0
- package/dist/cli/commands/merge.js +111 -0
- package/dist/cli/commands/merge.js.map +1 -0
- package/dist/cli/commands/promote.d.ts +1 -0
- package/dist/cli/commands/promote.js +157 -0
- package/dist/cli/commands/promote.js.map +1 -0
- package/dist/cli/commands/tui.d.ts +1 -0
- package/dist/cli/commands/tui.js +60 -0
- package/dist/cli/commands/tui.js.map +1 -0
- package/dist/cli/commands/undo.d.ts +1 -0
- package/dist/cli/commands/undo.js +157 -0
- package/dist/cli/commands/undo.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +85 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/tui/App.d.ts +8 -0
- package/dist/cli/tui/App.js +333 -0
- package/dist/cli/tui/App.js.map +1 -0
- package/dist/core/apply.d.ts +27 -0
- package/dist/core/apply.js +191 -0
- package/dist/core/apply.js.map +1 -0
- package/dist/core/claudeMd.d.ts +27 -0
- package/dist/core/claudeMd.js +103 -0
- package/dist/core/claudeMd.js.map +1 -0
- package/dist/core/dedupe.d.ts +78 -0
- package/dist/core/dedupe.js +212 -0
- package/dist/core/dedupe.js.map +1 -0
- package/dist/core/doctor.d.ts +35 -0
- package/dist/core/doctor.js +106 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/journal.d.ts +31 -0
- package/dist/core/journal.js +64 -0
- package/dist/core/journal.js.map +1 -0
- package/dist/core/lint.d.ts +26 -0
- package/dist/core/lint.js +254 -0
- package/dist/core/lint.js.map +1 -0
- package/dist/core/memoryIndex.d.ts +42 -0
- package/dist/core/memoryIndex.js +81 -0
- package/dist/core/memoryIndex.js.map +1 -0
- package/dist/core/merge.d.ts +19 -0
- package/dist/core/merge.js +58 -0
- package/dist/core/merge.js.map +1 -0
- package/dist/core/parse.d.ts +2 -0
- package/dist/core/parse.js +84 -0
- package/dist/core/parse.js.map +1 -0
- package/dist/core/plan.d.ts +34 -0
- package/dist/core/plan.js +85 -0
- package/dist/core/plan.js.map +1 -0
- package/dist/core/promote.d.ts +29 -0
- package/dist/core/promote.js +103 -0
- package/dist/core/promote.js.map +1 -0
- package/dist/core/scan.d.ts +16 -0
- package/dist/core/scan.js +81 -0
- package/dist/core/scan.js.map +1 -0
- package/dist/core/types.d.ts +36 -0
- package/dist/core/types.js +4 -0
- package/dist/core/types.js.map +1 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +211 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +71 -0
- package/skills/curate-memory/SKILL.md +60 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Helpers for managing `~/.claude/CLAUDE.md` — the always-loaded user-level
|
|
2
|
+
// instructions file Claude Code reads at the start of every session.
|
|
3
|
+
//
|
|
4
|
+
// memex never touches content outside its own marker blocks. The managed
|
|
5
|
+
// format:
|
|
6
|
+
//
|
|
7
|
+
// <!-- memex:start:<slug> -->
|
|
8
|
+
// ## <heading>
|
|
9
|
+
//
|
|
10
|
+
// <body>
|
|
11
|
+
//
|
|
12
|
+
// <!-- memex:end:<slug> -->
|
|
13
|
+
//
|
|
14
|
+
// Slug is a `[a-z0-9_-]+` identifier derived from the source filename. If
|
|
15
|
+
// a block with the same slug exists, it's replaced in place; otherwise the
|
|
16
|
+
// block is appended to the file under a memex-owned section header that's
|
|
17
|
+
// added lazily on the first promote.
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
const HEADER_BANNER = "## Promoted memories (managed by memex — do not edit between markers)\n";
|
|
21
|
+
export function defaultClaudeMdPath() {
|
|
22
|
+
return join(homedir(), ".claude", "CLAUDE.md");
|
|
23
|
+
}
|
|
24
|
+
/** Normalize an arbitrary string into a safe block slug. */
|
|
25
|
+
export function toBlockSlug(name) {
|
|
26
|
+
return name
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/\.md$/, "")
|
|
29
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
30
|
+
.replace(/^-+|-+$/g, "")
|
|
31
|
+
.slice(0, 64);
|
|
32
|
+
}
|
|
33
|
+
function startMarker(slug) {
|
|
34
|
+
return `<!-- memex:start:${slug} -->`;
|
|
35
|
+
}
|
|
36
|
+
function endMarker(slug) {
|
|
37
|
+
return `<!-- memex:end:${slug} -->`;
|
|
38
|
+
}
|
|
39
|
+
function escapeRegex(s) {
|
|
40
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
}
|
|
42
|
+
function blockPatternFor(slug) {
|
|
43
|
+
// Match the block including a trailing newline if present.
|
|
44
|
+
return new RegExp(`${escapeRegex(startMarker(slug))}[\\s\\S]*?${escapeRegex(endMarker(slug))}\\n?`, "g");
|
|
45
|
+
}
|
|
46
|
+
export function renderBlock(block) {
|
|
47
|
+
const body = block.body.trimEnd();
|
|
48
|
+
return [
|
|
49
|
+
startMarker(block.slug),
|
|
50
|
+
`## ${block.heading}`,
|
|
51
|
+
"",
|
|
52
|
+
body,
|
|
53
|
+
endMarker(block.slug),
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Upsert a managed block into a CLAUDE.md document.
|
|
58
|
+
*
|
|
59
|
+
* - If a block with the same slug exists, replace it in place.
|
|
60
|
+
* - Otherwise append after ensuring the header banner is present.
|
|
61
|
+
* - Returns the new content (caller decides what to do with it).
|
|
62
|
+
*/
|
|
63
|
+
export function upsertBlock(currentContent, block) {
|
|
64
|
+
const rendered = renderBlock(block);
|
|
65
|
+
const pattern = blockPatternFor(block.slug);
|
|
66
|
+
if (pattern.test(currentContent)) {
|
|
67
|
+
// Replace existing block.
|
|
68
|
+
return currentContent.replace(pattern, `${rendered}\n`);
|
|
69
|
+
}
|
|
70
|
+
// Append. Ensure the file ends with a newline, the banner is present, and
|
|
71
|
+
// there's exactly one blank line between the previous content and our
|
|
72
|
+
// new block.
|
|
73
|
+
let out = currentContent;
|
|
74
|
+
if (out.length > 0 && !out.endsWith("\n"))
|
|
75
|
+
out += "\n";
|
|
76
|
+
if (!out.includes(HEADER_BANNER)) {
|
|
77
|
+
if (out.length > 0)
|
|
78
|
+
out += "\n";
|
|
79
|
+
out += HEADER_BANNER;
|
|
80
|
+
}
|
|
81
|
+
out += "\n" + rendered + "\n";
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Remove a managed block by slug. Used by undo/promote-revert. Leaves
|
|
86
|
+
* unmanaged content alone. Returns { content, modified }.
|
|
87
|
+
*/
|
|
88
|
+
export function removeBlock(currentContent, slug) {
|
|
89
|
+
const pattern = blockPatternFor(slug);
|
|
90
|
+
if (!pattern.test(currentContent))
|
|
91
|
+
return { content: currentContent, modified: false };
|
|
92
|
+
return { content: currentContent.replace(pattern, ""), modified: true };
|
|
93
|
+
}
|
|
94
|
+
/** List the slugs of memex-managed blocks currently in a document. */
|
|
95
|
+
export function listBlockSlugs(currentContent) {
|
|
96
|
+
const re = /<!--\s*memex:start:([a-z0-9_-]+)\s*-->/g;
|
|
97
|
+
const out = [];
|
|
98
|
+
let m;
|
|
99
|
+
while ((m = re.exec(currentContent)) !== null)
|
|
100
|
+
out.push(m[1]);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=claudeMd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claudeMd.js","sourceRoot":"","sources":["../../src/core/claudeMd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type ScanOptions } from "./scan.js";
|
|
2
|
+
import type { Memory, MemoryTypeOrUntyped } from "./types.js";
|
|
3
|
+
/** Collapse `-foo-bar--claude-worktrees-xyz` → `-foo-bar`. */
|
|
4
|
+
export declare function collapseWorktreeSlug(slug: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a string for fingerprinting. Idempotent.
|
|
7
|
+
*
|
|
8
|
+
* The pipeline is intentionally conservative — too-aggressive normalization
|
|
9
|
+
* causes false positives (e.g. stripping code blocks merges unrelated logcli
|
|
10
|
+
* notes). Too-lenient causes false negatives. This sits in the middle.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeForHash(s: string): string;
|
|
13
|
+
/** Normalize a name field — additionally strips trailing parentheticals. */
|
|
14
|
+
export declare function normalizeName(name: string | null | undefined): string;
|
|
15
|
+
export declare function computeBodyHash(body: string): string;
|
|
16
|
+
export declare function computeFullHash(memory: Memory): string;
|
|
17
|
+
export interface ClusterMember {
|
|
18
|
+
id: string;
|
|
19
|
+
filePath: string;
|
|
20
|
+
/** Worktree-collapsed slug (the "logical" project). */
|
|
21
|
+
project: string;
|
|
22
|
+
/** Raw slug as it appears on disk. */
|
|
23
|
+
rawProjectSlug: string;
|
|
24
|
+
name: string | null;
|
|
25
|
+
description: string | null;
|
|
26
|
+
type: MemoryTypeOrUntyped;
|
|
27
|
+
bodyBytes: number;
|
|
28
|
+
mtime: string;
|
|
29
|
+
fullHash: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DuplicateCluster {
|
|
32
|
+
/** Short stable id like "c1", easier to reference than the hash. */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Which hash matched. v0.1 always reports "bodyHash". */
|
|
35
|
+
tier: "bodyHash";
|
|
36
|
+
/** Hex digest. */
|
|
37
|
+
fingerprint: string;
|
|
38
|
+
/** Number of members. */
|
|
39
|
+
count: number;
|
|
40
|
+
/** Type of the cluster (taken from the representative). */
|
|
41
|
+
type: MemoryTypeOrUntyped;
|
|
42
|
+
/** True if every member lives in one logical project. */
|
|
43
|
+
intraProject: boolean;
|
|
44
|
+
/** Number of *distinct* worktree-collapsed projects. */
|
|
45
|
+
distinctProjects: number;
|
|
46
|
+
/** True iff `distinctProjects >= 3 && type in {user, feedback}`. */
|
|
47
|
+
promotionCandidate: boolean;
|
|
48
|
+
/** The member best suited as the merge target. */
|
|
49
|
+
representative: {
|
|
50
|
+
id: string;
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
53
|
+
members: ClusterMember[];
|
|
54
|
+
}
|
|
55
|
+
export interface DedupeResult {
|
|
56
|
+
version: "1";
|
|
57
|
+
scannedAt: string;
|
|
58
|
+
config: {
|
|
59
|
+
normalization: "v1";
|
|
60
|
+
tier: "bodyHash";
|
|
61
|
+
};
|
|
62
|
+
clusters: DuplicateCluster[];
|
|
63
|
+
summary: {
|
|
64
|
+
memoriesScanned: number;
|
|
65
|
+
projectsScanned: number;
|
|
66
|
+
clusterCount: number;
|
|
67
|
+
duplicateFileCount: number;
|
|
68
|
+
wastedBytes: number;
|
|
69
|
+
promotionCandidates: number;
|
|
70
|
+
skippedEmptyBodies: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface DedupeOptions extends ScanOptions {
|
|
74
|
+
projectFilter?: string;
|
|
75
|
+
/** Include `untyped` memories in clustering. Default: true (treat as wildcard). */
|
|
76
|
+
includeUntyped?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export declare function findDuplicates(opts?: DedupeOptions): DedupeResult;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Cross-project duplicate detection (Phase 4).
|
|
2
|
+
//
|
|
3
|
+
// Design choices (see research/dedupe.md or commit message for rationale):
|
|
4
|
+
//
|
|
5
|
+
// - **Two-tier fingerprints**: `bodyHash` is the primary cluster key — Claude
|
|
6
|
+
// tends to write the same rule with slightly different `name`/`description`
|
|
7
|
+
// across projects but stable body text. `fullHash` (type+name+desc+body) is
|
|
8
|
+
// reported alongside so callers can subdivide clusters by stricter identity.
|
|
9
|
+
//
|
|
10
|
+
// - **Normalization**: NFC unicode → smart-quote ASCII fold → lowercase →
|
|
11
|
+
// strip trivial markdown wrappers → collapse whitespace. No full markdown
|
|
12
|
+
// AST — too expensive and too lossy. URLs/code blocks stay in (they're
|
|
13
|
+
// often *the* memory).
|
|
14
|
+
//
|
|
15
|
+
// - **Worktree collapse**: project slugs containing `--claude-worktrees-`
|
|
16
|
+
// collapse to their base slug for the `distinctProjects` count. Avoids
|
|
17
|
+
// counting worktree-A and worktree-B of the same repo as separate
|
|
18
|
+
// projects when computing `promotionCandidate`.
|
|
19
|
+
//
|
|
20
|
+
// - **Empty bodies skipped**: every empty-body memory would cluster with every
|
|
21
|
+
// other and dwarf real findings. Routed out of `clusters`.
|
|
22
|
+
//
|
|
23
|
+
// - **Representative selection**: longest normalized body → most recent
|
|
24
|
+
// mtime. That's the "merge target" if the user later runs `memex merge`.
|
|
25
|
+
//
|
|
26
|
+
// - **Promotion candidate**: `distinctProjects >= 3 && type ∈ {user,
|
|
27
|
+
// feedback}`. The realistic "this should live in ~/.claude/CLAUDE.md"
|
|
28
|
+
// trigger.
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import { scanAll } from "./scan.js";
|
|
31
|
+
const WORKTREE_SUFFIX_RE = /--claude-worktrees-.*$/;
|
|
32
|
+
const SMART_QUOTES = [
|
|
33
|
+
[/[‘’‚‛]/g, "'"], // ' ' ‚ ‛
|
|
34
|
+
[/[“”„‟]/g, '"'], // " " „ ‟
|
|
35
|
+
[/[–—]/g, "-"], // – —
|
|
36
|
+
[/…/g, "..."], // …
|
|
37
|
+
];
|
|
38
|
+
/** Collapse `-foo-bar--claude-worktrees-xyz` → `-foo-bar`. */
|
|
39
|
+
export function collapseWorktreeSlug(slug) {
|
|
40
|
+
return slug.replace(WORKTREE_SUFFIX_RE, "");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Normalize a string for fingerprinting. Idempotent.
|
|
44
|
+
*
|
|
45
|
+
* The pipeline is intentionally conservative — too-aggressive normalization
|
|
46
|
+
* causes false positives (e.g. stripping code blocks merges unrelated logcli
|
|
47
|
+
* notes). Too-lenient causes false negatives. This sits in the middle.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeForHash(s) {
|
|
50
|
+
if (!s)
|
|
51
|
+
return "";
|
|
52
|
+
let out = s.normalize("NFC");
|
|
53
|
+
out = out.replace(/\r\n/g, "\n");
|
|
54
|
+
for (const [re, replacement] of SMART_QUOTES)
|
|
55
|
+
out = out.replace(re, replacement);
|
|
56
|
+
out = out.toLowerCase();
|
|
57
|
+
// Strip trivial markdown wrappers WITHOUT touching code/URLs.
|
|
58
|
+
out = out
|
|
59
|
+
.replace(/^[ \t]*(?:[-*+]|\d+\.)[ \t]+/gm, "") // list markers
|
|
60
|
+
.replace(/^[ \t]*#{1,6}[ \t]+/gm, "") // headings
|
|
61
|
+
.replace(/(\*\*|__)([^*_]+?)\1/g, "$2") // bold
|
|
62
|
+
.replace(/(\*|_)([^*_]+?)\1/g, "$2"); // italic
|
|
63
|
+
out = out.replace(/\s+/g, " ").trim();
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/** Normalize a name field — additionally strips trailing parentheticals. */
|
|
67
|
+
export function normalizeName(name) {
|
|
68
|
+
if (!name)
|
|
69
|
+
return "";
|
|
70
|
+
return normalizeForHash(name.replace(/\s*\([^)]*\)\s*$/, ""));
|
|
71
|
+
}
|
|
72
|
+
function sha256Hex(input) {
|
|
73
|
+
return createHash("sha256").update(input).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
export function computeBodyHash(body) {
|
|
76
|
+
return sha256Hex(normalizeForHash(body));
|
|
77
|
+
}
|
|
78
|
+
export function computeFullHash(memory) {
|
|
79
|
+
// For type=untyped, treat type as wildcard so missing metadata doesn't
|
|
80
|
+
// split otherwise-identical clusters.
|
|
81
|
+
const typePart = memory.type === "untyped" ? "*" : memory.type;
|
|
82
|
+
const parts = [
|
|
83
|
+
typePart,
|
|
84
|
+
normalizeName(memory.name),
|
|
85
|
+
normalizeForHash(memory.description ?? ""),
|
|
86
|
+
normalizeForHash(memory.body),
|
|
87
|
+
];
|
|
88
|
+
return sha256Hex(parts.join("\n"));
|
|
89
|
+
}
|
|
90
|
+
const PROMOTION_TYPES = new Set(["user", "feedback"]);
|
|
91
|
+
function memberFromMemory(m, mtimeIso, fullHash) {
|
|
92
|
+
return {
|
|
93
|
+
id: m.id,
|
|
94
|
+
filePath: m.filePath,
|
|
95
|
+
project: collapseWorktreeSlug(m.projectSlug),
|
|
96
|
+
rawProjectSlug: m.projectSlug,
|
|
97
|
+
name: m.name,
|
|
98
|
+
description: m.description,
|
|
99
|
+
type: m.type,
|
|
100
|
+
bodyBytes: Buffer.byteLength(m.body, "utf8"),
|
|
101
|
+
mtime: mtimeIso,
|
|
102
|
+
fullHash,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function pickRepresentative(members) {
|
|
106
|
+
// longest body → most recent mtime → first by id as tiebreak
|
|
107
|
+
const sorted = [...members].sort((a, b) => {
|
|
108
|
+
if (a.bodyBytes !== b.bodyBytes)
|
|
109
|
+
return b.bodyBytes - a.bodyBytes;
|
|
110
|
+
if (a.mtime !== b.mtime)
|
|
111
|
+
return a.mtime < b.mtime ? 1 : -1;
|
|
112
|
+
return a.id.localeCompare(b.id);
|
|
113
|
+
});
|
|
114
|
+
return { id: sorted[0].id, reason: "longest body, most recent mtime" };
|
|
115
|
+
}
|
|
116
|
+
export function findDuplicates(opts = {}) {
|
|
117
|
+
const includeUntyped = opts.includeUntyped ?? true;
|
|
118
|
+
const projects = scanAll(opts).filter((p) => !opts.projectFilter || p.slug.includes(opts.projectFilter));
|
|
119
|
+
const memories = projects.flatMap((p) => p.memories);
|
|
120
|
+
const consideredMemories = includeUntyped
|
|
121
|
+
? memories
|
|
122
|
+
: memories.filter((m) => m.type !== "untyped");
|
|
123
|
+
// Group by bodyHash, skipping empty bodies.
|
|
124
|
+
const groups = new Map();
|
|
125
|
+
let skippedEmptyBodies = 0;
|
|
126
|
+
for (const m of consideredMemories) {
|
|
127
|
+
const normalized = normalizeForHash(m.body);
|
|
128
|
+
if (normalized.length === 0) {
|
|
129
|
+
skippedEmptyBodies++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const hash = computeBodyHash(m.body);
|
|
133
|
+
const arr = groups.get(hash) ?? [];
|
|
134
|
+
arr.push(m);
|
|
135
|
+
groups.set(hash, arr);
|
|
136
|
+
}
|
|
137
|
+
// Materialize clusters where count >= 2, stable-sorted by:
|
|
138
|
+
// distinctProjects DESC, count DESC, fingerprint ASC
|
|
139
|
+
const rawClusters = [];
|
|
140
|
+
for (const [fingerprint, mems] of groups.entries()) {
|
|
141
|
+
if (mems.length < 2)
|
|
142
|
+
continue;
|
|
143
|
+
const members = mems.map((m) => memberFromMemory(m, new Date(m.mtimeMs).toISOString(), computeFullHash(m)));
|
|
144
|
+
rawClusters.push({ fingerprint, members });
|
|
145
|
+
}
|
|
146
|
+
rawClusters.sort((a, b) => {
|
|
147
|
+
const aDP = new Set(a.members.map((m) => m.project)).size;
|
|
148
|
+
const bDP = new Set(b.members.map((m) => m.project)).size;
|
|
149
|
+
if (aDP !== bDP)
|
|
150
|
+
return bDP - aDP;
|
|
151
|
+
if (a.members.length !== b.members.length)
|
|
152
|
+
return b.members.length - a.members.length;
|
|
153
|
+
return a.fingerprint.localeCompare(b.fingerprint);
|
|
154
|
+
});
|
|
155
|
+
const clusters = rawClusters.map((raw, idx) => {
|
|
156
|
+
const distinctProjects = new Set(raw.members.map((m) => m.project)).size;
|
|
157
|
+
const intraProject = distinctProjects === 1;
|
|
158
|
+
// Cluster type = most common member type (ties broken alphabetically).
|
|
159
|
+
const typeCounts = new Map();
|
|
160
|
+
for (const m of raw.members)
|
|
161
|
+
typeCounts.set(m.type, (typeCounts.get(m.type) ?? 0) + 1);
|
|
162
|
+
const clusterType = [...typeCounts.entries()].sort((a, b) => {
|
|
163
|
+
if (a[1] !== b[1])
|
|
164
|
+
return b[1] - a[1];
|
|
165
|
+
return a[0].localeCompare(b[0]);
|
|
166
|
+
})[0][0];
|
|
167
|
+
const promotionCandidate = distinctProjects >= 3 && PROMOTION_TYPES.has(clusterType);
|
|
168
|
+
return {
|
|
169
|
+
id: `c${idx + 1}`,
|
|
170
|
+
tier: "bodyHash",
|
|
171
|
+
fingerprint: raw.fingerprint,
|
|
172
|
+
count: raw.members.length,
|
|
173
|
+
type: clusterType,
|
|
174
|
+
intraProject,
|
|
175
|
+
distinctProjects,
|
|
176
|
+
promotionCandidate,
|
|
177
|
+
representative: pickRepresentative(raw.members),
|
|
178
|
+
members: [...raw.members].sort((a, b) => a.id.localeCompare(b.id)),
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
// Summary stats
|
|
182
|
+
let duplicateFileCount = 0;
|
|
183
|
+
let wastedBytes = 0;
|
|
184
|
+
let promotionCandidates = 0;
|
|
185
|
+
for (const c of clusters) {
|
|
186
|
+
duplicateFileCount += c.count;
|
|
187
|
+
// "Wasted" = bytes used by every non-representative member.
|
|
188
|
+
const repId = c.representative.id;
|
|
189
|
+
for (const m of c.members) {
|
|
190
|
+
if (m.id !== repId)
|
|
191
|
+
wastedBytes += m.bodyBytes;
|
|
192
|
+
}
|
|
193
|
+
if (c.promotionCandidate)
|
|
194
|
+
promotionCandidates++;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
version: "1",
|
|
198
|
+
scannedAt: new Date().toISOString(),
|
|
199
|
+
config: { normalization: "v1", tier: "bodyHash" },
|
|
200
|
+
clusters,
|
|
201
|
+
summary: {
|
|
202
|
+
memoriesScanned: consideredMemories.length,
|
|
203
|
+
projectsScanned: projects.length,
|
|
204
|
+
clusterCount: clusters.length,
|
|
205
|
+
duplicateFileCount,
|
|
206
|
+
wastedBytes,
|
|
207
|
+
promotionCandidates,
|
|
208
|
+
skippedEmptyBodies,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=dedupe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/core/dedupe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ScanOptions } from "./scan.js";
|
|
2
|
+
import { type LintIssue, type Severity } from "./lint.js";
|
|
3
|
+
import type { MemoryTypeOrUntyped } from "./types.js";
|
|
4
|
+
declare const SOFT_LINE_THRESHOLD: number;
|
|
5
|
+
declare const SOFT_BYTE_THRESHOLD: number;
|
|
6
|
+
export interface ProjectHealth {
|
|
7
|
+
slug: string;
|
|
8
|
+
memoryCount: number;
|
|
9
|
+
hasIndex: boolean;
|
|
10
|
+
indexLineCount: number | null;
|
|
11
|
+
indexByteSize: number | null;
|
|
12
|
+
/** True when the index is over either soft threshold but under both hard caps. */
|
|
13
|
+
indexApproachingCap: boolean;
|
|
14
|
+
/** True when the index has crossed either hard cap. */
|
|
15
|
+
indexOverCap: boolean;
|
|
16
|
+
issues: LintIssue[];
|
|
17
|
+
worstSeverity: Severity | null;
|
|
18
|
+
}
|
|
19
|
+
export interface DoctorReport {
|
|
20
|
+
generatedAt: string;
|
|
21
|
+
projectsScanned: number;
|
|
22
|
+
filesScanned: number;
|
|
23
|
+
totals: {
|
|
24
|
+
issuesBySeverity: Record<Severity, number>;
|
|
25
|
+
memoriesByType: Record<MemoryTypeOrUntyped, number>;
|
|
26
|
+
};
|
|
27
|
+
projects: ProjectHealth[];
|
|
28
|
+
/** Prioritized next-action list (errors first, then warns). */
|
|
29
|
+
nextActions: LintIssue[];
|
|
30
|
+
}
|
|
31
|
+
export interface DoctorOptions extends ScanOptions {
|
|
32
|
+
projectFilter?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function runDoctor(opts?: DoctorOptions): DoctorReport;
|
|
35
|
+
export { SOFT_LINE_THRESHOLD, SOFT_BYTE_THRESHOLD };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Doctor — aggregates scan + lint into a single health snapshot.
|
|
2
|
+
//
|
|
3
|
+
// `lint` is the per-issue stream optimized for jq/MCP/CI. `doctor` is the
|
|
4
|
+
// human-readable "what's the state of my memory?" command. It adds stats,
|
|
5
|
+
// per-project grouping, and soft "approaching limit" signals that aren't
|
|
6
|
+
// lint-rule violations yet but are worth knowing about.
|
|
7
|
+
import { existsSync, statSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { scanAll } from "./scan.js";
|
|
10
|
+
import { lintAll, INDEX_BYTE_CAP, INDEX_LINE_CAP } from "./lint.js";
|
|
11
|
+
import { parseMemoryIndex } from "./memoryIndex.js";
|
|
12
|
+
// Soft cap = 80% of hard cap. When MEMORY.md crosses this we surface it in
|
|
13
|
+
// doctor but do NOT escalate to a lint rule (no data loss yet).
|
|
14
|
+
const SOFT_LINE_RATIO = 0.8;
|
|
15
|
+
const SOFT_BYTE_RATIO = 0.8;
|
|
16
|
+
const SOFT_LINE_THRESHOLD = Math.floor(INDEX_LINE_CAP * SOFT_LINE_RATIO);
|
|
17
|
+
const SOFT_BYTE_THRESHOLD = Math.floor(INDEX_BYTE_CAP * SOFT_BYTE_RATIO);
|
|
18
|
+
const SEV_ORDER = { error: 0, warn: 1, info: 2 };
|
|
19
|
+
function indexStats(project) {
|
|
20
|
+
const idxPath = join(project.path, "memory", "MEMORY.md");
|
|
21
|
+
if (!existsSync(idxPath))
|
|
22
|
+
return { lineCount: null, byteSize: null };
|
|
23
|
+
try {
|
|
24
|
+
const parsed = parseMemoryIndex(idxPath);
|
|
25
|
+
return { lineCount: parsed.lineCount, byteSize: parsed.byteSize };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// If we can't parse, fall back to raw byte size so the user still gets a signal.
|
|
29
|
+
try {
|
|
30
|
+
const size = statSync(idxPath).size;
|
|
31
|
+
return { lineCount: null, byteSize: size };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { lineCount: null, byteSize: null };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function runDoctor(opts = {}) {
|
|
39
|
+
const projects = scanAll(opts).filter((p) => !opts.projectFilter || p.slug.includes(opts.projectFilter));
|
|
40
|
+
const lintResult = lintAll(opts);
|
|
41
|
+
// Group issues by project for fast lookup.
|
|
42
|
+
const issuesByProject = new Map();
|
|
43
|
+
for (const issue of lintResult.issues) {
|
|
44
|
+
const arr = issuesByProject.get(issue.projectSlug) ?? [];
|
|
45
|
+
arr.push(issue);
|
|
46
|
+
issuesByProject.set(issue.projectSlug, arr);
|
|
47
|
+
}
|
|
48
|
+
const issuesBySeverity = { error: 0, warn: 0, info: 0 };
|
|
49
|
+
for (const i of lintResult.issues)
|
|
50
|
+
issuesBySeverity[i.severity]++;
|
|
51
|
+
const memoriesByType = {
|
|
52
|
+
user: 0,
|
|
53
|
+
feedback: 0,
|
|
54
|
+
project: 0,
|
|
55
|
+
reference: 0,
|
|
56
|
+
untyped: 0,
|
|
57
|
+
};
|
|
58
|
+
const projectHealth = projects.map((project) => {
|
|
59
|
+
for (const m of project.memories)
|
|
60
|
+
memoriesByType[m.type]++;
|
|
61
|
+
const { lineCount, byteSize } = indexStats(project);
|
|
62
|
+
const overCap = (lineCount !== null && lineCount > INDEX_LINE_CAP) ||
|
|
63
|
+
(byteSize !== null && byteSize > INDEX_BYTE_CAP);
|
|
64
|
+
const approachingCap = !overCap &&
|
|
65
|
+
((lineCount !== null && lineCount > SOFT_LINE_THRESHOLD) ||
|
|
66
|
+
(byteSize !== null && byteSize > SOFT_BYTE_THRESHOLD));
|
|
67
|
+
const issues = issuesByProject.get(project.slug) ?? [];
|
|
68
|
+
const worstSeverity = issues.length === 0
|
|
69
|
+
? null
|
|
70
|
+
: issues.reduce((acc, i) => (SEV_ORDER[i.severity] < SEV_ORDER[acc] ? i.severity : acc), "info");
|
|
71
|
+
return {
|
|
72
|
+
slug: project.slug,
|
|
73
|
+
memoryCount: project.memories.length,
|
|
74
|
+
hasIndex: project.hasIndex,
|
|
75
|
+
indexLineCount: lineCount,
|
|
76
|
+
indexByteSize: byteSize,
|
|
77
|
+
indexApproachingCap: approachingCap,
|
|
78
|
+
indexOverCap: overCap,
|
|
79
|
+
issues,
|
|
80
|
+
worstSeverity,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
// Sort projects: worst severity first, then by name.
|
|
84
|
+
projectHealth.sort((a, b) => {
|
|
85
|
+
const aSev = a.worstSeverity ? SEV_ORDER[a.worstSeverity] : 99;
|
|
86
|
+
const bSev = b.worstSeverity ? SEV_ORDER[b.worstSeverity] : 99;
|
|
87
|
+
if (aSev !== bSev)
|
|
88
|
+
return aSev - bSev;
|
|
89
|
+
return a.slug.localeCompare(b.slug);
|
|
90
|
+
});
|
|
91
|
+
// Next actions: errors first, then warns, in lint's original order (which is
|
|
92
|
+
// already deterministic by project + code).
|
|
93
|
+
const nextActions = [...lintResult.issues]
|
|
94
|
+
.filter((i) => i.severity !== "info")
|
|
95
|
+
.sort((a, b) => SEV_ORDER[a.severity] - SEV_ORDER[b.severity]);
|
|
96
|
+
return {
|
|
97
|
+
generatedAt: new Date().toISOString(),
|
|
98
|
+
projectsScanned: lintResult.projectsScanned,
|
|
99
|
+
filesScanned: lintResult.filesScanned,
|
|
100
|
+
totals: { issuesBySeverity, memoriesByType },
|
|
101
|
+
projects: projectHealth,
|
|
102
|
+
nextActions,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export { SOFT_LINE_THRESHOLD, SOFT_BYTE_THRESHOLD };
|
|
106
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Operation, Plan } from "./plan.js";
|
|
2
|
+
export interface AppliedOp {
|
|
3
|
+
op: Operation;
|
|
4
|
+
/** The op that reverses this one. Captured at apply time from on-disk state. */
|
|
5
|
+
inverse: Operation;
|
|
6
|
+
appliedAt: string;
|
|
7
|
+
result: "success" | "skipped" | "failure";
|
|
8
|
+
/** Populated when result === "failure". */
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface JournalEntry {
|
|
12
|
+
plan: Plan;
|
|
13
|
+
status: "complete" | "partial" | "failed";
|
|
14
|
+
startedAt: string;
|
|
15
|
+
finishedAt: string;
|
|
16
|
+
ops: AppliedOp[];
|
|
17
|
+
/** If this entry undoes another, the id of the original. */
|
|
18
|
+
undoOf?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function defaultJournalDir(): string;
|
|
21
|
+
export interface JournalOptions {
|
|
22
|
+
/** Override the journal directory (used in tests). */
|
|
23
|
+
journalDir?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function ensureJournalDir(opts?: JournalOptions): string;
|
|
26
|
+
export declare function writeJournalEntry(entry: JournalEntry, opts?: JournalOptions): string;
|
|
27
|
+
export declare function readJournalEntry(id: string, opts?: JournalOptions): JournalEntry | null;
|
|
28
|
+
/** Lists journal entries, newest first. */
|
|
29
|
+
export declare function listJournal(opts?: JournalOptions): JournalEntry[];
|
|
30
|
+
/** Most-recent entry, or null if the journal is empty. */
|
|
31
|
+
export declare function latestJournalEntry(opts?: JournalOptions): JournalEntry | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Write-ahead journal at `~/.claude/.memex/log/<id>.json`.
|
|
2
|
+
//
|
|
3
|
+
// Every applied plan produces one journal entry. The entry stores the
|
|
4
|
+
// original plan, the per-op execution result, and the inverse ops needed
|
|
5
|
+
// to undo the plan. `memex undo` reads the most recent entry and applies
|
|
6
|
+
// its inverses in reverse order.
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
const JOURNAL_DIRNAME = ".memex";
|
|
11
|
+
export function defaultJournalDir() {
|
|
12
|
+
return join(homedir(), ".claude", JOURNAL_DIRNAME, "log");
|
|
13
|
+
}
|
|
14
|
+
function resolveDir(opts) {
|
|
15
|
+
return opts.journalDir ?? defaultJournalDir();
|
|
16
|
+
}
|
|
17
|
+
export function ensureJournalDir(opts = {}) {
|
|
18
|
+
const dir = resolveDir(opts);
|
|
19
|
+
mkdirSync(dir, { recursive: true });
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
/** Sort key embedded in the filename: ISO timestamp prefix makes lex == time. */
|
|
23
|
+
function fileNameForId(id) {
|
|
24
|
+
return `${id}.json`;
|
|
25
|
+
}
|
|
26
|
+
export function writeJournalEntry(entry, opts = {}) {
|
|
27
|
+
const dir = ensureJournalDir(opts);
|
|
28
|
+
const file = join(dir, fileNameForId(entry.plan.id));
|
|
29
|
+
writeFileSync(file, JSON.stringify(entry, null, 2));
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
export function readJournalEntry(id, opts = {}) {
|
|
33
|
+
const file = join(resolveDir(opts), fileNameForId(id));
|
|
34
|
+
if (!existsSync(file))
|
|
35
|
+
return null;
|
|
36
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
/** Lists journal entries, newest first. */
|
|
39
|
+
export function listJournal(opts = {}) {
|
|
40
|
+
const dir = resolveDir(opts);
|
|
41
|
+
if (!existsSync(dir))
|
|
42
|
+
return [];
|
|
43
|
+
const files = readdirSync(dir)
|
|
44
|
+
.filter((n) => n.endsWith(".json"))
|
|
45
|
+
.sort()
|
|
46
|
+
.reverse();
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const name of files) {
|
|
49
|
+
try {
|
|
50
|
+
out.push(JSON.parse(readFileSync(join(dir, name), "utf8")));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Skip corrupt entries — they're informational only and shouldn't break
|
|
54
|
+
// listing of healthy entries.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** Most-recent entry, or null if the journal is empty. */
|
|
60
|
+
export function latestJournalEntry(opts = {}) {
|
|
61
|
+
const all = listJournal(opts);
|
|
62
|
+
return all.length > 0 ? all[0] : null;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=journal.js.map
|