memoryintel 1.0.1 → 1.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/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/adapters/genericPointer.js +34 -4
- package/dist/cli.js +12 -1
- package/dist/commands/doctor.js +65 -0
- package/dist/commands/init.js +26 -2
- package/dist/core/generatedFileHashes.js +42 -0
- package/package.json +1 -1
- package/skills/memoryintel/SKILL.md +4 -1
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Persistent, cross-session project memory for AI coding agents.",
|
|
9
|
-
"version": "1.0
|
|
9
|
+
"version": "1.1.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "memoryintel",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent project memory for AI coding agents — initialize once, then agents automatically load and update project understanding across sessions.",
|
|
16
|
-
"version": "1.0
|
|
16
|
+
"version": "1.1.0"
|
|
17
17
|
}
|
|
18
18
|
]
|
|
19
19
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoryintel",
|
|
3
3
|
"description": "Persistent project memory for AI coding agents — initialize once, then agents automatically load and update project understanding across sessions.",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Adeesh Sharma",
|
|
7
7
|
"url": "https://github.com/adeeshsharma"
|
|
@@ -3,10 +3,19 @@ import { join } from 'node:path';
|
|
|
3
3
|
const START_MARKER = '<!-- memoryintel:managed:start -->';
|
|
4
4
|
const END_MARKER = '<!-- memoryintel:managed:end -->';
|
|
5
5
|
const POINTER_BLOCK = `${START_MARKER}
|
|
6
|
-
This project uses Memory Intel
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
This project uses Memory Intel (\`.memoryintel/\`) for persistent, cross-session project memory.
|
|
7
|
+
Read \`.memoryintel/instructions.md\` for the full mechanism. Two hard requirements, not optional
|
|
8
|
+
background context:
|
|
9
|
+
|
|
10
|
+
1. **Session start:** run \`memoryintel load\` before doing anything else and treat its output as
|
|
11
|
+
real project context, not a formality to skip past.
|
|
12
|
+
2. **Before ending any task that changed project understanding** (new architecture, feature,
|
|
13
|
+
decision, integration, or roadmap item — not formatting/typos): draft an update-plan and run
|
|
14
|
+
\`memoryintel update <plan-file>\`. This applies even in a long session covering many sub-tasks —
|
|
15
|
+
that is exactly when it is easiest to reach the end and have forgotten this step was ever
|
|
16
|
+
pending. If nothing meaningful changed, skip it; do not skip it just because the task grew long.
|
|
17
|
+
|
|
18
|
+
Tell the user when you do this — do not do it silently.
|
|
10
19
|
${END_MARKER}`;
|
|
11
20
|
function upsertPointerBlock(filePath, existingContentIfNew) {
|
|
12
21
|
if (!existsSync(filePath)) {
|
|
@@ -19,6 +28,27 @@ function upsertPointerBlock(filePath, existingContentIfNew) {
|
|
|
19
28
|
const separator = content.endsWith('\n') ? '\n' : '\n\n';
|
|
20
29
|
writeFileSync(filePath, `${content}${separator}${POINTER_BLOCK}\n`);
|
|
21
30
|
}
|
|
31
|
+
export const ADAPTER_FILE_PATHS = ['AGENTS.md', 'GEMINI.md', join('.cursor', 'rules', 'memoryintel.mdc')];
|
|
32
|
+
// Unlike upsertPointerBlock (install-if-missing, used by init - never overwrites an existing
|
|
33
|
+
// block), this always resyncs an EXISTING block to the current POINTER_BLOCK. Safe by
|
|
34
|
+
// construction: the markers themselves are the proof this span is machine-owned, regardless of
|
|
35
|
+
// what real content surrounds it in the same file. Never installs a block that isn't already
|
|
36
|
+
// there - doctor only refreshes, it never adds.
|
|
37
|
+
export function refreshPointerBlock(filePath) {
|
|
38
|
+
if (!existsSync(filePath))
|
|
39
|
+
return 'missing-file';
|
|
40
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
41
|
+
const startIdx = content.indexOf(START_MARKER);
|
|
42
|
+
const endIdx = content.indexOf(END_MARKER);
|
|
43
|
+
if (startIdx === -1 || endIdx === -1)
|
|
44
|
+
return 'not-installed';
|
|
45
|
+
const endOfBlock = endIdx + END_MARKER.length;
|
|
46
|
+
const newContent = `${content.slice(0, startIdx)}${POINTER_BLOCK}${content.slice(endOfBlock)}`;
|
|
47
|
+
if (newContent === content)
|
|
48
|
+
return 'unchanged';
|
|
49
|
+
writeFileSync(filePath, newContent);
|
|
50
|
+
return 'refreshed';
|
|
51
|
+
}
|
|
22
52
|
const NATIVE_FILES = ['AGENTS.md', 'GEMINI.md'];
|
|
23
53
|
export function installPointerAdapters(projectRoot) {
|
|
24
54
|
const existingNativeFiles = NATIVE_FILES.filter((f) => existsSync(join(projectRoot, f)));
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runScan } from './commands/scan.js';
|
|
|
12
12
|
import { runCheckStop } from './adapters/claudeCode.js';
|
|
13
13
|
import { runDashboardEnable, runDashboardDisable } from './commands/dashboardToggle.js';
|
|
14
14
|
import { runDaemonStart } from './commands/daemonStart.js';
|
|
15
|
+
import { runDoctor } from './commands/doctor.js';
|
|
15
16
|
export const USAGE = `Usage: memoryintel <command> [options]
|
|
16
17
|
|
|
17
18
|
Commands:
|
|
@@ -25,7 +26,10 @@ Commands:
|
|
|
25
26
|
status Print a human-readable summary of current memory state
|
|
26
27
|
check-stop Stop-hook check: emit a JSON allow/block decision
|
|
27
28
|
dashboard <enable|disable> Turn the shared local dashboard on or off
|
|
28
|
-
|
|
29
|
+
doctor [--force] Refresh memoryintel's own generated files (instructions.md, pointer
|
|
30
|
+
blocks) to the current template wherever it's provably safe;
|
|
31
|
+
--force also overwrites instructions.md when it isn't
|
|
32
|
+
daemon start Run the dashboard daemon in the foreground (usually auto-started)
|
|
29
33
|
|
|
30
34
|
An update-plan row may set kind=compress to compact an oversized section; update() only applies
|
|
31
35
|
such a row when its target file is currently git-clean.
|
|
@@ -64,6 +68,13 @@ export function dispatch(argv) {
|
|
|
64
68
|
const result = runCheckStop(root);
|
|
65
69
|
return { exitCode: 0, stdout: JSON.stringify(result) + '\n', stderr: '' };
|
|
66
70
|
}
|
|
71
|
+
case 'doctor': {
|
|
72
|
+
const root = findMemoryIntelRoot(process.cwd());
|
|
73
|
+
if (!root)
|
|
74
|
+
return { exitCode: 1, stdout: '', stderr: 'No .memoryintel/ found.\n' };
|
|
75
|
+
const force = argv.includes('--force');
|
|
76
|
+
return { exitCode: 0, stdout: runDoctor(root, { force }), stderr: '' };
|
|
77
|
+
}
|
|
67
78
|
case 'dashboard': {
|
|
68
79
|
const sub = argv[1];
|
|
69
80
|
if (sub === 'enable') {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { INSTRUCTIONS_TEMPLATE } from './init.js';
|
|
4
|
+
import { hashContent, getGeneratedFileHash, setGeneratedFileHash } from '../core/generatedFileHashes.js';
|
|
5
|
+
import { refreshPointerBlock, ADAPTER_FILE_PATHS } from '../adapters/genericPointer.js';
|
|
6
|
+
import { atomicWriteFile } from '../core/atomicWrite.js';
|
|
7
|
+
const INSTRUCTIONS_REL_FILE = 'instructions.md';
|
|
8
|
+
function checkInstructions(root, options) {
|
|
9
|
+
const instructionsPath = join(root, INSTRUCTIONS_REL_FILE);
|
|
10
|
+
const newFilePath = `${instructionsPath}.new`;
|
|
11
|
+
if (!existsSync(instructionsPath)) {
|
|
12
|
+
return 'instructions.md: missing - run `memoryintel init` to create it.';
|
|
13
|
+
}
|
|
14
|
+
const diskContent = readFileSync(instructionsPath, 'utf-8');
|
|
15
|
+
const diskHash = hashContent(diskContent);
|
|
16
|
+
const templateHash = hashContent(INSTRUCTIONS_TEMPLATE);
|
|
17
|
+
if (diskHash === templateHash) {
|
|
18
|
+
if (existsSync(newFilePath))
|
|
19
|
+
unlinkSync(newFilePath);
|
|
20
|
+
// Self-healing: a project with no recorded hash that happens to already be pristine (a
|
|
21
|
+
// fresh init, or content that coincidentally matches) is now provably safe going forward -
|
|
22
|
+
// record it so a future run never has to fall back to the refuse-and-report path for it.
|
|
23
|
+
if (getGeneratedFileHash(root, INSTRUCTIONS_REL_FILE) !== templateHash) {
|
|
24
|
+
setGeneratedFileHash(root, INSTRUCTIONS_REL_FILE, templateHash);
|
|
25
|
+
}
|
|
26
|
+
return 'instructions.md: up to date.';
|
|
27
|
+
}
|
|
28
|
+
const recordedHash = getGeneratedFileHash(root, INSTRUCTIONS_REL_FILE);
|
|
29
|
+
const safeRefresh = recordedHash !== undefined && recordedHash === diskHash;
|
|
30
|
+
if (safeRefresh || options.force) {
|
|
31
|
+
atomicWriteFile(instructionsPath, INSTRUCTIONS_TEMPLATE);
|
|
32
|
+
setGeneratedFileHash(root, INSTRUCTIONS_REL_FILE, templateHash);
|
|
33
|
+
if (existsSync(newFilePath))
|
|
34
|
+
unlinkSync(newFilePath);
|
|
35
|
+
return safeRefresh
|
|
36
|
+
? 'instructions.md: refreshed to the current template.'
|
|
37
|
+
: 'instructions.md: refreshed (forced).';
|
|
38
|
+
}
|
|
39
|
+
atomicWriteFile(newFilePath, INSTRUCTIONS_TEMPLATE);
|
|
40
|
+
return ("instructions.md: differs from the current template and its last-known-safe state can't be " +
|
|
41
|
+
"confirmed (either hand-edited, or from before doctor existed). Wrote the current template " +
|
|
42
|
+
"to instructions.md.new for comparison (e.g. `diff .memoryintel/instructions.md " +
|
|
43
|
+
".memoryintel/instructions.md.new`). Run `memoryintel doctor --force` to adopt it anyway - " +
|
|
44
|
+
"this overwrites instructions.md and removes the .new file.");
|
|
45
|
+
}
|
|
46
|
+
function checkPointerBlocks(projectRoot) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
for (const relPath of ADAPTER_FILE_PATHS) {
|
|
49
|
+
const result = refreshPointerBlock(join(projectRoot, relPath));
|
|
50
|
+
if (result === 'refreshed')
|
|
51
|
+
lines.push(`${relPath}: pointer block refreshed.`);
|
|
52
|
+
else if (result === 'unchanged')
|
|
53
|
+
lines.push(`${relPath}: pointer block up to date.`);
|
|
54
|
+
else if (result === 'not-installed')
|
|
55
|
+
lines.push(`${relPath}: no pointer block found - skipped.`);
|
|
56
|
+
// 'missing-file' is not reported - most projects won't have all three adapter files, and
|
|
57
|
+
// that's not something worth flagging as noise on every doctor run.
|
|
58
|
+
}
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
export function runDoctor(root, options = {}) {
|
|
62
|
+
const projectRoot = dirname(root);
|
|
63
|
+
const lines = [checkInstructions(root, options), ...checkPointerBlocks(projectRoot)];
|
|
64
|
+
return lines.join('\n') + '\n';
|
|
65
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -2,7 +2,8 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { STARTER_FILES, MENTAL_MODEL_STARTER } from '../templates/starterFiles.js';
|
|
4
4
|
import { installPointerAdapters } from '../adapters/genericPointer.js';
|
|
5
|
-
|
|
5
|
+
import { hashContent, setGeneratedFileHash } from '../core/generatedFileHashes.js';
|
|
6
|
+
export const INSTRUCTIONS_TEMPLATE = `# Memory Intel Instructions
|
|
6
7
|
|
|
7
8
|
This project uses Memory Intel. Read this file at the start of every session.
|
|
8
9
|
|
|
@@ -58,6 +59,24 @@ on a real project: a "known limitation, deliberately left unfixed" entry was sti
|
|
|
58
59
|
\`currentMentalModel.md\`, but never reconciled against the older, now-wrong decisions entry right
|
|
59
60
|
next to it.
|
|
60
61
|
|
|
62
|
+
## Committing \`.memoryintel/\` itself
|
|
63
|
+
\`.memoryintel/\` is a normal, git-committed part of the project — \`memoryintel update\` only writes
|
|
64
|
+
files to disk, it never runs git for you. After a successful \`update\`, commit \`.memoryintel/\`'s
|
|
65
|
+
changes yourself, ideally in the same commit as (or immediately after) whatever code change
|
|
66
|
+
prompted the update, so it travels with the same commit/PR automatically instead of being left
|
|
67
|
+
behind as an uncommitted diff.
|
|
68
|
+
|
|
69
|
+
This matters most in a git worktree, a container, or any other isolated checkout: an uncommitted
|
|
70
|
+
\`.memoryintel/\` change only exists in that one working directory. Merging a feature branch's PR and
|
|
71
|
+
pulling the primary checkout up to date only brings memory updates along if they were actually
|
|
72
|
+
committed first, the same as any other file — there is nothing worktree-specific about the
|
|
73
|
+
mechanism itself, only that a worktree's own uncommitted state is easier to lose track of, since it
|
|
74
|
+
is invisible everywhere else once the session ends and that directory stops being looked at. Found
|
|
75
|
+
on a real project: a full feature built end-to-end in a worktree, with real, meaningful
|
|
76
|
+
architecture changes throughout, reached a merged PR with \`.memoryintel/\` never once updated or
|
|
77
|
+
committed — not because updating was hard, but because nothing in the session ever came back to it
|
|
78
|
+
before the worktree's job was considered done.
|
|
79
|
+
|
|
61
80
|
## Compaction
|
|
62
81
|
A file marked \`status: over\` in \`load\`'s manifest has grown past its configured line ceiling.
|
|
63
82
|
This is a signal, not a command — compact it only when it's a sensible moment to (the same
|
|
@@ -97,8 +116,13 @@ function ensureFile(path, content) {
|
|
|
97
116
|
export function runInit(targetDir) {
|
|
98
117
|
const root = join(targetDir, '.memoryintel');
|
|
99
118
|
mkdirSync(root, { recursive: true });
|
|
100
|
-
ensureFile(join(root, 'instructions.md'), INSTRUCTIONS_TEMPLATE);
|
|
101
119
|
ensureFile(join(root, 'memory-config.json'), JSON.stringify({ initializedAt: new Date().toISOString(), version: '0.1.0' }, null, 2) + '\n');
|
|
120
|
+
const instructionsPath = join(root, 'instructions.md');
|
|
121
|
+
const instructionsIsNew = !existsSync(instructionsPath);
|
|
122
|
+
ensureFile(instructionsPath, INSTRUCTIONS_TEMPLATE);
|
|
123
|
+
if (instructionsIsNew) {
|
|
124
|
+
setGeneratedFileHash(root, 'instructions.md', hashContent(INSTRUCTIONS_TEMPLATE));
|
|
125
|
+
}
|
|
102
126
|
ensureFile(join(root, 'memory-index.json'), '{}\n');
|
|
103
127
|
ensureFile(join(root, 'memory-events.jsonl'), '');
|
|
104
128
|
ensureFile(join(root, 'context', 'currentMentalModel.md'), MENTAL_MODEL_STARTER);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { atomicWriteFile } from './atomicWrite.js';
|
|
5
|
+
export function hashContent(content) {
|
|
6
|
+
return createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
7
|
+
}
|
|
8
|
+
function configPath(root) {
|
|
9
|
+
return join(root, 'memory-config.json');
|
|
10
|
+
}
|
|
11
|
+
// Missing file or corrupt JSON both fall back to an empty object - the same defensive,
|
|
12
|
+
// never-throw read pattern compressionConfig.ts already uses for this same file, since neither
|
|
13
|
+
// getGeneratedFileHash nor setGeneratedFileHash should ever abort doctor/init over a config
|
|
14
|
+
// read problem that isn't this feature's to fix.
|
|
15
|
+
function readConfig(root) {
|
|
16
|
+
const path = configPath(root);
|
|
17
|
+
if (!existsSync(path))
|
|
18
|
+
return {};
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
21
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function getGeneratedFileHash(root, relFile) {
|
|
28
|
+
const hashes = readConfig(root).generatedFileHashes;
|
|
29
|
+
if (hashes && typeof hashes === 'object') {
|
|
30
|
+
const value = hashes[relFile];
|
|
31
|
+
return typeof value === 'string' ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
export function setGeneratedFileHash(root, relFile, hash) {
|
|
36
|
+
const config = readConfig(root);
|
|
37
|
+
const existingHashes = config.generatedFileHashes && typeof config.generatedFileHashes === 'object'
|
|
38
|
+
? config.generatedFileHashes
|
|
39
|
+
: {};
|
|
40
|
+
config.generatedFileHashes = { ...existingHashes, [relFile]: hash };
|
|
41
|
+
atomicWriteFile(configPath(root), JSON.stringify(config, null, 2) + '\n');
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -48,7 +48,10 @@ Commands:
|
|
|
48
48
|
status Print a human-readable summary of current memory state
|
|
49
49
|
check-stop Stop-hook check: emit a JSON allow/block decision
|
|
50
50
|
dashboard <enable|disable> Turn the shared local dashboard on or off
|
|
51
|
-
|
|
51
|
+
doctor [--force] Refresh memoryintel's own generated files (instructions.md, pointer
|
|
52
|
+
blocks) to the current template wherever it's provably safe;
|
|
53
|
+
--force also overwrites instructions.md when it isn't
|
|
54
|
+
daemon start Run the dashboard daemon in the foreground (usually auto-started)
|
|
52
55
|
|
|
53
56
|
An update-plan row may set kind=compress to compact an oversized section; update() only applies
|
|
54
57
|
such a row when its target file is currently git-clean.
|