@memtensor/memos-local-openclaw-plugin 0.3.18 → 0.3.20

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 (69) hide show
  1. package/README.md +21 -11
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +7 -2
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/ingest/dedup.d.ts +2 -2
  11. package/dist/ingest/dedup.d.ts.map +1 -1
  12. package/dist/ingest/dedup.js +4 -4
  13. package/dist/ingest/dedup.js.map +1 -1
  14. package/dist/ingest/task-processor.d.ts +1 -1
  15. package/dist/ingest/task-processor.d.ts.map +1 -1
  16. package/dist/ingest/task-processor.js +14 -13
  17. package/dist/ingest/task-processor.js.map +1 -1
  18. package/dist/ingest/worker.d.ts.map +1 -1
  19. package/dist/ingest/worker.js +7 -3
  20. package/dist/ingest/worker.js.map +1 -1
  21. package/dist/recall/engine.d.ts +5 -1
  22. package/dist/recall/engine.d.ts.map +1 -1
  23. package/dist/recall/engine.js +77 -2
  24. package/dist/recall/engine.js.map +1 -1
  25. package/dist/skill/evolver.d.ts +2 -1
  26. package/dist/skill/evolver.d.ts.map +1 -1
  27. package/dist/skill/evolver.js +2 -2
  28. package/dist/skill/evolver.js.map +1 -1
  29. package/dist/skill/generator.d.ts +3 -1
  30. package/dist/skill/generator.d.ts.map +1 -1
  31. package/dist/skill/generator.js +15 -1
  32. package/dist/skill/generator.js.map +1 -1
  33. package/dist/storage/sqlite.d.ts +24 -8
  34. package/dist/storage/sqlite.d.ts.map +1 -1
  35. package/dist/storage/sqlite.js +233 -28
  36. package/dist/storage/sqlite.js.map +1 -1
  37. package/dist/storage/vector.d.ts +1 -1
  38. package/dist/storage/vector.d.ts.map +1 -1
  39. package/dist/storage/vector.js +3 -3
  40. package/dist/storage/vector.js.map +1 -1
  41. package/dist/types.d.ts +16 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/dist/viewer/html.d.ts +1 -1
  45. package/dist/viewer/html.d.ts.map +1 -1
  46. package/dist/viewer/html.js +107 -1
  47. package/dist/viewer/html.js.map +1 -1
  48. package/dist/viewer/server.d.ts +1 -0
  49. package/dist/viewer/server.d.ts.map +1 -1
  50. package/dist/viewer/server.js +52 -3
  51. package/dist/viewer/server.js.map +1 -1
  52. package/index.ts +187 -7
  53. package/package.json +1 -1
  54. package/skill/browserwing-admin/SKILL.md +521 -0
  55. package/skill/browserwing-executor/SKILL.md +510 -0
  56. package/skill/memos-memory-guide/SKILL.md +62 -36
  57. package/src/capture/index.ts +7 -1
  58. package/src/index.ts +3 -2
  59. package/src/ingest/dedup.ts +4 -2
  60. package/src/ingest/task-processor.ts +14 -13
  61. package/src/ingest/worker.ts +7 -3
  62. package/src/recall/engine.ts +94 -4
  63. package/src/skill/evolver.ts +3 -1
  64. package/src/skill/generator.ts +15 -0
  65. package/src/storage/sqlite.ts +262 -34
  66. package/src/storage/vector.ts +3 -2
  67. package/src/types.ts +18 -0
  68. package/src/viewer/html.ts +107 -1
  69. package/src/viewer/server.ts +48 -3
@@ -182,6 +182,13 @@ input,textarea,select{font-family:inherit;font-size:inherit}
182
182
  .dedup-badge.merged{background:rgba(59,130,246,.12);color:#60a5fa}
183
183
  .import-badge{display:inline-flex;align-items:center;gap:4px;background:rgba(236,72,153,.1);color:#ec4899;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
184
184
  [data-theme="light"] .import-badge{background:rgba(219,39,119,.08);color:#db2777}
185
+ .owner-badge{display:inline-flex;align-items:center;gap:3px;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
186
+ .owner-badge.public{background:rgba(52,211,153,.12);color:#34d399}
187
+ .owner-badge.agent{background:rgba(255,255,255,.06);color:var(--text-sec)}
188
+ [data-theme="light"] .owner-badge.public{background:rgba(16,185,129,.08);color:#059669}
189
+ [data-theme="light"] .owner-badge.agent{background:rgba(0,0,0,.04);color:var(--text-sec)}
190
+ .skill-badge.visibility-public{background:rgba(0,229,255,.12);color:#00bcd4}
191
+ [data-theme="light"] .skill-badge.visibility-public{background:rgba(0,172,193,.08);color:#00838f}
185
192
  .memory-card.dedup-inactive{opacity:.55;border-style:dashed}
186
193
  .memory-card.dedup-inactive:hover{opacity:.85}
187
194
  .dedup-target-link{font-size:11px;color:var(--pri);cursor:pointer;text-decoration:underline;margin-left:4px}
@@ -385,6 +392,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
385
392
  .skill-file-size{font-size:10px;color:var(--text-muted)}
386
393
  .skill-download-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:8px;background:var(--pri-grad);color:#fff;font-size:12px;font-weight:600;border:none;cursor:pointer;transition:all .2s}
387
394
  .skill-download-btn:hover{opacity:.85;transform:translateY(-1px)}
395
+ .skill-vis-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;border:none;cursor:pointer;transition:all .2s}
396
+ .skill-vis-btn:hover{opacity:.85;transform:translateY(-1px)}
397
+ .skill-vis-btn.is-public{background:linear-gradient(135deg,#34d399,#10b981);color:#fff}
398
+ .skill-vis-btn.is-private{background:var(--pri-grad);color:#fff}
399
+ .mem-public-btn{color:var(--pri)!important}
388
400
  .task-skill-section{margin-bottom:16px;padding:14px 16px;border-radius:var(--radius);border:1px solid var(--border)}
389
401
  .task-skill-section.status-generated{border-color:var(--green);background:var(--green-bg)}
390
402
  .task-skill-section.status-generating{border-color:var(--amber);background:var(--amber-bg)}
@@ -760,6 +772,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
760
772
  <option value="newest" data-i18n="filter.newest">Newest first</option>
761
773
  <option value="oldest" data-i18n="filter.oldest">Oldest first</option>
762
774
  </select>
775
+ <span class="filter-sep"></span>
776
+ <select id="filterOwner" class="filter-select" onchange="applyFilters()">
777
+ <option value="" data-i18n="filter.allowners">All owners</option>
778
+ <option value="public" data-i18n="filter.public">Public</option>
779
+ </select>
763
780
  </div>
764
781
  <div class="date-filter">
765
782
  <label data-i18n="filter.from">From</label><input type="datetime-local" id="dateFrom" step="1" onchange="applyFilters()">
@@ -809,12 +826,19 @@ input,textarea,select{font-family:inherit;font-size:inherit}
809
826
  <div class="tasks-stat" style="border-left:3px solid var(--green)"><span class="tasks-stat-value" id="skillsActiveCount">-</span><span class="tasks-stat-label" data-i18n="skills.active">Active</span></div>
810
827
  <div class="tasks-stat" style="border-left:3px solid var(--amber)"><span class="tasks-stat-value" id="skillsDraftCount">-</span><span class="tasks-stat-label" data-i18n="skills.draft">Draft</span></div>
811
828
  <div class="tasks-stat" style="border-left:3px solid var(--violet)"><span class="tasks-stat-value" id="skillsInstalledCount">-</span><span class="tasks-stat-label" data-i18n="skills.installed">Installed</span></div>
829
+ <div class="tasks-stat" style="border-left:3px solid var(--cyan)"><span class="tasks-stat-value" id="skillsPublicCount">-</span><span class="tasks-stat-label" data-i18n="skills.public">Public</span></div>
812
830
  </div>
813
831
  <div class="tasks-filters">
814
832
  <button class="filter-chip active" data-skill-status="" onclick="setSkillStatusFilter(this,'')" data-i18n="filter.all">All</button>
815
833
  <button class="filter-chip" data-skill-status="active" onclick="setSkillStatusFilter(this,'active')" data-i18n="skills.filter.active">Active</button>
816
834
  <button class="filter-chip" data-skill-status="draft" onclick="setSkillStatusFilter(this,'draft')" data-i18n="skills.filter.draft">Draft</button>
817
835
  <button class="filter-chip" data-skill-status="archived" onclick="setSkillStatusFilter(this,'archived')" data-i18n="skills.filter.archived">Archived</button>
836
+ <span class="filter-sep"></span>
837
+ <select id="skillVisibilityFilter" class="filter-select" onchange="loadSkills()">
838
+ <option value="" data-i18n="filter.allvisibility">All visibility</option>
839
+ <option value="public" data-i18n="filter.public">Public</option>
840
+ <option value="private" data-i18n="filter.private">Private</option>
841
+ </select>
818
842
  <button class="btn btn-sm btn-ghost" onclick="loadSkills()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
819
843
  </div>
820
844
  </div>
@@ -825,6 +849,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
825
849
  <div class="task-detail-header">
826
850
  <h2 id="skillDetailTitle"></h2>
827
851
  <div style="display:flex;gap:8px;align-items:center">
852
+ <button class="skill-vis-btn" id="skillVisibilityBtn" onclick="toggleSkillVisibility()"></button>
828
853
  <button class="skill-download-btn" id="skillDownloadBtn" onclick="downloadSkill()" data-i18n="skills.download">\u2B07 Download</button>
829
854
  <button class="btn btn-icon" onclick="closeSkillDetail()" title="Close">\u2715</button>
830
855
  </div>
@@ -1269,6 +1294,11 @@ const I18N={
1269
1294
  'skills.total':'Total Skills',
1270
1295
  'skills.active':'Active',
1271
1296
  'skills.installed':'Installed',
1297
+ 'skills.public':'Public',
1298
+ 'skills.visibility.public':'Public',
1299
+ 'skills.visibility.private':'Private',
1300
+ 'skills.setPublic':'Set Public',
1301
+ 'skills.setPrivate':'Set Private',
1272
1302
  'tasks.total':'Total Tasks',
1273
1303
  'tasks.active':'Active',
1274
1304
  'tasks.completed':'Completed',
@@ -1307,6 +1337,10 @@ const I18N={
1307
1337
  'filter.command':'Command',
1308
1338
  'filter.newest':'Newest first',
1309
1339
  'filter.oldest':'Oldest first',
1340
+ 'filter.allowners':'All owners',
1341
+ 'filter.public':'Public',
1342
+ 'filter.private':'Private',
1343
+ 'filter.allvisibility':'All visibility',
1310
1344
  'filter.from':'From',
1311
1345
  'filter.to':'To',
1312
1346
  'filter.clear':'Clear',
@@ -1358,6 +1392,8 @@ const I18N={
1358
1392
  'toast.deleted':'Memory deleted',
1359
1393
  'toast.opfail':'Operation failed',
1360
1394
  'toast.delfail':'Delete failed',
1395
+ 'toast.setPublic':'Set to public',
1396
+ 'toast.setPrivate':'Set to private',
1361
1397
  'toast.cleared':'All memories cleared',
1362
1398
  'toast.clearfail':'Clear failed',
1363
1399
  'toast.notfound':'Memory not found in cache',
@@ -1529,6 +1565,11 @@ const I18N={
1529
1565
  'skills.total':'技能总数',
1530
1566
  'skills.active':'生效中',
1531
1567
  'skills.installed':'已安装',
1568
+ 'skills.public':'公开',
1569
+ 'skills.visibility.public':'公开',
1570
+ 'skills.visibility.private':'私有',
1571
+ 'skills.setPublic':'设为公开',
1572
+ 'skills.setPrivate':'设为私有',
1532
1573
  'tasks.total':'任务总数',
1533
1574
  'tasks.active':'进行中',
1534
1575
  'tasks.completed':'已完成',
@@ -1567,6 +1608,10 @@ const I18N={
1567
1608
  'filter.command':'命令',
1568
1609
  'filter.newest':'最新优先',
1569
1610
  'filter.oldest':'最早优先',
1611
+ 'filter.allowners':'所有归属',
1612
+ 'filter.public':'公开',
1613
+ 'filter.private':'私有',
1614
+ 'filter.allvisibility':'所有可见性',
1570
1615
  'filter.from':'起始',
1571
1616
  'filter.to':'截止',
1572
1617
  'filter.clear':'清除',
@@ -1618,6 +1663,8 @@ const I18N={
1618
1663
  'toast.deleted':'记忆已删除',
1619
1664
  'toast.opfail':'操作失败',
1620
1665
  'toast.delfail':'删除失败',
1666
+ 'toast.setPublic':'已设为公开',
1667
+ 'toast.setPrivate':'已设为私有',
1621
1668
  'toast.cleared':'所有记忆已清除',
1622
1669
  'toast.clearfail':'清除失败',
1623
1670
  'toast.notfound':'缓存中未找到此记忆',
@@ -2309,6 +2356,8 @@ async function loadSkills(){
2309
2356
  try{
2310
2357
  const params=new URLSearchParams();
2311
2358
  if(skillsStatusFilter) params.set('status',skillsStatusFilter);
2359
+ const visFilter=document.getElementById('skillVisibilityFilter')?.value;
2360
+ if(visFilter) params.set('visibility',visFilter);
2312
2361
  const r=await fetch('/api/skills?'+params);
2313
2362
  const data=await r.json();
2314
2363
 
@@ -2316,6 +2365,7 @@ async function loadSkills(){
2316
2365
  document.getElementById('skillsActiveCount').textContent=formatNum(data.skills.filter(s=>s.status==='active').length);
2317
2366
  document.getElementById('skillsDraftCount').textContent=formatNum(data.skills.filter(s=>s.status==='draft').length);
2318
2367
  document.getElementById('skillsInstalledCount').textContent=formatNum(data.skills.filter(s=>s.installed).length);
2368
+ document.getElementById('skillsPublicCount').textContent=formatNum(data.skills.filter(s=>s.visibility==='public').length);
2319
2369
 
2320
2370
  if(!data.skills||data.skills.length===0){
2321
2371
  list.innerHTML='<div style="text-align:center;padding:48px;color:var(--text-muted);font-size:14px">'+t('skills.empty')+'</div>';
@@ -2329,12 +2379,14 @@ async function loadSkills(){
2329
2379
  const statusClass=skill.status==='archived'?'archived':(skill.status==='draft'?'draft':'');
2330
2380
  const qs=skill.qualityScore;
2331
2381
  const qsBadge=qs!==null&&qs!==undefined?'<span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+qs.toFixed(1)+'</span>':'';
2382
+ const visBadge=skill.visibility==='public'?'<span class="skill-badge visibility-public">\\u{1F310} '+t('skills.visibility.public')+'</span>':'';
2332
2383
  return '<div class="skill-card '+installedClass+' '+statusClass+'" onclick="openSkillDetail(\\''+skill.id+'\\')">'+
2333
2384
  '<div class="skill-card-top">'+
2334
2385
  '<div class="skill-card-name">\\u{1F9E0} '+esc(skill.name)+'</div>'+
2335
2386
  '<div class="skill-card-badges">'+
2336
2387
  qsBadge+
2337
2388
  '<span class="skill-badge version">v'+skill.version+'</span>'+
2389
+ visBadge+
2338
2390
  (skill.installed?'<span class="skill-badge installed">'+t('skills.installed.badge')+'</span>':'')+
2339
2391
  '<span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span>'+
2340
2392
  '</div>'+
@@ -2389,15 +2441,29 @@ async function openSkillDetail(skillId){
2389
2441
 
2390
2442
  const qs=skill.qualityScore;
2391
2443
  const qsBadge=qs!==null&&qs!==undefined?'<span class="meta-item"><span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+qs.toFixed(1)+'/10</span></span>':'';
2444
+ const visMeta=skill.visibility==='public'?'<span class="meta-item"><span class="skill-badge visibility-public">\\u{1F310} '+t('skills.visibility.public')+'</span></span>':'<span class="meta-item"><span class="skill-badge">\\u{1F512} '+t('skills.visibility.private')+'</span></span>';
2392
2445
  document.getElementById('skillDetailMeta').innerHTML=[
2393
2446
  '<span class="meta-item"><span class="skill-badge version">v'+skill.version+'</span></span>',
2394
2447
  '<span class="meta-item"><span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span></span>',
2448
+ visMeta,
2395
2449
  qsBadge,
2396
2450
  skill.installed?'<span class="meta-item"><span class="skill-badge installed">'+t('skills.installed.badge')+'</span></span>':'',
2397
2451
  '<span class="meta-item">\\u{1F4C5} '+formatTime(skill.createdAt)+'</span>',
2398
2452
  '<span class="meta-item">\\u270F '+t('skills.updated')+formatTime(skill.updatedAt)+'</span>',
2399
2453
  ].filter(Boolean).join('');
2400
2454
 
2455
+ const visBtn=document.getElementById('skillVisibilityBtn');
2456
+ visBtn.className='skill-vis-btn';
2457
+ if(skill.visibility==='public'){
2458
+ visBtn.textContent='\\u{1F512} '+t('skills.setPrivate');
2459
+ visBtn.classList.add('is-public');
2460
+ visBtn.dataset.vis='public';
2461
+ } else {
2462
+ visBtn.textContent='\\u{1F310} '+t('skills.setPublic');
2463
+ visBtn.classList.add('is-private');
2464
+ visBtn.dataset.vis='private';
2465
+ }
2466
+
2401
2467
  document.getElementById('skillDetailDesc').textContent=skill.description;
2402
2468
 
2403
2469
  if(files.length>0){
@@ -2464,6 +2530,20 @@ function downloadSkill(){
2464
2530
  window.open('/api/skill/'+currentSkillId+'/download','_blank');
2465
2531
  }
2466
2532
 
2533
+ async function toggleSkillVisibility(){
2534
+ if(!currentSkillId) return;
2535
+ const btn=document.getElementById('skillVisibilityBtn');
2536
+ const newVis=btn.dataset.vis==='public'?'private':'public';
2537
+ try{
2538
+ const r=await fetch('/api/skill/'+currentSkillId+'/visibility',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({visibility:newVis})});
2539
+ if(!r.ok) throw new Error('Failed: '+r.status);
2540
+ openSkillDetail(currentSkillId);
2541
+ loadSkills();
2542
+ }catch(e){
2543
+ alert('Error: '+e.message);
2544
+ }
2545
+ }
2546
+
2467
2547
  /* ─── Settings / Config ─── */
2468
2548
  async function loadConfig(){
2469
2549
  try{
@@ -2911,6 +2991,16 @@ async function loadStats(){
2911
2991
  const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;
2912
2992
  sl.innerHTML+='<div class="session-item'+(isActive?' active':'')+'" onclick="filterSession(\\''+s.session_key.replace(/'/g,"\\\\'")+'\\')"><span title="'+s.session_key+'">'+name+'</span><span class="count">'+s.count+'</span></div>';
2913
2993
  });
2994
+
2995
+ const ownerSel=document.getElementById('filterOwner');
2996
+ if(ownerSel && d.owners && d.owners.length>0){
2997
+ const curVal=ownerSel.value;
2998
+ ownerSel.innerHTML='<option value="">'+t('filter.allowners')+'</option>'+'<option value="public">'+t('filter.public')+'</option>';
2999
+ d.owners.filter(o=>o && o!=='public').forEach(o=>{
3000
+ ownerSel.innerHTML+='<option value="'+o+'">'+o+'</option>';
3001
+ });
3002
+ ownerSel.value=curVal;
3003
+ }
2914
3004
  }
2915
3005
 
2916
3006
  function getFilterParams(){
@@ -2925,6 +3015,8 @@ function getFilterParams(){
2925
3015
  if(dt) p.set('dateTo',dt);
2926
3016
  const sort=document.getElementById('filterSort').value;
2927
3017
  if(sort==='oldest') p.set('sort','oldest');
3018
+ const owner=document.getElementById('filterOwner').value;
3019
+ if(owner) p.set('owner',owner);
2928
3020
  return p;
2929
3021
  }
2930
3022
 
@@ -3028,6 +3120,9 @@ function renderMemories(items){
3028
3120
  const dedupBadge=ds==='duplicate'?'<span class="dedup-badge duplicate">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class="dedup-badge merged">'+t('card.dedupMerged')+'</span>':'';
3029
3121
  const isImported=sid.startsWith('openclaw-import-')||sid.startsWith('openclaw-session-');
3030
3122
  const importBadge=isImported?'<span class="import-badge">\u{1F990} '+t('card.imported')+'</span>':'';
3123
+ const ownerVal=m.owner||'agent:main';
3124
+ const isPublicMem=ownerVal==='public';
3125
+ const ownerBadge=isPublicMem?'<span class="owner-badge public">\\u{1F310} '+t('filter.public')+'</span>':'<span class="owner-badge agent">\\u{1F512} '+t('filter.private')+'</span>';
3031
3126
  let dedupInfo='';
3032
3127
  if(isInactive){
3033
3128
  const reason=m.dedup_reason?'<span style="font-size:11px;color:var(--text-muted)">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';
@@ -3052,7 +3147,7 @@ function renderMemories(items){
3052
3147
  }catch(e){}
3053
3148
  }
3054
3149
  return '<div class="memory-card'+(isInactive?' dedup-inactive':'')+'">'+
3055
- '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span><span class="kind-tag">'+kind+'</span>'+importBadge+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
3150
+ '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span><span class="kind-tag">'+kind+'</span>'+ownerBadge+importBadge+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
3056
3151
  '<div class="card-summary">'+summary+'</div>'+
3057
3152
  dedupInfo+
3058
3153
  '<div class="card-content" id="content-'+id+'"><pre>'+content+'</pre></div>'+
@@ -3061,6 +3156,7 @@ function renderMemories(items){
3061
3156
  '<button class="btn btn-sm btn-ghost" onclick="toggleContent(\\''+id+'\\')">'+t('card.expand')+'</button>'+
3062
3157
  (mc>0?'<button class="btn btn-sm btn-ghost" onclick="toggleHistory(\\''+id+'\\')">'+t('card.evolveHistory')+'</button>':'')+
3063
3158
  '<button class="btn btn-sm btn-ghost" onclick="openEditModal(\\''+id+'\\')">'+t('card.edit')+'</button>'+
3159
+ (isPublicMem?'<button class="btn btn-sm btn-ghost" onclick="toggleMemoryPublic(\\''+id+'\\',false)">\\u{1F512} '+t('skills.setPrivate')+'</button>':'<button class="btn btn-sm btn-ghost mem-public-btn" onclick="toggleMemoryPublic(\\''+id+'\\',true)">\\u{1F310} '+t('skills.setPublic')+'</button>')+
3064
3160
  '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteMemory(\\''+id+'\\')">'+t('card.delete')+'</button>'+
3065
3161
  vscore+
3066
3162
  '</div></div>';
@@ -3249,6 +3345,16 @@ async function deleteMemory(id){
3249
3345
  else{toast(t('toast.delfail'),'error')}
3250
3346
  }
3251
3347
 
3348
+ async function toggleMemoryPublic(id,setPublic){
3349
+ const newOwner=setPublic?'public':'agent:main';
3350
+ try{
3351
+ const r=await fetch('/api/memory/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({owner:newOwner})});
3352
+ const d=await r.json();
3353
+ if(d.ok){toast(setPublic?t('toast.setPublic'):t('toast.setPrivate'),'success');loadAll();}
3354
+ else{toast(d.error||t('toast.opfail'),'error')}
3355
+ }catch(e){toast('Error: '+e.message,'error')}
3356
+ }
3357
+
3252
3358
  async function clearAll(){
3253
3359
  if(!confirm(t('confirm.clearall')))return;
3254
3360
  if(!confirm(t('confirm.clearall2')))return;
@@ -198,6 +198,7 @@ export class ViewerServer {
198
198
  else if (p === "/api/skills" && req.method === "GET") this.serveSkills(res, url);
199
199
  else if (p.match(/^\/api\/skill\/[^/]+\/download$/) && req.method === "GET") this.serveSkillDownload(res, p);
200
200
  else if (p.match(/^\/api\/skill\/[^/]+\/files$/) && req.method === "GET") this.serveSkillFiles(res, p);
201
+ else if (p.match(/^\/api\/skill\/[^/]+\/visibility$/) && req.method === "PUT") this.handleSkillVisibility(req, res, p);
201
202
  else if (p.startsWith("/api/skill/") && req.method === "GET") this.serveSkillDetail(res, p);
202
203
  else if (p === "/api/memory" && req.method === "POST") this.handleCreate(req, res);
203
204
  else if (p.startsWith("/api/memory/") && req.method === "GET") this.serveMemoryDetail(res, p);
@@ -344,6 +345,7 @@ export class ViewerServer {
344
345
  const kind = url.searchParams.get("kind") ?? undefined;
345
346
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
346
347
  const dateTo = url.searchParams.get("dateTo") ?? undefined;
348
+ const owner = url.searchParams.get("owner") ?? undefined;
347
349
  const sortBy = url.searchParams.get("sort") === "oldest" ? "ASC" : "DESC";
348
350
 
349
351
  const db = (this.store as any).db;
@@ -352,6 +354,7 @@ export class ViewerServer {
352
354
  if (session) { conditions.push("session_key = ?"); params.push(session); }
353
355
  if (role) { conditions.push("role = ?"); params.push(role); }
354
356
  if (kind) { conditions.push("kind = ?"); params.push(kind); }
357
+ if (owner) { conditions.push("owner = ?"); params.push(owner); }
355
358
  if (dateFrom) { conditions.push("created_at >= ?"); params.push(new Date(dateFrom).getTime()); }
356
359
  if (dateTo) { conditions.push("created_at <= ?"); params.push(new Date(dateTo).getTime()); }
357
360
 
@@ -486,6 +489,12 @@ export class ViewerServer {
486
489
  dedupBreakdown = Object.fromEntries(dedupRows.map((d: any) => [d.dedup_status ?? "active", d.count]));
487
490
  } catch { /* column may not exist yet */ }
488
491
 
492
+ let owners: string[] = [];
493
+ try {
494
+ const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL ORDER BY owner").all() as any[];
495
+ owners = ownerRows.map((o: any) => o.owner);
496
+ } catch { /* column may not exist yet */ }
497
+
489
498
  this.jsonResponse(res, {
490
499
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
491
500
  totalSkills: skillCount,
@@ -495,6 +504,7 @@ export class ViewerServer {
495
504
  dedupBreakdown,
496
505
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
497
506
  sessions: sessionList,
507
+ owners,
498
508
  });
499
509
  } catch (e) {
500
510
  this.log.warn(`stats error: ${e}`);
@@ -571,7 +581,11 @@ export class ViewerServer {
571
581
 
572
582
  private serveSkills(res: http.ServerResponse, url: URL): void {
573
583
  const status = url.searchParams.get("status") ?? undefined;
574
- const skills = this.store.listSkills({ status });
584
+ const visibility = url.searchParams.get("visibility") ?? undefined;
585
+ let skills = this.store.listSkills({ status });
586
+ if (visibility) {
587
+ skills = skills.filter(s => s.visibility === visibility);
588
+ }
575
589
  this.jsonResponse(res, { skills });
576
590
  }
577
591
 
@@ -697,6 +711,34 @@ export class ViewerServer {
697
711
  }
698
712
  }
699
713
 
714
+ private handleSkillVisibility(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
715
+ const segments = urlPath.split("/");
716
+ const skillId = segments[segments.length - 2];
717
+ this.readBody(req, (body) => {
718
+ try {
719
+ const parsed = JSON.parse(body);
720
+ const visibility = parsed.visibility;
721
+ if (visibility !== "public" && visibility !== "private") {
722
+ res.writeHead(400, { "Content-Type": "application/json" });
723
+ res.end(JSON.stringify({ error: `visibility must be 'public' or 'private', got: '${visibility}'` }));
724
+ return;
725
+ }
726
+ const skill = this.store.getSkill(skillId);
727
+ if (!skill) {
728
+ res.writeHead(404, { "Content-Type": "application/json" });
729
+ res.end(JSON.stringify({ error: `Skill not found: ${skillId}` }));
730
+ return;
731
+ }
732
+ this.store.setSkillVisibility(skillId, visibility);
733
+ this.jsonResponse(res, { ok: true, skillId, visibility });
734
+ } catch (err) {
735
+ this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${err}`);
736
+ res.writeHead(400, { "Content-Type": "application/json" });
737
+ res.end(JSON.stringify({ error: String(err) }));
738
+ }
739
+ });
740
+ }
741
+
700
742
  // ─── CRUD ───
701
743
 
702
744
  private handleCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
@@ -710,7 +752,8 @@ export class ViewerServer {
710
752
  id, sessionKey: data.session_key || "manual", turnId: `manual-${now}`, seq: 0,
711
753
  role: data.role || "user", content: data.content || "", kind: data.kind || "paragraph",
712
754
  summary: data.summary || data.content?.slice(0, 100) || "",
713
- taskId: null, skillId: null, dedupStatus: "active", dedupTarget: null, dedupReason: null,
755
+ taskId: null, skillId: null, owner: data.owner || "agent:main",
756
+ dedupStatus: "active", dedupTarget: null, dedupReason: null,
714
757
  mergeCount: 0, lastHitAt: null, mergeHistory: "[]",
715
758
  createdAt: now, updatedAt: now, embedding: null,
716
759
  });
@@ -741,7 +784,7 @@ export class ViewerServer {
741
784
  this.readBody(req, (body) => {
742
785
  try {
743
786
  const data = JSON.parse(body);
744
- const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, kind: data.kind });
787
+ const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, kind: data.kind, owner: data.owner });
745
788
  if (ok) this.jsonResponse(res, { ok: true, message: "Memory updated" });
746
789
  else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
747
790
  } catch (err) {
@@ -1227,6 +1270,7 @@ export class ViewerServer {
1227
1270
  embedding: null,
1228
1271
  taskId: null,
1229
1272
  skillId: null,
1273
+ owner: `agent:${agentId}`,
1230
1274
  dedupStatus,
1231
1275
  dedupTarget,
1232
1276
  dedupReason,
@@ -1420,6 +1464,7 @@ export class ViewerServer {
1420
1464
  embedding: null,
1421
1465
  taskId: null,
1422
1466
  skillId: null,
1467
+ owner: "agent:main",
1423
1468
  dedupStatus,
1424
1469
  dedupTarget,
1425
1470
  dedupReason,