@openanonymity/nanomem 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 (66) hide show
  1. package/README.md +194 -0
  2. package/package.json +85 -0
  3. package/src/backends/BaseStorage.js +177 -0
  4. package/src/backends/filesystem.js +177 -0
  5. package/src/backends/indexeddb.js +208 -0
  6. package/src/backends/ram.js +113 -0
  7. package/src/backends/schema.js +42 -0
  8. package/src/bullets/bulletIndex.js +125 -0
  9. package/src/bullets/compaction.js +109 -0
  10. package/src/bullets/index.js +16 -0
  11. package/src/bullets/normalize.js +241 -0
  12. package/src/bullets/parser.js +199 -0
  13. package/src/bullets/scoring.js +53 -0
  14. package/src/cli/auth.js +323 -0
  15. package/src/cli/commands.js +411 -0
  16. package/src/cli/config.js +120 -0
  17. package/src/cli/diff.js +68 -0
  18. package/src/cli/help.js +84 -0
  19. package/src/cli/output.js +269 -0
  20. package/src/cli/spinner.js +54 -0
  21. package/src/cli.js +178 -0
  22. package/src/engine/compactor.js +247 -0
  23. package/src/engine/executors.js +152 -0
  24. package/src/engine/ingester.js +229 -0
  25. package/src/engine/retriever.js +414 -0
  26. package/src/engine/toolLoop.js +176 -0
  27. package/src/imports/chatgpt.js +160 -0
  28. package/src/imports/index.js +14 -0
  29. package/src/imports/markdown.js +104 -0
  30. package/src/imports/oaFastchat.js +124 -0
  31. package/src/index.js +199 -0
  32. package/src/llm/anthropic.js +264 -0
  33. package/src/llm/openai.js +179 -0
  34. package/src/prompt_sets/conversation/ingestion.js +51 -0
  35. package/src/prompt_sets/document/ingestion.js +43 -0
  36. package/src/prompt_sets/index.js +31 -0
  37. package/src/types.js +382 -0
  38. package/src/utils/portability.js +174 -0
  39. package/types/backends/BaseStorage.d.ts +42 -0
  40. package/types/backends/filesystem.d.ts +11 -0
  41. package/types/backends/indexeddb.d.ts +12 -0
  42. package/types/backends/ram.d.ts +8 -0
  43. package/types/backends/schema.d.ts +14 -0
  44. package/types/bullets/bulletIndex.d.ts +47 -0
  45. package/types/bullets/compaction.d.ts +10 -0
  46. package/types/bullets/index.d.ts +36 -0
  47. package/types/bullets/normalize.d.ts +95 -0
  48. package/types/bullets/parser.d.ts +31 -0
  49. package/types/bullets/scoring.d.ts +12 -0
  50. package/types/engine/compactor.d.ts +27 -0
  51. package/types/engine/executors.d.ts +46 -0
  52. package/types/engine/ingester.d.ts +29 -0
  53. package/types/engine/retriever.d.ts +50 -0
  54. package/types/engine/toolLoop.d.ts +9 -0
  55. package/types/imports/chatgpt.d.ts +14 -0
  56. package/types/imports/index.d.ts +3 -0
  57. package/types/imports/markdown.d.ts +31 -0
  58. package/types/imports/oaFastchat.d.ts +30 -0
  59. package/types/index.d.ts +21 -0
  60. package/types/llm/anthropic.d.ts +16 -0
  61. package/types/llm/openai.d.ts +16 -0
  62. package/types/prompt_sets/conversation/ingestion.d.ts +7 -0
  63. package/types/prompt_sets/document/ingestion.d.ts +7 -0
  64. package/types/prompt_sets/index.d.ts +11 -0
  65. package/types/types.d.ts +293 -0
  66. package/types/utils/portability.d.ts +33 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Normalization utilities for memory bullet metadata.
3
+ * @import { Tier, Status, Source, Confidence, Bullet, EnsureBulletMetadataOptions } from '../types.js'
4
+ */
5
+
6
+ /**
7
+ * @param {string | number | null | undefined} value
8
+ * @returns {string | null}
9
+ */
10
+ export function safeDateIso(value) {
11
+ if (!value) return null;
12
+ const date = new Date(value);
13
+ if (Number.isNaN(date.getTime())) return null;
14
+ return date.toISOString().slice(0, 10);
15
+ }
16
+
17
+ /** @returns {string} */
18
+ export function todayIsoDate() {
19
+ return new Date().toISOString().slice(0, 10);
20
+ }
21
+
22
+ /**
23
+ * @param {string} path
24
+ * @returns {string}
25
+ */
26
+ export function inferTopicFromPath(path) {
27
+ if (!path || typeof path !== 'string') return 'general';
28
+ const first = path.split('/')[0]?.trim().toLowerCase();
29
+ return first || 'general';
30
+ }
31
+
32
+ /**
33
+ * @param {string} value
34
+ * @param {string} [fallback]
35
+ * @returns {string}
36
+ */
37
+ export function normalizeTopic(value, fallback = 'general') {
38
+ const source = String(value || '').trim().toLowerCase();
39
+ const normalized = source
40
+ .replace(/[^a-z0-9/_-]+/g, '-')
41
+ .replace(/-+/g, '-')
42
+ .replace(/^[-/]+|[-/]+$/g, '');
43
+ return normalized || fallback;
44
+ }
45
+
46
+ /**
47
+ * @param {string} value
48
+ * @returns {string}
49
+ */
50
+ export function normalizeFactText(value) {
51
+ return String(value || '')
52
+ .toLowerCase()
53
+ .replace(/[^\w\s]/g, ' ')
54
+ .replace(/\s+/g, ' ')
55
+ .trim();
56
+ }
57
+
58
+ /**
59
+ * @param {string | null | undefined} value
60
+ * @param {Tier} [fallback]
61
+ * @returns {Tier}
62
+ */
63
+ export function normalizeTier(value, fallback = 'long_term') {
64
+ const source = String(value || '').trim().toLowerCase();
65
+ if (['working', 'short_term', 'short-term'].includes(source)) return 'working';
66
+ if (['long_term', 'long-term', 'longterm', 'active'].includes(source)) return 'long_term';
67
+ if (['history', 'archive', 'archived'].includes(source)) return 'history';
68
+ return fallback;
69
+ }
70
+
71
+ /**
72
+ * @param {string | null | undefined} value
73
+ * @param {Status} [fallback]
74
+ * @returns {Status}
75
+ */
76
+ export function normalizeStatus(value, fallback = 'active') {
77
+ const source = String(value || '').trim().toLowerCase();
78
+ if (['active', 'current'].includes(source)) return 'active';
79
+ if (['superseded', 'replaced', 'resolved'].includes(source)) return 'superseded';
80
+ if (['expired', 'stale'].includes(source)) return 'expired';
81
+ if (['uncertain', 'tentative'].includes(source)) return 'uncertain';
82
+ return fallback;
83
+ }
84
+
85
+ /**
86
+ * @param {string | null | undefined} value
87
+ * @param {Source} [fallback]
88
+ * @returns {Source}
89
+ */
90
+ export function normalizeSource(value, fallback = 'user_statement') {
91
+ const source = String(value || '').trim().toLowerCase();
92
+ if (['user_statement', 'user', 'explicit_user'].includes(source)) return 'user_statement';
93
+ if (['assistant_summary', 'assistant', 'summary'].includes(source)) return 'assistant_summary';
94
+ if (['inference', 'inferred'].includes(source)) return 'inference';
95
+ if (['system', 'system_note'].includes(source)) return 'system';
96
+ return fallback;
97
+ }
98
+
99
+ /**
100
+ * @param {string | null | undefined} value
101
+ * @param {Confidence} [fallback]
102
+ * @returns {Confidence}
103
+ */
104
+ export function normalizeConfidence(value, fallback = 'medium') {
105
+ const source = String(value || '').trim().toLowerCase();
106
+ if (['high', 'strong'].includes(source)) return 'high';
107
+ if (['medium', 'med', 'moderate'].includes(source)) return 'medium';
108
+ if (['low', 'weak'].includes(source)) return 'low';
109
+ return fallback;
110
+ }
111
+
112
+ /**
113
+ * @param {Source | string | null | undefined} source
114
+ * @returns {Confidence}
115
+ */
116
+ export function defaultConfidenceForSource(source) {
117
+ if (source === 'user_statement') return 'high';
118
+ if (source === 'assistant_summary') return 'medium';
119
+ if (source === 'system') return 'medium';
120
+ return 'low';
121
+ }
122
+
123
+ /**
124
+ * @param {string} section
125
+ * @returns {Tier}
126
+ */
127
+ export function inferTierFromSection(section) {
128
+ if (section === 'working') return 'working';
129
+ if (section === 'history') return 'history';
130
+ return 'long_term';
131
+ }
132
+
133
+ /**
134
+ * @param {string} section
135
+ * @returns {Status}
136
+ */
137
+ export function inferStatusFromSection(section) {
138
+ return section === 'history' ? 'superseded' : 'active';
139
+ }
140
+
141
+ /**
142
+ * @param {string} value
143
+ * @returns {Tier}
144
+ */
145
+ export function normalizeTierToSection(value) {
146
+ const tier = normalizeTier(value);
147
+ if (tier === 'working') return 'working';
148
+ if (tier === 'history') return 'history';
149
+ return 'long_term';
150
+ }
151
+
152
+ /**
153
+ * @param {Partial<Bullet>} bullet
154
+ * @param {Tier} [fallback]
155
+ * @returns {Tier}
156
+ */
157
+ export function inferTierFromBullet(bullet, fallback = 'long_term') {
158
+ if (bullet?.reviewAt || bullet?.expiresAt) return 'working';
159
+
160
+ const text = String(bullet?.text || '').toLowerCase();
161
+ if (!text) return fallback;
162
+
163
+ const workingPatterns = [
164
+ /\bcurrently\b/,
165
+ /\bright now\b/,
166
+ /\bthis (week|month|quarter|year)\b/,
167
+ /\bnext (week|month|quarter|year)\b/,
168
+ /\bplanning\b/,
169
+ /\bevaluating\b/,
170
+ /\bconsidering\b/,
171
+ /\btrying to\b/,
172
+ /\bworking on\b/,
173
+ /\bdebugging\b/,
174
+ /\bpreparing\b/,
175
+ /\binterviewing\b/,
176
+ /\bin progress\b/,
177
+ /\btemporary\b/,
178
+ /\bfor now\b/,
179
+ /\bas of \d{4}-\d{2}-\d{2}\b/
180
+ ];
181
+
182
+ return workingPatterns.some((pattern) => pattern.test(text)) ? 'working' : fallback;
183
+ }
184
+
185
+ /**
186
+ * @param {Partial<Bullet>} bullet
187
+ * @param {EnsureBulletMetadataOptions} [options]
188
+ * @returns {Bullet}
189
+ */
190
+ export function ensureBulletMetadata(bullet, options = {}) {
191
+ const fallbackTopic = normalizeTopic(options.defaultTopic || 'general');
192
+ const fallbackUpdatedAt = options.updatedAt || todayIsoDate();
193
+ const inferredTier = inferTierFromBullet(bullet, options.defaultTier || 'long_term');
194
+ const preferredTier = bullet?.explicitTier ? bullet?.tier : inferredTier;
195
+ const fallbackTier = normalizeTier(options.defaultTier || preferredTier || bullet?.tier || 'long_term');
196
+ const fallbackStatus = normalizeStatus(
197
+ options.defaultStatus
198
+ || (bullet?.explicitStatus ? bullet?.status : null)
199
+ || inferStatusFromSection(normalizeTierToSection(fallbackTier))
200
+ );
201
+ return {
202
+ text: String(bullet?.text || '').trim(),
203
+ topic: normalizeTopic(bullet?.topic || fallbackTopic, fallbackTopic),
204
+ updatedAt: safeDateIso(bullet?.updatedAt) || fallbackUpdatedAt,
205
+ expiresAt: safeDateIso(bullet?.expiresAt),
206
+ reviewAt: safeDateIso(bullet?.reviewAt),
207
+ tier: normalizeTier(preferredTier, fallbackTier),
208
+ status: normalizeStatus(bullet?.status, fallbackStatus),
209
+ source: normalizeSource(
210
+ bullet?.source,
211
+ normalizeSource(options.defaultSource, 'user_statement')
212
+ ),
213
+ confidence: normalizeConfidence(
214
+ bullet?.confidence,
215
+ normalizeConfidence(
216
+ options.defaultConfidence,
217
+ defaultConfidenceForSource(normalizeSource(
218
+ bullet?.source,
219
+ normalizeSource(options.defaultSource, 'user_statement')
220
+ ))
221
+ )
222
+ ),
223
+ explicitTier: Boolean(bullet?.explicitTier || bullet?.tier),
224
+ explicitStatus: Boolean(bullet?.explicitStatus || bullet?.status),
225
+ explicitSource: Boolean(bullet?.explicitSource || bullet?.source),
226
+ explicitConfidence: Boolean(bullet?.explicitConfidence || bullet?.confidence),
227
+ heading: String(bullet?.heading || 'General'),
228
+ section: normalizeTierToSection(preferredTier || fallbackTier),
229
+ lineIndex: Number.isFinite(bullet?.lineIndex) ? /** @type {number} */ (bullet.lineIndex) : 0
230
+ };
231
+ }
232
+
233
+ /**
234
+ * @param {Bullet} bullet
235
+ * @param {string} [today]
236
+ * @returns {boolean}
237
+ */
238
+ export function isExpiredBullet(bullet, today = todayIsoDate()) {
239
+ if (!bullet?.expiresAt) return false;
240
+ return String(bullet.expiresAt) < String(today);
241
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Parsing and rendering for memory bullets.
3
+ */
4
+ /** @import { Bullet } from '../types.js' */
5
+ import {
6
+ safeDateIso,
7
+ normalizeTier,
8
+ normalizeStatus,
9
+ normalizeSource,
10
+ normalizeConfidence,
11
+ normalizeTopic,
12
+ normalizeTierToSection,
13
+ inferTierFromSection,
14
+ inferStatusFromSection,
15
+ ensureBulletMetadata,
16
+ } from './normalize.js';
17
+
18
+ const BULLET_REGEX = /^\s*-\s+(.*)$/;
19
+ const HEADING_REGEX = /^\s{0,3}#{1,6}\s+(.*)$/;
20
+
21
+ /**
22
+ * @param {string} content
23
+ * @returns {Bullet[]}
24
+ */
25
+ export function parseBullets(content) {
26
+ const lines = String(content || '').split('\n');
27
+ const bullets = [];
28
+ let currentHeading = 'General';
29
+ let section = 'long_term';
30
+
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const line = lines[i];
33
+ const headingMatch = line.match(HEADING_REGEX);
34
+ if (headingMatch) {
35
+ currentHeading = headingMatch[1].trim() || currentHeading;
36
+ if (/^(working)$/i.test(currentHeading)) {
37
+ section = 'working';
38
+ } else if (/^(long[- ]?term|active)$/i.test(currentHeading)) {
39
+ section = 'long_term';
40
+ } else if (/^(history|archive)$/i.test(currentHeading)) {
41
+ section = 'history';
42
+ }
43
+ continue;
44
+ }
45
+
46
+ const bulletMatch = line.match(BULLET_REGEX);
47
+ if (!bulletMatch) continue;
48
+
49
+ const raw = bulletMatch[1].trim();
50
+ const parts = raw.split('|').map((part) => part.trim()).filter(Boolean);
51
+ if (parts.length === 0) continue;
52
+
53
+ const text = parts.shift() || '';
54
+ let topic = null;
55
+ let updatedAt = null;
56
+ let expiresAt = null;
57
+ let reviewAt = null;
58
+ let tier = null;
59
+ let status = null;
60
+ let source = null;
61
+ let confidence = null;
62
+
63
+ for (const part of parts) {
64
+ const kv = part.match(/^([a-z_]+)\s*=\s*(.+)$/i);
65
+ if (!kv) continue;
66
+ const key = kv[1].toLowerCase();
67
+ const value = kv[2].trim();
68
+ if (key === 'topic') topic = value;
69
+ if (key === 'updated_at') updatedAt = safeDateIso(value);
70
+ if (key === 'expires_at') expiresAt = safeDateIso(value);
71
+ if (key === 'review_at') reviewAt = safeDateIso(value);
72
+ if (key === 'tier') tier = normalizeTier(value);
73
+ if (key === 'status') status = normalizeStatus(value);
74
+ if (key === 'source') source = normalizeSource(value);
75
+ if (key === 'confidence') confidence = normalizeConfidence(value);
76
+ }
77
+
78
+ bullets.push({
79
+ text,
80
+ topic: topic ? normalizeTopic(topic) : null,
81
+ updatedAt,
82
+ expiresAt,
83
+ reviewAt,
84
+ tier: tier || inferTierFromSection(section),
85
+ status: status || inferStatusFromSection(section),
86
+ source: source || null,
87
+ confidence: confidence || null,
88
+ explicitTier: Boolean(tier),
89
+ explicitStatus: Boolean(status),
90
+ explicitSource: Boolean(source),
91
+ explicitConfidence: Boolean(confidence),
92
+ heading: currentHeading,
93
+ section,
94
+ lineIndex: i
95
+ });
96
+ }
97
+
98
+ return bullets;
99
+ }
100
+
101
+ /**
102
+ * @param {string} content
103
+ * @returns {number}
104
+ */
105
+ export function countBullets(content) {
106
+ return parseBullets(content).length;
107
+ }
108
+
109
+ /**
110
+ * @param {string} content
111
+ * @returns {string[]}
112
+ */
113
+ export function extractTitles(content) {
114
+ const lines = String(content || '').split('\n');
115
+ const titles = [];
116
+
117
+ for (const line of lines) {
118
+ const headingMatch = line.match(HEADING_REGEX);
119
+ if (!headingMatch) continue;
120
+
121
+ const title = headingMatch[1].trim();
122
+ if (!title) continue;
123
+ if (/^(working|long[- ]?term|history|active|archive|current context|stable facts|no longer current)$/i.test(title)) continue;
124
+ titles.push(title);
125
+ }
126
+
127
+ return titles;
128
+ }
129
+
130
+ /**
131
+ * @param {Partial<Bullet>} bullet
132
+ * @returns {string}
133
+ */
134
+ export function renderBullet(bullet) {
135
+ const clean = ensureBulletMetadata(bullet);
136
+ const metadata = [
137
+ `topic=${clean.topic}`,
138
+ `tier=${clean.tier}`,
139
+ `status=${clean.status}`,
140
+ `source=${clean.source}`,
141
+ `confidence=${clean.confidence}`,
142
+ `updated_at=${clean.updatedAt}`
143
+ ];
144
+ if (clean.reviewAt) metadata.push(`review_at=${clean.reviewAt}`);
145
+ if (clean.expiresAt) metadata.push(`expires_at=${clean.expiresAt}`);
146
+ return `- ${clean.text} | ${metadata.join(' | ')}`;
147
+ }
148
+
149
+ function topicHeading(topic) {
150
+ const clean = String(topic || 'general').trim();
151
+ if (!clean) return 'General';
152
+ return clean
153
+ .split(/[\/_-]+/)
154
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
155
+ .join(' ');
156
+ }
157
+
158
+ function inferDocumentTopic(bullets, fallback = 'general') {
159
+ const firstTopic = (bullets || []).find((bullet) => bullet?.topic)?.topic;
160
+ return firstTopic || fallback;
161
+ }
162
+
163
+ function renderSection(lines, title, subsectionTitle, bullets, forceHistory = false) {
164
+ lines.push(`## ${title}`);
165
+ lines.push(`### ${subsectionTitle}`);
166
+
167
+ if (!bullets || bullets.length === 0) {
168
+ lines.push('_No entries yet._');
169
+ return;
170
+ }
171
+
172
+ for (const bullet of bullets) {
173
+ const nextBullet = forceHistory
174
+ ? { ...bullet, tier: 'history', status: bullet.status === 'active' ? 'superseded' : bullet.status, section: 'history' }
175
+ : bullet;
176
+ lines.push(renderBullet(nextBullet));
177
+ }
178
+ }
179
+
180
+ /**
181
+ * @param {Bullet[]} working
182
+ * @param {Bullet[]} longTerm
183
+ * @param {Bullet[]} history
184
+ * @param {{ titleTopic?: string }} [options]
185
+ * @returns {string}
186
+ */
187
+ export function renderCompactedDocument(working, longTerm, history, options = {}) {
188
+ const lines = [];
189
+ const docTopic = normalizeTopic(options.titleTopic || inferDocumentTopic([...working, ...longTerm, ...history], 'general'));
190
+ lines.push(`# Memory: ${topicHeading(docTopic)}`);
191
+ lines.push('');
192
+ renderSection(lines, 'Working', 'Current context', working);
193
+ lines.push('');
194
+ renderSection(lines, 'Long-Term', 'Stable facts', longTerm);
195
+ lines.push('');
196
+ renderSection(lines, 'History', 'No longer current', history, true);
197
+
198
+ return lines.join('\n').trim();
199
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Relevance scoring for memory bullet retrieval.
3
+ */
4
+ /** @import { Bullet } from '../types.js' */
5
+ import {
6
+ normalizeTier,
7
+ normalizeStatus,
8
+ normalizeSource,
9
+ normalizeTierToSection,
10
+ inferStatusFromSection,
11
+ } from './normalize.js';
12
+
13
+ /**
14
+ * @param {Partial<Bullet>} bullet
15
+ * @param {string[]} [queryTerms]
16
+ * @returns {number}
17
+ */
18
+ export function scoreBullet(bullet, queryTerms = []) {
19
+ const text = String(bullet?.text || '').toLowerCase();
20
+ const topic = String(bullet?.topic || '').toLowerCase();
21
+ const tier = normalizeTier(bullet?.tier || bullet?.section || 'long_term');
22
+ const status = normalizeStatus(bullet?.status || inferStatusFromSection(normalizeTierToSection(tier)));
23
+ if (!text) return 0;
24
+
25
+ let score = 0;
26
+ for (const term of queryTerms) {
27
+ if (!term) continue;
28
+ if (text.includes(term)) score += 2;
29
+ if (topic.includes(term)) score += 1;
30
+ }
31
+ if (tier === 'working') score += 2;
32
+ if (tier === 'long_term') score += 1;
33
+ if (status === 'active') score += 2;
34
+ if (status === 'uncertain') score -= 1;
35
+ if (status === 'expired' || status === 'superseded' || tier === 'history') score -= 3;
36
+ if (bullet?.source === 'user_statement') score += 2;
37
+ if (bullet?.source === 'inference') score -= 1;
38
+ if (bullet?.confidence === 'high') score += 1;
39
+ if (bullet?.confidence === 'low') score -= 1;
40
+ return score;
41
+ }
42
+
43
+ /**
44
+ * @param {string} query
45
+ * @returns {string[]}
46
+ */
47
+ export function tokenizeQuery(query) {
48
+ return String(query || '')
49
+ .toLowerCase()
50
+ .split(/[^a-z0-9]+/)
51
+ .map((t) => t.trim())
52
+ .filter((t) => t.length >= 3);
53
+ }