memoryintel 1.0.2 → 1.1.1

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.
@@ -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.2"
9
+ "version": "1.1.1"
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.2"
16
+ "version": "1.1.1"
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.2",
4
+ "version": "1.1.1",
5
5
  "author": {
6
6
  "name": "Adeesh Sharma",
7
7
  "url": "https://github.com/adeeshsharma"
@@ -28,6 +28,27 @@ function upsertPointerBlock(filePath, existingContentIfNew) {
28
28
  const separator = content.endsWith('\n') ? '\n' : '\n\n';
29
29
  writeFileSync(filePath, `${content}${separator}${POINTER_BLOCK}\n`);
30
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
+ }
31
52
  const NATIVE_FILES = ['AGENTS.md', 'GEMINI.md'];
32
53
  export function installPointerAdapters(projectRoot) {
33
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
- daemon start Run the dashboard daemon in the foreground (usually auto-started)
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
+ }
@@ -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
- const INSTRUCTIONS_TEMPLATE = `# Memory Intel Instructions
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
 
@@ -39,6 +40,28 @@ section, content, reason), write it to a file, and run \`memoryintel update <pla
39
40
  nothing piped to stdin fails (there is no plan to apply). Reuse exact existing heading names from
40
41
  the manifest \`load\` gave you. If nothing meaningful changed, do nothing — do not call \`update\`.
41
42
 
43
+ **The exact TOON format \`update\` expects** - this is the one place a malformed plan fails
44
+ outright rather than degrading gracefully, so match it exactly rather than improvising:
45
+
46
+ items[2]{file,action,section,content,reason}:
47
+ "path/to/file.md","append","Section Heading","New paragraph to add.","Why this changed"
48
+ "path/to/other.md","create-section","New Heading","Content, comma and all.","Why"
49
+
50
+ - The header line is literal: \`items[\`, the row count, \`]{\`, the field names in this exact
51
+ order and spelling (\`file,action,section,content,reason\` - add \`,kind\` only for a compaction
52
+ row, see "Compaction" below), \`}:\`. The row count MUST equal the number of rows that follow, or
53
+ \`update\` rejects the whole plan with nothing applied.
54
+ - \`action\` is exactly one of three values: \`append\` (add to the end of an existing section's
55
+ content), \`replace\` (overwrite the section's entire content), or \`create-section\` (add a new
56
+ \`##\` heading if it doesn't already exist - degrades to a plain \`append\` if it does).
57
+ - Each row is comma-separated fields in that same header order, indented two spaces.
58
+ - **Quote a field in double quotes whenever its content contains a comma, a double quote, a
59
+ newline, or starts with whitespace** - leave every other field unquoted. Double any literal
60
+ \`"\` inside a quoted field (\`"\` becomes \`""\`). A quoted field may span multiple physical lines.
61
+ Example: content \`He said "hi", then left\` must be written as \`"He said ""hi"", then left"\`.
62
+ - \`context/currentMentalModel.md\` is the one exception to \`action\`: its row's \`content\` replaces
63
+ the file's entire content verbatim, regardless of what \`action\`/\`section\` say.
64
+
42
65
  Also include a row for \`context/currentMentalModel.md\` whenever the update is more than a small,
43
66
  localized fact — anything that shifts what the project *is* or where it currently stands (not
44
67
  every single decision/progress entry needs one). Unlike every other file, it is a **whole-file
@@ -115,8 +138,13 @@ function ensureFile(path, content) {
115
138
  export function runInit(targetDir) {
116
139
  const root = join(targetDir, '.memoryintel');
117
140
  mkdirSync(root, { recursive: true });
118
- ensureFile(join(root, 'instructions.md'), INSTRUCTIONS_TEMPLATE);
119
141
  ensureFile(join(root, 'memory-config.json'), JSON.stringify({ initializedAt: new Date().toISOString(), version: '0.1.0' }, null, 2) + '\n');
142
+ const instructionsPath = join(root, 'instructions.md');
143
+ const instructionsIsNew = !existsSync(instructionsPath);
144
+ ensureFile(instructionsPath, INSTRUCTIONS_TEMPLATE);
145
+ if (instructionsIsNew) {
146
+ setGeneratedFileHash(root, 'instructions.md', hashContent(INSTRUCTIONS_TEMPLATE));
147
+ }
120
148
  ensureFile(join(root, 'memory-index.json'), '{}\n');
121
149
  ensureFile(join(root, 'memory-events.jsonl'), '');
122
150
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoryintel",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "Persistent, cross-session project memory for AI coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- daemon start Run the dashboard daemon in the foreground (usually auto-started)
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.