@vishnuvmx/recall-memory 0.0.0-stage → 0.1.0

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -2
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +451 -0
  5. package/dist/core/agent.d.ts +21 -0
  6. package/dist/core/agent.js +140 -0
  7. package/dist/core/checkpoint.d.ts +42 -0
  8. package/dist/core/checkpoint.js +233 -0
  9. package/dist/core/config.d.ts +5 -0
  10. package/dist/core/config.js +36 -0
  11. package/dist/core/cost.d.ts +19 -0
  12. package/dist/core/cost.js +78 -0
  13. package/dist/core/doctor.d.ts +7 -0
  14. package/dist/core/doctor.js +69 -0
  15. package/dist/core/git.d.ts +27 -0
  16. package/dist/core/git.js +107 -0
  17. package/dist/core/index-builder.d.ts +11 -0
  18. package/dist/core/index-builder.js +84 -0
  19. package/dist/core/init.d.ts +13 -0
  20. package/dist/core/init.js +52 -0
  21. package/dist/core/landmarks.d.ts +19 -0
  22. package/dist/core/landmarks.js +323 -0
  23. package/dist/core/mapper.d.ts +28 -0
  24. package/dist/core/mapper.js +357 -0
  25. package/dist/core/memory.d.ts +44 -0
  26. package/dist/core/memory.js +199 -0
  27. package/dist/core/project.d.ts +12 -0
  28. package/dist/core/project.js +34 -0
  29. package/dist/core/retrieval.d.ts +27 -0
  30. package/dist/core/retrieval.js +180 -0
  31. package/dist/core/seed.d.ts +25 -0
  32. package/dist/core/seed.js +196 -0
  33. package/dist/core/service.d.ts +76 -0
  34. package/dist/core/service.js +228 -0
  35. package/dist/core/where.d.ts +14 -0
  36. package/dist/core/where.js +86 -0
  37. package/dist/index.d.ts +11 -0
  38. package/dist/index.js +11 -0
  39. package/dist/mcp/server.d.ts +65 -0
  40. package/dist/mcp/server.js +121 -0
  41. package/dist/schemas/index.d.ts +68 -0
  42. package/dist/schemas/index.js +43 -0
  43. package/dist/templates/index.d.ts +21 -0
  44. package/dist/templates/index.js +62 -0
  45. package/dist/utils/fs.d.ts +15 -0
  46. package/dist/utils/fs.js +73 -0
  47. package/dist/utils/managed.d.ts +5 -0
  48. package/dist/utils/managed.js +41 -0
  49. package/dist/utils/secrets.d.ts +3 -0
  50. package/dist/utils/secrets.js +23 -0
  51. package/package.json +65 -4
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import { resolveInside } from '../utils/fs.js';
3
+ import { atomicWrite } from '../utils/fs.js';
4
+ import { applyManagedBlock, extractManagedBlock } from '../utils/managed.js';
5
+ import { loadAll } from './memory.js';
6
+ const SECTIONS = [
7
+ ['decision', 'Decisions'],
8
+ ['convention', 'Conventions'],
9
+ ['design_system', 'Design system'],
10
+ ['fact', 'Facts'],
11
+ ['lesson', 'Lessons'],
12
+ ['bugfix', 'Bugfixes'],
13
+ ['failed_approach', 'Failed approaches'],
14
+ ['task', 'Tasks'],
15
+ ];
16
+ const NAME = 'recall:index';
17
+ const esc = (s) => s.replace(/([[\]])/g, '\\$1');
18
+ /** Deterministic: depends only on memory metadata, sorted by id. */
19
+ export function buildIndexBlock(loaded) {
20
+ const out = [];
21
+ const active = loaded.memories.filter((m) => m.status === 'active');
22
+ for (const [type, heading] of SECTIONS) {
23
+ const items = active.filter((m) => m.type === type);
24
+ if (items.length === 0)
25
+ continue;
26
+ out.push(`## ${heading}`, ...items.map((m) => `- [${esc(m.title)}](${m.file})`), '');
27
+ }
28
+ if (loaded.plainDocs.length > 0) {
29
+ out.push('## Rule documents', ...[...loaded.plainDocs].sort().map((f) => `- [${f.replace(/^rules\//, '')}](${f})`), '');
30
+ }
31
+ const proposed = loaded.memories.filter((m) => m.status === 'proposed');
32
+ if (proposed.length > 0) {
33
+ out.push('## Proposed (awaiting `recall approve`)', ...proposed.map((m) => `- [${esc(m.title)}](${m.file}) (${m.id})`), '');
34
+ }
35
+ if (out.length === 0)
36
+ out.push('_No memories yet. Use `recall remember` to add one._', '');
37
+ return out.join('\n').trimEnd();
38
+ }
39
+ export function indexPath(recallDir) {
40
+ return resolveInside(recallDir, 'INDEX.md');
41
+ }
42
+ export function renderIndex(existing, loaded) {
43
+ const base = existing.trim() === '' ? '# Recall Memory Index\n' : existing;
44
+ return applyManagedBlock(base, buildIndexBlock(loaded), NAME);
45
+ }
46
+ /** Rebuild INDEX.md; text outside the managed block is preserved. */
47
+ export function rebuildIndex(recallDir) {
48
+ const loaded = loadAll(recallDir);
49
+ const file = indexPath(recallDir);
50
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
51
+ atomicWrite(file, renderIndex(existing, loaded));
52
+ return loaded;
53
+ }
54
+ export function indexState(recallDir) {
55
+ const file = indexPath(recallDir);
56
+ if (!fs.existsSync(file))
57
+ return 'missing';
58
+ const existing = fs.readFileSync(file, 'utf8');
59
+ const block = extractManagedBlock(existing, NAME);
60
+ if (block === null)
61
+ return 'no_managed_block';
62
+ return block === buildIndexBlock(loadAll(recallDir)).trim() ? 'up_to_date' : 'stale';
63
+ }
64
+ /** Relative link targets in INDEX.md that do not exist. */
65
+ export function brokenIndexLinks(recallDir) {
66
+ const file = indexPath(recallDir);
67
+ if (!fs.existsSync(file))
68
+ return [];
69
+ const text = fs.readFileSync(file, 'utf8');
70
+ const broken = [];
71
+ for (const m of text.matchAll(/\]\(([^)\s]+\.md)\)/g)) {
72
+ const target = m[1];
73
+ if (/^[a-z]+:/i.test(target))
74
+ continue;
75
+ try {
76
+ if (!fs.existsSync(resolveInside(recallDir, target)))
77
+ broken.push(target);
78
+ }
79
+ catch {
80
+ broken.push(target);
81
+ }
82
+ }
83
+ return [...new Set(broken)];
84
+ }
@@ -0,0 +1,13 @@
1
+ import { type Project } from './project.js';
2
+ import { type ProfileTier } from '../schemas/index.js';
3
+ export interface InitReport {
4
+ project: Project;
5
+ created: string[];
6
+ skipped: string[];
7
+ agent: 'created' | 'updated' | 'unchanged' | 'skipped';
8
+ }
9
+ /** Idempotent scaffolding. Never overwrites, never touches Git, never reads source files. */
10
+ export declare function initProject(start: string, opts?: {
11
+ agent?: boolean;
12
+ profile?: ProfileTier;
13
+ }): InitReport;
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { installAgentBlock } from './agent.js';
4
+ import { initialCurrentStateMarkdown } from './checkpoint.js';
5
+ import { defaultConfig, loadConfig, writeConfig, configPath } from './config.js';
6
+ import { rebuildIndex } from './index-builder.js';
7
+ import { generateProjectMap } from './mapper.js';
8
+ import { findProject } from './project.js';
9
+ import { CHANGELOG_MD, MEMORY_MD, RULE_TEMPLATES } from '../templates/index.js';
10
+ import { writeNew } from '../utils/fs.js';
11
+ const DIRS = ['rules', 'decisions', 'lessons', 'tasks', 'sessions', 'logs'];
12
+ /** Idempotent scaffolding. Never overwrites, never touches Git, never reads source files. */
13
+ export function initProject(start, opts = {}) {
14
+ const project = findProject(start);
15
+ const { recallDir, root } = project;
16
+ const created = [];
17
+ const skipped = [];
18
+ const put = (rel, content) => {
19
+ (writeNew(path.join(recallDir, rel), content) ? created : skipped).push(`.recall/${rel}`);
20
+ };
21
+ fs.mkdirSync(recallDir, { recursive: true });
22
+ for (const d of DIRS)
23
+ fs.mkdirSync(path.join(recallDir, d), { recursive: true });
24
+ if (fs.existsSync(configPath(recallDir))) {
25
+ loadConfig(recallDir); // fail loudly on a malformed config rather than overwrite it
26
+ skipped.push('.recall/config.json');
27
+ }
28
+ else {
29
+ const cfg = defaultConfig();
30
+ if (opts.profile)
31
+ cfg.profile = opts.profile;
32
+ writeConfig(recallDir, cfg);
33
+ created.push('.recall/config.json');
34
+ }
35
+ put('MEMORY.md', MEMORY_MD);
36
+ put('.gitignore', '# Local ephemeral agent state\n# PROJECT_MAP.md\n# CURRENT_STATE.md\n# sessions/\n.cache/\n');
37
+ for (const [name, text] of Object.entries(RULE_TEMPLATES))
38
+ put(`rules/${name}`, text);
39
+ put('logs/CHANGELOG.md', CHANGELOG_MD);
40
+ put('CURRENT_STATE.md', initialCurrentStateMarkdown());
41
+ for (const d of ['decisions', 'lessons', 'tasks', 'sessions'])
42
+ put(`${d}/.gitkeep`, '');
43
+ const cfg = loadConfig(recallDir);
44
+ const agent = opts.agent === false ? 'skipped' : installAgentBlock(root, cfg.agentFile);
45
+ const hadMap = fs.existsSync(path.join(recallDir, 'PROJECT_MAP.md'));
46
+ generateProjectMap(root, recallDir, cfg.profile);
47
+ (hadMap ? skipped : created).push('.recall/PROJECT_MAP.md');
48
+ const hadIndex = fs.existsSync(path.join(recallDir, 'INDEX.md'));
49
+ rebuildIndex(recallDir);
50
+ (hadIndex ? skipped : created).push('.recall/INDEX.md');
51
+ return { project: { ...project, initialized: true }, created, skipped, agent };
52
+ }
@@ -0,0 +1,19 @@
1
+ export interface Landmark {
2
+ name: string;
3
+ kind: 'class' | 'method' | 'function' | 'section' | 'interface' | 'type';
4
+ line: number;
5
+ }
6
+ export interface FileLandmarks {
7
+ relativePath: string;
8
+ totalLines: number;
9
+ landmarks: Landmark[];
10
+ }
11
+ /**
12
+ * Extracts key landmark symbols from a source file using fast regex pattern matching.
13
+ */
14
+ export declare function extractLandmarks(content: string, ext: string): Landmark[];
15
+ /**
16
+ * Scans the project for files (>= minLines) and extracts landmark locations with MTime caching.
17
+ */
18
+ export declare function scanLandmarks(root: string, minLines?: number, maxFiles?: number, maxSymbols?: number, recallDir?: string): FileLandmarks[];
19
+ export declare function formatLandmarksMarkdown(files: FileLandmarks[]): string;
@@ -0,0 +1,323 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { toPosix } from '../utils/fs.js';
4
+ import { DEFAULT_IGNORED_DIRS, createGitignoreFilter } from './mapper.js';
5
+ const SUPPORTED_EXTS = new Set([
6
+ '.dart',
7
+ '.kt',
8
+ '.java',
9
+ '.swift',
10
+ '.ts',
11
+ '.tsx',
12
+ '.js',
13
+ '.jsx',
14
+ '.py',
15
+ '.rs',
16
+ '.go',
17
+ '.cs',
18
+ '.c',
19
+ '.cpp',
20
+ '.h',
21
+ '.hpp',
22
+ '.php',
23
+ '.rb',
24
+ ]);
25
+ /**
26
+ * Extracts key landmark symbols from a source file using fast regex pattern matching.
27
+ */
28
+ export function extractLandmarks(content, ext) {
29
+ const lines = content.split('\n');
30
+ const landmarks = [];
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const lineNum = i + 1;
33
+ const line = lines[i].trim();
34
+ if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
35
+ // Check for explicit section comments (e.g. // MARK: or // ===)
36
+ if (line.includes('MARK:') || line.startsWith('// ===') || line.startsWith('// ---') || line.includes('SECTION:')) {
37
+ const clean = line.replace(/^\/\/\s*[-=]*\s*/, '').replace(/[-=]*$/, '').trim();
38
+ if (clean.length > 3 && clean.length < 50) {
39
+ landmarks.push({ name: clean, kind: 'section', line: lineNum });
40
+ }
41
+ }
42
+ continue;
43
+ }
44
+ if (ext === '.dart') {
45
+ // Classes
46
+ const classMatch = /^(?:abstract\s+)?class\s+([A-Za-z0-9_]+)/.exec(line);
47
+ if (classMatch) {
48
+ landmarks.push({ name: classMatch[1], kind: 'class', line: lineNum });
49
+ continue;
50
+ }
51
+ // Methods / Functions
52
+ const methodMatch = /^(?:(?:static|override|final|late|const)\s+)*(?:[A-Za-z0-9_<>?]+\s+)?(_?[a-z][A-Za-z0-9_]*)\s*\([^)]*\)\s*(?:async\s*)?(?:=>|\{)/.exec(line);
53
+ if (methodMatch) {
54
+ const name = methodMatch[1];
55
+ if (!['if', 'for', 'while', 'switch', 'catch', 'when', 'assert', 'setState'].includes(name)) {
56
+ landmarks.push({ name, kind: 'method', line: lineNum });
57
+ }
58
+ }
59
+ }
60
+ else if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx') {
61
+ // Classes
62
+ const classMatch = /^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z0-9_]+)/.exec(line);
63
+ if (classMatch) {
64
+ landmarks.push({ name: classMatch[1], kind: 'class', line: lineNum });
65
+ continue;
66
+ }
67
+ // Interfaces
68
+ const ifaceMatch = /^(?:export\s+)?interface\s+([A-Za-z0-9_]+)/.exec(line);
69
+ if (ifaceMatch) {
70
+ landmarks.push({ name: ifaceMatch[1], kind: 'interface', line: lineNum });
71
+ continue;
72
+ }
73
+ // Types
74
+ const typeMatch = /^(?:export\s+)?type\s+([A-Za-z0-9_]+)\s*(?:<[^=]+>)?\s*=/.exec(line);
75
+ if (typeMatch) {
76
+ landmarks.push({ name: typeMatch[1], kind: 'type', line: lineNum });
77
+ continue;
78
+ }
79
+ // Functions / Arrow functions / Methods
80
+ const fnMatch = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?function(?:\s*\*|\s+([A-Za-z0-9_]+))\s*(?:\(|<)/.exec(line);
81
+ if (fnMatch && fnMatch[1]) {
82
+ landmarks.push({ name: fnMatch[1], kind: 'function', line: lineNum });
83
+ continue;
84
+ }
85
+ const constFnMatch = /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_]+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z0-9_]+)\s*=>/.exec(line);
86
+ if (constFnMatch) {
87
+ landmarks.push({ name: constFnMatch[1], kind: 'function', line: lineNum });
88
+ continue;
89
+ }
90
+ const methodMatch = /^\s*(?:(?:public|private|protected|static|async|\*|override|readonly)\s+)+([a-zA-Z0-9_]+)\s*(?:<[^>]+>)?\s*\([^)]*\)\s*(?::\s*[^{]+)?\{/.exec(line);
91
+ if (methodMatch) {
92
+ const name = methodMatch[1];
93
+ if (!['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(name)) {
94
+ landmarks.push({ name, kind: 'method', line: lineNum });
95
+ }
96
+ }
97
+ }
98
+ else if (ext === '.kt') {
99
+ const ktClass = /^(?:(?:data|sealed|open|abstract|internal|public|private)\s+)*(?:class|interface|object)\s+([A-Za-z0-9_]+)/.exec(line);
100
+ if (ktClass) {
101
+ landmarks.push({ name: ktClass[1], kind: 'class', line: lineNum });
102
+ continue;
103
+ }
104
+ const ktFn = /^(?:(?:override|open|suspend|private|public|internal)\s+)*fun\s+(?:<[^>]+>\s+)?([A-Za-z0-9_]+)\s*\(/.exec(line);
105
+ if (ktFn) {
106
+ landmarks.push({ name: ktFn[1], kind: 'function', line: lineNum });
107
+ }
108
+ }
109
+ else if (ext === '.java') {
110
+ const javaClass = /^(?:public|protected|private)?\s*(?:static)?\s*(?:abstract|final)?\s*(?:class|interface|enum)\s+([A-Za-z0-9_]+)/.exec(line);
111
+ if (javaClass) {
112
+ landmarks.push({ name: javaClass[1], kind: 'class', line: lineNum });
113
+ continue;
114
+ }
115
+ const javaMethod = /^(?:public|protected|private)\s+(?:static\s+)?(?:final\s+)?(?:[A-Za-z0-9_<>[\],]+\s+)+([a-z][A-Za-z0-9_]*)\s*\([^)]*\)\s*(?:throws\s+[^{]+)?\{/.exec(line);
116
+ if (javaMethod) {
117
+ landmarks.push({ name: javaMethod[1], kind: 'method', line: lineNum });
118
+ }
119
+ }
120
+ else if (ext === '.swift') {
121
+ const swiftType = /^(?:public|private|internal|open|final)?\s*(?:class|struct|enum|protocol|extension)\s+([A-Za-z0-9_]+)/.exec(line);
122
+ if (swiftType) {
123
+ landmarks.push({ name: swiftType[1], kind: 'class', line: lineNum });
124
+ continue;
125
+ }
126
+ const swiftFn = /^(?:public|private|internal|override|static)?\s*func\s+([A-Za-z0-9_]+)\s*\(/.exec(line);
127
+ if (swiftFn) {
128
+ landmarks.push({ name: swiftFn[1], kind: 'function', line: lineNum });
129
+ }
130
+ }
131
+ else if (ext === '.py') {
132
+ const pyClass = /^\s*class\s+([A-Za-z0-9_]+)/.exec(line);
133
+ if (pyClass) {
134
+ landmarks.push({ name: pyClass[1], kind: 'class', line: lineNum });
135
+ continue;
136
+ }
137
+ const pyFn = /^\s*(?:async\s+)?def\s+([A-Za-z0-9_]+)\s*\(/.exec(line);
138
+ if (pyFn) {
139
+ landmarks.push({ name: pyFn[1], kind: 'function', line: lineNum });
140
+ }
141
+ }
142
+ else if (ext === '.go') {
143
+ const goType = /^type\s+([A-Za-z0-9_]+)\s+(?:struct|interface)/.exec(line);
144
+ if (goType) {
145
+ landmarks.push({ name: goType[1], kind: 'class', line: lineNum });
146
+ continue;
147
+ }
148
+ const goFn = /^func\s+(?:\([^)]+\)\s+)?([A-Za-z0-9_]+)/.exec(line);
149
+ if (goFn) {
150
+ landmarks.push({ name: goFn[1], kind: 'function', line: lineNum });
151
+ }
152
+ }
153
+ else if (ext === '.rs') {
154
+ const rsType = /^(?:pub\s+)?(?:struct|enum|trait)\s+([A-Za-z0-9_]+)/.exec(line);
155
+ if (rsType) {
156
+ landmarks.push({ name: rsType[1], kind: 'class', line: lineNum });
157
+ continue;
158
+ }
159
+ const rsFn = /^(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)/.exec(line);
160
+ if (rsFn) {
161
+ landmarks.push({ name: rsFn[1], kind: 'function', line: lineNum });
162
+ }
163
+ }
164
+ else if (ext === '.cs') {
165
+ const csType = /^(?:(?:public|private|protected|internal|static|abstract|sealed|partial)\s+)*(?:class|interface|struct|record)\s+([A-Za-z0-9_]+)/.exec(line);
166
+ if (csType) {
167
+ landmarks.push({ name: csType[1], kind: 'class', line: lineNum });
168
+ continue;
169
+ }
170
+ const csMethod = /^(?:(?:public|private|protected|internal|static|virtual|override|async)\s+)+[A-Za-z0-9_<>?[\]]+\s+([A-Za-z0-9_]+)\s*\([^)]*\)\s*(?:=>|\{)/.exec(line);
171
+ if (csMethod) {
172
+ landmarks.push({ name: csMethod[1], kind: 'method', line: lineNum });
173
+ }
174
+ }
175
+ else if (ext === '.c' || ext === '.cpp' || ext === '.h' || ext === '.hpp') {
176
+ const cppType = /^(?:class|struct)\s+([A-Za-z0-9_]+)\s*(?::|\{)/.exec(line);
177
+ if (cppType) {
178
+ landmarks.push({ name: cppType[1], kind: 'class', line: lineNum });
179
+ continue;
180
+ }
181
+ const cppFn = /^(?:[A-Za-z0-9_*&<>]+\s+)+([A-Za-z0-9_]+)\s*\([^;)]*\)\s*(?:const)?\s*\{/.exec(line);
182
+ if (cppFn && !['if', 'for', 'while', 'switch', 'catch'].includes(cppFn[1])) {
183
+ landmarks.push({ name: cppFn[1], kind: 'function', line: lineNum });
184
+ }
185
+ }
186
+ else if (ext === '.php') {
187
+ const phpClass = /^(?:final\s+|abstract\s+)?(?:class|interface|trait|enum)\s+([A-Za-z0-9_]+)/.exec(line);
188
+ if (phpClass) {
189
+ landmarks.push({ name: phpClass[1], kind: 'class', line: lineNum });
190
+ continue;
191
+ }
192
+ const phpFn = /^(?:(?:public|protected|private|static|final)\s+)*function\s+([A-Za-z0-9_]+)\s*\(/.exec(line);
193
+ if (phpFn) {
194
+ landmarks.push({ name: phpFn[1], kind: 'function', line: lineNum });
195
+ }
196
+ }
197
+ else if (ext === '.rb') {
198
+ const rbType = /^\s*(?:class|module)\s+([A-Za-z0-9_:]+)/.exec(line);
199
+ if (rbType) {
200
+ landmarks.push({ name: rbType[1], kind: 'class', line: lineNum });
201
+ continue;
202
+ }
203
+ const rbMethod = /^\s*def\s+([A-Za-z0-9_!?.]+)/.exec(line);
204
+ if (rbMethod) {
205
+ landmarks.push({ name: rbMethod[1], kind: 'method', line: lineNum });
206
+ }
207
+ }
208
+ }
209
+ return landmarks;
210
+ }
211
+ /**
212
+ * Scans the project for files (>= minLines) and extracts landmark locations with MTime caching.
213
+ */
214
+ export function scanLandmarks(root, minLines = 300, maxFiles = 5, maxSymbols = 12, recallDir) {
215
+ const results = [];
216
+ const isGitignored = createGitignoreFilter(root);
217
+ // Load MTime cache if recallDir is provided
218
+ let cacheFile = '';
219
+ let cache = {};
220
+ let cacheDirty = false;
221
+ if (recallDir) {
222
+ cacheFile = path.join(recallDir, '.cache', 'landmarks-cache.json');
223
+ if (fs.existsSync(cacheFile)) {
224
+ try {
225
+ cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
226
+ }
227
+ catch {
228
+ cache = {};
229
+ }
230
+ }
231
+ }
232
+ function walk(currentDir) {
233
+ let entries = [];
234
+ try {
235
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
236
+ }
237
+ catch {
238
+ return;
239
+ }
240
+ for (const entry of entries) {
241
+ if (entry.name.startsWith('.') && entry.name !== '.gitignore') {
242
+ if (entry.isDirectory())
243
+ continue;
244
+ }
245
+ const rel = toPosix(path.relative(root, path.join(currentDir, entry.name)));
246
+ if (entry.isDirectory()) {
247
+ if (DEFAULT_IGNORED_DIRS.has(entry.name) || isGitignored(rel, true))
248
+ continue;
249
+ walk(path.join(currentDir, entry.name));
250
+ }
251
+ else if (entry.isFile()) {
252
+ if (isGitignored(rel, false))
253
+ continue;
254
+ const ext = path.extname(entry.name).toLowerCase();
255
+ if (!SUPPORTED_EXTS.has(ext))
256
+ continue;
257
+ try {
258
+ const fullPath = path.join(currentDir, entry.name);
259
+ const stat = fs.statSync(fullPath);
260
+ if (stat.size > 2 * 1024 * 1024)
261
+ continue; // skip files > 2 MB
262
+ let linesCount = 0;
263
+ let landmarks = [];
264
+ const cached = cache[rel];
265
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
266
+ linesCount = cached.totalLines;
267
+ landmarks = cached.landmarks;
268
+ }
269
+ else {
270
+ const content = fs.readFileSync(fullPath, 'utf8');
271
+ linesCount = content.split('\n').length;
272
+ landmarks = extractLandmarks(content, ext);
273
+ cache[rel] = { mtimeMs: stat.mtimeMs, totalLines: linesCount, landmarks };
274
+ cacheDirty = true;
275
+ }
276
+ if (linesCount >= minLines && landmarks.length > 0) {
277
+ results.push({
278
+ relativePath: rel,
279
+ totalLines: linesCount,
280
+ landmarks: landmarks.slice(0, maxSymbols),
281
+ });
282
+ }
283
+ }
284
+ catch {
285
+ // ignore unreadable files
286
+ }
287
+ }
288
+ }
289
+ }
290
+ walk(root);
291
+ // Write updated cache if dirty
292
+ if (recallDir && cacheDirty) {
293
+ try {
294
+ const cacheDir = path.dirname(cacheFile);
295
+ if (!fs.existsSync(cacheDir))
296
+ fs.mkdirSync(cacheDir, { recursive: true });
297
+ fs.writeFileSync(cacheFile, JSON.stringify(cache), 'utf8');
298
+ }
299
+ catch {
300
+ // ignore cache write failures
301
+ }
302
+ }
303
+ // Sort descending by totalLines so the biggest files come first
304
+ results.sort((a, b) => b.totalLines - a.totalLines);
305
+ return results.slice(0, maxFiles);
306
+ }
307
+ export function formatLandmarksMarkdown(files) {
308
+ if (files.length === 0)
309
+ return '';
310
+ const lines = [
311
+ '## Large File Landmarks & Codebase API Surface (Direct Jump Targets)',
312
+ '> **AI AGENTS**: Do NOT grep inside these files or re-read them to discover APIs. Use these line numbers and symbol signatures directly.',
313
+ '',
314
+ ];
315
+ for (const f of files) {
316
+ lines.push(`### \`${f.relativePath}\` (${f.totalLines.toLocaleString()} lines)`);
317
+ for (const lm of f.landmarks) {
318
+ lines.push(`- **L${lm.line}**: \`${lm.name}\` (${lm.kind})`);
319
+ }
320
+ lines.push('');
321
+ }
322
+ return lines.join('\n');
323
+ }
@@ -0,0 +1,28 @@
1
+ import { type FileLandmarks } from './landmarks.js';
2
+ import { type ProfileTier } from '../schemas/index.js';
3
+ export declare const DEFAULT_IGNORED_DIRS: Set<string>;
4
+ export interface DirectoryInfo {
5
+ path: string;
6
+ filesCount: number;
7
+ extensions: string[];
8
+ }
9
+ export interface ProjectMapData {
10
+ projectType: string;
11
+ entryPoints: string[];
12
+ configFiles: string[];
13
+ themeFiles: string[];
14
+ directories: DirectoryInfo[];
15
+ treeView: string;
16
+ landmarks: FileLandmarks[];
17
+ }
18
+ export declare function createGitignoreFilter(root: string): (relPath: string, isDir: boolean) => boolean;
19
+ export declare function parseGitignore(root: string): Set<string>;
20
+ export interface ScanProjectOptions {
21
+ profile?: ProfileTier;
22
+ recallDir?: string;
23
+ }
24
+ export declare function scanProject(root: string, maxDepth?: number, opts?: ScanProjectOptions): ProjectMapData;
25
+ export declare function buildProjectMapMarkdown(data: ProjectMapData, maxChars?: number): string;
26
+ export declare const MAP_BLOCK_NAME = "recall:project-map";
27
+ export declare function renderProjectMap(existing: string, data: ProjectMapData, profile?: ProfileTier): string;
28
+ export declare function generateProjectMap(root: string, recallDir: string, profile?: ProfileTier): string;