memoryintel 1.0.0 → 1.0.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.0"
9
+ "version": "1.0.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.0"
16
+ "version": "1.0.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.0",
4
+ "version": "1.0.1",
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),
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
+ }
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoryintel",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Persistent, cross-session project memory for AI coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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