memoryintel 1.1.0 → 1.1.2

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.1.0"
9
+ "version": "1.1.2"
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.1.0"
16
+ "version": "1.1.2"
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.1.0",
4
+ "version": "1.1.2",
5
5
  "author": {
6
6
  "name": "Adeesh Sharma",
7
7
  "url": "https://github.com/adeeshsharma"
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, realpathSync } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { findMemoryIntelRoot } from './core/discovery.js';
6
6
  import { runUpdate } from './commands/update.js';
@@ -13,6 +13,26 @@ import { runCheckStop } from './adapters/claudeCode.js';
13
13
  import { runDashboardEnable, runDashboardDisable } from './commands/dashboardToggle.js';
14
14
  import { runDaemonStart } from './commands/daemonStart.js';
15
15
  import { runDoctor } from './commands/doctor.js';
16
+ // Every command below that resolves .memoryintel/ by walking up from a starting directory
17
+ // previously always used process.cwd() with no override - meaning a long, multi-project
18
+ // session had to religiously `cd` before every single call, with silently-wrong output the
19
+ // only feedback for getting it wrong (mitigated separately by printing the resolved root, but
20
+ // that only helps you NOTICE the mistake, not avoid making it). --root <path> lets a caller
21
+ // that already knows its target project say so directly; MEMORYINTEL_ROOT is the same thing
22
+ // as an env var, for a session-start hook or similar context where passing an extra CLI flag
23
+ // isn't convenient. --root wins when both are given. Either way this is resolved exactly like
24
+ // process.cwd() always was - a starting point findMemoryIntelRoot() walks up from, not
25
+ // required to already BE the exact .memoryintel directory itself.
26
+ function resolveStartDir(argv) {
27
+ const rootFlagIndex = argv.indexOf('--root');
28
+ if (rootFlagIndex !== -1 && argv[rootFlagIndex + 1]) {
29
+ return resolve(process.cwd(), argv[rootFlagIndex + 1]);
30
+ }
31
+ if (process.env.MEMORYINTEL_ROOT) {
32
+ return resolve(process.cwd(), process.env.MEMORYINTEL_ROOT);
33
+ }
34
+ return process.cwd();
35
+ }
16
36
  export const USAGE = `Usage: memoryintel <command> [options]
17
37
 
18
38
  Commands:
@@ -52,24 +72,24 @@ export function dispatch(argv) {
52
72
  case 'load': {
53
73
  const domainFlagIndex = argv.indexOf('--domain');
54
74
  const domain = domainFlagIndex !== -1 ? argv[domainFlagIndex + 1] : undefined;
55
- const output = runLoad(process.cwd(), domain);
75
+ const output = runLoad(resolveStartDir(argv), domain);
56
76
  return { exitCode: 0, stdout: output, stderr: '' };
57
77
  }
58
78
  case 'status': {
59
- const root = findMemoryIntelRoot(process.cwd());
79
+ const root = findMemoryIntelRoot(resolveStartDir(argv));
60
80
  if (!root)
61
81
  return { exitCode: 1, stdout: '', stderr: 'No .memoryintel/ found.\n' };
62
82
  return { exitCode: 0, stdout: runStatus(root), stderr: '' };
63
83
  }
64
84
  case 'check-stop': {
65
- const root = findMemoryIntelRoot(process.cwd());
85
+ const root = findMemoryIntelRoot(resolveStartDir(argv));
66
86
  if (!root)
67
87
  return { exitCode: 0, stdout: '', stderr: '' };
68
88
  const result = runCheckStop(root);
69
89
  return { exitCode: 0, stdout: JSON.stringify(result) + '\n', stderr: '' };
70
90
  }
71
91
  case 'doctor': {
72
- const root = findMemoryIntelRoot(process.cwd());
92
+ const root = findMemoryIntelRoot(resolveStartDir(argv));
73
93
  if (!root)
74
94
  return { exitCode: 1, stdout: '', stderr: 'No .memoryintel/ found.\n' };
75
95
  const force = argv.includes('--force');
@@ -130,13 +150,18 @@ async function main() {
130
150
  return;
131
151
  }
132
152
  if (command === 'update') {
133
- const root = findMemoryIntelRoot(process.cwd());
153
+ const root = findMemoryIntelRoot(resolveStartDir(argv));
134
154
  if (!root) {
135
155
  process.stderr.write('No .memoryintel/ found.\n');
136
156
  process.exitCode = 1;
137
157
  return;
138
158
  }
139
- const source = argv[1] ?? '-';
159
+ // The plan-file path is the first REMAINING positional argument after stripping the
160
+ // command name itself and a --root <path> pair, if given - otherwise "--root"'s own
161
+ // value would get misread as the plan-file path.
162
+ const rootFlagIndex = argv.indexOf('--root');
163
+ const positional = argv.filter((_, i) => i !== 0 && i !== rootFlagIndex && i !== rootFlagIndex + 1);
164
+ const source = positional[0] ?? '-';
140
165
  const planText = source === '-' ? readFileSync(0, 'utf-8') : readFileSync(source, 'utf-8');
141
166
  // Caught live: an agent ran bare `memoryintel update` (no plan-file argument, no piped
142
167
  // stdin) as a one-shot Bash tool call. `source` defaulted to '-' (read stdin), stdin was
@@ -144,12 +169,14 @@ async function main() {
144
169
  // generic "Malformed TOON table header" error - true, but useless for figuring out what
145
170
  // actually went wrong. This is the one case worth naming explicitly before it gets there.
146
171
  if (planText.trim().length === 0) {
147
- process.stderr.write('memoryintel: No update-plan given. Pass a TOON plan file (`memoryintel update <path>`) or pipe TOON content via stdin. See .memoryintel/instructions.md for the update-plan format.\n');
172
+ process.stderr.write('memoryintel: No update-plan given. Pass a plan file (TOON or JSON, `memoryintel update <path>`) or pipe it via stdin. See .memoryintel/instructions.md for the update-plan format.\n');
148
173
  process.exitCode = 1;
149
174
  return;
150
175
  }
151
176
  const result = await runUpdate(root, planText);
152
- process.stdout.write(`Applied: ${result.applied.join(', ') || '(none)'}\nSkipped: ${result.skipped.join(', ') || '(none)'}\n`);
177
+ // root printed first, same reasoning as load/status: a wrong-directory update should
178
+ // never be silent.
179
+ process.stdout.write(`root: ${root}\nApplied: ${result.applied.join(', ') || '(none)'}\nSkipped: ${result.skipped.join(', ') || '(none)'}\n`);
153
180
  process.exitCode = 0;
154
181
  return;
155
182
  }
@@ -40,6 +40,36 @@ section, content, reason), write it to a file, and run \`memoryintel update <pla
40
40
  nothing piped to stdin fails (there is no plan to apply). Reuse exact existing heading names from
41
41
  the manifest \`load\` gave you. If nothing meaningful changed, do nothing — do not call \`update\`.
42
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
+
65
+ **\`update\` also accepts a plain JSON array of the same rows instead of TOON**, auto-detected by
66
+ whether the file/stdin content starts with \`[\` or \`{\` after trimming - if you'd rather
67
+ \`JSON.stringify\` a plan than hand-write TOON's quoting rule above, this is the safer default:
68
+ \`[{"file": "path/to/file.md", "action": "append", "section": "Section Heading", "content": "New
69
+ paragraph to add.", "reason": "Why this changed"}]\`. Same fields, same required order doesn't
70
+ matter (JSON is keyed, not positional), same \`context/currentMentalModel.md\` exception. Neither
71
+ format is preferred - use whichever you're less likely to get wrong.
72
+
43
73
  Also include a row for \`context/currentMentalModel.md\` whenever the update is more than a small,
44
74
  localized fact — anything that shifts what the project *is* or where it currently stands (not
45
75
  every single decision/progress entry needs one). Unlike every other file, it is a **whole-file
@@ -7,6 +7,7 @@ import { getCeilingLines, countLines } from '../core/compressionConfig.js';
7
7
  import { ensureDaemonRunning } from '../daemon/lifecycle.js';
8
8
  import { upsertRegistryEntry } from '../daemon/registry.js';
9
9
  import { appendEvent } from '../core/eventLog.js';
10
+ import { readIndex } from '../core/memoryIndex.js';
10
11
  const ALWAYS_LOAD = ['context/currentMentalModel.md', 'context/activeContext.md'];
11
12
  const DOMAIN_FILES = {
12
13
  technical: ['technical/architecture.md', 'technical/techContext.md', 'technical/patterns.md'],
@@ -44,6 +45,11 @@ export function runLoad(cwd, domain) {
44
45
  const files = [...ALWAYS_LOAD, ...(domain ? DOMAIN_FILES[domain] : [])];
45
46
  const sections = [];
46
47
  const manifestRows = [];
48
+ // `status` already surfaces lastUpdated from this same index - `load` is the one command
49
+ // instructions.md tells every session to run FIRST, though, and previously gave zero signal
50
+ // for "just updated" vs. "nobody has touched this in months" without a separate `status`
51
+ // call nothing prompts an agent to make.
52
+ const index = readIndex(join(root, 'memory-index.json'));
47
53
  for (const relFile of files) {
48
54
  const absPath = join(root, relFile);
49
55
  if (!existsSync(absPath))
@@ -58,7 +64,8 @@ export function runLoad(cwd, domain) {
58
64
  headings: extractHeadings(content).join('|'),
59
65
  lines: String(lines),
60
66
  ceiling: String(ceiling),
61
- status
67
+ status,
68
+ lastUpdated: index[relFile]?.lastUpdated ?? 'never'
62
69
  });
63
70
  }
64
71
  try {
@@ -78,5 +85,10 @@ export function runLoad(cwd, domain) {
78
85
  // KPI telemetry is best-effort - never let logging a load break the load itself.
79
86
  }
80
87
  const manifest = encodeToonTable(manifestRows);
81
- return `${manifest}\n${sections.join('\n')}`;
88
+ // A leading, plainly-labeled root line - silently loading the wrong project's (or wrong
89
+ // worktree/branch's) memory has happened in practice: findMemoryIntelRoot() walks up from
90
+ // cwd with no built-in visibility into which root it actually found, so confidently-wrong
91
+ // content came back with nothing to flag it. This is always the first thing printed,
92
+ // whether or not --domain is given.
93
+ return `root: ${root}\n${manifest}\n${sections.join('\n')}`;
82
94
  }
@@ -3,8 +3,12 @@ import { join } from 'node:path';
3
3
  import { readIndex } from '../core/memoryIndex.js';
4
4
  export function runStatus(root) {
5
5
  const lines = [];
6
+ // Always first: silently reading the wrong project's (or wrong worktree/branch's) memory
7
+ // has happened in practice - findMemoryIntelRoot() walks up from cwd with no built-in
8
+ // visibility into which root it actually resolved.
9
+ lines.push('=== Root ===', root);
6
10
  const mentalModelPath = join(root, 'context', 'currentMentalModel.md');
7
- lines.push('=== Current Mental Model ===');
11
+ lines.push('', '=== Current Mental Model ===');
8
12
  lines.push(existsSync(mentalModelPath) ? readFileSync(mentalModelPath, 'utf-8').trim() : '(none)');
9
13
  lines.push('', '=== Memory Index ===');
10
14
  const index = readIndex(join(root, 'memory-index.json'));
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join, posix, dirname } from 'node:path';
3
- import { decodeToonTable } from '../core/toon.js';
3
+ import { decodePlanRows } from '../core/toon.js';
4
4
  import { applySectionUpdate, isNearDuplicate, getSectionContent } from '../core/sectionWriter.js';
5
5
  import { assertSafePath } from '../core/pathSafety.js';
6
6
  import { upsertIndexEntry } from '../core/memoryIndex.js';
@@ -20,7 +20,7 @@ export async function runUpdate(root, planText) {
20
20
  catch {
21
21
  // Dashboard visibility is best-effort — never let it break `update`.
22
22
  }
23
- const rows = decodeToonTable(planText);
23
+ const rows = decodePlanRows(planText);
24
24
  return withLock(join(root, '.lock'), () => {
25
25
  // Phase 1: validate every entry against current disk state, compute the writes, write nothing yet.
26
26
  const writes = [];
package/dist/core/toon.js CHANGED
@@ -66,7 +66,10 @@ function parseCsvRows(text) {
66
66
  }
67
67
  }
68
68
  if (inQuotes) {
69
- throw new Error('Malformed TOON table: unterminated quoted field.');
69
+ // rows.length is exactly how many COMPLETE rows were parsed before the failure - the
70
+ // failing row is the next one, at that same 0-indexed position (matching how a
71
+ // field-count-mismatch error below reports rowIndex).
72
+ throw new Error(`Malformed TOON table: unterminated quoted field in row ${rows.length}.`);
70
73
  }
71
74
  if (rowStarted)
72
75
  endRow();
@@ -116,3 +119,63 @@ export function decodeToonTable(text) {
116
119
  return row;
117
120
  });
118
121
  }
122
+ const REQUIRED_PLAN_ROW_FIELDS = ['file', 'action', 'section', 'content', 'reason'];
123
+ const OPTIONAL_PLAN_ROW_FIELDS = ['kind'];
124
+ function decodeJsonPlan(text) {
125
+ let parsed;
126
+ try {
127
+ parsed = JSON.parse(text);
128
+ }
129
+ catch (err) {
130
+ const message = err instanceof Error ? err.message : String(err);
131
+ throw new Error(`Malformed JSON plan: ${message}`);
132
+ }
133
+ if (!Array.isArray(parsed)) {
134
+ throw new Error('Malformed JSON plan: expected an array of rows.');
135
+ }
136
+ return parsed.map((row, rowIndex) => {
137
+ if (row === null || typeof row !== 'object' || Array.isArray(row)) {
138
+ throw new Error(`Malformed JSON plan: row ${rowIndex} must be an object.`);
139
+ }
140
+ const record = row;
141
+ for (const field of REQUIRED_PLAN_ROW_FIELDS) {
142
+ if (!(field in record)) {
143
+ throw new Error(`Malformed JSON plan: row ${rowIndex} is missing required field "${field}".`);
144
+ }
145
+ }
146
+ const result = {};
147
+ for (const field of [...REQUIRED_PLAN_ROW_FIELDS, ...OPTIONAL_PLAN_ROW_FIELDS]) {
148
+ if (!(field in record))
149
+ continue;
150
+ const value = record[field];
151
+ if (typeof value !== 'string') {
152
+ throw new Error(`Malformed JSON plan: row ${rowIndex}'s "${field}" must be a string.`);
153
+ }
154
+ result[field] = value;
155
+ }
156
+ return result;
157
+ });
158
+ }
159
+ /**
160
+ * Accepts either format update() itself needs to parse: TOON text (the
161
+ * default, more compact for a human/agent to scan), or a JSON array of the
162
+ * same row shape - auto-detected by a leading '[' after trimming, since a
163
+ * real TOON header always starts with the literal 'items['; this can never
164
+ * misfire on a genuinely malformed TOON table. JSON exists specifically so
165
+ * an agent that would rather JSON.stringify a plan than hand-author TOON's
166
+ * own quoting rules (double an internal '"' as '""', not backslash-escape
167
+ * it - a real, repeated mistake in practice, not hypothetical) never has to
168
+ * risk getting that escaping wrong at all. decodeToonTable itself is
169
+ * unchanged and still exported directly for anyone who wants TOON
170
+ * specifically.
171
+ */
172
+ export function decodePlanRows(text) {
173
+ const trimmed = text.trimStart();
174
+ // A real TOON header never starts with '[' or '{' either (always the literal
175
+ // 'items[') - catching '{' too means a plan mistakenly wrapped as a single
176
+ // object instead of an array gets the clear "expected an array" error below,
177
+ // not an unrelated, confusing TOON header error.
178
+ if (trimmed.startsWith('[') || trimmed.startsWith('{'))
179
+ return decodeJsonPlan(trimmed);
180
+ return decodeToonTable(text);
181
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoryintel",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Persistent, cross-session project memory for AI coding agents.",
5
5
  "type": "module",
6
6
  "bin": {