@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.
- package/LICENSE +21 -0
- package/README.md +386 -2
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +451 -0
- package/dist/core/agent.d.ts +21 -0
- package/dist/core/agent.js +140 -0
- package/dist/core/checkpoint.d.ts +42 -0
- package/dist/core/checkpoint.js +233 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +36 -0
- package/dist/core/cost.d.ts +19 -0
- package/dist/core/cost.js +78 -0
- package/dist/core/doctor.d.ts +7 -0
- package/dist/core/doctor.js +69 -0
- package/dist/core/git.d.ts +27 -0
- package/dist/core/git.js +107 -0
- package/dist/core/index-builder.d.ts +11 -0
- package/dist/core/index-builder.js +84 -0
- package/dist/core/init.d.ts +13 -0
- package/dist/core/init.js +52 -0
- package/dist/core/landmarks.d.ts +19 -0
- package/dist/core/landmarks.js +323 -0
- package/dist/core/mapper.d.ts +28 -0
- package/dist/core/mapper.js +357 -0
- package/dist/core/memory.d.ts +44 -0
- package/dist/core/memory.js +199 -0
- package/dist/core/project.d.ts +12 -0
- package/dist/core/project.js +34 -0
- package/dist/core/retrieval.d.ts +27 -0
- package/dist/core/retrieval.js +180 -0
- package/dist/core/seed.d.ts +25 -0
- package/dist/core/seed.js +196 -0
- package/dist/core/service.d.ts +76 -0
- package/dist/core/service.js +228 -0
- package/dist/core/where.d.ts +14 -0
- package/dist/core/where.js +86 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/mcp/server.d.ts +65 -0
- package/dist/mcp/server.js +121 -0
- package/dist/schemas/index.d.ts +68 -0
- package/dist/schemas/index.js +43 -0
- package/dist/templates/index.d.ts +21 -0
- package/dist/templates/index.js +62 -0
- package/dist/utils/fs.d.ts +15 -0
- package/dist/utils/fs.js +73 -0
- package/dist/utils/managed.d.ts +5 -0
- package/dist/utils/managed.js +41 -0
- package/dist/utils/secrets.d.ts +3 -0
- package/dist/utils/secrets.js +23 -0
- package/package.json +65 -4
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const STOP = new Set([
|
|
2
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'from', 'into', 'are', 'was',
|
|
3
|
+
'how', 'why', 'what', 'when', 'should', 'can', 'could', 'would', 'will',
|
|
4
|
+
'have', 'has', 'had', 'been', 'about', 'each', 'more', 'some', 'than',
|
|
5
|
+
'them', 'then', 'they', 'does', 'did', 'done', 'also', 'such',
|
|
6
|
+
]);
|
|
7
|
+
export function stemCandidates(word) {
|
|
8
|
+
const s = word.toLowerCase();
|
|
9
|
+
const cands = new Set();
|
|
10
|
+
if (s.endsWith('ing') && s.length > 4) {
|
|
11
|
+
const base = s.slice(0, -3);
|
|
12
|
+
cands.add(base);
|
|
13
|
+
cands.add(`${base}e`);
|
|
14
|
+
if (/(.)\1$/.test(base)) {
|
|
15
|
+
cands.add(base.slice(0, -1));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else if (s.endsWith('ies') && s.length > 4) {
|
|
19
|
+
cands.add(s.slice(0, -3) + 'y');
|
|
20
|
+
}
|
|
21
|
+
else if (s.endsWith('es') && s.length > 4) {
|
|
22
|
+
cands.add(s.slice(0, -2));
|
|
23
|
+
cands.add(s.slice(0, -1));
|
|
24
|
+
}
|
|
25
|
+
else if (s.endsWith('ed') && s.length > 4) {
|
|
26
|
+
const base = s.slice(0, -2);
|
|
27
|
+
cands.add(base);
|
|
28
|
+
cands.add(`${base}e`);
|
|
29
|
+
if (/(.)\1$/.test(base)) {
|
|
30
|
+
cands.add(base.slice(0, -1));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (s.endsWith('s') && !s.endsWith('ss') && s.length > 3) {
|
|
34
|
+
cands.add(s.slice(0, -1));
|
|
35
|
+
}
|
|
36
|
+
return [...cands].filter((c) => c.length > 2 && c !== s);
|
|
37
|
+
}
|
|
38
|
+
export function stem(word) {
|
|
39
|
+
const c = stemCandidates(word);
|
|
40
|
+
return c[0] ?? word.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
export function tokenize(text) {
|
|
43
|
+
const raw = text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
44
|
+
const words = raw.filter((t) => t.length > 1 && !STOP.has(t));
|
|
45
|
+
return [...new Set(words)];
|
|
46
|
+
}
|
|
47
|
+
function headings(body) {
|
|
48
|
+
return body
|
|
49
|
+
.split('\n')
|
|
50
|
+
.filter((l) => /^#{1,6}\s/.test(l))
|
|
51
|
+
.join(' ')
|
|
52
|
+
.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
function hasWord(haystack, needle) {
|
|
55
|
+
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
56
|
+
return new RegExp(`(?:^|[^\\p{L}\\p{N}])${escaped}(?:$|[^\\p{L}\\p{N}])`, 'iu').test(haystack);
|
|
57
|
+
}
|
|
58
|
+
function excerptFor(body, tokens) {
|
|
59
|
+
const lines = body.split('\n').map((l) => l.trim()).filter((l) => l && !/^#{1,6}\s/.test(l));
|
|
60
|
+
const hit = lines.find((l) => tokens.some((t) => hasWord(l.toLowerCase(), t))) ?? lines[0] ?? '';
|
|
61
|
+
return hit.length > 220 ? `${hit.slice(0, 217)}...` : hit;
|
|
62
|
+
}
|
|
63
|
+
const STATUS_WEIGHT = { active: 1, proposed: 0.5, superseded: 0.3, archived: 0.1 };
|
|
64
|
+
function buildMatcher(token) {
|
|
65
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
66
|
+
const wordRe = new RegExp(`(?:^|[^\\p{L}\\p{N}])${escaped}(?:$|[^\\p{L}\\p{N}])`, 'iu');
|
|
67
|
+
const prefixRe = new RegExp(`(?:^|[^\\p{L}\\p{N}])${escaped}[\\p{L}\\p{N}]*`, 'iu');
|
|
68
|
+
const countRe = new RegExp(`(?:^|[^\\p{L}\\p{N}])${escaped}(?:$|[^\\p{L}\\p{N}])`, 'giu');
|
|
69
|
+
const stemRes = stemCandidates(token).map((st) => {
|
|
70
|
+
const escSt = st.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
71
|
+
return {
|
|
72
|
+
wordRe: new RegExp(`(?:^|[^\\p{L}\\p{N}])${escSt}(?:$|[^\\p{L}\\p{N}])`, 'iu'),
|
|
73
|
+
prefixRe: new RegExp(`(?:^|[^\\p{L}\\p{N}])${escSt}[\\p{L}\\p{N}]*`, 'iu'),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
return { token, wordRe, prefixRe, countRe, stemRes };
|
|
77
|
+
}
|
|
78
|
+
/** Deterministic keyword search. This is NOT semantic search. */
|
|
79
|
+
export function search(memories, query, opts = {}) {
|
|
80
|
+
const tokens = tokenize(query);
|
|
81
|
+
const phrase = query.trim().toLowerCase();
|
|
82
|
+
if (tokens.length === 0)
|
|
83
|
+
return [];
|
|
84
|
+
const matchers = tokens.map(buildMatcher);
|
|
85
|
+
const hits = [];
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
for (const m of memories) {
|
|
88
|
+
if (m.status === 'archived' && !opts.includeArchived)
|
|
89
|
+
continue;
|
|
90
|
+
if (m.status === 'proposed' && !opts.includeProposed)
|
|
91
|
+
continue;
|
|
92
|
+
if (opts.type && m.type !== opts.type)
|
|
93
|
+
continue;
|
|
94
|
+
if (opts.tag && !m.tags.some((t) => t.toLowerCase() === opts.tag.toLowerCase()))
|
|
95
|
+
continue;
|
|
96
|
+
const title = m.title.toLowerCase();
|
|
97
|
+
const tags = m.tags.join(' ').toLowerCase();
|
|
98
|
+
const heads = headings(m.body);
|
|
99
|
+
const body = m.body.toLowerCase();
|
|
100
|
+
let score = 0;
|
|
101
|
+
let matched = 0;
|
|
102
|
+
if (title === phrase)
|
|
103
|
+
score += 15;
|
|
104
|
+
else if (title.includes(phrase))
|
|
105
|
+
score += 8;
|
|
106
|
+
for (const mtr of matchers) {
|
|
107
|
+
let s = 0;
|
|
108
|
+
if (mtr.wordRe.test(title))
|
|
109
|
+
s += 3;
|
|
110
|
+
else if (mtr.stemRes.some((sr) => sr.wordRe.test(title) || sr.prefixRe.test(title)))
|
|
111
|
+
s += 2;
|
|
112
|
+
if (mtr.wordRe.test(tags))
|
|
113
|
+
s += 2;
|
|
114
|
+
else if (mtr.stemRes.some((sr) => sr.wordRe.test(tags) || sr.prefixRe.test(tags)))
|
|
115
|
+
s += 1.5;
|
|
116
|
+
if (mtr.wordRe.test(heads))
|
|
117
|
+
s += 2;
|
|
118
|
+
else if (mtr.stemRes.some((sr) => sr.wordRe.test(heads) || sr.prefixRe.test(heads)))
|
|
119
|
+
s += 1.5;
|
|
120
|
+
const matches = body.match(mtr.countRe);
|
|
121
|
+
const c = matches ? matches.length : 0;
|
|
122
|
+
if (c > 0)
|
|
123
|
+
s += Math.min(c, 3);
|
|
124
|
+
else if (mtr.stemRes.some((sr) => sr.wordRe.test(body) || sr.prefixRe.test(body)))
|
|
125
|
+
s += 1;
|
|
126
|
+
if (s > 0)
|
|
127
|
+
matched++;
|
|
128
|
+
score += s;
|
|
129
|
+
}
|
|
130
|
+
if (matched === 0)
|
|
131
|
+
continue;
|
|
132
|
+
if (tokens.length > 1)
|
|
133
|
+
score += (matched / tokens.length) * 5;
|
|
134
|
+
if (m.verification === 'verified')
|
|
135
|
+
score += 0.5;
|
|
136
|
+
// Recency bonus: recent memories are slightly more relevant
|
|
137
|
+
try {
|
|
138
|
+
const dateStr = m.updated || m.created;
|
|
139
|
+
const ageDays = (now - new Date(dateStr).getTime()) / (1000 * 60 * 60 * 24);
|
|
140
|
+
if (ageDays <= 14)
|
|
141
|
+
score += 0.2;
|
|
142
|
+
else if (ageDays <= 30)
|
|
143
|
+
score += 0.1;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// ignore invalid dates
|
|
147
|
+
}
|
|
148
|
+
score *= STATUS_WEIGHT[m.status];
|
|
149
|
+
const finalScore = Math.round(score * 100) / 100;
|
|
150
|
+
if (opts.minScore && finalScore < opts.minScore)
|
|
151
|
+
continue;
|
|
152
|
+
hits.push({ memory: m, score: finalScore, excerpt: excerptFor(m.body, tokens) });
|
|
153
|
+
}
|
|
154
|
+
hits.sort((a, b) => b.score - a.score || a.memory.id.localeCompare(b.memory.id));
|
|
155
|
+
return hits.slice(0, opts.limit ?? 5);
|
|
156
|
+
}
|
|
157
|
+
/** Default cap for agent-facing context output: ~400 tokens at ~4 chars/token. */
|
|
158
|
+
export const CONTEXT_MAX_CHARS = 1600;
|
|
159
|
+
/**
|
|
160
|
+
* Agent-facing rendering: one header line + one excerpt line per hit, hard-capped.
|
|
161
|
+
* Hits scoring under 30% of the best are dropped: they cost tokens and rarely help.
|
|
162
|
+
*/
|
|
163
|
+
export function formatHitsCompact(hits, maxChars = CONTEXT_MAX_CHARS) {
|
|
164
|
+
if (hits.length === 0)
|
|
165
|
+
return 'No relevant memory.';
|
|
166
|
+
const top = hits[0].score;
|
|
167
|
+
const out = [];
|
|
168
|
+
let used = 0;
|
|
169
|
+
for (const h of hits) {
|
|
170
|
+
if (h.score < top * 0.3)
|
|
171
|
+
break;
|
|
172
|
+
const flag = h.memory.status === 'active' ? '' : `, ${h.memory.status}`;
|
|
173
|
+
const entry = `- ${h.memory.title} [${h.memory.type}${flag}] .recall/${h.memory.file}${h.excerpt ? `\n ${h.excerpt}` : ''}`;
|
|
174
|
+
if (out.length > 0 && used + entry.length + 1 > maxChars)
|
|
175
|
+
break;
|
|
176
|
+
out.push(entry.length > maxChars ? `${entry.slice(0, maxChars - 3)}...` : entry);
|
|
177
|
+
used += entry.length + 1;
|
|
178
|
+
}
|
|
179
|
+
return out.join('\n');
|
|
180
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MemoryType } from '../schemas/index.js';
|
|
2
|
+
export interface SeedOptions {
|
|
3
|
+
from?: 'git' | 'docs' | 'all';
|
|
4
|
+
limit?: number;
|
|
5
|
+
dryRun?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface SeedCandidate {
|
|
8
|
+
title: string;
|
|
9
|
+
text: string;
|
|
10
|
+
type: MemoryType;
|
|
11
|
+
tags: string[];
|
|
12
|
+
source: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SeedResult {
|
|
15
|
+
proposed: Array<{
|
|
16
|
+
id?: string;
|
|
17
|
+
title: string;
|
|
18
|
+
type: string;
|
|
19
|
+
source: string;
|
|
20
|
+
}>;
|
|
21
|
+
candidates: SeedCandidate[];
|
|
22
|
+
dryRun: boolean;
|
|
23
|
+
sourcesUsed: string[];
|
|
24
|
+
}
|
|
25
|
+
export declare function seed(start: string, opts?: SeedOptions): SeedResult;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { requireInitialized } from './project.js';
|
|
5
|
+
import { propose } from './service.js';
|
|
6
|
+
import { loadAll } from './memory.js';
|
|
7
|
+
import { findSecrets } from '../utils/secrets.js';
|
|
8
|
+
const COMMIT_TYPE_MAP = [
|
|
9
|
+
{ prefix: /^feat(\([^)]+\))?:\s*/i, type: 'decision', tag: 'feature' },
|
|
10
|
+
{ prefix: /^fix(\([^)]+\))?:\s*/i, type: 'bugfix', tag: 'bugfix' },
|
|
11
|
+
{ prefix: /^refactor(\([^)]+\))?:\s*/i, type: 'decision', tag: 'refactor' },
|
|
12
|
+
{ prefix: /^perf(\([^)]+\))?:\s*/i, type: 'lesson', tag: 'performance' },
|
|
13
|
+
{ prefix: /^arch(\([^)]+\))?:\s*/i, type: 'decision', tag: 'architecture' },
|
|
14
|
+
];
|
|
15
|
+
const IGNORED_SUBJECTS = [
|
|
16
|
+
/^bump\b/i,
|
|
17
|
+
/^merge\b/i,
|
|
18
|
+
/^release\b/i,
|
|
19
|
+
/^update dependencies/i,
|
|
20
|
+
/^chore\b/i,
|
|
21
|
+
/^wip\b/i,
|
|
22
|
+
/^typo\b/i,
|
|
23
|
+
/^format\b/i,
|
|
24
|
+
];
|
|
25
|
+
function extractGitCandidates(root, maxCommits = 40) {
|
|
26
|
+
let output = '';
|
|
27
|
+
try {
|
|
28
|
+
output = execFileSync('git', ['log', `-n`, String(maxCommits), '--pretty=format:%h|%ad|%s', '--date=short'], {
|
|
29
|
+
cwd: root,
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
38
|
+
const candidates = [];
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const parts = line.split('|');
|
|
41
|
+
if (parts.length < 3)
|
|
42
|
+
continue;
|
|
43
|
+
const hash = parts[0]?.trim() ?? '';
|
|
44
|
+
const date = parts[1]?.trim() ?? '';
|
|
45
|
+
const subject = parts.slice(2).join('|').trim();
|
|
46
|
+
if (!subject || IGNORED_SUBJECTS.some((re) => re.test(subject)))
|
|
47
|
+
continue;
|
|
48
|
+
let matchedType = 'decision';
|
|
49
|
+
let matchedTag = 'git-history';
|
|
50
|
+
let cleanTitle = subject;
|
|
51
|
+
for (const mapping of COMMIT_TYPE_MAP) {
|
|
52
|
+
if (mapping.prefix.test(subject)) {
|
|
53
|
+
matchedType = mapping.type;
|
|
54
|
+
matchedTag = mapping.tag;
|
|
55
|
+
cleanTitle = subject.replace(mapping.prefix, '').trim();
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (cleanTitle.length < 5)
|
|
60
|
+
continue;
|
|
61
|
+
// Capitalize first letter
|
|
62
|
+
cleanTitle = cleanTitle.charAt(0).toUpperCase() + cleanTitle.slice(1);
|
|
63
|
+
const text = `Seeded from commit ${hash} (${date}):\n${subject}`;
|
|
64
|
+
if (findSecrets(cleanTitle).length > 0 || findSecrets(text).length > 0)
|
|
65
|
+
continue;
|
|
66
|
+
candidates.push({
|
|
67
|
+
title: cleanTitle.slice(0, 80),
|
|
68
|
+
text,
|
|
69
|
+
type: matchedType,
|
|
70
|
+
tags: [matchedTag, 'seed'],
|
|
71
|
+
source: `git:${hash}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return candidates;
|
|
75
|
+
}
|
|
76
|
+
function extractDocsCandidates(root) {
|
|
77
|
+
const candidates = [];
|
|
78
|
+
const searchDirs = ['docs', 'doc'];
|
|
79
|
+
for (const d of searchDirs) {
|
|
80
|
+
const absDir = path.join(root, d);
|
|
81
|
+
if (!fs.existsSync(absDir))
|
|
82
|
+
continue;
|
|
83
|
+
let entries = [];
|
|
84
|
+
try {
|
|
85
|
+
entries = fs.readdirSync(absDir).filter((f) => f.endsWith('.md'));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const file of entries) {
|
|
91
|
+
const fullPath = path.join(absDir, file);
|
|
92
|
+
let content = '';
|
|
93
|
+
try {
|
|
94
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Look for H2/H3 headings that suggest decisions or architecture
|
|
100
|
+
const sections = content.split(/\n(?=##\s+)/);
|
|
101
|
+
for (const section of sections) {
|
|
102
|
+
const match = /^##\s+([^\n]+)\n([\s\S]*)/.exec(section);
|
|
103
|
+
if (!match)
|
|
104
|
+
continue;
|
|
105
|
+
const heading = match[1]?.trim() ?? '';
|
|
106
|
+
const body = match[2]?.trim() ?? '';
|
|
107
|
+
if (/^(overview|introduction|table of contents|license|links)/i.test(heading))
|
|
108
|
+
continue;
|
|
109
|
+
if (body.length < 40)
|
|
110
|
+
continue;
|
|
111
|
+
const firstPara = body.split(/\n\s*\n/)[0]?.trim() ?? '';
|
|
112
|
+
if (firstPara.length < 20)
|
|
113
|
+
continue;
|
|
114
|
+
const title = heading.slice(0, 80);
|
|
115
|
+
const text = firstPara.slice(0, 500);
|
|
116
|
+
if (findSecrets(title).length > 0 || findSecrets(text).length > 0)
|
|
117
|
+
continue;
|
|
118
|
+
candidates.push({
|
|
119
|
+
title,
|
|
120
|
+
text: `# ${heading}\n\n${text}\n\nSource: \`${d}/${file}\``,
|
|
121
|
+
type: 'decision',
|
|
122
|
+
tags: ['architecture', 'docs', 'seed'],
|
|
123
|
+
source: `${d}/${file}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return candidates;
|
|
129
|
+
}
|
|
130
|
+
export function seed(start, opts = {}) {
|
|
131
|
+
const p = requireInitialized(start);
|
|
132
|
+
const from = opts.from ?? 'all';
|
|
133
|
+
const limit = opts.limit ?? 5;
|
|
134
|
+
const isDryRun = !!opts.dryRun;
|
|
135
|
+
const existing = loadAll(p.recallDir).memories;
|
|
136
|
+
const existingTitles = new Set(existing.map((m) => m.title.toLowerCase().trim()));
|
|
137
|
+
const allCandidates = [];
|
|
138
|
+
const sourcesUsed = [];
|
|
139
|
+
if (from === 'all' || from === 'git') {
|
|
140
|
+
const gitCands = extractGitCandidates(p.root);
|
|
141
|
+
if (gitCands.length > 0) {
|
|
142
|
+
allCandidates.push(...gitCands);
|
|
143
|
+
sourcesUsed.push('git');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (from === 'all' || from === 'docs') {
|
|
147
|
+
const docCands = extractDocsCandidates(p.root);
|
|
148
|
+
if (docCands.length > 0) {
|
|
149
|
+
allCandidates.push(...docCands);
|
|
150
|
+
sourcesUsed.push('docs');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Deduplicate against existing memories and within candidates
|
|
154
|
+
const uniqueCandidates = [];
|
|
155
|
+
const seenTitles = new Set();
|
|
156
|
+
for (const cand of allCandidates) {
|
|
157
|
+
const norm = cand.title.toLowerCase().trim();
|
|
158
|
+
if (existingTitles.has(norm) || seenTitles.has(norm))
|
|
159
|
+
continue;
|
|
160
|
+
seenTitles.add(norm);
|
|
161
|
+
uniqueCandidates.push(cand);
|
|
162
|
+
if (uniqueCandidates.length >= limit)
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
const proposed = [];
|
|
166
|
+
for (const cand of uniqueCandidates) {
|
|
167
|
+
if (isDryRun) {
|
|
168
|
+
proposed.push({
|
|
169
|
+
title: cand.title,
|
|
170
|
+
type: cand.type,
|
|
171
|
+
source: cand.source,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const m = propose(p.root, {
|
|
176
|
+
title: cand.title,
|
|
177
|
+
text: cand.text,
|
|
178
|
+
type: cand.type,
|
|
179
|
+
tags: cand.tags,
|
|
180
|
+
sources: [cand.source],
|
|
181
|
+
});
|
|
182
|
+
proposed.push({
|
|
183
|
+
id: m.id,
|
|
184
|
+
title: m.title,
|
|
185
|
+
type: m.type,
|
|
186
|
+
source: cand.source,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
proposed,
|
|
192
|
+
candidates: uniqueCandidates,
|
|
193
|
+
dryRun: isDryRun,
|
|
194
|
+
sourcesUsed,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type AgentState } from './agent.js';
|
|
2
|
+
import { type CheckpointOptions, type CheckpointResult } from './checkpoint.js';
|
|
3
|
+
import { type Commit, type UndoResult } from './git.js';
|
|
4
|
+
import { type IndexState } from './index-builder.js';
|
|
5
|
+
import { type Memory, type NewMemory } from './memory.js';
|
|
6
|
+
import { type Project } from './project.js';
|
|
7
|
+
import { type SearchHit, type SearchOptions } from './retrieval.js';
|
|
8
|
+
import { type WhereHit } from './where.js';
|
|
9
|
+
import { type ProfileTier } from '../schemas/index.js';
|
|
10
|
+
/** Create a memory, log it, and refresh the index. */
|
|
11
|
+
export declare function remember(start: string, input: NewMemory, action?: string): Memory;
|
|
12
|
+
/** Agent-facing: always lands as `proposed` and is excluded from search until approved. */
|
|
13
|
+
export declare function propose(start: string, input: NewMemory): Memory;
|
|
14
|
+
/** Same normalized title, or high word overlap (Jaccard >= 0.7 over >= 2 words each). */
|
|
15
|
+
export declare function findDuplicate(candidates: Memory[], title: string): Memory | undefined;
|
|
16
|
+
export interface RememberResult {
|
|
17
|
+
memory: Memory;
|
|
18
|
+
action: 'created' | 'updated';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The write path for agents (CLI `remember` and MCP `recall_remember`).
|
|
22
|
+
* - `writes: auto` (default): active immediately, so memory is useful without human ceremony.
|
|
23
|
+
* - `writes: review` or RECALL_AGENT=1: lands as `proposed`.
|
|
24
|
+
* A near-duplicate title updates the existing memory instead of adding another file, which keeps
|
|
25
|
+
* retrieval results (and their token cost) from growing with repetition.
|
|
26
|
+
*/
|
|
27
|
+
export declare function agentRemember(start: string, input: NewMemory): RememberResult;
|
|
28
|
+
export declare function whereIs(start: string, query: string, limit?: number): WhereHit[];
|
|
29
|
+
export declare function approve(start: string, id: string): Memory;
|
|
30
|
+
export declare function archive(start: string, id: string): Memory;
|
|
31
|
+
export declare function context(start: string, query: string, opts?: SearchOptions): SearchHit[];
|
|
32
|
+
export interface TokenFootprint {
|
|
33
|
+
totalTokens: number;
|
|
34
|
+
budgetTokens: number;
|
|
35
|
+
profile: ProfileTier;
|
|
36
|
+
files: Record<string, number>;
|
|
37
|
+
}
|
|
38
|
+
export interface StatusReport {
|
|
39
|
+
project: Project;
|
|
40
|
+
byType: Record<string, number>;
|
|
41
|
+
byStatus: Record<string, number>;
|
|
42
|
+
problems: number;
|
|
43
|
+
index: IndexState;
|
|
44
|
+
agentFile: string;
|
|
45
|
+
agent: AgentState | 'unknown';
|
|
46
|
+
git: {
|
|
47
|
+
available: boolean;
|
|
48
|
+
isRepo: boolean;
|
|
49
|
+
changes: string[] | null;
|
|
50
|
+
};
|
|
51
|
+
tokens: TokenFootprint;
|
|
52
|
+
profile: ProfileTier;
|
|
53
|
+
}
|
|
54
|
+
export declare function status(start: string): StatusReport;
|
|
55
|
+
export declare function getProposals(start: string): Memory[];
|
|
56
|
+
export declare function approveAll(start: string): Memory[];
|
|
57
|
+
export declare function archiveAll(start: string): Memory[];
|
|
58
|
+
export interface LogReport {
|
|
59
|
+
source: 'git' | 'changelog';
|
|
60
|
+
committed: Commit[];
|
|
61
|
+
uncommitted: string[];
|
|
62
|
+
changelog: string[];
|
|
63
|
+
}
|
|
64
|
+
export declare function log(start: string, limit: number): LogReport;
|
|
65
|
+
export declare function map(start: string, profile?: ProfileTier): string;
|
|
66
|
+
export declare function checkpoint(start: string, summary?: string, opts?: CheckpointOptions): CheckpointResult;
|
|
67
|
+
export declare function prune(start: string, opts?: {
|
|
68
|
+
retentionDays?: number;
|
|
69
|
+
maxEntries?: number;
|
|
70
|
+
}): {
|
|
71
|
+
removedCount: number;
|
|
72
|
+
remainingCount: number;
|
|
73
|
+
};
|
|
74
|
+
export declare function undo(start: string, opts?: {
|
|
75
|
+
dryRun?: boolean;
|
|
76
|
+
}): UndoResult;
|