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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli.js +36 -9
- package/dist/commands/init.js +30 -0
- package/dist/commands/load.js +14 -2
- package/dist/commands/status.js +5 -1
- package/dist/commands/update.js +2 -2
- package/dist/core/toon.js +64 -1
- package/package.json +1 -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.1.
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
package/dist/commands/load.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -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'));
|
package/dist/commands/update.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join, posix, dirname } from 'node:path';
|
|
3
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|