@nguyentamdat/mempalace 1.0.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.
@@ -0,0 +1,487 @@
1
+ type MemoryType = "decision" | "preference" | "milestone" | "problem" | "emotional";
2
+ type Sentiment = "positive" | "negative" | "neutral";
3
+
4
+ export type ExtractedMemory = {
5
+ content: string;
6
+ memory_type: MemoryType;
7
+ chunk_index: number;
8
+ };
9
+
10
+ const DECISION_MARKERS = [
11
+ String.raw`\blet'?s (use|go with|try|pick|choose|switch to)\b`,
12
+ String.raw`\bwe (should|decided|chose|went with|picked|settled on)\b`,
13
+ String.raw`\bi'?m going (to|with)\b`,
14
+ String.raw`\bbetter (to|than|approach|option|choice)\b`,
15
+ String.raw`\binstead of\b`,
16
+ String.raw`\brather than\b`,
17
+ String.raw`\bthe reason (is|was|being)\b`,
18
+ String.raw`\bbecause\b`,
19
+ String.raw`\btrade-?off\b`,
20
+ String.raw`\bpros and cons\b`,
21
+ String.raw`\bover\b.*\bbecause\b`,
22
+ String.raw`\barchitecture\b`,
23
+ String.raw`\bapproach\b`,
24
+ String.raw`\bstrategy\b`,
25
+ String.raw`\bpattern\b`,
26
+ String.raw`\bstack\b`,
27
+ String.raw`\bframework\b`,
28
+ String.raw`\binfrastructure\b`,
29
+ String.raw`\bset (it |this )?to\b`,
30
+ String.raw`\bconfigure\b`,
31
+ String.raw`\bdefault\b`,
32
+ ] as const;
33
+
34
+ const PREFERENCE_MARKERS = [
35
+ String.raw`\bi prefer\b`,
36
+ String.raw`\balways use\b`,
37
+ String.raw`\bnever use\b`,
38
+ String.raw`\bdon'?t (ever |like to )?(use|do|mock|stub|import)\b`,
39
+ String.raw`\bi like (to|when|how)\b`,
40
+ String.raw`\bi hate (when|how|it when)\b`,
41
+ String.raw`\bplease (always|never|don'?t)\b`,
42
+ String.raw`\bmy (rule|preference|style|convention) is\b`,
43
+ String.raw`\bwe (always|never)\b`,
44
+ String.raw`\bfunctional\b.*\bstyle\b`,
45
+ String.raw`\bimperative\b`,
46
+ String.raw`\bsnake_?case\b`,
47
+ String.raw`\bcamel_?case\b`,
48
+ String.raw`\btabs\b.*\bspaces\b`,
49
+ String.raw`\bspaces\b.*\btabs\b`,
50
+ String.raw`\buse\b.*\binstead of\b`,
51
+ ] as const;
52
+
53
+ const MILESTONE_MARKERS = [
54
+ String.raw`\bit works\b`,
55
+ String.raw`\bit worked\b`,
56
+ String.raw`\bgot it working\b`,
57
+ String.raw`\bfixed\b`,
58
+ String.raw`\bsolved\b`,
59
+ String.raw`\bbreakthrough\b`,
60
+ String.raw`\bfigured (it )?out\b`,
61
+ String.raw`\bnailed it\b`,
62
+ String.raw`\bcracked (it|the)\b`,
63
+ String.raw`\bfinally\b`,
64
+ String.raw`\bfirst time\b`,
65
+ String.raw`\bfirst ever\b`,
66
+ String.raw`\bnever (done|been|had) before\b`,
67
+ String.raw`\bdiscovered\b`,
68
+ String.raw`\brealized\b`,
69
+ String.raw`\bfound (out|that)\b`,
70
+ String.raw`\bturns out\b`,
71
+ String.raw`\bthe key (is|was|insight)\b`,
72
+ String.raw`\bthe trick (is|was)\b`,
73
+ String.raw`\bnow i (understand|see|get it)\b`,
74
+ String.raw`\bbuilt\b`,
75
+ String.raw`\bcreated\b`,
76
+ String.raw`\bimplemented\b`,
77
+ String.raw`\bshipped\b`,
78
+ String.raw`\blaunched\b`,
79
+ String.raw`\bdeployed\b`,
80
+ String.raw`\breleased\b`,
81
+ String.raw`\bprototype\b`,
82
+ String.raw`\bproof of concept\b`,
83
+ String.raw`\bdemo\b`,
84
+ String.raw`\bversion \d`,
85
+ String.raw`\bv\d+\.\d+`,
86
+ String.raw`\d+x (compression|faster|slower|better|improvement|reduction)`,
87
+ String.raw`\d+% (reduction|improvement|faster|better|smaller)`,
88
+ ] as const;
89
+
90
+ const PROBLEM_MARKERS = [
91
+ String.raw`\b(bug|error|crash|fail|broke|broken|issue|problem)\b`,
92
+ String.raw`\bdoesn'?t work\b`,
93
+ String.raw`\bnot working\b`,
94
+ String.raw`\bwon'?t\b.*\bwork\b`,
95
+ String.raw`\bkeeps? (failing|crashing|breaking|erroring)\b`,
96
+ String.raw`\broot cause\b`,
97
+ String.raw`\bthe (problem|issue|bug) (is|was)\b`,
98
+ String.raw`\bturns out\b.*\b(was|because|due to)\b`,
99
+ String.raw`\bthe fix (is|was)\b`,
100
+ String.raw`\bworkaround\b`,
101
+ String.raw`\bthat'?s why\b`,
102
+ String.raw`\bthe reason it\b`,
103
+ String.raw`\bfixed (it |the |by )\b`,
104
+ String.raw`\bsolution (is|was)\b`,
105
+ String.raw`\bresolved\b`,
106
+ String.raw`\bpatched\b`,
107
+ String.raw`\bthe answer (is|was)\b`,
108
+ String.raw`\b(had|need) to\b.*\binstead\b`,
109
+ ] as const;
110
+
111
+ const EMOTION_MARKERS = [
112
+ String.raw`\blove\b`,
113
+ String.raw`\bscared\b`,
114
+ String.raw`\bafraid\b`,
115
+ String.raw`\bproud\b`,
116
+ String.raw`\bhurt\b`,
117
+ String.raw`\bhappy\b`,
118
+ String.raw`\bsad\b`,
119
+ String.raw`\bcry\b`,
120
+ String.raw`\bcrying\b`,
121
+ String.raw`\bmiss\b`,
122
+ String.raw`\bsorry\b`,
123
+ String.raw`\bgrateful\b`,
124
+ String.raw`\bangry\b`,
125
+ String.raw`\bworried\b`,
126
+ String.raw`\blonely\b`,
127
+ String.raw`\bbeautiful\b`,
128
+ String.raw`\bamazing\b`,
129
+ String.raw`\bwonderful\b`,
130
+ "i feel",
131
+ "i'm scared",
132
+ "i love you",
133
+ "i'm sorry",
134
+ "i can't",
135
+ "i wish",
136
+ "i miss",
137
+ "i need",
138
+ "never told anyone",
139
+ "nobody knows",
140
+ String.raw`\*[^*]+\*`,
141
+ ] as const;
142
+
143
+ const ALL_MARKERS: Record<MemoryType, readonly string[]> = {
144
+ decision: DECISION_MARKERS,
145
+ preference: PREFERENCE_MARKERS,
146
+ milestone: MILESTONE_MARKERS,
147
+ problem: PROBLEM_MARKERS,
148
+ emotional: EMOTION_MARKERS,
149
+ };
150
+
151
+ const POSITIVE_WORDS = new Set([
152
+ "pride",
153
+ "proud",
154
+ "joy",
155
+ "happy",
156
+ "love",
157
+ "loving",
158
+ "beautiful",
159
+ "amazing",
160
+ "wonderful",
161
+ "incredible",
162
+ "fantastic",
163
+ "brilliant",
164
+ "perfect",
165
+ "excited",
166
+ "thrilled",
167
+ "grateful",
168
+ "warm",
169
+ "breakthrough",
170
+ "success",
171
+ "works",
172
+ "working",
173
+ "solved",
174
+ "fixed",
175
+ "nailed",
176
+ "heart",
177
+ "hug",
178
+ "precious",
179
+ "adore",
180
+ ]);
181
+
182
+ const NEGATIVE_WORDS = new Set([
183
+ "bug",
184
+ "error",
185
+ "crash",
186
+ "crashing",
187
+ "crashed",
188
+ "fail",
189
+ "failed",
190
+ "failing",
191
+ "failure",
192
+ "broken",
193
+ "broke",
194
+ "breaking",
195
+ "breaks",
196
+ "issue",
197
+ "problem",
198
+ "wrong",
199
+ "stuck",
200
+ "blocked",
201
+ "unable",
202
+ "impossible",
203
+ "missing",
204
+ "terrible",
205
+ "horrible",
206
+ "awful",
207
+ "worse",
208
+ "worst",
209
+ "panic",
210
+ "disaster",
211
+ "mess",
212
+ ]);
213
+
214
+ const _CODE_LINE_PATTERNS = [
215
+ /^\s*[$#]\s/,
216
+ /^\s*(cd|source|echo|export|pip|npm|git|python|bash|curl|wget|mkdir|rm|cp|mv|ls|cat|grep|find|chmod|sudo|brew|docker)\s/,
217
+ /^\s*```/,
218
+ /^\s*(import|from|def|class|function|const|let|var|return)\s/,
219
+ /^\s*[A-Z_]{2,}=/,
220
+ /^\s*\|/,
221
+ /^\s*[-]{2,}/,
222
+ /^\s*(?:[{}]|[\[\]])\s*$/,
223
+ /^\s*(if|for|while|try|except|elif|else:)\b/,
224
+ /^\s*\w+\.\w+\(/,
225
+ /^\s*\w+ = \w+\.\w+/,
226
+ ] as const;
227
+
228
+ function _getSentiment(text: string): Sentiment {
229
+ const words = new Set((text.toLowerCase().match(/\b\w+\b/g) ?? []));
230
+ let pos = 0;
231
+ let neg = 0;
232
+
233
+ for (const word of words) {
234
+ if (POSITIVE_WORDS.has(word)) pos += 1;
235
+ if (NEGATIVE_WORDS.has(word)) neg += 1;
236
+ }
237
+
238
+ if (pos > neg) {
239
+ return "positive";
240
+ }
241
+
242
+ if (neg > pos) {
243
+ return "negative";
244
+ }
245
+
246
+ return "neutral";
247
+ }
248
+
249
+ function _hasResolution(text: string): boolean {
250
+ const textLower = text.toLowerCase();
251
+ const patterns = [
252
+ String.raw`\bfixed\b`,
253
+ String.raw`\bsolved\b`,
254
+ String.raw`\bresolved\b`,
255
+ String.raw`\bpatched\b`,
256
+ String.raw`\bgot it working\b`,
257
+ String.raw`\bit works\b`,
258
+ String.raw`\bnailed it\b`,
259
+ String.raw`\bfigured (it )?out\b`,
260
+ String.raw`\bthe (fix|answer|solution)\b`,
261
+ ] as const;
262
+
263
+ return patterns.some((pattern) => new RegExp(pattern).test(textLower));
264
+ }
265
+
266
+ function _disambiguate(memoryType: MemoryType, text: string, scores: Partial<Record<MemoryType, number>>): MemoryType {
267
+ const sentiment = _getSentiment(text);
268
+
269
+ if (memoryType === "problem" && _hasResolution(text)) {
270
+ if ((scores.emotional ?? 0) > 0 && sentiment === "positive") {
271
+ return "emotional";
272
+ }
273
+
274
+ return "milestone";
275
+ }
276
+
277
+ if (memoryType === "problem" && sentiment === "positive") {
278
+ if ((scores.milestone ?? 0) > 0) {
279
+ return "milestone";
280
+ }
281
+
282
+ if ((scores.emotional ?? 0) > 0) {
283
+ return "emotional";
284
+ }
285
+ }
286
+
287
+ return memoryType;
288
+ }
289
+
290
+ function _isCodeLine(line: string): boolean {
291
+ const stripped = line.trim();
292
+ if (!stripped) {
293
+ return false;
294
+ }
295
+
296
+ for (const pattern of _CODE_LINE_PATTERNS) {
297
+ if (pattern.test(stripped)) {
298
+ return true;
299
+ }
300
+ }
301
+
302
+ let alphaCount = 0;
303
+ for (const char of stripped) {
304
+ if ((char >= "a" && char <= "z") || (char >= "A" && char <= "Z")) {
305
+ alphaCount += 1;
306
+ }
307
+ }
308
+
309
+ const alphaRatio = alphaCount / Math.max(stripped.length, 1);
310
+ if (alphaRatio < 0.4 && stripped.length > 10) {
311
+ return true;
312
+ }
313
+
314
+ return false;
315
+ }
316
+
317
+ function _extractProse(text: string): string {
318
+ const lines = text.split("\n");
319
+ const prose: string[] = [];
320
+ let inCode = false;
321
+
322
+ for (const line of lines) {
323
+ if (line.trim().startsWith("```")) {
324
+ inCode = !inCode;
325
+ continue;
326
+ }
327
+
328
+ if (inCode) {
329
+ continue;
330
+ }
331
+
332
+ if (!_isCodeLine(line)) {
333
+ prose.push(line);
334
+ }
335
+ }
336
+
337
+ const result = prose.join("\n").trim();
338
+ return result || text;
339
+ }
340
+
341
+ function _scoreMarkers(text: string, markers: readonly string[]): [number, string[]] {
342
+ const textLower = text.toLowerCase();
343
+ let score = 0;
344
+ const keywords: string[] = [];
345
+
346
+ for (const marker of markers) {
347
+ const regex = new RegExp(marker, "g");
348
+ const matches = Array.from(textLower.matchAll(regex));
349
+ if (matches.length > 0) {
350
+ score += matches.length;
351
+ for (const match of matches) {
352
+ if (match.length > 1 && match[1]) {
353
+ keywords.push(match[1]);
354
+ } else if (match[0]) {
355
+ keywords.push(match[0]);
356
+ } else {
357
+ keywords.push(marker);
358
+ }
359
+ }
360
+ }
361
+ }
362
+
363
+ return [score, [...new Set(keywords)]];
364
+ }
365
+
366
+ export function extractMemories(text: string, minConfidence = 0.3): ExtractedMemory[] {
367
+ const paragraphs = _splitIntoSegments(text);
368
+ const memories: ExtractedMemory[] = [];
369
+
370
+ for (const para of paragraphs) {
371
+ if (para.trim().length < 20) {
372
+ continue;
373
+ }
374
+
375
+ const prose = _extractProse(para);
376
+ const scores: Partial<Record<MemoryType, number>> = {};
377
+
378
+ for (const [memoryType, markers] of Object.entries(ALL_MARKERS) as [MemoryType, readonly string[]][]) {
379
+ const [score] = _scoreMarkers(prose, markers);
380
+ if (score > 0) {
381
+ scores[memoryType] = score;
382
+ }
383
+ }
384
+
385
+ const scoreEntries = Object.entries(scores) as [MemoryType, number][];
386
+ if (scoreEntries.length === 0) {
387
+ continue;
388
+ }
389
+
390
+ let lengthBonus = 0;
391
+ if (para.length > 500) {
392
+ lengthBonus = 2;
393
+ } else if (para.length > 200) {
394
+ lengthBonus = 1;
395
+ }
396
+
397
+ let maxType = scoreEntries[0][0];
398
+ let maxBaseScore = scoreEntries[0][1];
399
+ for (const [memoryType, score] of scoreEntries.slice(1)) {
400
+ if (score > maxBaseScore) {
401
+ maxType = memoryType;
402
+ maxBaseScore = score;
403
+ }
404
+ }
405
+
406
+ const maxScore = maxBaseScore + lengthBonus;
407
+ maxType = _disambiguate(maxType, prose, scores);
408
+
409
+ const confidence = Math.min(1.0, maxScore / 5.0);
410
+ if (confidence < minConfidence) {
411
+ continue;
412
+ }
413
+
414
+ memories.push({
415
+ content: para.trim(),
416
+ memory_type: maxType,
417
+ chunk_index: memories.length,
418
+ });
419
+ }
420
+
421
+ return memories;
422
+ }
423
+
424
+ function _splitIntoSegments(text: string): string[] {
425
+ const lines = text.split("\n");
426
+ const turnPatterns = [
427
+ /^>\s/,
428
+ /^(Human|User|Q)\s*:/i,
429
+ /^(Assistant|AI|A|Claude|ChatGPT)\s*:/i,
430
+ ] as const;
431
+
432
+ let turnCount = 0;
433
+ for (const line of lines) {
434
+ const stripped = line.trim();
435
+ for (const pattern of turnPatterns) {
436
+ if (pattern.test(stripped)) {
437
+ turnCount += 1;
438
+ break;
439
+ }
440
+ }
441
+ }
442
+
443
+ if (turnCount >= 3) {
444
+ return _splitByTurns(lines, turnPatterns);
445
+ }
446
+
447
+ const paragraphs = text
448
+ .split("\n\n")
449
+ .map((paragraph) => paragraph.trim())
450
+ .filter((paragraph) => paragraph.length > 0);
451
+
452
+ if (paragraphs.length <= 1 && lines.length > 20) {
453
+ const segments: string[] = [];
454
+ for (let index = 0; index < lines.length; index += 25) {
455
+ const group = lines.slice(index, index + 25).join("\n").trim();
456
+ if (group) {
457
+ segments.push(group);
458
+ }
459
+ }
460
+ return segments;
461
+ }
462
+
463
+ return paragraphs;
464
+ }
465
+
466
+ function _splitByTurns(lines: string[], turnPatterns: readonly RegExp[]): string[] {
467
+ const segments: string[] = [];
468
+ let current: string[] = [];
469
+
470
+ for (const line of lines) {
471
+ const stripped = line.trim();
472
+ const isTurn = turnPatterns.some((pattern) => pattern.test(stripped));
473
+
474
+ if (isTurn && current.length > 0) {
475
+ segments.push(current.join("\n"));
476
+ current = [line];
477
+ } else {
478
+ current.push(line);
479
+ }
480
+ }
481
+
482
+ if (current.length > 0) {
483
+ segments.push(current.join("\n"));
484
+ }
485
+
486
+ return segments;
487
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+ import { runMain } from "citty";
3
+ import mainCommand from "./cli";
4
+
5
+ runMain(mainCommand);