memoryintel 1.0.0 → 1.0.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/README.md +31 -0
- package/dist/adapters/genericPointer.js +13 -4
- package/dist/cli.js +28 -0
- package/dist/commands/import.js +76 -0
- package/dist/commands/init.js +38 -0
- package/dist/commands/scan.js +28 -0
- package/dist/core/repoScan.js +250 -0
- package/package.json +1 -1
- package/skills/memoryintel/SKILL.md +4 -0
|
@@ -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.0.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.0.
|
|
16
|
+
"version": "1.0.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.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Adeesh Sharma",
|
|
7
7
|
"url": "https://github.com/adeeshsharma"
|
package/README.md
CHANGED
|
@@ -120,6 +120,37 @@ when something meaningful changes — never a changelog, always a maintained und
|
|
|
120
120
|
project as it currently is. See `.memoryintel/context/decisions.md` in this very repository for
|
|
121
121
|
the specific design decisions behind that, with rationale.
|
|
122
122
|
|
|
123
|
+
## Existing projects (not greenfield)
|
|
124
|
+
|
|
125
|
+
`memoryintel init` scaffolds empty files — fine for a brand-new project, wasteful for one that
|
|
126
|
+
already has real history. Two commands help, and most existing projects want both, in order:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
memoryintel scan # read-only: stack + top-level layout, nothing more
|
|
130
|
+
memoryintel import # write: pulls every real doc in the repo into the matching section
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Neither command tries to reverse-engineer architecture from code — no import-graph analysis, no
|
|
134
|
+
git-history mining, no keyword extraction. That's a real judgment task, and this project already
|
|
135
|
+
has a mechanism for judgment: the agent's own accumulated `update` calls as it actually works in
|
|
136
|
+
the repo, exactly like it already works for a greenfield project. These two commands exist only
|
|
137
|
+
to stop session one from flailing, not to front-load understanding they can't honestly derive.
|
|
138
|
+
|
|
139
|
+
**`scan`** never writes anything — it prints detected stack (`package.json`/`pyproject.toml`/
|
|
140
|
+
`go.mod`/`Cargo.toml`: dependencies, scripts, entry points) and a one-level-deep directory
|
|
141
|
+
listing. That's it. Enough to answer "how do I even run this" without reading the tree cold.
|
|
142
|
+
|
|
143
|
+
**`import`** walks the whole repo for real documentation — any `.md`/`.html` file, anywhere, not
|
|
144
|
+
just a fixed list of known filenames like `memory-bank/`'s convention. An HTML file only counts
|
|
145
|
+
if it's an actual document (real prose, not a bundled SPA shell — index.html with a mount div and
|
|
146
|
+
a script tag doesn't count). Each document's content is copied verbatim into a `.memoryintel/`
|
|
147
|
+
section chosen by matching keywords in its filename/title (`architecture.md` → `technical/
|
|
148
|
+
architecture.md`, `product-notes.md` → `business/productContext.md`, anything unrecognized still
|
|
149
|
+
lands in `context/projectBrief.md` rather than being silently dropped). Purely mechanical — no
|
|
150
|
+
summarizing, splitting, or interpreting, and memoryintel's own pointer-file boilerplate (in
|
|
151
|
+
`AGENTS.md`/`GEMINI.md`) is filtered out so it's never mistaken for real project content. Safe to
|
|
152
|
+
re-run; already-imported content is skipped, not duplicated.
|
|
153
|
+
|
|
123
154
|
## Benchmarks: with vs. without
|
|
124
155
|
|
|
125
156
|
Measured on a real second project ([distilled-docs](https://github.com/adeeshsharma/distilled-docs),
|
|
@@ -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)) {
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,8 @@ import { runUpdate } from './commands/update.js';
|
|
|
7
7
|
import { runLoad } from './commands/load.js';
|
|
8
8
|
import { runStatus } from './commands/status.js';
|
|
9
9
|
import { runInit } from './commands/init.js';
|
|
10
|
+
import { runImport } from './commands/import.js';
|
|
11
|
+
import { runScan } from './commands/scan.js';
|
|
10
12
|
import { runCheckStop } from './adapters/claudeCode.js';
|
|
11
13
|
import { runDashboardEnable, runDashboardDisable } from './commands/dashboardToggle.js';
|
|
12
14
|
import { runDaemonStart } from './commands/daemonStart.js';
|
|
@@ -14,6 +16,10 @@ export const USAGE = `Usage: memoryintel <command> [options]
|
|
|
14
16
|
|
|
15
17
|
Commands:
|
|
16
18
|
init [path] Initialize .memoryintel/ in the current or given directory
|
|
19
|
+
scan [path] Print a quick, no-LLM digest of an existing codebase's stack and
|
|
20
|
+
top-level layout - orientation only, not architecture
|
|
21
|
+
import [path] Pull every real .md/.html document in the repo (not just
|
|
22
|
+
memory-bank/-style files) into the matching .memoryintel/ section
|
|
17
23
|
load [--domain <d>] Print resolved memory context to stdout
|
|
18
24
|
update <plan.toon|-> Apply an update-plan (file path, or - for stdin)
|
|
19
25
|
status Print a human-readable summary of current memory state
|
|
@@ -35,6 +41,10 @@ export function dispatch(argv) {
|
|
|
35
41
|
runInit(target);
|
|
36
42
|
return { exitCode: 0, stdout: `Initialized Memory Intel in ${join(target, '.memoryintel')}\n`, stderr: '' };
|
|
37
43
|
}
|
|
44
|
+
case 'scan': {
|
|
45
|
+
const target = argv[1] ? join(process.cwd(), argv[1]) : process.cwd();
|
|
46
|
+
return { exitCode: 0, stdout: runScan(target), stderr: '' };
|
|
47
|
+
}
|
|
38
48
|
case 'load': {
|
|
39
49
|
const domainFlagIndex = argv.indexOf('--domain');
|
|
40
50
|
const domain = domainFlagIndex !== -1 ? argv[domainFlagIndex + 1] : undefined;
|
|
@@ -90,6 +100,24 @@ async function main() {
|
|
|
90
100
|
// listening HTTP server, until `memoryintel dashboard disable` sends it SIGTERM.
|
|
91
101
|
return;
|
|
92
102
|
}
|
|
103
|
+
if (command === 'import') {
|
|
104
|
+
const root = findMemoryIntelRoot(process.cwd());
|
|
105
|
+
if (!root) {
|
|
106
|
+
process.stderr.write('No .memoryintel/ found. Run `memoryintel init` first.\n');
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const targetDir = argv[1] ? join(process.cwd(), argv[1]) : process.cwd();
|
|
111
|
+
const result = await runImport(root, targetDir);
|
|
112
|
+
if (result.sourcesFound.length === 0) {
|
|
113
|
+
process.stdout.write('No brownfield sources found (looked for memory-bank/, ARCHITECTURE.md, README.md).\n');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
process.stdout.write(`Sources found: ${result.sourcesFound.join(', ')}\nApplied: ${result.applied.join(', ') || '(none)'}\nSkipped: ${result.skipped.join(', ') || '(none)'}\n`);
|
|
117
|
+
}
|
|
118
|
+
process.exitCode = 0;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
93
121
|
if (command === 'update') {
|
|
94
122
|
const root = findMemoryIntelRoot(process.cwd());
|
|
95
123
|
if (!root) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { relative, sep } from 'node:path';
|
|
2
|
+
import { runUpdate } from './update.js';
|
|
3
|
+
import { encodeToonTable } from '../core/toon.js';
|
|
4
|
+
import { walkFiles, findDocuments } from '../core/repoScan.js';
|
|
5
|
+
// relative() returns backslash-separated paths on Windows. These labels get baked into
|
|
6
|
+
// permanent memory content (the `reason` field, the annotate() header) alongside every other
|
|
7
|
+
// file reference in this project, which is always forward-slash (e.g. `technical/architecture.md`)
|
|
8
|
+
// - a Windows-only backslash would be a visible, permanent inconsistency in stored memory, not
|
|
9
|
+
// just a display quirk. Same class of bug already hit and fixed elsewhere in this codebase
|
|
10
|
+
// (assertSafePath, the compression git-clean check) - normalize once, right at the source.
|
|
11
|
+
function toPosixRelative(targetDir, absPath) {
|
|
12
|
+
return relative(targetDir, absPath).split(sep).join('/');
|
|
13
|
+
}
|
|
14
|
+
// Ordered, first-match-wins keyword -> target mapping. Deliberately just keyword matching on a
|
|
15
|
+
// document's filename + extracted title, not any real understanding of its content - the general
|
|
16
|
+
// replacement for hardcoding a table of known filenames (memory-bank's convention happens to
|
|
17
|
+
// fall out of this for free: "productContext.md" matches "product", "systemPatterns.md" matches
|
|
18
|
+
// "pattern", with no special-casing needed), generalized to any document anywhere in the repo,
|
|
19
|
+
// under any naming convention.
|
|
20
|
+
const ROUTES = [
|
|
21
|
+
{ keywords: ['architecture', 'design'], file: 'technical/architecture.md', section: 'Overview' },
|
|
22
|
+
{ keywords: ['pattern'], file: 'technical/patterns.md', section: 'Design Patterns' },
|
|
23
|
+
{ keywords: ['tech', 'stack'], file: 'technical/techContext.md', section: 'Stack' },
|
|
24
|
+
{ keywords: ['integration'], file: 'technical/integrations.md', section: 'External Services' },
|
|
25
|
+
{ keywords: ['infra', 'deploy'], file: 'technical/infrastructure.md', section: 'Deployment' },
|
|
26
|
+
{ keywords: ['product'], file: 'business/productContext.md', section: 'Product Overview' },
|
|
27
|
+
{ keywords: ['roadmap'], file: 'business/roadmap.md', section: 'Now' },
|
|
28
|
+
{ keywords: ['stakeholder', 'team'], file: 'business/stakeholders.md', section: 'Team' },
|
|
29
|
+
{ keywords: ['market'], file: 'business/marketContext.md', section: 'Market Overview' },
|
|
30
|
+
{ keywords: ['progress', 'status'], file: 'context/progress.md', section: 'Status' },
|
|
31
|
+
{ keywords: ['active', 'focus', 'current'], file: 'context/activeContext.md', section: 'Current Focus' },
|
|
32
|
+
{ keywords: ['decision'], file: 'context/decisions.md', section: 'Decisions Log' },
|
|
33
|
+
{ keywords: ['learn'], file: 'context/learnings.md', section: 'Learnings' },
|
|
34
|
+
{ keywords: ['objective', 'goal'], file: 'context/objectives.md', section: 'Objectives' },
|
|
35
|
+
{ keywords: ['hypothes'], file: 'research/hypotheses.md', section: 'Open Hypotheses' },
|
|
36
|
+
{ keywords: ['finding', 'research'], file: 'research/findings.md', section: 'Key Findings' },
|
|
37
|
+
{ keywords: ['reference'], file: 'research/references.md', section: 'Sources' },
|
|
38
|
+
{ keywords: ['readme', 'brief', 'overview'], file: 'context/projectBrief.md', section: 'Overview' }
|
|
39
|
+
];
|
|
40
|
+
// Anything matching no keyword still lands somewhere a human/agent will see it, rather than
|
|
41
|
+
// being silently dropped for not fitting a known bucket.
|
|
42
|
+
const DEFAULT_ROUTE = { file: 'context/projectBrief.md', section: 'Overview' };
|
|
43
|
+
function route(doc, targetDir) {
|
|
44
|
+
const haystack = `${toPosixRelative(targetDir, doc.path)} ${doc.title}`.toLowerCase();
|
|
45
|
+
for (const r of ROUTES) {
|
|
46
|
+
if (r.keywords.some((k) => haystack.includes(k)))
|
|
47
|
+
return { file: r.file, section: r.section };
|
|
48
|
+
}
|
|
49
|
+
return DEFAULT_ROUTE;
|
|
50
|
+
}
|
|
51
|
+
function annotate(content, sourceLabel) {
|
|
52
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
53
|
+
return `_Imported verbatim from \`${sourceLabel}\` on ${today} — not yet re-filed into per-section structure; treat as raw source material for the next real update._\n\n${content.trim()}`;
|
|
54
|
+
}
|
|
55
|
+
export async function runImport(root, targetDir) {
|
|
56
|
+
const files = walkFiles(targetDir);
|
|
57
|
+
const docs = findDocuments(targetDir, files);
|
|
58
|
+
if (docs.length === 0) {
|
|
59
|
+
return { applied: [], skipped: [], sourcesFound: [] };
|
|
60
|
+
}
|
|
61
|
+
const sourcesFound = docs.map((d) => toPosixRelative(targetDir, d.path));
|
|
62
|
+
const candidates = docs.map((doc) => {
|
|
63
|
+
const target = route(doc, targetDir);
|
|
64
|
+
const label = toPosixRelative(targetDir, doc.path);
|
|
65
|
+
return {
|
|
66
|
+
file: target.file,
|
|
67
|
+
action: 'append',
|
|
68
|
+
section: target.section,
|
|
69
|
+
content: annotate(doc.content, label),
|
|
70
|
+
reason: `Brownfield import from ${label}`
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
const planText = encodeToonTable(candidates);
|
|
74
|
+
const { applied, skipped } = await runUpdate(root, planText);
|
|
75
|
+
return { applied, skipped, sourcesFound };
|
|
76
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -6,6 +6,26 @@ const INSTRUCTIONS_TEMPLATE = `# Memory Intel Instructions
|
|
|
6
6
|
|
|
7
7
|
This project uses Memory Intel. Read this file at the start of every session.
|
|
8
8
|
|
|
9
|
+
## First session on an existing project
|
|
10
|
+
If \`.memoryintel/\` was just initialized on a project that already has real history (not a fresh
|
|
11
|
+
scaffold), run both before anything else - neither tries to reverse-engineer architecture, so
|
|
12
|
+
neither is a substitute for the other:
|
|
13
|
+
|
|
14
|
+
1. \`memoryintel import\` walks the whole repo for real documentation (any \`.md\`/\`.html\` file,
|
|
15
|
+
not a fixed list of known filenames) and mechanically copies each one's content verbatim into
|
|
16
|
+
the \`.memoryintel/\` section its filename/title best matches. Deterministic, no judgment. Its
|
|
17
|
+
output is raw and unfiled by design; treat it as source material to read and properly re-file
|
|
18
|
+
yourself, not as finished memory.
|
|
19
|
+
2. \`memoryintel scan\` never writes anything - it prints detected stack and a one-level-deep
|
|
20
|
+
directory listing, nothing more. Enough to answer "how do I run this", not an attempt at
|
|
21
|
+
understanding the codebase's architecture.
|
|
22
|
+
|
|
23
|
+
Real understanding - architecture, patterns, why things are built the way they are - is not
|
|
24
|
+
something either command can honestly derive. It builds the same way it already does on a
|
|
25
|
+
greenfield project: through your own judgment as you actually work here, one real \`update\` at a
|
|
26
|
+
time. Both commands are safe to run more than once - \`import\`'s already-imported content is
|
|
27
|
+
skipped, not duplicated, and \`scan\` never writes anything at all.
|
|
28
|
+
|
|
9
29
|
## Session start
|
|
10
30
|
Run \`memoryintel load [--domain technical|business|research]\` and treat its output as project context.
|
|
11
31
|
Its manifest reports each loaded file's \`lines\`, \`ceiling\`, and \`status\` (\`over\`/\`under\`) — see
|
|
@@ -38,6 +58,24 @@ on a real project: a "known limitation, deliberately left unfixed" entry was sti
|
|
|
38
58
|
\`currentMentalModel.md\`, but never reconciled against the older, now-wrong decisions entry right
|
|
39
59
|
next to it.
|
|
40
60
|
|
|
61
|
+
## Committing \`.memoryintel/\` itself
|
|
62
|
+
\`.memoryintel/\` is a normal, git-committed part of the project — \`memoryintel update\` only writes
|
|
63
|
+
files to disk, it never runs git for you. After a successful \`update\`, commit \`.memoryintel/\`'s
|
|
64
|
+
changes yourself, ideally in the same commit as (or immediately after) whatever code change
|
|
65
|
+
prompted the update, so it travels with the same commit/PR automatically instead of being left
|
|
66
|
+
behind as an uncommitted diff.
|
|
67
|
+
|
|
68
|
+
This matters most in a git worktree, a container, or any other isolated checkout: an uncommitted
|
|
69
|
+
\`.memoryintel/\` change only exists in that one working directory. Merging a feature branch's PR and
|
|
70
|
+
pulling the primary checkout up to date only brings memory updates along if they were actually
|
|
71
|
+
committed first, the same as any other file — there is nothing worktree-specific about the
|
|
72
|
+
mechanism itself, only that a worktree's own uncommitted state is easier to lose track of, since it
|
|
73
|
+
is invisible everywhere else once the session ends and that directory stops being looked at. Found
|
|
74
|
+
on a real project: a full feature built end-to-end in a worktree, with real, meaningful
|
|
75
|
+
architecture changes throughout, reached a merged PR with \`.memoryintel/\` never once updated or
|
|
76
|
+
committed — not because updating was hard, but because nothing in the session ever came back to it
|
|
77
|
+
before the worktree's job was considered done.
|
|
78
|
+
|
|
41
79
|
## Compaction
|
|
42
80
|
A file marked \`status: over\` in \`load\`'s manifest has grown past its configured line ceiling.
|
|
43
81
|
This is a signal, not a command — compact it only when it's a sensible moment to (the same
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { detectStack, listTopLevel } from '../core/repoScan.js';
|
|
2
|
+
// A quick, deterministic, no-LLM digest of an existing codebase's stack and setup - the
|
|
3
|
+
// brownfield equivalent of `load`'s first orientation, nothing more. Deliberately does not try
|
|
4
|
+
// to infer architecture (no import graphs, no git-churn ranking, no keyword extraction): that's
|
|
5
|
+
// real judgment, and this project already has a mechanism for judgment - the agent's own
|
|
6
|
+
// accumulated `update` calls as it actually works in the repo, same as it already works for a
|
|
7
|
+
// greenfield project. scan's only job is to stop session one from flailing on "how do I even run
|
|
8
|
+
// this", not to front-load understanding a scan can't actually derive honestly.
|
|
9
|
+
export function runScan(targetDir) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
const stack = detectStack(targetDir);
|
|
12
|
+
lines.push('=== Detected Stack ===');
|
|
13
|
+
lines.push(stack.manifests.length > 0 ? `Manifests: ${stack.manifests.join(', ')}` : '(no recognized manifest found)');
|
|
14
|
+
if (stack.dependencies.length > 0) {
|
|
15
|
+
const shown = stack.dependencies.slice(0, 30);
|
|
16
|
+
const suffix = stack.dependencies.length > 30 ? ` (+${stack.dependencies.length - 30} more)` : '';
|
|
17
|
+
lines.push(`Dependencies: ${shown.join(', ')}${suffix}`);
|
|
18
|
+
}
|
|
19
|
+
if (Object.keys(stack.scripts).length > 0) {
|
|
20
|
+
lines.push(`Scripts: ${Object.entries(stack.scripts).map(([k, v]) => `${k}="${v}"`).join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
if (stack.entryPoints.length > 0)
|
|
23
|
+
lines.push(`Entry points: ${stack.entryPoints.join(', ')}`);
|
|
24
|
+
lines.push('', '=== Top-Level Layout ===');
|
|
25
|
+
const topLevel = listTopLevel(targetDir);
|
|
26
|
+
lines.push(topLevel.length > 0 ? topLevel.join(', ') : '(empty directory)');
|
|
27
|
+
return lines.join('\n') + '\n';
|
|
28
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, extname } from 'node:path';
|
|
3
|
+
// Common vendor/build/cache directories across the ecosystems this scans (JS/TS, Python, Go,
|
|
4
|
+
// Rust). Not full .gitignore parsing - a real ignore-file parser is its own project, and this
|
|
5
|
+
// hardcoded list already keeps a scan quick on the repos it's meant for. Missing a project's own
|
|
6
|
+
// unusual output dir just means a few extra files get walked, not a correctness bug.
|
|
7
|
+
// `.memoryintel` is excluded deliberately, not an oversight: it's this tool's own bookkeeping
|
|
8
|
+
// (constantly-churning JSON/event logs, and markdown that's *output*, not source material) -
|
|
9
|
+
// including it here would make a scan review its own scratch state instead of the actual project.
|
|
10
|
+
export const IGNORED_DIRS = new Set([
|
|
11
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt', '.svelte-kit',
|
|
12
|
+
'target', 'vendor', '__pycache__', '.venv', 'venv', 'env', 'coverage', '.cache',
|
|
13
|
+
'.turbo', '.parcel-cache', '.pytest_cache', '.mypy_cache', '.idea', '.vscode', '.memoryintel'
|
|
14
|
+
]);
|
|
15
|
+
// Safety cap so a scan stays "quick" even pointed at an enormous or pathologically deep tree.
|
|
16
|
+
const MAX_FILES = 20000;
|
|
17
|
+
// Every file under targetDir, skipping IGNORED_DIRS and symlinks (symlinks risk cycles and can
|
|
18
|
+
// point outside the repo entirely - neither is a case worth handling for a quick scan). Returns
|
|
19
|
+
// absolute paths.
|
|
20
|
+
export function walkFiles(targetDir) {
|
|
21
|
+
const results = [];
|
|
22
|
+
function visit(dir) {
|
|
23
|
+
if (results.length >= MAX_FILES)
|
|
24
|
+
return;
|
|
25
|
+
let entries;
|
|
26
|
+
try {
|
|
27
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (results.length >= MAX_FILES)
|
|
34
|
+
return;
|
|
35
|
+
if (entry.isSymbolicLink())
|
|
36
|
+
continue;
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
if (IGNORED_DIRS.has(entry.name))
|
|
39
|
+
continue;
|
|
40
|
+
visit(join(dir, entry.name));
|
|
41
|
+
}
|
|
42
|
+
else if (entry.isFile()) {
|
|
43
|
+
results.push(join(dir, entry.name));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
visit(targetDir);
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
// A single level deep, not recursive - "repo/project setup" means "what's here", not a full
|
|
51
|
+
// tree. Directories are listed with a trailing slash so the shape is legible at a glance.
|
|
52
|
+
export function listTopLevel(targetDir) {
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = readdirSync(targetDir, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return entries
|
|
61
|
+
.filter((e) => !(e.isDirectory() && IGNORED_DIRS.has(e.name)))
|
|
62
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
|
|
63
|
+
.sort();
|
|
64
|
+
}
|
|
65
|
+
function readJsonSafe(path) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Extracts every quoted string or `key =`/`key = "` entry between a section header and the next
|
|
74
|
+
// `[` - good enough to list dependency names out of TOML without pulling in a TOML parser
|
|
75
|
+
// dependency this project has none of today. Not a real TOML parser: won't handle inline
|
|
76
|
+
// tables or multi-line arrays split across many lines with comments in between.
|
|
77
|
+
function extractTomlSectionKeys(text, sectionHeader) {
|
|
78
|
+
const startIdx = text.indexOf(sectionHeader);
|
|
79
|
+
if (startIdx === -1)
|
|
80
|
+
return [];
|
|
81
|
+
const afterHeader = text.slice(startIdx + sectionHeader.length);
|
|
82
|
+
const nextSectionIdx = afterHeader.search(/\n\[/);
|
|
83
|
+
const body = nextSectionIdx === -1 ? afterHeader : afterHeader.slice(0, nextSectionIdx);
|
|
84
|
+
const keys = [];
|
|
85
|
+
for (const line of body.split('\n')) {
|
|
86
|
+
const match = /^\s*"?([\w.-]+)"?\s*=/.exec(line);
|
|
87
|
+
if (match)
|
|
88
|
+
keys.push(match[1]);
|
|
89
|
+
}
|
|
90
|
+
return keys;
|
|
91
|
+
}
|
|
92
|
+
function extractQuotedStrings(text) {
|
|
93
|
+
const matches = text.matchAll(/["']([^"'\s]+)["']/g);
|
|
94
|
+
return [...matches].map((m) => m[1]);
|
|
95
|
+
}
|
|
96
|
+
export function detectStack(targetDir) {
|
|
97
|
+
const manifests = [];
|
|
98
|
+
const dependencies = new Set();
|
|
99
|
+
const scripts = {};
|
|
100
|
+
const entryPoints = [];
|
|
101
|
+
const pkgPath = join(targetDir, 'package.json');
|
|
102
|
+
if (existsSync(pkgPath)) {
|
|
103
|
+
const pkg = readJsonSafe(pkgPath);
|
|
104
|
+
if (pkg) {
|
|
105
|
+
manifests.push('package.json');
|
|
106
|
+
for (const dep of Object.keys(pkg.dependencies ?? {}))
|
|
107
|
+
dependencies.add(dep);
|
|
108
|
+
for (const dep of Object.keys(pkg.devDependencies ?? {}))
|
|
109
|
+
dependencies.add(dep);
|
|
110
|
+
Object.assign(scripts, pkg.scripts ?? {});
|
|
111
|
+
if (typeof pkg.main === 'string')
|
|
112
|
+
entryPoints.push(pkg.main);
|
|
113
|
+
if (typeof pkg.bin === 'string')
|
|
114
|
+
entryPoints.push(pkg.bin);
|
|
115
|
+
else if (pkg.bin && typeof pkg.bin === 'object')
|
|
116
|
+
entryPoints.push(...Object.values(pkg.bin));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const reqPath = join(targetDir, 'requirements.txt');
|
|
120
|
+
if (existsSync(reqPath)) {
|
|
121
|
+
manifests.push('requirements.txt');
|
|
122
|
+
for (const rawLine of readFileSync(reqPath, 'utf-8').split('\n')) {
|
|
123
|
+
const line = rawLine.split('#')[0].trim();
|
|
124
|
+
if (!line)
|
|
125
|
+
continue;
|
|
126
|
+
const name = line.split(/[=<>~!;\[]/)[0].trim();
|
|
127
|
+
if (name)
|
|
128
|
+
dependencies.add(name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const pyprojectPath = join(targetDir, 'pyproject.toml');
|
|
132
|
+
if (existsSync(pyprojectPath)) {
|
|
133
|
+
manifests.push('pyproject.toml');
|
|
134
|
+
const text = readFileSync(pyprojectPath, 'utf-8');
|
|
135
|
+
const projectDepsMatch = /dependencies\s*=\s*\[([\s\S]*?)\]/.exec(text);
|
|
136
|
+
if (projectDepsMatch) {
|
|
137
|
+
for (const dep of extractQuotedStrings(projectDepsMatch[1])) {
|
|
138
|
+
dependencies.add(dep.split(/[=<>~!\s]/)[0]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const dep of extractTomlSectionKeys(text, '[tool.poetry.dependencies]')) {
|
|
142
|
+
if (dep !== 'python')
|
|
143
|
+
dependencies.add(dep);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const goModPath = join(targetDir, 'go.mod');
|
|
147
|
+
if (existsSync(goModPath)) {
|
|
148
|
+
manifests.push('go.mod');
|
|
149
|
+
const text = readFileSync(goModPath, 'utf-8');
|
|
150
|
+
for (const match of text.matchAll(/^\s*([\w.\-/]+\.[\w.\-/]+)\s+v[\d.]/gm))
|
|
151
|
+
dependencies.add(match[1]);
|
|
152
|
+
}
|
|
153
|
+
const cargoPath = join(targetDir, 'Cargo.toml');
|
|
154
|
+
if (existsSync(cargoPath)) {
|
|
155
|
+
manifests.push('Cargo.toml');
|
|
156
|
+
const text = readFileSync(cargoPath, 'utf-8');
|
|
157
|
+
for (const dep of extractTomlSectionKeys(text, '[dependencies]'))
|
|
158
|
+
dependencies.add(dep);
|
|
159
|
+
}
|
|
160
|
+
return { manifests, dependencies: [...dependencies], scripts, entryPoints };
|
|
161
|
+
}
|
|
162
|
+
function extractMarkdownTitle(text) {
|
|
163
|
+
const h1 = /^#\s+(.+)$/m.exec(text);
|
|
164
|
+
if (h1)
|
|
165
|
+
return h1[1].trim();
|
|
166
|
+
const firstLine = text.split('\n').map((l) => l.trim()).find((l) => l.length > 0);
|
|
167
|
+
return firstLine ? firstLine.replace(/^#+\s*/, '').slice(0, 80) : '(untitled)';
|
|
168
|
+
}
|
|
169
|
+
function extractHtmlTitle(text) {
|
|
170
|
+
const title = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(text);
|
|
171
|
+
if (title)
|
|
172
|
+
return title[1].replace(/<[^>]+>/g, '').trim().slice(0, 80);
|
|
173
|
+
const h1 = /<h1[^>]*>([\s\S]*?)<\/h1>/i.exec(text);
|
|
174
|
+
if (h1)
|
|
175
|
+
return h1[1].replace(/<[^>]+>/g, '').trim().slice(0, 80);
|
|
176
|
+
return '(untitled)';
|
|
177
|
+
}
|
|
178
|
+
// SPA shells (a React/Vue/etc. app's index.html) are markup with almost no prose: a mount div
|
|
179
|
+
// and a script tag. A real document is prose-heavy. Neither signal alone is reliable (a tiny
|
|
180
|
+
// real doc could dip under the text threshold; a doc-heavy landing page could avoid the mount-div
|
|
181
|
+
// markers) but together they cover the common cases without needing to know any framework by name.
|
|
182
|
+
const SPA_SHELL_MARKERS = /id=["'](root|app|__next|__nuxt)["']/i;
|
|
183
|
+
const MIN_DOCUMENT_TEXT_LENGTH = 150;
|
|
184
|
+
function visibleText(html) {
|
|
185
|
+
return html
|
|
186
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
187
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
188
|
+
.replace(/<[^>]+>/g, ' ')
|
|
189
|
+
.replace(/\s+/g, ' ')
|
|
190
|
+
.trim();
|
|
191
|
+
}
|
|
192
|
+
export function isDocumentHtml(html) {
|
|
193
|
+
if (SPA_SHELL_MARKERS.test(html))
|
|
194
|
+
return false;
|
|
195
|
+
return visibleText(html).length >= MIN_DOCUMENT_TEXT_LENGTH;
|
|
196
|
+
}
|
|
197
|
+
// True if anything survives once every heading line is stripped out - a file that's only
|
|
198
|
+
// headings (a bare `init` starter section, or a title-only stub) has no actual information.
|
|
199
|
+
function hasMarkdownBody(content) {
|
|
200
|
+
return content
|
|
201
|
+
.split('\n')
|
|
202
|
+
.filter((line) => !/^#+\s/.test(line.trim()))
|
|
203
|
+
.some((line) => line.trim().length > 0);
|
|
204
|
+
}
|
|
205
|
+
const DOC_EXTENSIONS = new Set(['.md', '.markdown', '.html', '.htm']);
|
|
206
|
+
// The genericPointer adapter (src/adapters/genericPointer.ts) upserts this managed block into
|
|
207
|
+
// AGENTS.md/GEMINI.md - it's memoryintel's own boilerplate, not project documentation. A file
|
|
208
|
+
// that already had real content keeps it and the block is just noise mixed in; a fresh stub file
|
|
209
|
+
// created by `init` with nothing but this block has zero real information and must not be
|
|
210
|
+
// imported as if it described the project (worst case: a brand-new stub's entire content is
|
|
211
|
+
// "this project uses Memory Intel, run `memoryintel load`", which would land in projectBrief.md
|
|
212
|
+
// looking like the project's own description).
|
|
213
|
+
const MANAGED_BLOCK_PATTERN = /<!-- memoryintel:managed:start -->[\s\S]*?<!-- memoryintel:managed:end -->/g;
|
|
214
|
+
// Every real document (not app markup, not memoryintel's own managed content) anywhere under
|
|
215
|
+
// targetDir - the general replacement for hardcoding a table of known filenames like
|
|
216
|
+
// memory-bank's convention. A markdown file is always a document (there's no equivalent "app
|
|
217
|
+
// shell" concept for markdown); an HTML file has to pass isDocumentHtml first.
|
|
218
|
+
export function findDocuments(targetDir, files) {
|
|
219
|
+
const results = [];
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
const ext = extname(file).toLowerCase();
|
|
222
|
+
if (!DOC_EXTENSIONS.has(ext))
|
|
223
|
+
continue;
|
|
224
|
+
let content;
|
|
225
|
+
try {
|
|
226
|
+
content = readFileSync(file, 'utf-8');
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
content = content.replace(MANAGED_BLOCK_PATTERN, '').trim();
|
|
232
|
+
if (!content)
|
|
233
|
+
continue;
|
|
234
|
+
if (ext === '.html' || ext === '.htm') {
|
|
235
|
+
if (!isDocumentHtml(content))
|
|
236
|
+
continue;
|
|
237
|
+
results.push({ path: file, title: extractHtmlTitle(content), content: visibleText(content) });
|
|
238
|
+
}
|
|
239
|
+
else if (!hasMarkdownBody(content)) {
|
|
240
|
+
// A file that's nothing but a heading (a bare `init` starter section, or an AGENTS.md
|
|
241
|
+
// stub reduced to just its "# Project Instructions" title once the managed block above is
|
|
242
|
+
// stripped) has no real information - importing it would be pure noise, not source material.
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
results.push({ path: file, title: extractMarkdownTitle(content), content });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return results;
|
|
250
|
+
}
|
package/package.json
CHANGED
|
@@ -39,6 +39,10 @@ Usage: memoryintel <command> [options]
|
|
|
39
39
|
|
|
40
40
|
Commands:
|
|
41
41
|
init [path] Initialize .memoryintel/ in the current or given directory
|
|
42
|
+
scan [path] Print a quick, no-LLM digest of an existing codebase's stack and
|
|
43
|
+
top-level layout - orientation only, not architecture
|
|
44
|
+
import [path] Pull every real .md/.html document in the repo (not just
|
|
45
|
+
memory-bank/-style files) into the matching .memoryintel/ section
|
|
42
46
|
load [--domain <d>] Print resolved memory context to stdout
|
|
43
47
|
update <plan.toon|-> Apply an update-plan (file path, or - for stdin)
|
|
44
48
|
status Print a human-readable summary of current memory state
|