@haisto/opencode-mem 2.16.0-beta.1 → 2.16.0-beta.2
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/README.md +3 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/services/context.d.ts +1 -1
- package/dist/services/context.d.ts.map +1 -1
- package/dist/services/context.js +2 -2
- package/dist/services/logger.js +1 -1
- package/dist/services/user-memory-learning.js +6 -0
- package/dist/services/user-profile/profile-context.d.ts +1 -1
- package/dist/services/user-profile/profile-context.d.ts.map +1 -1
- package/dist/services/user-profile/profile-context.js +113 -22
- package/dist/web/styles.css +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/config.d.ts
CHANGED
|
@@ -55,6 +55,9 @@ export declare let CONFIG: {
|
|
|
55
55
|
excludeCurrentSession: boolean | undefined;
|
|
56
56
|
maxAgeDays: number | undefined;
|
|
57
57
|
injectOn: "first" | "always";
|
|
58
|
+
injectMaxPreferences: number | undefined;
|
|
59
|
+
injectMaxPatterns: number | undefined;
|
|
60
|
+
injectMaxWorkflows: number | undefined;
|
|
58
61
|
};
|
|
59
62
|
};
|
|
60
63
|
export declare function initConfig(directory: string): void;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAulBA,eAAO,IAAI,MAAM;;;;;;;;;;;;;;;;;oBAjET,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;;;mBASX,eAAe,GACf,SAAS,GACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAuCV,OAAO,GACP,QAAQ;;;;;CAYgC,CAAC;AAEnD,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CASlD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
|
package/dist/config.js
CHANGED
|
@@ -59,6 +59,9 @@ const DEFAULTS = {
|
|
|
59
59
|
excludeCurrentSession: true,
|
|
60
60
|
maxAgeDays: undefined,
|
|
61
61
|
injectOn: "first",
|
|
62
|
+
injectMaxPreferences: 5,
|
|
63
|
+
injectMaxPatterns: 5,
|
|
64
|
+
injectMaxWorkflows: 3,
|
|
62
65
|
},
|
|
63
66
|
};
|
|
64
67
|
function expandPath(path) {
|
|
@@ -319,6 +322,22 @@ const CONFIG_TEMPLATE = `{
|
|
|
319
322
|
// Maximum number of memories to return in search results
|
|
320
323
|
"maxMemories": 10,
|
|
321
324
|
|
|
325
|
+
// ============================================
|
|
326
|
+
// Chat Message Injection
|
|
327
|
+
// ============================================
|
|
328
|
+
|
|
329
|
+
// Inject relevant memories into AI chat context
|
|
330
|
+
"chatMessage": {
|
|
331
|
+
"enabled": true,
|
|
332
|
+
"maxMemories": 3,
|
|
333
|
+
"excludeCurrentSession": true,
|
|
334
|
+
"maxAgeDays": undefined,
|
|
335
|
+
"injectOn": "first",
|
|
336
|
+
"injectMaxPreferences": 5,
|
|
337
|
+
"injectMaxPatterns": 5,
|
|
338
|
+
"injectMaxWorkflows": 3
|
|
339
|
+
},
|
|
340
|
+
|
|
322
341
|
// ============================================
|
|
323
342
|
// Advanced Settings
|
|
324
343
|
// ============================================
|
|
@@ -437,6 +456,9 @@ function buildConfig(fileConfig) {
|
|
|
437
456
|
excludeCurrentSession: fileConfig.chatMessage?.excludeCurrentSession ?? DEFAULTS.chatMessage.excludeCurrentSession,
|
|
438
457
|
maxAgeDays: fileConfig.chatMessage?.maxAgeDays,
|
|
439
458
|
injectOn: (fileConfig.chatMessage?.injectOn ?? DEFAULTS.chatMessage.injectOn),
|
|
459
|
+
injectMaxPreferences: fileConfig.chatMessage?.injectMaxPreferences ?? DEFAULTS.chatMessage.injectMaxPreferences,
|
|
460
|
+
injectMaxPatterns: fileConfig.chatMessage?.injectMaxPatterns ?? DEFAULTS.chatMessage.injectMaxPatterns,
|
|
461
|
+
injectMaxWorkflows: fileConfig.chatMessage?.injectMaxWorkflows ?? DEFAULTS.chatMessage.injectMaxWorkflows,
|
|
440
462
|
},
|
|
441
463
|
};
|
|
442
464
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAmB/D,eAAO,MAAM,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAmB/D,eAAO,MAAM,iBAAiB,EAAE,MAqiB/B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { performUserProfileLearning } from "./services/user-memory-learning.js";
|
|
|
8
8
|
import { userPromptManager } from "./services/user-prompt/user-prompt-manager.js";
|
|
9
9
|
import { startWebServer, WebServer } from "./services/web-server.js";
|
|
10
10
|
import { isConfigured, CONFIG, initConfig } from "./config.js";
|
|
11
|
-
import { log } from "./services/logger.js";
|
|
11
|
+
import { log, logTrace } from "./services/logger.js";
|
|
12
12
|
import { getLanguageName } from "./services/language-detector.js";
|
|
13
13
|
export const OpenCodeMemPlugin = async (ctx) => {
|
|
14
14
|
const { directory } = ctx;
|
|
@@ -165,8 +165,14 @@ export const OpenCodeMemPlugin = async (ctx) => {
|
|
|
165
165
|
const cutoffDate = Date.now() - CONFIG.chatMessage.maxAgeDays * 86400000;
|
|
166
166
|
memories = memories.filter((m) => new Date(m.createdAt).getTime() > cutoffDate);
|
|
167
167
|
}
|
|
168
|
-
if (memories.length === 0)
|
|
168
|
+
if (memories.length === 0) {
|
|
169
|
+
logTrace("inject: no memories found, skipping injection");
|
|
169
170
|
return;
|
|
171
|
+
}
|
|
172
|
+
logTrace("inject: memories loaded", {
|
|
173
|
+
count: memories.length,
|
|
174
|
+
summaries: memories.map((m) => m.summary?.slice(0, 120)),
|
|
175
|
+
});
|
|
170
176
|
const projectMemories = {
|
|
171
177
|
results: memories.map((m) => ({
|
|
172
178
|
similarity: 1.0,
|
|
@@ -176,8 +182,9 @@ export const OpenCodeMemPlugin = async (ctx) => {
|
|
|
176
182
|
timing: 0,
|
|
177
183
|
};
|
|
178
184
|
const userId = tags.user.userEmail || null;
|
|
179
|
-
const memoryContext = formatContextForPrompt(userId, projectMemories);
|
|
185
|
+
const memoryContext = formatContextForPrompt(userId, projectMemories, userMessage);
|
|
180
186
|
if (memoryContext) {
|
|
187
|
+
logTrace("inject: memoryContext", { context: memoryContext });
|
|
181
188
|
const contextPart = {
|
|
182
189
|
id: `prt-memory-context-${Date.now()}`,
|
|
183
190
|
sessionID: input.sessionID,
|
|
@@ -6,6 +6,6 @@ interface MemoryResultMinimal {
|
|
|
6
6
|
interface MemoriesResponseMinimal {
|
|
7
7
|
results?: MemoryResultMinimal[];
|
|
8
8
|
}
|
|
9
|
-
export declare function formatContextForPrompt(userId: string | null, projectMemories: MemoriesResponseMinimal): string;
|
|
9
|
+
export declare function formatContextForPrompt(userId: string | null, projectMemories: MemoriesResponseMinimal, userMessage?: string): string;
|
|
10
10
|
export {};
|
|
11
11
|
//# sourceMappingURL=context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/services/context.ts"],"names":[],"mappings":"AAIA,UAAU,mBAAmB;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,uBAAuB;IAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,eAAe,EAAE,uBAAuB,
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/services/context.ts"],"names":[],"mappings":"AAIA,UAAU,mBAAmB;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,uBAAuB;IAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,eAAe,EAAE,uBAAuB,EACxC,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAkCR"}
|
package/dist/services/context.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CONFIG } from "../config.js";
|
|
2
2
|
import { getUserProfileContext } from "./user-profile/profile-context.js";
|
|
3
3
|
import { logDebug } from "./logger.js";
|
|
4
|
-
export function formatContextForPrompt(userId, projectMemories) {
|
|
4
|
+
export function formatContextForPrompt(userId, projectMemories, userMessage) {
|
|
5
5
|
const parts = ["[MEMORY]"];
|
|
6
6
|
if (CONFIG.injectProfile && userId) {
|
|
7
|
-
const profileContext = getUserProfileContext(userId);
|
|
7
|
+
const profileContext = getUserProfileContext(userId, userMessage);
|
|
8
8
|
if (profileContext) {
|
|
9
9
|
parts.push("\n" + profileContext);
|
|
10
10
|
}
|
package/dist/services/logger.js
CHANGED
|
@@ -72,7 +72,7 @@ function writeLog(level, message, data) {
|
|
|
72
72
|
const timestamp = localTimestamp();
|
|
73
73
|
const levelTag = level.toUpperCase().padEnd(5);
|
|
74
74
|
const line = data
|
|
75
|
-
? `[${timestamp}] [${levelTag}] ${message}: ${JSON.stringify(data)}\n`
|
|
75
|
+
? `[${timestamp}] [${levelTag}] ${message}: ${JSON.stringify(data, null, 2)}\n`
|
|
76
76
|
: `[${timestamp}] [${levelTag}] ${message}\n`;
|
|
77
77
|
appendFileSync(logFile, line);
|
|
78
78
|
}
|
|
@@ -88,6 +88,8 @@ ${prompts.map((p, i) => `${i + 1}. ${p.content}`).join("\n\n")}
|
|
|
88
88
|
|
|
89
89
|
## Analysis Guidelines
|
|
90
90
|
|
|
91
|
+
This profile is **cross-project** — it should capture the user's personal style, not project-specific details.
|
|
92
|
+
|
|
91
93
|
Identify and ${existingProfile ? "update" : "create"}:
|
|
92
94
|
|
|
93
95
|
1. **Preferences** (max ${CONFIG.userProfileMaxPreferences})
|
|
@@ -119,6 +121,8 @@ async function analyzeUserProfile(context, existingProfile) {
|
|
|
119
121
|
|
|
120
122
|
Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
|
|
121
123
|
|
|
124
|
+
The profile is **cross-project** — capture the user's personal style and preferences, not project-specific details.
|
|
125
|
+
|
|
122
126
|
CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
|
|
123
127
|
|
|
124
128
|
Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
|
|
@@ -169,6 +173,8 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne
|
|
|
169
173
|
|
|
170
174
|
Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
|
|
171
175
|
|
|
176
|
+
The profile is **cross-project** — capture the user's personal style and preferences, not project-specific details.
|
|
177
|
+
|
|
172
178
|
CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
|
|
173
179
|
|
|
174
180
|
Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function getUserProfileContext(userId: string): string | null;
|
|
1
|
+
export declare function getUserProfileContext(userId: string, userMessage?: string): string | null;
|
|
2
2
|
//# sourceMappingURL=profile-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-context.d.ts","sourceRoot":"","sources":["../../../src/services/user-profile/profile-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"profile-context.d.ts","sourceRoot":"","sources":["../../../src/services/user-profile/profile-context.ts"],"names":[],"mappings":"AAuGA,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoDzF"}
|
|
@@ -1,37 +1,128 @@
|
|
|
1
1
|
import { userProfileManager } from "./user-profile-manager.js";
|
|
2
|
-
|
|
2
|
+
import { CONFIG } from "../../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compute relevance score between a query and a target text.
|
|
5
|
+
* Uses term overlap across all languages via Intl.Segmenter + CJK bigrams.
|
|
6
|
+
*/
|
|
7
|
+
function computeRelevance(query, target) {
|
|
8
|
+
const q = query.toLowerCase();
|
|
9
|
+
const t = target.toLowerCase();
|
|
10
|
+
const terms = extractTerms(q);
|
|
11
|
+
if (terms.length === 0)
|
|
12
|
+
return 0;
|
|
13
|
+
let matches = 0;
|
|
14
|
+
for (const term of terms) {
|
|
15
|
+
if (t.includes(term))
|
|
16
|
+
matches++;
|
|
17
|
+
}
|
|
18
|
+
return matches / terms.length;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract indexable terms from text, supporting all languages.
|
|
22
|
+
*
|
|
23
|
+
* Strategy:
|
|
24
|
+
* 1. Intl.Segmenter (word granularity) across the whole text — captures
|
|
25
|
+
* words/identifiers in any script (Latin, Arabic, Cyrillic, Devanagari, etc.)
|
|
26
|
+
* 2. CJK bigram pass — adds adjacent pairs of CJK characters because word
|
|
27
|
+
* segmenters often under-split Chinese/Japanese/Korean text.
|
|
28
|
+
* 3. All terms are lowercased, deduplicated, and must be >= 2 characters.
|
|
29
|
+
*/
|
|
30
|
+
function extractTerms(text) {
|
|
31
|
+
const terms = new Set();
|
|
32
|
+
// Step 1: Intl.Segmenter — works for all languages
|
|
33
|
+
try {
|
|
34
|
+
const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
|
|
35
|
+
for (const seg of segmenter.segment(text)) {
|
|
36
|
+
const word = seg.segment.toLowerCase();
|
|
37
|
+
if (seg.isWordLike && word.length >= 2) {
|
|
38
|
+
terms.add(word);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Intl.Segmenter not available (very old runtime), fallback to simple regex
|
|
44
|
+
const words = text.toLowerCase().match(/[a-z][a-z0-9_]{2,}/g);
|
|
45
|
+
if (words)
|
|
46
|
+
words.forEach((w) => terms.add(w));
|
|
47
|
+
}
|
|
48
|
+
// Step 2: CJK bigram — catches sub-word units in CJK text
|
|
49
|
+
const cjkChars = [...text].filter((ch) => /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(ch));
|
|
50
|
+
if (cjkChars.length > 1) {
|
|
51
|
+
for (let i = 0; i < cjkChars.length - 1; i++) {
|
|
52
|
+
const bigram = (cjkChars[i] + cjkChars[i + 1]).toLowerCase();
|
|
53
|
+
terms.add(bigram);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return Array.from(terms);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Filter items by relevance to query.
|
|
60
|
+
* When query is empty/undefined, returns top items (must be pre-sorted).
|
|
61
|
+
*/
|
|
62
|
+
function filterRelevant(items, query, maxItems, getText) {
|
|
63
|
+
if (!query || !query.trim()) {
|
|
64
|
+
return items.slice(0, maxItems);
|
|
65
|
+
}
|
|
66
|
+
const q = query.trim();
|
|
67
|
+
const scored = items.map((item, index) => ({
|
|
68
|
+
item,
|
|
69
|
+
index,
|
|
70
|
+
score: computeRelevance(q, getText(item)),
|
|
71
|
+
}));
|
|
72
|
+
const matched = scored
|
|
73
|
+
.filter(({ score }) => score > 0)
|
|
74
|
+
.sort((a, b) => {
|
|
75
|
+
if (b.score !== a.score)
|
|
76
|
+
return b.score - a.score;
|
|
77
|
+
return a.index - b.index; // preserve original order
|
|
78
|
+
})
|
|
79
|
+
.slice(0, maxItems)
|
|
80
|
+
.map(({ item }) => item);
|
|
81
|
+
// Fallback: no items matched the query, return default top maxItems
|
|
82
|
+
if (matched.length === 0) {
|
|
83
|
+
return items.slice(0, maxItems);
|
|
84
|
+
}
|
|
85
|
+
return matched;
|
|
86
|
+
}
|
|
87
|
+
export function getUserProfileContext(userId, userMessage) {
|
|
3
88
|
const profile = userProfileManager.getActiveProfile(userId);
|
|
4
89
|
if (!profile) {
|
|
5
90
|
return null;
|
|
6
91
|
}
|
|
7
92
|
const profileData = JSON.parse(profile.profileData);
|
|
8
93
|
const parts = [];
|
|
94
|
+
const prefMax = CONFIG.chatMessage.injectMaxPreferences ?? 5;
|
|
95
|
+
const patternMax = CONFIG.chatMessage.injectMaxPatterns ?? 5;
|
|
96
|
+
const workflowMax = CONFIG.chatMessage.injectMaxWorkflows ?? 3;
|
|
9
97
|
if (profileData.preferences.length > 0) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.forEach((pref) => {
|
|
15
|
-
|
|
16
|
-
|
|
98
|
+
const items = [...profileData.preferences].sort((a, b) => b.confidence - a.confidence);
|
|
99
|
+
const relevant = filterRelevant(items, userMessage, prefMax, (p) => `${p.category} ${p.description}`);
|
|
100
|
+
if (relevant.length > 0) {
|
|
101
|
+
parts.push("User Preferences:");
|
|
102
|
+
relevant.forEach((pref) => {
|
|
103
|
+
parts.push(`- [${pref.category}] ${pref.description}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
17
106
|
}
|
|
18
107
|
if (profileData.patterns.length > 0) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
.forEach((pattern) => {
|
|
24
|
-
|
|
25
|
-
|
|
108
|
+
const items = [...profileData.patterns].sort((a, b) => b.frequency - a.frequency);
|
|
109
|
+
const relevant = filterRelevant(items, userMessage, patternMax, (p) => `${p.category} ${p.description}`);
|
|
110
|
+
if (relevant.length > 0) {
|
|
111
|
+
parts.push("\nUser Patterns:");
|
|
112
|
+
relevant.forEach((pattern) => {
|
|
113
|
+
parts.push(`- [${pattern.category}] ${pattern.description}`);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
26
116
|
}
|
|
27
117
|
if (profileData.workflows.length > 0) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
.forEach((workflow) => {
|
|
33
|
-
|
|
34
|
-
|
|
118
|
+
const items = [...profileData.workflows].sort((a, b) => b.frequency - a.frequency);
|
|
119
|
+
const relevant = filterRelevant(items, userMessage, workflowMax, (w) => w.description);
|
|
120
|
+
if (relevant.length > 0) {
|
|
121
|
+
parts.push("\nUser Workflows:");
|
|
122
|
+
relevant.forEach((workflow) => {
|
|
123
|
+
parts.push(`- ${workflow.description}`);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
35
126
|
}
|
|
36
127
|
if (parts.length === 0) {
|
|
37
128
|
return null;
|
package/dist/web/styles.css
CHANGED
|
@@ -1842,7 +1842,7 @@ textarea:focus-visible {
|
|
|
1842
1842
|
.pref-bar-btn:hover {
|
|
1843
1843
|
background: var(--om-accent-red);
|
|
1844
1844
|
color: var(--om-text-inverse);
|
|
1845
|
-
}
|
|
1845
|
+
}
|
|
1846
1846
|
|
|
1847
1847
|
.pref-delete-bar .btn-danger {
|
|
1848
1848
|
background: var(--om-accent-red);
|
|
@@ -1909,4 +1909,4 @@ textarea:focus-visible {
|
|
|
1909
1909
|
border-width: 2px;
|
|
1910
1910
|
background: var(--om-blue-tint);
|
|
1911
1911
|
box-shadow: 0 0 0 1px var(--om-blue-border-tint);
|
|
1912
|
-
}
|
|
1912
|
+
}
|
package/package.json
CHANGED