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

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 (99) hide show
  1. package/.env.example +7 -0
  2. package/README.md +94 -27
  3. package/dist/capture/index.js +3 -1
  4. package/dist/capture/index.js.map +1 -1
  5. package/dist/client/connector.d.ts +5 -0
  6. package/dist/client/connector.d.ts.map +1 -1
  7. package/dist/client/connector.js +89 -8
  8. package/dist/client/connector.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +2 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/hub/server.d.ts +2 -0
  13. package/dist/hub/server.d.ts.map +1 -1
  14. package/dist/hub/server.js +240 -35
  15. package/dist/hub/server.js.map +1 -1
  16. package/dist/hub/user-manager.d.ts +9 -0
  17. package/dist/hub/user-manager.d.ts.map +1 -1
  18. package/dist/hub/user-manager.js +26 -2
  19. package/dist/hub/user-manager.js.map +1 -1
  20. package/dist/ingest/chunker.d.ts +2 -1
  21. package/dist/ingest/chunker.d.ts.map +1 -1
  22. package/dist/ingest/chunker.js +14 -10
  23. package/dist/ingest/chunker.js.map +1 -1
  24. package/dist/ingest/providers/index.js +2 -2
  25. package/dist/ingest/providers/index.js.map +1 -1
  26. package/dist/recall/engine.d.ts.map +1 -1
  27. package/dist/recall/engine.js +22 -4
  28. package/dist/recall/engine.js.map +1 -1
  29. package/dist/shared/llm-call.d.ts.map +1 -1
  30. package/dist/shared/llm-call.js +2 -1
  31. package/dist/shared/llm-call.js.map +1 -1
  32. package/dist/sharing/types.d.ts +1 -1
  33. package/dist/sharing/types.d.ts.map +1 -1
  34. package/dist/skill/evolver.d.ts +2 -0
  35. package/dist/skill/evolver.d.ts.map +1 -1
  36. package/dist/skill/evolver.js +56 -5
  37. package/dist/skill/evolver.js.map +1 -1
  38. package/dist/skill/generator.d.ts +2 -0
  39. package/dist/skill/generator.d.ts.map +1 -1
  40. package/dist/skill/generator.js +45 -3
  41. package/dist/skill/generator.js.map +1 -1
  42. package/dist/skill/installer.d.ts +26 -0
  43. package/dist/skill/installer.d.ts.map +1 -1
  44. package/dist/skill/installer.js +80 -4
  45. package/dist/skill/installer.js.map +1 -1
  46. package/dist/skill/upgrader.d.ts +2 -0
  47. package/dist/skill/upgrader.d.ts.map +1 -1
  48. package/dist/skill/upgrader.js +139 -1
  49. package/dist/skill/upgrader.js.map +1 -1
  50. package/dist/skill/validator.d.ts +3 -0
  51. package/dist/skill/validator.d.ts.map +1 -1
  52. package/dist/skill/validator.js +75 -0
  53. package/dist/skill/validator.js.map +1 -1
  54. package/dist/storage/sqlite.d.ts +57 -0
  55. package/dist/storage/sqlite.d.ts.map +1 -1
  56. package/dist/storage/sqlite.js +290 -35
  57. package/dist/storage/sqlite.js.map +1 -1
  58. package/dist/telemetry.d.ts.map +1 -1
  59. package/dist/telemetry.js +27 -8
  60. package/dist/telemetry.js.map +1 -1
  61. package/dist/types.d.ts +10 -0
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +4 -0
  64. package/dist/types.js.map +1 -1
  65. package/dist/viewer/html.d.ts.map +1 -1
  66. package/dist/viewer/html.js +564 -225
  67. package/dist/viewer/html.js.map +1 -1
  68. package/dist/viewer/server.d.ts +9 -0
  69. package/dist/viewer/server.d.ts.map +1 -1
  70. package/dist/viewer/server.js +357 -108
  71. package/dist/viewer/server.js.map +1 -1
  72. package/index.ts +411 -52
  73. package/openclaw.plugin.json +1 -1
  74. package/package.json +2 -1
  75. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  76. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  77. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  78. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  79. package/src/capture/index.ts +4 -1
  80. package/src/client/connector.ts +92 -8
  81. package/src/config.ts +2 -1
  82. package/src/hub/server.ts +235 -35
  83. package/src/hub/user-manager.ts +42 -6
  84. package/src/ingest/chunker.ts +19 -13
  85. package/src/ingest/providers/index.ts +2 -2
  86. package/src/recall/engine.ts +20 -4
  87. package/src/shared/llm-call.ts +2 -1
  88. package/src/sharing/types.ts +1 -1
  89. package/src/skill/evolver.ts +58 -6
  90. package/src/skill/generator.ts +44 -5
  91. package/src/skill/installer.ts +107 -4
  92. package/src/skill/upgrader.ts +139 -1
  93. package/src/skill/validator.ts +79 -0
  94. package/src/storage/sqlite.ts +318 -40
  95. package/src/telemetry.ts +27 -9
  96. package/src/types.ts +11 -0
  97. package/src/viewer/html.ts +564 -225
  98. package/src/viewer/server.ts +333 -105
  99. package/telemetry.credentials.json +5 -0
@@ -146,7 +146,10 @@ class SqliteStore {
146
146
  this.migrateSkillEmbeddingsAndFts();
147
147
  this.migrateFtsToTrigram();
148
148
  this.migrateHubTables();
149
+ this.migrateHubFtsToTrigram();
149
150
  this.migrateLocalSharedTasksOwner();
151
+ this.migrateHubUserIdentityFields();
152
+ this.migrateClientHubConnectionIdentityFields();
150
153
  this.log.debug("Database schema initialized");
151
154
  }
152
155
  migrateChunksIndexesForRecall() {
@@ -162,6 +165,51 @@ class SqliteStore {
162
165
  }
163
166
  catch { /* table may not exist yet */ }
164
167
  }
168
+ migrateHubUserIdentityFields() {
169
+ try {
170
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
171
+ if (cols.length === 0)
172
+ return;
173
+ if (!cols.some(c => c.name === "identity_key")) {
174
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
175
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_hub_users_identity_key ON hub_users(identity_key)");
176
+ this.log.info("Migrated: added identity_key to hub_users");
177
+ }
178
+ if (!cols.some(c => c.name === "left_at")) {
179
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN left_at INTEGER");
180
+ this.log.info("Migrated: added left_at to hub_users");
181
+ }
182
+ if (!cols.some(c => c.name === "removed_at")) {
183
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN removed_at INTEGER");
184
+ this.log.info("Migrated: added removed_at to hub_users");
185
+ }
186
+ if (!cols.some(c => c.name === "rejected_at")) {
187
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN rejected_at INTEGER");
188
+ this.log.info("Migrated: added rejected_at to hub_users");
189
+ }
190
+ if (!cols.some(c => c.name === "rejoin_requested_at")) {
191
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN rejoin_requested_at INTEGER");
192
+ this.log.info("Migrated: added rejoin_requested_at to hub_users");
193
+ }
194
+ }
195
+ catch { /* table may not exist yet */ }
196
+ }
197
+ migrateClientHubConnectionIdentityFields() {
198
+ try {
199
+ const cols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all();
200
+ if (cols.length === 0)
201
+ return;
202
+ if (!cols.some(c => c.name === "identity_key")) {
203
+ this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
204
+ this.log.info("Migrated: added identity_key to client_hub_connection");
205
+ }
206
+ if (!cols.some(c => c.name === "last_known_status")) {
207
+ this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN last_known_status TEXT NOT NULL DEFAULT ''");
208
+ this.log.info("Migrated: added last_known_status to client_hub_connection");
209
+ }
210
+ }
211
+ catch { /* table may not exist yet */ }
212
+ }
165
213
  migrateOwnerFields() {
166
214
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all();
167
215
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -309,6 +357,54 @@ class SqliteStore {
309
357
  this.log.warn(`Failed to migrate skills_fts to trigram: ${err}`);
310
358
  }
311
359
  }
360
+ migrateHubFtsToTrigram() {
361
+ const tables = [
362
+ {
363
+ fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
364
+ triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
365
+ },
366
+ {
367
+ fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
368
+ triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
369
+ },
370
+ {
371
+ fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
372
+ triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
373
+ },
374
+ ];
375
+ for (const t of tables) {
376
+ try {
377
+ const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get();
378
+ if (!row || !row.sql)
379
+ continue;
380
+ if (row.sql.includes("trigram"))
381
+ continue;
382
+ this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
383
+ for (const tr of t.triggers)
384
+ this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
385
+ this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
386
+ this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
387
+ this.db.exec(`
388
+ CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
389
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
390
+ END;
391
+ CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
392
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
393
+ END;
394
+ CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
395
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
396
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
397
+ END
398
+ `);
399
+ this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
400
+ const cnt = this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get().c;
401
+ this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
402
+ }
403
+ catch (err) {
404
+ this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
405
+ }
406
+ }
407
+ }
312
408
  migrateTaskId() {
313
409
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
314
410
  if (!cols.some((c) => c.name === "task_id")) {
@@ -662,6 +758,15 @@ class SqliteStore {
662
758
  shared_at INTEGER NOT NULL
663
759
  );
664
760
 
761
+ -- Client: team share UI metadata only (no hub_memories row — avoids local FTS/embed recall duplication)
762
+ CREATE TABLE IF NOT EXISTS team_shared_chunks (
763
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
764
+ hub_memory_id TEXT NOT NULL DEFAULT '',
765
+ visibility TEXT NOT NULL DEFAULT 'public',
766
+ group_id TEXT,
767
+ shared_at INTEGER NOT NULL
768
+ );
769
+
665
770
  CREATE TABLE IF NOT EXISTS hub_users (
666
771
  id TEXT PRIMARY KEY,
667
772
  username TEXT NOT NULL UNIQUE,
@@ -677,6 +782,20 @@ class SqliteStore {
677
782
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
678
783
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
679
784
 
785
+ CREATE TABLE IF NOT EXISTS hub_groups (
786
+ id TEXT PRIMARY KEY,
787
+ name TEXT NOT NULL,
788
+ description TEXT NOT NULL DEFAULT '',
789
+ created_at INTEGER NOT NULL
790
+ );
791
+
792
+ CREATE TABLE IF NOT EXISTS hub_group_members (
793
+ group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
794
+ user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
795
+ joined_at INTEGER NOT NULL,
796
+ PRIMARY KEY (group_id, user_id)
797
+ );
798
+
680
799
  CREATE TABLE IF NOT EXISTS hub_tasks (
681
800
  id TEXT PRIMARY KEY,
682
801
  source_task_id TEXT NOT NULL,
@@ -718,7 +837,7 @@ class SqliteStore {
718
837
  content,
719
838
  content='hub_chunks',
720
839
  content_rowid='rowid',
721
- tokenize='porter unicode61'
840
+ tokenize='trigram'
722
841
  );
723
842
 
724
843
  CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
@@ -768,7 +887,7 @@ class SqliteStore {
768
887
  description,
769
888
  content='hub_skills',
770
889
  content_rowid='rowid',
771
- tokenize='porter unicode61'
890
+ tokenize='trigram'
772
891
  );
773
892
 
774
893
  CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
@@ -818,7 +937,7 @@ class SqliteStore {
818
937
  content,
819
938
  content='hub_memories',
820
939
  content_rowid='rowid',
821
- tokenize='porter unicode61'
940
+ tokenize='trigram'
822
941
  );
823
942
 
824
943
  CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
@@ -989,6 +1108,30 @@ class SqliteStore {
989
1108
  return [];
990
1109
  }
991
1110
  }
1111
+ hubMemoryPatternSearch(patterns, opts = {}) {
1112
+ if (patterns.length === 0)
1113
+ return [];
1114
+ const limit = opts.limit ?? 10;
1115
+ const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
1116
+ const params = [];
1117
+ for (const p of patterns) {
1118
+ params.push(`%${p}%`, `%${p}%`);
1119
+ }
1120
+ params.push(limit);
1121
+ try {
1122
+ const rows = this.db.prepare(`
1123
+ SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
1124
+ FROM hub_memories hm
1125
+ WHERE ${conditions.join(" OR ")}
1126
+ ORDER BY hm.created_at DESC
1127
+ LIMIT ?
1128
+ `).all(...params);
1129
+ return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
1130
+ }
1131
+ catch {
1132
+ return [];
1133
+ }
1134
+ }
992
1135
  // ─── Vector Search ───
993
1136
  getAllEmbeddings(ownerFilter) {
994
1137
  let sql = `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
@@ -1128,6 +1271,7 @@ class SqliteStore {
1128
1271
  "skill_versions",
1129
1272
  "skills",
1130
1273
  "local_shared_memories",
1274
+ "team_shared_chunks",
1131
1275
  "local_shared_tasks",
1132
1276
  "embeddings",
1133
1277
  "chunks",
@@ -1240,8 +1384,8 @@ class SqliteStore {
1240
1384
  params.push(opts.status);
1241
1385
  }
1242
1386
  if (opts.owner) {
1243
- conditions.push("owner = ?");
1244
- params.push(opts.owner);
1387
+ conditions.push("(owner = ? OR (owner = 'public' AND id IN (SELECT task_id FROM local_shared_tasks WHERE original_owner = ?)))");
1388
+ params.push(opts.owner, opts.owner);
1245
1389
  }
1246
1390
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1247
1391
  const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params);
@@ -1507,16 +1651,18 @@ class SqliteStore {
1507
1651
  // ─── Hub / Client connection ───
1508
1652
  setClientHubConnection(conn) {
1509
1653
  this.db.prepare(`
1510
- INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
1511
- VALUES (1, ?, ?, ?, ?, ?, ?)
1654
+ INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status)
1655
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
1512
1656
  ON CONFLICT(id) DO UPDATE SET
1513
1657
  hub_url = excluded.hub_url,
1514
1658
  user_id = excluded.user_id,
1515
1659
  username = excluded.username,
1516
1660
  user_token = excluded.user_token,
1517
1661
  role = excluded.role,
1518
- connected_at = excluded.connected_at
1519
- `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt);
1662
+ connected_at = excluded.connected_at,
1663
+ identity_key = excluded.identity_key,
1664
+ last_known_status = excluded.last_known_status
1665
+ `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "");
1520
1666
  }
1521
1667
  getClientHubConnection() {
1522
1668
  const row = this.db.prepare('SELECT * FROM client_hub_connection WHERE id = 1').get();
@@ -1609,8 +1755,8 @@ class SqliteStore {
1609
1755
  // ─── Hub Users / Groups ───
1610
1756
  upsertHubUser(user) {
1611
1757
  this.db.prepare(`
1612
- INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
1613
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1758
+ INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at, identity_key, left_at, removed_at, rejected_at, rejoin_requested_at)
1759
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1614
1760
  ON CONFLICT(id) DO UPDATE SET
1615
1761
  username = excluded.username,
1616
1762
  device_name = excluded.device_name,
@@ -1618,33 +1764,80 @@ class SqliteStore {
1618
1764
  status = excluded.status,
1619
1765
  token_hash = excluded.token_hash,
1620
1766
  created_at = excluded.created_at,
1621
- approved_at = excluded.approved_at
1622
- `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt);
1767
+ approved_at = excluded.approved_at,
1768
+ identity_key = excluded.identity_key,
1769
+ left_at = excluded.left_at,
1770
+ removed_at = excluded.removed_at,
1771
+ rejected_at = excluded.rejected_at,
1772
+ rejoin_requested_at = excluded.rejoin_requested_at
1773
+ `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt, user.identityKey ?? "", user.leftAt ?? null, user.removedAt ?? null, user.rejectedAt ?? null, user.rejoinRequestedAt ?? null);
1623
1774
  }
1624
1775
  getHubUser(userId) {
1625
1776
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId);
1626
1777
  if (!row)
1627
1778
  return null;
1628
- return rowToHubUser(row);
1779
+ const user = rowToHubUser(row);
1780
+ user.groups = this.getGroupsForHubUser(userId);
1781
+ return user;
1629
1782
  }
1630
1783
  listHubUsers(status) {
1631
1784
  const rows = status
1632
1785
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status)
1633
1786
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all();
1634
- return rows.map(rowToHubUser);
1787
+ return rows.map(r => {
1788
+ const user = rowToHubUser(r);
1789
+ user.groups = this.getGroupsForHubUser(r.id);
1790
+ return user;
1791
+ });
1635
1792
  }
1636
1793
  deleteHubUser(userId, cleanResources = false) {
1637
1794
  if (cleanResources) {
1638
1795
  this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1639
1796
  this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
1640
1797
  this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
1798
+ const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1799
+ return result.changes > 0;
1641
1800
  }
1642
- const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1801
+ const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
1802
+ return result.changes > 0;
1803
+ }
1804
+ findHubUserByIdentityKey(identityKey) {
1805
+ if (!identityKey)
1806
+ return null;
1807
+ const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey);
1808
+ return row ? rowToHubUser(row) : null;
1809
+ }
1810
+ markHubUserLeft(userId) {
1811
+ const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
1643
1812
  return result.changes > 0;
1644
1813
  }
1645
1814
  updateHubUserActivity(userId, ip, timestamp) {
1646
1815
  this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1647
1816
  }
1817
+ // ─── Hub Groups ───
1818
+ upsertHubGroup(group) {
1819
+ this.db.prepare(`
1820
+ INSERT INTO hub_groups (id, name, description, created_at)
1821
+ VALUES (?, ?, ?, ?)
1822
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description
1823
+ `).run(group.id, group.name, group.description ?? "", group.createdAt);
1824
+ }
1825
+ addHubGroupMember(groupId, userId, joinedAt) {
1826
+ this.db.prepare(`
1827
+ INSERT OR IGNORE INTO hub_group_members (group_id, user_id, joined_at)
1828
+ VALUES (?, ?, ?)
1829
+ `).run(groupId, userId, joinedAt);
1830
+ }
1831
+ removeHubGroupMember(groupId, userId) {
1832
+ this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1833
+ }
1834
+ getGroupsForHubUser(userId) {
1835
+ return this.db.prepare(`
1836
+ SELECT g.id, g.name, g.description FROM hub_groups g
1837
+ JOIN hub_group_members m ON m.group_id = g.id
1838
+ WHERE m.user_id = ?
1839
+ `).all(userId);
1840
+ }
1648
1841
  getHubUserContributions() {
1649
1842
  const result = {};
1650
1843
  const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all();
@@ -1761,20 +1954,36 @@ class SqliteStore {
1761
1954
  out.push(row.vector.readFloatLE(i * 4));
1762
1955
  return out;
1763
1956
  }
1957
+ getVisibleHubSkillEmbeddings() {
1958
+ const rows = this.db.prepare(`
1959
+ SELECT hse.skill_id, hse.vector, hse.dimensions
1960
+ FROM hub_skill_embeddings hse
1961
+ JOIN hub_skills hs ON hs.id = hse.skill_id
1962
+ `).all();
1963
+ return rows.map(r => ({
1964
+ skillId: r.skill_id,
1965
+ vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
1966
+ }));
1967
+ }
1764
1968
  searchHubChunks(query, options) {
1765
1969
  const limit = options?.maxResults ?? 10;
1766
1970
  const userId = options?.userId ?? "";
1767
1971
  const rows = this.db.prepare(`
1768
- 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,
1972
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
1973
+ COALESCE(hg.name, '') as group_name, hu.username as owner_name,
1769
1974
  bm25(hub_chunks_fts) as rank
1770
1975
  FROM hub_chunks_fts f
1771
1976
  JOIN hub_chunks hc ON hc.rowid = f.rowid
1772
1977
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1773
1978
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1979
+ LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1774
1980
  WHERE hub_chunks_fts MATCH ?
1981
+ AND (ht.visibility = 'public'
1982
+ OR ht.source_user_id = ?
1983
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
1775
1984
  ORDER BY rank
1776
1985
  LIMIT ?
1777
- `).all(sanitizeFtsQuery(query), limit);
1986
+ `).all(sanitizeFtsQuery(query), userId, userId, limit);
1778
1987
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1779
1988
  }
1780
1989
  upsertHubEmbedding(chunkId, vector) {
@@ -1797,7 +2006,10 @@ class SqliteStore {
1797
2006
  FROM hub_embeddings he
1798
2007
  JOIN hub_chunks hc ON hc.id = he.chunk_id
1799
2008
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1800
- `).all();
2009
+ WHERE ht.visibility = 'public'
2010
+ OR ht.source_user_id = ?
2011
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?)
2012
+ `).all(userId, userId);
1801
2013
  return rows.map(r => ({
1802
2014
  chunkId: r.chunk_id,
1803
2015
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -1805,14 +2017,19 @@ class SqliteStore {
1805
2017
  }
1806
2018
  getVisibleHubSearchHitByChunkId(chunkId, userId) {
1807
2019
  const row = this.db.prepare(`
1808
- 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,
2020
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
2021
+ COALESCE(hg.name, '') as group_name, hu.username as owner_name,
1809
2022
  0 as rank
1810
2023
  FROM hub_chunks hc
1811
2024
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1812
2025
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
2026
+ LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1813
2027
  WHERE hc.id = ?
2028
+ AND (ht.visibility = 'public'
2029
+ OR ht.source_user_id = ?
2030
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
1814
2031
  LIMIT 1
1815
- `).get(chunkId);
2032
+ `).get(chunkId, userId, userId);
1816
2033
  return row ?? null;
1817
2034
  }
1818
2035
  getHubChunkById(chunkId) {
@@ -1826,7 +2043,7 @@ class SqliteStore {
1826
2043
  let rows;
1827
2044
  if (sanitized) {
1828
2045
  rows = this.db.prepare(`
1829
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
2046
+ 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,
1830
2047
  bm25(hub_skills_fts) as rank
1831
2048
  FROM hub_skills_fts f
1832
2049
  JOIN hub_skills hs ON hs.rowid = f.rowid
@@ -1838,7 +2055,7 @@ class SqliteStore {
1838
2055
  }
1839
2056
  else {
1840
2057
  rows = this.db.prepare(`
1841
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
2058
+ 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,
1842
2059
  0 as rank
1843
2060
  FROM hub_skills hs
1844
2061
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
@@ -1853,7 +2070,7 @@ class SqliteStore {
1853
2070
  }
1854
2071
  listVisibleHubTasks(userId, limit = 40) {
1855
2072
  const rows = this.db.prepare(`
1856
- SELECT t.*, u.username AS owner_name, NULL AS group_name,
2073
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
1857
2074
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1858
2075
  FROM hub_tasks t
1859
2076
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -1863,13 +2080,13 @@ class SqliteStore {
1863
2080
  return rows.map(r => ({
1864
2081
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1865
2082
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1866
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2083
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1867
2084
  createdAt: r.created_at, updatedAt: r.updated_at,
1868
2085
  }));
1869
2086
  }
1870
2087
  listAllHubTasks() {
1871
2088
  const rows = this.db.prepare(`
1872
- SELECT t.*, u.username AS owner_name,
2089
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status,
1873
2090
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1874
2091
  FROM hub_tasks t
1875
2092
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -1878,7 +2095,7 @@ class SqliteStore {
1878
2095
  return rows.map(r => ({
1879
2096
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1880
2097
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: null,
1881
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2098
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1882
2099
  createdAt: r.created_at, updatedAt: r.updated_at,
1883
2100
  }));
1884
2101
  }
@@ -1892,7 +2109,7 @@ class SqliteStore {
1892
2109
  }
1893
2110
  listVisibleHubSkills(userId, limit = 40) {
1894
2111
  const rows = this.db.prepare(`
1895
- SELECT s.*, u.username AS owner_name, NULL AS group_name
2112
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
1896
2113
  FROM hub_skills s
1897
2114
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1898
2115
  ORDER BY s.updated_at DESC
@@ -1902,13 +2119,13 @@ class SqliteStore {
1902
2119
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1903
2120
  name: r.name, description: r.description, version: r.version,
1904
2121
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1905
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2122
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1906
2123
  createdAt: r.created_at, updatedAt: r.updated_at,
1907
2124
  }));
1908
2125
  }
1909
2126
  listAllHubSkills() {
1910
2127
  const rows = this.db.prepare(`
1911
- SELECT s.*, u.username AS owner_name
2128
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status
1912
2129
  FROM hub_skills s
1913
2130
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1914
2131
  ORDER BY s.updated_at DESC
@@ -1917,7 +2134,7 @@ class SqliteStore {
1917
2134
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1918
2135
  name: r.name, description: r.description, version: r.version,
1919
2136
  groupId: r.group_id, groupName: null, visibility: r.visibility,
1920
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2137
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1921
2138
  createdAt: r.created_at, updatedAt: r.updated_at,
1922
2139
  }));
1923
2140
  }
@@ -1956,6 +2173,37 @@ class SqliteStore {
1956
2173
  const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
1957
2174
  return info.changes > 0;
1958
2175
  }
2176
+ // ─── Team share metadata (Client role — UI only, not used for local recall / FTS) ───
2177
+ upsertTeamSharedChunk(chunkId, row) {
2178
+ const now = Date.now();
2179
+ const vis = row.visibility === "group" ? "group" : "public";
2180
+ const gid = vis === "group" ? (row.groupId ?? null) : null;
2181
+ this.db.prepare(`
2182
+ INSERT INTO team_shared_chunks (chunk_id, hub_memory_id, visibility, group_id, shared_at)
2183
+ VALUES (?, ?, ?, ?, ?)
2184
+ ON CONFLICT(chunk_id) DO UPDATE SET
2185
+ hub_memory_id = excluded.hub_memory_id,
2186
+ visibility = excluded.visibility,
2187
+ group_id = excluded.group_id,
2188
+ shared_at = excluded.shared_at
2189
+ `).run(chunkId, row.hubMemoryId ?? "", vis, gid, now);
2190
+ }
2191
+ getTeamSharedChunk(chunkId) {
2192
+ const r = this.db.prepare("SELECT chunk_id, hub_memory_id, visibility, group_id, shared_at FROM team_shared_chunks WHERE chunk_id = ?").get(chunkId);
2193
+ if (!r)
2194
+ return null;
2195
+ return {
2196
+ chunkId: r.chunk_id,
2197
+ hubMemoryId: r.hub_memory_id,
2198
+ visibility: r.visibility,
2199
+ groupId: r.group_id,
2200
+ sharedAt: r.shared_at,
2201
+ };
2202
+ }
2203
+ deleteTeamSharedChunk(chunkId) {
2204
+ const info = this.db.prepare("DELETE FROM team_shared_chunks WHERE chunk_id = ?").run(chunkId);
2205
+ return info.changes > 0;
2206
+ }
1959
2207
  // ─── Hub Notifications ───
1960
2208
  insertHubNotification(n) {
1961
2209
  this.db.prepare('INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)').run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
@@ -2043,7 +2291,7 @@ class SqliteStore {
2043
2291
  }
2044
2292
  listVisibleHubMemories(userId, limit = 40) {
2045
2293
  const rows = this.db.prepare(`
2046
- SELECT m.*, u.username AS owner_name, NULL AS group_name
2294
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2047
2295
  FROM hub_memories m
2048
2296
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2049
2297
  ORDER BY m.updated_at DESC
@@ -2053,12 +2301,12 @@ class SqliteStore {
2053
2301
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2054
2302
  role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2055
2303
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2056
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2304
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2057
2305
  }));
2058
2306
  }
2059
2307
  listAllHubMemories() {
2060
2308
  const rows = this.db.prepare(`
2061
- SELECT m.*, u.username AS owner_name
2309
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status
2062
2310
  FROM hub_memories m
2063
2311
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2064
2312
  ORDER BY m.updated_at DESC
@@ -2067,7 +2315,7 @@ class SqliteStore {
2067
2315
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2068
2316
  role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2069
2317
  groupId: r.group_id, groupName: null, visibility: r.visibility,
2070
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2318
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2071
2319
  }));
2072
2320
  }
2073
2321
  resolveCanonicalHubTaskId(taskId, sourceUserId, sourceTaskId) {
@@ -2201,6 +2449,8 @@ function rowToClientHubConnection(row) {
2201
2449
  userToken: row.user_token,
2202
2450
  role: row.role,
2203
2451
  connectedAt: row.connected_at,
2452
+ identityKey: row.identity_key || "",
2453
+ lastKnownStatus: row.last_known_status || "",
2204
2454
  };
2205
2455
  }
2206
2456
  function rowToHubUser(row) {
@@ -2216,6 +2466,11 @@ function rowToHubUser(row) {
2216
2466
  approvedAt: row.approved_at,
2217
2467
  lastIp: row.last_ip || "",
2218
2468
  lastActiveAt: row.last_active_at ?? null,
2469
+ identityKey: row.identity_key || "",
2470
+ leftAt: row.left_at ?? null,
2471
+ removedAt: row.removed_at ?? null,
2472
+ rejectedAt: row.rejected_at ?? null,
2473
+ rejoinRequestedAt: row.rejoin_requested_at ?? null,
2219
2474
  };
2220
2475
  }
2221
2476
  function rowToHubTask(row) {