@memtensor/memos-local-openclaw-plugin 1.0.4-beta.0 → 1.0.4-beta.10

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 (98) hide show
  1. package/.env.example +7 -0
  2. package/README.md +24 -24
  3. package/dist/capture/index.d.ts +1 -1
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +34 -2
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/client/connector.d.ts +5 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +173 -14
  10. package/dist/client/connector.js.map +1 -1
  11. package/dist/client/hub.d.ts.map +1 -1
  12. package/dist/client/hub.js +22 -0
  13. package/dist/client/hub.js.map +1 -1
  14. package/dist/client/skill-sync.d.ts +7 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -1
  16. package/dist/client/skill-sync.js +10 -0
  17. package/dist/client/skill-sync.js.map +1 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +9 -11
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +7 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +301 -106
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +3 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +18 -1
  28. package/dist/hub/user-manager.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +7 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/ingest/providers/index.d.ts.map +1 -1
  33. package/dist/ingest/providers/index.js +37 -6
  34. package/dist/ingest/providers/index.js.map +1 -1
  35. package/dist/recall/engine.d.ts.map +1 -1
  36. package/dist/recall/engine.js +91 -1
  37. package/dist/recall/engine.js.map +1 -1
  38. package/dist/shared/llm-call.d.ts +1 -0
  39. package/dist/shared/llm-call.d.ts.map +1 -1
  40. package/dist/shared/llm-call.js +82 -8
  41. package/dist/shared/llm-call.js.map +1 -1
  42. package/dist/sharing/types.d.ts +1 -1
  43. package/dist/sharing/types.d.ts.map +1 -1
  44. package/dist/skill/evolver.d.ts +2 -0
  45. package/dist/skill/evolver.d.ts.map +1 -1
  46. package/dist/skill/evolver.js +3 -0
  47. package/dist/skill/evolver.js.map +1 -1
  48. package/dist/storage/ensure-binding.d.ts +12 -0
  49. package/dist/storage/ensure-binding.d.ts.map +1 -0
  50. package/dist/storage/ensure-binding.js +53 -0
  51. package/dist/storage/ensure-binding.js.map +1 -0
  52. package/dist/storage/sqlite.d.ts +74 -20
  53. package/dist/storage/sqlite.d.ts.map +1 -1
  54. package/dist/storage/sqlite.js +301 -207
  55. package/dist/storage/sqlite.js.map +1 -1
  56. package/dist/telemetry.d.ts +12 -5
  57. package/dist/telemetry.d.ts.map +1 -1
  58. package/dist/telemetry.js +156 -40
  59. package/dist/telemetry.js.map +1 -1
  60. package/dist/tools/memory-search.d.ts +3 -1
  61. package/dist/tools/memory-search.d.ts.map +1 -1
  62. package/dist/tools/memory-search.js +3 -1
  63. package/dist/tools/memory-search.js.map +1 -1
  64. package/dist/types.d.ts +1 -2
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/dist/viewer/html.d.ts.map +1 -1
  68. package/dist/viewer/html.js +2991 -1041
  69. package/dist/viewer/html.js.map +1 -1
  70. package/dist/viewer/server.d.ts +32 -8
  71. package/dist/viewer/server.d.ts.map +1 -1
  72. package/dist/viewer/server.js +1122 -261
  73. package/dist/viewer/server.js.map +1 -1
  74. package/index.ts +384 -43
  75. package/openclaw.plugin.json +1 -1
  76. package/package.json +3 -2
  77. package/scripts/postinstall.cjs +1 -1
  78. package/skill/memos-memory-guide/SKILL.md +64 -26
  79. package/src/capture/index.ts +37 -1
  80. package/src/client/connector.ts +173 -16
  81. package/src/client/hub.ts +18 -0
  82. package/src/client/skill-sync.ts +14 -0
  83. package/src/config.ts +9 -11
  84. package/src/hub/server.ts +285 -98
  85. package/src/hub/user-manager.ts +20 -3
  86. package/src/index.ts +10 -2
  87. package/src/ingest/providers/index.ts +41 -7
  88. package/src/recall/engine.ts +84 -1
  89. package/src/shared/llm-call.ts +97 -9
  90. package/src/sharing/types.ts +1 -1
  91. package/src/skill/evolver.ts +5 -0
  92. package/src/storage/ensure-binding.ts +52 -0
  93. package/src/storage/sqlite.ts +310 -233
  94. package/src/telemetry.ts +172 -41
  95. package/src/tools/memory-search.ts +2 -1
  96. package/src/types.ts +1 -2
  97. package/src/viewer/html.ts +2991 -1041
  98. package/src/viewer/server.ts +984 -190
@@ -3,7 +3,7 @@ import { createHash } from "crypto";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVisibility, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
6
- import type { GroupInfo, SharedVisibility, UserInfo, UserRole, UserStatus } from "../sharing/types";
6
+ import type { SharedVisibility, UserInfo, UserRole, UserStatus } from "../sharing/types";
7
7
 
8
8
  export class SqliteStore {
9
9
  private db: Database.Database;
@@ -112,6 +112,8 @@ export class SqliteStore {
112
112
  this.migrateSkillEmbeddingsAndFts();
113
113
  this.migrateFtsToTrigram();
114
114
  this.migrateHubTables();
115
+ this.migrateHubFtsToTrigram();
116
+ this.migrateLocalSharedTasksOwner();
115
117
  this.log.debug("Database schema initialized");
116
118
  }
117
119
 
@@ -119,6 +121,16 @@ export class SqliteStore {
119
121
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
120
122
  }
121
123
 
124
+ private migrateLocalSharedTasksOwner(): void {
125
+ try {
126
+ const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
127
+ if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
128
+ this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
129
+ this.log.info("Migrated: added original_owner column to local_shared_tasks");
130
+ }
131
+ } catch { /* table may not exist yet */ }
132
+ }
133
+
122
134
  private migrateOwnerFields(): void {
123
135
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
124
136
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -273,6 +285,51 @@ export class SqliteStore {
273
285
  }
274
286
  }
275
287
 
288
+ private migrateHubFtsToTrigram(): void {
289
+ const tables: Array<{ fts: string; source: string; columns: string; triggers: string[] }> = [
290
+ {
291
+ fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
292
+ triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
293
+ },
294
+ {
295
+ fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
296
+ triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
297
+ },
298
+ {
299
+ fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
300
+ triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
301
+ },
302
+ ];
303
+ for (const t of tables) {
304
+ try {
305
+ const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get() as { sql: string } | undefined;
306
+ if (!row || !row.sql) continue;
307
+ if (row.sql.includes("trigram")) continue;
308
+ this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
309
+ for (const tr of t.triggers) this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
310
+ this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
311
+ this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
312
+ this.db.exec(`
313
+ CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
314
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
315
+ END;
316
+ CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
317
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
318
+ END;
319
+ CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
320
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
321
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
322
+ END
323
+ `);
324
+ this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
325
+ const cnt = (this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get() as { c: number }).c;
326
+ this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
327
+ } catch (err) {
328
+ this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
329
+ }
330
+ }
331
+ }
332
+
276
333
  private migrateTaskId(): void {
277
334
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
278
335
  if (!cols.some((c) => c.name === "task_id")) {
@@ -516,12 +573,13 @@ export class SqliteStore {
516
573
  ).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
517
574
  }
518
575
 
519
- getToolMetrics(minutes: number): {
576
+ getToolMetrics(minutes: number, fromMs?: number, toMs?: number): {
520
577
  tools: string[];
521
578
  series: Array<{ minute: string; [tool: string]: number | string }>;
522
579
  aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
523
580
  } {
524
- const since = Date.now() - minutes * 60 * 1000;
581
+ const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
582
+ const until = toMs ?? Date.now();
525
583
 
526
584
  const rows = this.db.prepare(
527
585
  `SELECT tool_name,
@@ -529,9 +587,9 @@ export class SqliteStore {
529
587
  success,
530
588
  strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
531
589
  FROM tool_calls
532
- WHERE called_at >= ?
590
+ WHERE called_at >= ? AND called_at <= ?
533
591
  ORDER BY called_at`,
534
- ).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
592
+ ).all(since, until) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
535
593
 
536
594
  const toolSet = new Set<string>();
537
595
  const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
@@ -683,34 +741,27 @@ export class SqliteStore {
683
741
  shared_at INTEGER NOT NULL
684
742
  );
685
743
 
744
+ CREATE TABLE IF NOT EXISTS local_shared_memories (
745
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
746
+ original_owner TEXT NOT NULL,
747
+ shared_at INTEGER NOT NULL
748
+ );
749
+
686
750
  CREATE TABLE IF NOT EXISTS hub_users (
687
- id TEXT PRIMARY KEY,
688
- username TEXT NOT NULL UNIQUE,
689
- device_name TEXT NOT NULL DEFAULT '',
690
- role TEXT NOT NULL,
691
- status TEXT NOT NULL,
692
- token_hash TEXT NOT NULL DEFAULT '',
693
- created_at INTEGER NOT NULL,
694
- approved_at INTEGER
751
+ id TEXT PRIMARY KEY,
752
+ username TEXT NOT NULL UNIQUE,
753
+ device_name TEXT NOT NULL DEFAULT '',
754
+ role TEXT NOT NULL,
755
+ status TEXT NOT NULL,
756
+ token_hash TEXT NOT NULL DEFAULT '',
757
+ created_at INTEGER NOT NULL,
758
+ approved_at INTEGER,
759
+ last_ip TEXT NOT NULL DEFAULT '',
760
+ last_active_at INTEGER
695
761
  );
696
762
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
697
763
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
698
764
 
699
- CREATE TABLE IF NOT EXISTS hub_groups (
700
- id TEXT PRIMARY KEY,
701
- name TEXT NOT NULL UNIQUE,
702
- description TEXT NOT NULL DEFAULT '',
703
- created_at INTEGER NOT NULL
704
- );
705
-
706
- CREATE TABLE IF NOT EXISTS hub_group_members (
707
- group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
708
- user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
709
- joined_at INTEGER NOT NULL,
710
- PRIMARY KEY (group_id, user_id)
711
- );
712
- CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
713
-
714
765
  CREATE TABLE IF NOT EXISTS hub_tasks (
715
766
  id TEXT PRIMARY KEY,
716
767
  source_task_id TEXT NOT NULL,
@@ -752,7 +803,7 @@ export class SqliteStore {
752
803
  content,
753
804
  content='hub_chunks',
754
805
  content_rowid='rowid',
755
- tokenize='porter unicode61'
806
+ tokenize='trigram'
756
807
  );
757
808
 
758
809
  CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
@@ -802,7 +853,7 @@ export class SqliteStore {
802
853
  description,
803
854
  content='hub_skills',
804
855
  content_rowid='rowid',
805
- tokenize='porter unicode61'
856
+ tokenize='trigram'
806
857
  );
807
858
 
808
859
  CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
@@ -852,7 +903,7 @@ export class SqliteStore {
852
903
  content,
853
904
  content='hub_memories',
854
905
  content_rowid='rowid',
855
- tokenize='porter unicode61'
906
+ tokenize='trigram'
856
907
  );
857
908
 
858
909
  CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
@@ -872,6 +923,32 @@ export class SqliteStore {
872
923
  VALUES (new.rowid, new.summary, new.content);
873
924
  END;
874
925
  `);
926
+
927
+ this.db.exec(`
928
+ CREATE TABLE IF NOT EXISTS hub_notifications (
929
+ id TEXT PRIMARY KEY,
930
+ user_id TEXT NOT NULL,
931
+ type TEXT NOT NULL,
932
+ resource TEXT NOT NULL,
933
+ title TEXT NOT NULL,
934
+ message TEXT NOT NULL DEFAULT '',
935
+ read INTEGER NOT NULL DEFAULT 0,
936
+ created_at INTEGER NOT NULL
937
+ );
938
+ CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
939
+ `);
940
+
941
+ try {
942
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
943
+ if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
944
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
945
+ this.log.info("Migrated: added last_ip column to hub_users");
946
+ }
947
+ if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
948
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
949
+ this.log.info("Migrated: added last_active_at column to hub_users");
950
+ }
951
+ } catch { /* table may not exist yet */ }
875
952
  }
876
953
 
877
954
  // ─── Write ───
@@ -1047,6 +1124,25 @@ export class SqliteStore {
1047
1124
  }
1048
1125
  }
1049
1126
 
1127
+ hubMemoryPatternSearch(patterns: string[], opts: { limit?: number } = {}): Array<{ memoryId: string; content: string; role: string; createdAt: number }> {
1128
+ if (patterns.length === 0) return [];
1129
+ const limit = opts.limit ?? 10;
1130
+ const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
1131
+ const params: (string | number)[] = [];
1132
+ for (const p of patterns) { params.push(`%${p}%`, `%${p}%`); }
1133
+ params.push(limit);
1134
+ try {
1135
+ const rows = this.db.prepare(`
1136
+ SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
1137
+ FROM hub_memories hm
1138
+ WHERE ${conditions.join(" OR ")}
1139
+ ORDER BY hm.created_at DESC
1140
+ LIMIT ?
1141
+ `).all(...params) as Array<{ memory_id: string; content: string; role: string; created_at: number }>;
1142
+ return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
1143
+ } catch { return []; }
1144
+ }
1145
+
1050
1146
  // ─── Vector Search ───
1051
1147
 
1052
1148
  getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
@@ -1213,6 +1309,8 @@ export class SqliteStore {
1213
1309
  "skill_embeddings",
1214
1310
  "skill_versions",
1215
1311
  "skills",
1312
+ "local_shared_memories",
1313
+ "local_shared_tasks",
1216
1314
  "embeddings",
1217
1315
  "chunks",
1218
1316
  "tasks",
@@ -1684,6 +1782,67 @@ export class SqliteStore {
1684
1782
  return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks }));
1685
1783
  }
1686
1784
 
1785
+ // ─── Local Shared Memories (client-side tracking) ───
1786
+
1787
+ markMemorySharedLocally(chunkId: string): { ok: boolean; owner?: string; originalOwner?: string; sharedAt?: number; reason?: string } {
1788
+ const chunk = this.getChunk(chunkId);
1789
+ if (!chunk) return { ok: false, reason: "not_found" };
1790
+ if (chunk.owner === "public") {
1791
+ const existing = this.getLocalSharedMemory(chunkId);
1792
+ return {
1793
+ ok: true,
1794
+ owner: "public",
1795
+ originalOwner: existing?.originalOwner ?? undefined,
1796
+ sharedAt: existing?.sharedAt ?? undefined,
1797
+ };
1798
+ }
1799
+
1800
+ const sharedAt = Date.now();
1801
+ this.db.transaction(() => {
1802
+ this.db.prepare(`
1803
+ INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
1804
+ VALUES (?, ?, ?)
1805
+ ON CONFLICT(chunk_id) DO UPDATE SET
1806
+ original_owner = excluded.original_owner,
1807
+ shared_at = excluded.shared_at
1808
+ `).run(chunkId, chunk.owner, sharedAt);
1809
+ this.updateChunk(chunkId, { owner: "public" });
1810
+ })();
1811
+
1812
+ return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
1813
+ }
1814
+
1815
+ unmarkMemorySharedLocally(chunkId: string, fallbackOwner?: string): { ok: boolean; owner?: string; originalOwner?: string; reason?: string } {
1816
+ const chunk = this.getChunk(chunkId);
1817
+ if (!chunk) return { ok: false, reason: "not_found" };
1818
+ if (chunk.owner !== "public") {
1819
+ return { ok: true, owner: chunk.owner };
1820
+ }
1821
+
1822
+ const existing = this.getLocalSharedMemory(chunkId);
1823
+ const restoreOwner = existing?.originalOwner ?? fallbackOwner;
1824
+ if (!restoreOwner || restoreOwner === "public") {
1825
+ return { ok: false, reason: "original_owner_missing" };
1826
+ }
1827
+
1828
+ this.db.transaction(() => {
1829
+ this.updateChunk(chunkId, { owner: restoreOwner });
1830
+ this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
1831
+ })();
1832
+
1833
+ return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
1834
+ }
1835
+
1836
+ getLocalSharedMemory(chunkId: string): { chunkId: string; originalOwner: string; sharedAt: number } | null {
1837
+ const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId) as any;
1838
+ if (!row) return null;
1839
+ return {
1840
+ chunkId: row.chunk_id,
1841
+ originalOwner: row.original_owner,
1842
+ sharedAt: row.shared_at,
1843
+ };
1844
+ }
1845
+
1687
1846
  // ─── Hub Users / Groups ───
1688
1847
 
1689
1848
  upsertHubUser(user: HubUserRecord): void {
@@ -1704,74 +1863,41 @@ export class SqliteStore {
1704
1863
  getHubUser(userId: string): HubUserRecord | null {
1705
1864
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
1706
1865
  if (!row) return null;
1707
- return this.attachGroupsToHubUser(rowToHubUser(row));
1866
+ return rowToHubUser(row);
1708
1867
  }
1709
1868
 
1710
1869
  listHubUsers(status?: UserStatus): HubUserRecord[] {
1711
1870
  const rows = status
1712
1871
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
1713
1872
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
1714
- return rows.map((row) => this.attachGroupsToHubUser(rowToHubUser(row)));
1873
+ return rows.map(rowToHubUser);
1715
1874
  }
1716
1875
 
1717
- upsertHubGroup(group: HubGroupRecord): void {
1718
- this.db.prepare(`
1719
- INSERT INTO hub_groups (id, name, description, created_at)
1720
- VALUES (?, ?, ?, ?)
1721
- ON CONFLICT(id) DO UPDATE SET
1722
- name = excluded.name,
1723
- description = excluded.description,
1724
- created_at = excluded.created_at
1725
- `).run(group.id, group.name, group.description, group.createdAt);
1726
- }
1727
-
1728
- listHubGroups(): HubGroupRecord[] {
1729
- const rows = this.db.prepare('SELECT * FROM hub_groups ORDER BY name').all() as HubGroupRow[];
1730
- return rows.map(rowToHubGroup);
1731
- }
1732
-
1733
- addHubGroupMember(groupId: string, userId: string, joinedAt = Date.now()): void {
1734
- this.db.prepare(`
1735
- INSERT INTO hub_group_members (group_id, user_id, joined_at)
1736
- VALUES (?, ?, ?)
1737
- ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
1738
- `).run(groupId, userId, joinedAt);
1739
- }
1740
-
1741
- getHubGroupById(groupId: string): HubGroupRecord | undefined {
1742
- const row = this.db.prepare('SELECT * FROM hub_groups WHERE id = ?').get(groupId) as HubGroupRow | undefined;
1743
- return row ? rowToHubGroup(row) : undefined;
1744
- }
1745
-
1746
- deleteHubGroup(groupId: string): boolean {
1747
- const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
1876
+ deleteHubUser(userId: string, cleanResources = false): boolean {
1877
+ if (cleanResources) {
1878
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1879
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
1880
+ this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
1881
+ const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1882
+ return result.changes > 0;
1883
+ }
1884
+ const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '' WHERE id = ?").run(userId);
1748
1885
  return result.changes > 0;
1749
1886
  }
1750
1887
 
1751
- listHubGroupMembers(groupId: string): Array<{ userId: string; username: string; joinedAt: number }> {
1752
- const rows = this.db.prepare(`
1753
- SELECT gm.user_id, hu.username, gm.joined_at
1754
- FROM hub_group_members gm
1755
- JOIN hub_users hu ON hu.id = gm.user_id
1756
- WHERE gm.group_id = ?
1757
- ORDER BY gm.joined_at
1758
- `).all(groupId) as Array<{ user_id: string; username: string; joined_at: number }>;
1759
- return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
1760
- }
1761
-
1762
- removeHubGroupMember(groupId: string, userId: string): void {
1763
- this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1888
+ updateHubUserActivity(userId: string, ip: string, timestamp?: number): void {
1889
+ this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1764
1890
  }
1765
1891
 
1766
- getGroupsForHubUser(userId: string): GroupInfo[] {
1767
- const rows = this.db.prepare(`
1768
- SELECT g.*
1769
- FROM hub_group_members gm
1770
- JOIN hub_groups g ON g.id = gm.group_id
1771
- WHERE gm.user_id = ?
1772
- ORDER BY g.name
1773
- `).all(userId) as HubGroupRow[];
1774
- return rows.map((row) => ({ id: row.id, name: row.name, description: row.description || undefined }));
1892
+ getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
1893
+ const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
1894
+ const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
1895
+ const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
1896
+ const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
1897
+ for (const r of memRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].memoryCount = r.cnt; }
1898
+ for (const r of taskRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].taskCount = r.cnt; }
1899
+ for (const r of skillRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].skillCount = r.cnt; }
1900
+ return result;
1775
1901
  }
1776
1902
 
1777
1903
  // ─── Hub Shared Data ───
@@ -1795,6 +1921,11 @@ export class SqliteStore {
1795
1921
  return row ? rowToHubTask(row) : null;
1796
1922
  }
1797
1923
 
1924
+ getHubTaskById(taskId: string): HubTaskRecord | null {
1925
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId) as HubTaskRow | undefined;
1926
+ return row ? rowToHubTask(row) : null;
1927
+ }
1928
+
1798
1929
  upsertHubChunk(chunk: HubChunkUpsertInput): void {
1799
1930
  if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
1800
1931
  const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
@@ -1874,24 +2005,16 @@ export class SqliteStore {
1874
2005
  const limit = options?.maxResults ?? 10;
1875
2006
  const userId = options?.userId ?? "";
1876
2007
  const rows = this.db.prepare(`
1877
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, hg.name as group_name, hu.username as owner_name,
2008
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
1878
2009
  bm25(hub_chunks_fts) as rank
1879
2010
  FROM hub_chunks_fts f
1880
2011
  JOIN hub_chunks hc ON hc.rowid = f.rowid
1881
2012
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1882
- LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1883
2013
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1884
2014
  WHERE hub_chunks_fts MATCH ?
1885
- AND (
1886
- ht.visibility = 'public'
1887
- OR EXISTS (
1888
- SELECT 1 FROM hub_group_members gm
1889
- WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1890
- )
1891
- )
1892
2015
  ORDER BY rank
1893
2016
  LIMIT ?
1894
- `).all(sanitizeFtsQuery(query), userId, limit) as HubSearchRow[];
2017
+ `).all(sanitizeFtsQuery(query), limit) as HubSearchRow[];
1895
2018
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1896
2019
  }
1897
2020
 
@@ -1916,12 +2039,7 @@ export class SqliteStore {
1916
2039
  FROM hub_embeddings he
1917
2040
  JOIN hub_chunks hc ON hc.id = he.chunk_id
1918
2041
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1919
- WHERE ht.visibility = 'public'
1920
- OR EXISTS (
1921
- SELECT 1 FROM hub_group_members gm
1922
- WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1923
- )
1924
- `).all(userId) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
2042
+ `).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
1925
2043
  return rows.map(r => ({
1926
2044
  chunkId: r.chunk_id,
1927
2045
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -1930,22 +2048,14 @@ export class SqliteStore {
1930
2048
 
1931
2049
  getVisibleHubSearchHitByChunkId(chunkId: string, userId: string): HubSearchRow | null {
1932
2050
  const row = this.db.prepare(`
1933
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, hg.name as group_name, hu.username as owner_name,
2051
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
1934
2052
  0 as rank
1935
2053
  FROM hub_chunks hc
1936
2054
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1937
- LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1938
2055
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1939
2056
  WHERE hc.id = ?
1940
- AND (
1941
- ht.visibility = 'public'
1942
- OR EXISTS (
1943
- SELECT 1 FROM hub_group_members gm
1944
- WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1945
- )
1946
- )
1947
2057
  LIMIT 1
1948
- `).get(chunkId, userId) as HubSearchRow | undefined;
2058
+ `).get(chunkId) as HubSearchRow | undefined;
1949
2059
  return row ?? null;
1950
2060
  }
1951
2061
 
@@ -1961,38 +2071,24 @@ export class SqliteStore {
1961
2071
  let rows: HubSkillSearchRow[];
1962
2072
  if (sanitized) {
1963
2073
  rows = this.db.prepare(`
1964
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, hg.name AS group_name, hu.username AS owner_name, hs.quality_score,
2074
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
1965
2075
  bm25(hub_skills_fts) as rank
1966
2076
  FROM hub_skills_fts f
1967
2077
  JOIN hub_skills hs ON hs.rowid = f.rowid
1968
- LEFT JOIN hub_groups hg ON hg.id = hs.group_id
1969
2078
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
1970
2079
  WHERE hub_skills_fts MATCH ?
1971
- AND (
1972
- hs.visibility = 'public'
1973
- OR EXISTS (
1974
- SELECT 1 FROM hub_group_members gm
1975
- WHERE gm.group_id = hs.group_id AND gm.user_id = ?
1976
- )
1977
- )
1978
2080
  ORDER BY rank
1979
2081
  LIMIT ?
1980
- `).all(sanitized, userId, limit) as HubSkillSearchRow[];
2082
+ `).all(sanitized, limit) as HubSkillSearchRow[];
1981
2083
  } else {
1982
2084
  rows = this.db.prepare(`
1983
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, hg.name AS group_name, hu.username AS owner_name, hs.quality_score,
2085
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
1984
2086
  0 as rank
1985
2087
  FROM hub_skills hs
1986
- LEFT JOIN hub_groups hg ON hg.id = hs.group_id
1987
2088
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
1988
- WHERE hs.visibility = 'public'
1989
- OR EXISTS (
1990
- SELECT 1 FROM hub_group_members gm
1991
- WHERE gm.group_id = hs.group_id AND gm.user_id = ?
1992
- )
1993
2089
  ORDER BY hs.updated_at DESC
1994
2090
  LIMIT ?
1995
- `).all(userId, limit) as HubSkillSearchRow[];
2091
+ `).all(limit) as HubSkillSearchRow[];
1996
2092
  }
1997
2093
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1998
2094
  }
@@ -2001,87 +2097,78 @@ export class SqliteStore {
2001
2097
  this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
2002
2098
  }
2003
2099
 
2004
- listVisibleHubTasks(userId: string, limit = 40): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; chunkCount: number; createdAt: number; updatedAt: number }> {
2100
+ listVisibleHubTasks(userId: string, limit = 40): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; chunkCount: number; createdAt: number; updatedAt: number }> {
2005
2101
  const rows = this.db.prepare(`
2006
- SELECT t.*, u.username AS owner_name, g.name AS group_name,
2102
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
2007
2103
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2008
2104
  FROM hub_tasks t
2009
2105
  LEFT JOIN hub_users u ON u.id = t.source_user_id
2010
- LEFT JOIN hub_groups g ON g.id = t.group_id
2011
- WHERE t.visibility = 'public'
2012
- OR EXISTS (
2013
- SELECT 1 FROM hub_group_members gm
2014
- WHERE gm.group_id = t.group_id AND gm.user_id = ?
2015
- )
2016
2106
  ORDER BY t.updated_at DESC
2017
2107
  LIMIT ?
2018
- `).all(userId, limit) as any[];
2108
+ `).all(limit) as any[];
2019
2109
  return rows.map(r => ({
2020
2110
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2021
2111
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
2022
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2112
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
2023
2113
  createdAt: r.created_at, updatedAt: r.updated_at,
2024
2114
  }));
2025
2115
  }
2026
2116
 
2027
- listAllHubTasks(): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; chunkCount: number; createdAt: number; updatedAt: number }> {
2117
+ listAllHubTasks(): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; chunkCount: number; createdAt: number; updatedAt: number }> {
2028
2118
  const rows = this.db.prepare(`
2029
- SELECT t.*, u.username AS owner_name, g.name AS group_name,
2119
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status,
2030
2120
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2031
2121
  FROM hub_tasks t
2032
2122
  LEFT JOIN hub_users u ON u.id = t.source_user_id
2033
- LEFT JOIN hub_groups g ON g.id = t.group_id
2034
2123
  ORDER BY t.updated_at DESC
2035
2124
  `).all() as any[];
2036
2125
  return rows.map(r => ({
2037
2126
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2038
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
2039
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2127
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
2128
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
2040
2129
  createdAt: r.created_at, updatedAt: r.updated_at,
2041
2130
  }));
2042
2131
  }
2043
2132
 
2133
+ listHubChunksByTaskId(hubTaskId: string): HubChunkRecord[] {
2134
+ const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId) as HubChunkRow[];
2135
+ return rows.map(rowToHubChunk);
2136
+ }
2137
+
2044
2138
  deleteHubTaskById(taskId: string): boolean {
2045
2139
  const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
2046
2140
  return info.changes > 0;
2047
2141
  }
2048
2142
 
2049
- listVisibleHubSkills(userId: string, limit = 40): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
2143
+ listVisibleHubSkills(userId: string, limit = 40): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
2050
2144
  const rows = this.db.prepare(`
2051
- SELECT s.*, u.username AS owner_name, g.name AS group_name
2145
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2052
2146
  FROM hub_skills s
2053
2147
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2054
- LEFT JOIN hub_groups g ON g.id = s.group_id
2055
- WHERE s.visibility = 'public'
2056
- OR EXISTS (
2057
- SELECT 1 FROM hub_group_members gm
2058
- WHERE gm.group_id = s.group_id AND gm.user_id = ?
2059
- )
2060
2148
  ORDER BY s.updated_at DESC
2061
2149
  LIMIT ?
2062
- `).all(userId, limit) as any[];
2150
+ `).all(limit) as any[];
2063
2151
  return rows.map(r => ({
2064
2152
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2065
2153
  name: r.name, description: r.description, version: r.version,
2066
2154
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2067
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2155
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2068
2156
  createdAt: r.created_at, updatedAt: r.updated_at,
2069
2157
  }));
2070
2158
  }
2071
2159
 
2072
- listAllHubSkills(): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
2160
+ listAllHubSkills(): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
2073
2161
  const rows = this.db.prepare(`
2074
- SELECT s.*, u.username AS owner_name, g.name AS group_name
2162
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status
2075
2163
  FROM hub_skills s
2076
2164
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2077
- LEFT JOIN hub_groups g ON g.id = s.group_id
2078
2165
  ORDER BY s.updated_at DESC
2079
2166
  `).all() as any[];
2080
2167
  return rows.map(r => ({
2081
2168
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2082
2169
  name: r.name, description: r.description, version: r.version,
2083
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2084
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2170
+ groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2171
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2085
2172
  createdAt: r.created_at, updatedAt: r.updated_at,
2086
2173
  }));
2087
2174
  }
@@ -2128,6 +2215,47 @@ export class SqliteStore {
2128
2215
  return info.changes > 0;
2129
2216
  }
2130
2217
 
2218
+ // ─── Hub Notifications ───
2219
+
2220
+ insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
2221
+ this.db.prepare(
2222
+ 'INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)'
2223
+ ).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
2224
+ }
2225
+
2226
+ hasRecentHubNotification(userId: string, type: string, resource: string, windowMs: number = 300_000): boolean {
2227
+ const since = Date.now() - windowMs;
2228
+ const row = this.db.prepare(
2229
+ 'SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?'
2230
+ ).get(userId, type, resource, since) as { cnt: number };
2231
+ return row.cnt > 0;
2232
+ }
2233
+
2234
+ listHubNotifications(userId: string, opts?: { unreadOnly?: boolean; limit?: number }): Array<{ id: string; userId: string; type: string; resource: string; title: string; message: string; read: boolean; createdAt: number }> {
2235
+ const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2236
+ const limit = opts?.limit ?? 50;
2237
+ const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit) as any[];
2238
+ return rows.map(r => ({ id: r.id, userId: r.user_id, type: r.type, resource: r.resource, title: r.title, message: r.message, read: !!r.read, createdAt: r.created_at }));
2239
+ }
2240
+
2241
+ countUnreadHubNotifications(userId: string): number {
2242
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId) as { cnt: number };
2243
+ return row.cnt;
2244
+ }
2245
+
2246
+ markHubNotificationsRead(userId: string, ids?: string[]): void {
2247
+ if (ids && ids.length > 0) {
2248
+ const placeholders = ids.map(() => '?').join(',');
2249
+ this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
2250
+ } else {
2251
+ this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
2252
+ }
2253
+ }
2254
+
2255
+ clearHubNotifications(userId: string): void {
2256
+ this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
2257
+ }
2258
+
2131
2259
  upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
2132
2260
  const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
2133
2261
  this.db.prepare(`
@@ -2149,23 +2277,15 @@ export class SqliteStore {
2149
2277
  const sanitized = sanitizeFtsQuery(query);
2150
2278
  if (!sanitized) return [];
2151
2279
  const rows = this.db.prepare(`
2152
- SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, hg.name as group_name, hu.username as owner_name,
2280
+ SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
2153
2281
  bm25(hub_memories_fts) as rank
2154
2282
  FROM hub_memories_fts f
2155
2283
  JOIN hub_memories hm ON hm.rowid = f.rowid
2156
- LEFT JOIN hub_groups hg ON hg.id = hm.group_id
2157
2284
  LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2158
2285
  WHERE hub_memories_fts MATCH ?
2159
- AND (
2160
- hm.visibility = 'public'
2161
- OR EXISTS (
2162
- SELECT 1 FROM hub_group_members gm
2163
- WHERE gm.group_id = hm.group_id AND gm.user_id = ?
2164
- )
2165
- )
2166
2286
  ORDER BY rank
2167
2287
  LIMIT ?
2168
- `).all(sanitized, userId, limit) as HubMemorySearchRow[];
2288
+ `).all(sanitized, limit) as HubMemorySearchRow[];
2169
2289
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
2170
2290
  }
2171
2291
 
@@ -2174,12 +2294,7 @@ export class SqliteStore {
2174
2294
  SELECT hme.memory_id, hme.vector, hme.dimensions
2175
2295
  FROM hub_memory_embeddings hme
2176
2296
  JOIN hub_memories hm ON hm.id = hme.memory_id
2177
- WHERE hm.visibility = 'public'
2178
- OR EXISTS (
2179
- SELECT 1 FROM hub_group_members gm
2180
- WHERE gm.group_id = hm.group_id AND gm.user_id = ?
2181
- )
2182
- `).all(userId) as Array<{ memory_id: string; vector: Buffer; dimensions: number }>;
2297
+ `).all() as Array<{ memory_id: string; vector: Buffer; dimensions: number }>;
2183
2298
  return rows.map(r => ({
2184
2299
  memoryId: r.memory_id,
2185
2300
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -2188,59 +2303,44 @@ export class SqliteStore {
2188
2303
 
2189
2304
  getVisibleHubSearchHitByMemoryId(memoryId: string, userId: string): HubMemorySearchRow | null {
2190
2305
  const row = this.db.prepare(`
2191
- SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, hg.name as group_name, hu.username as owner_name,
2306
+ SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
2192
2307
  0 as rank
2193
2308
  FROM hub_memories hm
2194
- LEFT JOIN hub_groups hg ON hg.id = hm.group_id
2195
2309
  LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2196
2310
  WHERE hm.id = ?
2197
- AND (
2198
- hm.visibility = 'public'
2199
- OR EXISTS (
2200
- SELECT 1 FROM hub_group_members gm
2201
- WHERE gm.group_id = hm.group_id AND gm.user_id = ?
2202
- )
2203
- )
2204
2311
  LIMIT 1
2205
- `).get(memoryId, userId) as HubMemorySearchRow | undefined;
2312
+ `).get(memoryId) as HubMemorySearchRow | undefined;
2206
2313
  return row ?? null;
2207
2314
  }
2208
2315
 
2209
- listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
2316
+ listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; createdAt: number; updatedAt: number }> {
2210
2317
  const rows = this.db.prepare(`
2211
- SELECT m.*, u.username AS owner_name, g.name AS group_name
2318
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2212
2319
  FROM hub_memories m
2213
2320
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2214
- LEFT JOIN hub_groups g ON g.id = m.group_id
2215
- WHERE m.visibility = 'public'
2216
- OR EXISTS (
2217
- SELECT 1 FROM hub_group_members gm
2218
- WHERE gm.group_id = m.group_id AND gm.user_id = ?
2219
- )
2220
2321
  ORDER BY m.updated_at DESC
2221
2322
  LIMIT ?
2222
- `).all(userId, limit) as any[];
2323
+ `).all(limit) as any[];
2223
2324
  return rows.map(r => ({
2224
2325
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2225
- role: r.role, summary: r.summary, kind: r.kind,
2326
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2226
2327
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2227
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2328
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2228
2329
  }));
2229
2330
  }
2230
2331
 
2231
- listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
2332
+ listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; createdAt: number; updatedAt: number }> {
2232
2333
  const rows = this.db.prepare(`
2233
- SELECT m.*, u.username AS owner_name, g.name AS group_name
2334
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status
2234
2335
  FROM hub_memories m
2235
2336
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2236
- LEFT JOIN hub_groups g ON g.id = m.group_id
2237
2337
  ORDER BY m.updated_at DESC
2238
2338
  `).all() as any[];
2239
2339
  return rows.map(r => ({
2240
2340
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2241
- role: r.role, summary: r.summary, kind: r.kind,
2242
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2243
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2341
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2342
+ groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2343
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2244
2344
  }));
2245
2345
  }
2246
2346
 
@@ -2264,13 +2364,6 @@ export class SqliteStore {
2264
2364
  throw new Error(`source skill not found for skillId=${skillId}`);
2265
2365
  }
2266
2366
 
2267
- private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
2268
- return {
2269
- ...user,
2270
- groups: this.getGroupsForHubUser(user.id),
2271
- };
2272
- }
2273
-
2274
2367
  getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
2275
2368
  const result = new Map<string, string>();
2276
2369
  if (sessionKeys.length === 0) return result;
@@ -2482,6 +2575,8 @@ interface HubUserRecord extends UserInfo {
2482
2575
  tokenHash: string;
2483
2576
  createdAt: number;
2484
2577
  approvedAt: number | null;
2578
+ lastIp: string;
2579
+ lastActiveAt: number | null;
2485
2580
  }
2486
2581
 
2487
2582
  interface HubUserRow {
@@ -2493,6 +2588,8 @@ interface HubUserRow {
2493
2588
  token_hash: string;
2494
2589
  created_at: number;
2495
2590
  approved_at: number | null;
2591
+ last_ip: string;
2592
+ last_active_at: number | null;
2496
2593
  }
2497
2594
 
2498
2595
  function rowToHubUser(row: HubUserRow): HubUserRecord {
@@ -2506,29 +2603,8 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
2506
2603
  tokenHash: row.token_hash,
2507
2604
  createdAt: row.created_at,
2508
2605
  approvedAt: row.approved_at,
2509
- };
2510
- }
2511
-
2512
- interface HubGroupRecord {
2513
- id: string;
2514
- name: string;
2515
- description: string;
2516
- createdAt: number;
2517
- }
2518
-
2519
- interface HubGroupRow {
2520
- id: string;
2521
- name: string;
2522
- description: string;
2523
- created_at: number;
2524
- }
2525
-
2526
- function rowToHubGroup(row: HubGroupRow): HubGroupRecord {
2527
- return {
2528
- id: row.id,
2529
- name: row.name,
2530
- description: row.description,
2531
- createdAt: row.created_at,
2606
+ lastIp: row.last_ip || "",
2607
+ lastActiveAt: row.last_active_at ?? null,
2532
2608
  };
2533
2609
  }
2534
2610
 
@@ -2677,6 +2753,7 @@ interface HubSkillSearchRow {
2677
2753
  visibility: string;
2678
2754
  group_name: string | null;
2679
2755
  owner_name: string | null;
2756
+ owner_status: string | null;
2680
2757
  quality_score: number | null;
2681
2758
  }
2682
2759