Skip to content

Commit 90dec1b

Browse files
committed
feat(profile): enhance data handling and UI rendering for user profiles
- Update safeArray and safeObject utilities to handle stringified JSON inputs and nested arrays - Add data cleaning in UserProfileManager to ensure consistent array structures - Implement ensureArray method for robust array validation in merge operations - Improve profile rendering in web UI with better parsing and error handling - Add jsonrepair library for handling malformed JSON in profile data - Update version to 2.7.2
1 parent 50afd52 commit 90dec1b

5 files changed

Lines changed: 139 additions & 41 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-mem",
3-
"version": "2.7.1",
3+
"version": "2.7.2",
44
"description": "OpenCode plugin that gives coding agents persistent memory using local vector database",
55
"type": "module",
66
"main": "dist/plugin.js",
Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,40 @@
1-
export const safeArray = <T>(arr: T[] | null | undefined): T[] => arr ?? [];
2-
export const safeObject = <T extends object>(obj: T | null | undefined, fallback: T): T =>
3-
obj ?? fallback;
1+
export const safeArray = <T>(arr: any): T[] => {
2+
if (!arr) return [];
3+
let result = arr;
4+
if (typeof result === "string") {
5+
try {
6+
result = JSON.parse(result);
7+
} catch {
8+
try {
9+
result = JSON.parse(result.trim().replace(/,$/, ""));
10+
} catch {
11+
return [];
12+
}
13+
}
14+
}
15+
if (!Array.isArray(result)) return [];
16+
17+
const flattened: T[] = [];
18+
const walk = (item: any) => {
19+
if (Array.isArray(item)) {
20+
item.forEach(walk);
21+
} else if (item) {
22+
flattened.push(item);
23+
}
24+
};
25+
walk(result);
26+
return flattened;
27+
};
28+
29+
export const safeObject = <T extends object>(obj: any, fallback: T): T => {
30+
if (!obj) return fallback;
31+
let result = obj;
32+
if (typeof result === "string") {
33+
try {
34+
result = JSON.parse(result);
35+
} catch {
36+
return fallback;
37+
}
38+
}
39+
return result && typeof result === "object" && !Array.isArray(result) ? (result as T) : fallback;
40+
};

src/services/user-profile/user-profile-manager.ts

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ export class UserProfileManager {
8383
const id = `profile_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
8484
const now = Date.now();
8585

86+
const cleanedData: UserProfileData = {
87+
preferences: safeArray(profileData.preferences),
88+
patterns: safeArray(profileData.patterns),
89+
workflows: safeArray(profileData.workflows),
90+
};
91+
8692
const stmt = this.db.prepare(`
8793
INSERT INTO user_profiles (
8894
id, user_id, display_name, user_name, user_email,
@@ -98,13 +104,13 @@ export class UserProfileManager {
98104
displayName,
99105
userName,
100106
userEmail,
101-
JSON.stringify(profileData),
107+
JSON.stringify(cleanedData),
102108
now,
103109
now,
104110
promptsAnalyzed
105111
);
106112

107-
this.addChangelog(id, 1, "create", "Initial profile creation", profileData);
113+
this.addChangelog(id, 1, "create", "Initial profile creation", cleanedData);
108114

109115
return id;
110116
}
@@ -117,6 +123,12 @@ export class UserProfileManager {
117123
): void {
118124
const now = Date.now();
119125

126+
const cleanedData: UserProfileData = {
127+
preferences: safeArray(profileData.preferences),
128+
patterns: safeArray(profileData.patterns),
129+
workflows: safeArray(profileData.workflows),
130+
};
131+
120132
const getVersionStmt = this.db.prepare(`SELECT version FROM user_profiles WHERE id = ?`);
121133
const versionRow = getVersionStmt.get(profileId) as any;
122134
const newVersion = (versionRow?.version || 0) + 1;
@@ -131,14 +143,14 @@ export class UserProfileManager {
131143
`);
132144

133145
updateStmt.run(
134-
JSON.stringify(profileData),
146+
JSON.stringify(cleanedData),
135147
newVersion,
136148
now,
137149
additionalPromptsAnalyzed,
138150
profileId
139151
);
140152

141-
this.addChangelog(profileId, newVersion, "update", changeSummary, profileData);
153+
this.addChangelog(profileId, newVersion, "update", changeSummary, cleanedData);
142154

143155
this.cleanupOldChangelogs(profileId);
144156
}
@@ -268,25 +280,29 @@ export class UserProfileManager {
268280

269281
mergeProfileData(existing: UserProfileData, updates: Partial<UserProfileData>): UserProfileData {
270282
const merged: UserProfileData = {
271-
preferences: safeArray(existing?.preferences),
272-
patterns: safeArray(existing?.patterns),
273-
workflows: safeArray(existing?.workflows),
283+
preferences: this.ensureArray(existing?.preferences),
284+
patterns: this.ensureArray(existing?.patterns),
285+
workflows: this.ensureArray(existing?.workflows),
274286
};
275287

276288
if (updates.preferences) {
277-
for (const newPref of updates.preferences) {
289+
const incomingPrefs = this.ensureArray(updates.preferences);
290+
for (const newPref of incomingPrefs) {
278291
const existingIndex = merged.preferences.findIndex(
279292
(p) => p.category === newPref.category && p.description === newPref.description
280293
);
281294

282295
if (existingIndex >= 0) {
283-
const existing = merged.preferences[existingIndex];
284-
if (existing) {
296+
const existingItem = merged.preferences[existingIndex];
297+
if (existingItem) {
285298
merged.preferences[existingIndex] = {
286299
...newPref,
287-
confidence: Math.min(1, existing.confidence + 0.1),
300+
confidence: Math.min(1, (existingItem.confidence || 0) + 0.1),
288301
evidence: [
289-
...new Set([...safeArray(existing.evidence), ...safeArray(newPref.evidence)]),
302+
...new Set([
303+
...this.ensureArray(existingItem.evidence),
304+
...this.ensureArray(newPref.evidence),
305+
]),
290306
].slice(0, 5),
291307
lastUpdated: Date.now(),
292308
};
@@ -296,22 +312,23 @@ export class UserProfileManager {
296312
}
297313
}
298314

299-
merged.preferences.sort((a, b) => b.confidence - a.confidence);
315+
merged.preferences.sort((a, b) => (b.confidence || 0) - (a.confidence || 0));
300316
merged.preferences = merged.preferences.slice(0, CONFIG.userProfileMaxPreferences);
301317
}
302318

303319
if (updates.patterns) {
304-
for (const newPattern of updates.patterns) {
320+
const incomingPatterns = this.ensureArray(updates.patterns);
321+
for (const newPattern of incomingPatterns) {
305322
const existingIndex = merged.patterns.findIndex(
306323
(p) => p.category === newPattern.category && p.description === newPattern.description
307324
);
308325

309326
if (existingIndex >= 0) {
310-
const existing = merged.patterns[existingIndex];
311-
if (existing) {
327+
const existingItem = merged.patterns[existingIndex];
328+
if (existingItem) {
312329
merged.patterns[existingIndex] = {
313330
...newPattern,
314-
frequency: existing.frequency + 1,
331+
frequency: (existingItem.frequency || 1) + 1,
315332
lastSeen: Date.now(),
316333
};
317334
}
@@ -320,35 +337,48 @@ export class UserProfileManager {
320337
}
321338
}
322339

323-
merged.patterns.sort((a, b) => b.frequency - a.frequency);
340+
merged.patterns.sort((a, b) => (b.frequency || 0) - (a.frequency || 0));
324341
merged.patterns = merged.patterns.slice(0, CONFIG.userProfileMaxPatterns);
325342
}
326343

327344
if (updates.workflows) {
328-
for (const newWorkflow of updates.workflows) {
345+
const incomingWorkflows = this.ensureArray(updates.workflows);
346+
for (const newWorkflow of incomingWorkflows) {
329347
const existingIndex = merged.workflows.findIndex(
330348
(w) => w.description === newWorkflow.description
331349
);
332350

333351
if (existingIndex >= 0) {
334-
const existing = merged.workflows[existingIndex];
335-
if (existing) {
352+
const existingItem = merged.workflows[existingIndex];
353+
if (existingItem) {
336354
merged.workflows[existingIndex] = {
337355
...newWorkflow,
338-
frequency: existing.frequency + 1,
356+
frequency: (existingItem.frequency || 1) + 1,
339357
};
340358
}
341359
} else {
342360
merged.workflows.push({ ...newWorkflow, frequency: 1 });
343361
}
344362
}
345363

346-
merged.workflows.sort((a, b) => b.frequency - a.frequency);
364+
merged.workflows.sort((a, b) => (b.frequency || 0) - (a.frequency || 0));
347365
merged.workflows = merged.workflows.slice(0, CONFIG.userProfileMaxWorkflows);
348366
}
349367

350368
return merged;
351369
}
370+
371+
private ensureArray(val: any): any[] {
372+
if (typeof val === "string") {
373+
try {
374+
const parsed = JSON.parse(val);
375+
return Array.isArray(parsed) ? parsed : [];
376+
} catch {
377+
return [];
378+
}
379+
}
380+
return Array.isArray(val) ? val : [];
381+
}
352382
}
353383

354384
export const userProfileManager = new UserProfileManager();

src/web/app.js

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -927,10 +927,40 @@ function renderUserProfile() {
927927
return;
928928
}
929929

930-
const data = profile.profileData;
931-
const preferences = data.preferences || [];
932-
const patterns = data.patterns || [];
933-
const workflows = data.workflows || [];
930+
let data = profile.profileData;
931+
if (typeof data === "string") {
932+
try {
933+
data = JSON.parse(data);
934+
} catch (e) {
935+
console.error("Failed to parse profileData string", e);
936+
}
937+
}
938+
939+
const parseField = (field) => {
940+
if (!field) return [];
941+
let result = field;
942+
let lastResult = null;
943+
while (typeof result === "string" && result !== lastResult) {
944+
lastResult = result;
945+
try {
946+
result = JSON.parse(typeof jsonrepair === "function" ? jsonrepair(result) : result);
947+
} catch {
948+
break;
949+
}
950+
}
951+
if (!Array.isArray(result)) return [];
952+
const flattened = [];
953+
const walk = (item) => {
954+
if (Array.isArray(item)) item.forEach(walk);
955+
else if (item && typeof item === "object") flattened.push(item);
956+
};
957+
walk(result);
958+
return flattened;
959+
};
960+
961+
const preferences = parseField(data.preferences);
962+
const patterns = parseField(data.patterns);
963+
const workflows = parseField(data.workflows);
934964

935965
container.innerHTML = `
936966
<div class="profile-header">
@@ -965,18 +995,18 @@ function renderUserProfile() {
965995
: `
966996
<div class="cards-grid">
967997
${preferences
968-
.sort((a, b) => b.confidence - a.confidence)
998+
.sort((a, b) => (b.confidence || 0) - (a.confidence || 0))
969999
.map(
9701000
(p) => `
9711001
<div class="compact-card preference-card">
9721002
<div class="card-top">
973-
<span class="category-tag">${escapeHtml(p.category)}</span>
974-
<div class="confidence-ring" style="--p:${Math.round(p.confidence * 100)}">
975-
<span>${Math.round(p.confidence * 100)}%</span>
1003+
<span class="category-tag">${escapeHtml(p.category || "General")}</span>
1004+
<div class="confidence-ring" style="--p:${Math.round((p.confidence || 0) * 100)}">
1005+
<span>${Math.round((p.confidence || 0) * 100)}%</span>
9761006
</div>
9771007
</div>
9781008
<div class="card-body">
979-
<p class="card-text">${escapeHtml(p.description)}</p>
1009+
<p class="card-text">${escapeHtml(p.description || "")}</p>
9801010
</div>
9811011
${
9821012
p.evidence && p.evidence.length > 0
@@ -1009,10 +1039,10 @@ function renderUserProfile() {
10091039
(p) => `
10101040
<div class="compact-card pattern-card">
10111041
<div class="card-top">
1012-
<span class="category-tag">${escapeHtml(p.category)}</span>
1042+
<span class="category-tag">${escapeHtml(p.category || "General")}</span>
10131043
</div>
10141044
<div class="card-body">
1015-
<p class="card-text">${escapeHtml(p.description)}</p>
1045+
<p class="card-text">${escapeHtml(p.description || "")}</p>
10161046
</div>
10171047
</div>
10181048
`
@@ -1034,16 +1064,16 @@ function renderUserProfile() {
10341064
.map(
10351065
(w) => `
10361066
<div class="workflow-row">
1037-
<div class="workflow-title">${escapeHtml(w.description)}</div>
1067+
<div class="workflow-title">${escapeHtml(w.description || "")}</div>
10381068
<div class="workflow-steps-horizontal">
1039-
${w.steps
1069+
${(w.steps || [])
10401070
.map(
10411071
(step, i) => `
10421072
<div class="step-node">
10431073
<span class="step-idx">${i + 1}</span>
10441074
<span class="step-content">${escapeHtml(step)}</span>
10451075
</div>
1046-
${i < w.steps.length - 1 ? '<i data-lucide="arrow-right" class="step-arrow"></i>' : ""}
1076+
${i < (w.steps || []).length - 1 ? '<i data-lucide="arrow-right" class="step-arrow"></i>' : ""}
10471077
`
10481078
)
10491079
.join("")}

src/web/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<script src="https://unpkg.com/lucide@latest"></script>
1010
<script src="https://cdn.jsdelivr.net/npm/marked@17.0.1/lib/marked.umd.min.js"></script>
1111
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.2/dist/purify.min.js"></script>
12+
<script src="https://cdn.jsdelivr.net/npm/jsonrepair@latest/lib/umd/jsonrepair.min.js"></script>
1213
</head>
1314
<body>
1415
<div class="container">

0 commit comments

Comments
 (0)