@memtensor/memos-local-openclaw-plugin 1.0.4-beta.8 → 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 +132 -10
  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 +251 -38
  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 +96 -1
  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 +58 -0
  55. package/dist/storage/sqlite.d.ts.map +1 -1
  56. package/dist/storage/sqlite.js +295 -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 +796 -289
  67. package/dist/viewer/html.js.map +1 -1
  68. package/dist/viewer/server.d.ts +11 -0
  69. package/dist/viewer/server.d.ts.map +1 -1
  70. package/dist/viewer/server.js +456 -92
  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 +136 -10
  81. package/src/config.ts +2 -1
  82. package/src/hub/server.ts +246 -38
  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 +89 -1
  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 +326 -40
  95. package/src/telemetry.ts +27 -9
  96. package/src/types.ts +11 -0
  97. package/src/viewer/html.ts +796 -289
  98. package/src/viewer/server.ts +430 -89
  99. package/telemetry.credentials.json +5 -0
@@ -112,7 +112,10 @@ export class SqliteStore {
112
112
  this.migrateSkillEmbeddingsAndFts();
113
113
  this.migrateFtsToTrigram();
114
114
  this.migrateHubTables();
115
+ this.migrateHubFtsToTrigram();
115
116
  this.migrateLocalSharedTasksOwner();
117
+ this.migrateHubUserIdentityFields();
118
+ this.migrateClientHubConnectionIdentityFields();
116
119
  this.log.debug("Database schema initialized");
117
120
  }
118
121
 
@@ -130,6 +133,49 @@ export class SqliteStore {
130
133
  } catch { /* table may not exist yet */ }
131
134
  }
132
135
 
136
+ private migrateHubUserIdentityFields(): void {
137
+ try {
138
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
139
+ if (cols.length === 0) return;
140
+ if (!cols.some(c => c.name === "identity_key")) {
141
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
142
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_hub_users_identity_key ON hub_users(identity_key)");
143
+ this.log.info("Migrated: added identity_key to hub_users");
144
+ }
145
+ if (!cols.some(c => c.name === "left_at")) {
146
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN left_at INTEGER");
147
+ this.log.info("Migrated: added left_at to hub_users");
148
+ }
149
+ if (!cols.some(c => c.name === "removed_at")) {
150
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN removed_at INTEGER");
151
+ this.log.info("Migrated: added removed_at to hub_users");
152
+ }
153
+ if (!cols.some(c => c.name === "rejected_at")) {
154
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN rejected_at INTEGER");
155
+ this.log.info("Migrated: added rejected_at to hub_users");
156
+ }
157
+ if (!cols.some(c => c.name === "rejoin_requested_at")) {
158
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN rejoin_requested_at INTEGER");
159
+ this.log.info("Migrated: added rejoin_requested_at to hub_users");
160
+ }
161
+ } catch { /* table may not exist yet */ }
162
+ }
163
+
164
+ private migrateClientHubConnectionIdentityFields(): void {
165
+ try {
166
+ const cols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all() as Array<{ name: string }>;
167
+ if (cols.length === 0) return;
168
+ if (!cols.some(c => c.name === "identity_key")) {
169
+ this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
170
+ this.log.info("Migrated: added identity_key to client_hub_connection");
171
+ }
172
+ if (!cols.some(c => c.name === "last_known_status")) {
173
+ this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN last_known_status TEXT NOT NULL DEFAULT ''");
174
+ this.log.info("Migrated: added last_known_status to client_hub_connection");
175
+ }
176
+ } catch { /* table may not exist yet */ }
177
+ }
178
+
133
179
  private migrateOwnerFields(): void {
134
180
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
135
181
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -284,6 +330,51 @@ export class SqliteStore {
284
330
  }
285
331
  }
286
332
 
333
+ private migrateHubFtsToTrigram(): void {
334
+ const tables: Array<{ fts: string; source: string; columns: string; triggers: string[] }> = [
335
+ {
336
+ fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
337
+ triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
338
+ },
339
+ {
340
+ fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
341
+ triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
342
+ },
343
+ {
344
+ fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
345
+ triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
346
+ },
347
+ ];
348
+ for (const t of tables) {
349
+ try {
350
+ const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get() as { sql: string } | undefined;
351
+ if (!row || !row.sql) continue;
352
+ if (row.sql.includes("trigram")) continue;
353
+ this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
354
+ for (const tr of t.triggers) this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
355
+ this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
356
+ this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
357
+ this.db.exec(`
358
+ CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
359
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
360
+ END;
361
+ CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
362
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
363
+ END;
364
+ CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
365
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
366
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
367
+ END
368
+ `);
369
+ this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
370
+ const cnt = (this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get() as { c: number }).c;
371
+ this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
372
+ } catch (err) {
373
+ this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
374
+ }
375
+ }
376
+ }
377
+
287
378
  private migrateTaskId(): void {
288
379
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
289
380
  if (!cols.some((c) => c.name === "task_id")) {
@@ -701,6 +792,15 @@ export class SqliteStore {
701
792
  shared_at INTEGER NOT NULL
702
793
  );
703
794
 
795
+ -- Client: team share UI metadata only (no hub_memories row — avoids local FTS/embed recall duplication)
796
+ CREATE TABLE IF NOT EXISTS team_shared_chunks (
797
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
798
+ hub_memory_id TEXT NOT NULL DEFAULT '',
799
+ visibility TEXT NOT NULL DEFAULT 'public',
800
+ group_id TEXT,
801
+ shared_at INTEGER NOT NULL
802
+ );
803
+
704
804
  CREATE TABLE IF NOT EXISTS hub_users (
705
805
  id TEXT PRIMARY KEY,
706
806
  username TEXT NOT NULL UNIQUE,
@@ -716,6 +816,20 @@ export class SqliteStore {
716
816
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
717
817
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
718
818
 
819
+ CREATE TABLE IF NOT EXISTS hub_groups (
820
+ id TEXT PRIMARY KEY,
821
+ name TEXT NOT NULL,
822
+ description TEXT NOT NULL DEFAULT '',
823
+ created_at INTEGER NOT NULL
824
+ );
825
+
826
+ CREATE TABLE IF NOT EXISTS hub_group_members (
827
+ group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
828
+ user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
829
+ joined_at INTEGER NOT NULL,
830
+ PRIMARY KEY (group_id, user_id)
831
+ );
832
+
719
833
  CREATE TABLE IF NOT EXISTS hub_tasks (
720
834
  id TEXT PRIMARY KEY,
721
835
  source_task_id TEXT NOT NULL,
@@ -757,7 +871,7 @@ export class SqliteStore {
757
871
  content,
758
872
  content='hub_chunks',
759
873
  content_rowid='rowid',
760
- tokenize='porter unicode61'
874
+ tokenize='trigram'
761
875
  );
762
876
 
763
877
  CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
@@ -807,7 +921,7 @@ export class SqliteStore {
807
921
  description,
808
922
  content='hub_skills',
809
923
  content_rowid='rowid',
810
- tokenize='porter unicode61'
924
+ tokenize='trigram'
811
925
  );
812
926
 
813
927
  CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
@@ -857,7 +971,7 @@ export class SqliteStore {
857
971
  content,
858
972
  content='hub_memories',
859
973
  content_rowid='rowid',
860
- tokenize='porter unicode61'
974
+ tokenize='trigram'
861
975
  );
862
976
 
863
977
  CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
@@ -1078,6 +1192,25 @@ export class SqliteStore {
1078
1192
  }
1079
1193
  }
1080
1194
 
1195
+ hubMemoryPatternSearch(patterns: string[], opts: { limit?: number } = {}): Array<{ memoryId: string; content: string; role: string; createdAt: number }> {
1196
+ if (patterns.length === 0) return [];
1197
+ const limit = opts.limit ?? 10;
1198
+ const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
1199
+ const params: (string | number)[] = [];
1200
+ for (const p of patterns) { params.push(`%${p}%`, `%${p}%`); }
1201
+ params.push(limit);
1202
+ try {
1203
+ const rows = this.db.prepare(`
1204
+ SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
1205
+ FROM hub_memories hm
1206
+ WHERE ${conditions.join(" OR ")}
1207
+ ORDER BY hm.created_at DESC
1208
+ LIMIT ?
1209
+ `).all(...params) as Array<{ memory_id: string; content: string; role: string; created_at: number }>;
1210
+ return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
1211
+ } catch { return []; }
1212
+ }
1213
+
1081
1214
  // ─── Vector Search ───
1082
1215
 
1083
1216
  getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
@@ -1245,6 +1378,7 @@ export class SqliteStore {
1245
1378
  "skill_versions",
1246
1379
  "skills",
1247
1380
  "local_shared_memories",
1381
+ "team_shared_chunks",
1248
1382
  "local_shared_tasks",
1249
1383
  "embeddings",
1250
1384
  "chunks",
@@ -1366,7 +1500,10 @@ export class SqliteStore {
1366
1500
  const conditions: string[] = [];
1367
1501
  const params: unknown[] = [];
1368
1502
  if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
1369
- if (opts.owner) { conditions.push("owner = ?"); params.push(opts.owner); }
1503
+ if (opts.owner) {
1504
+ conditions.push("(owner = ? OR (owner = 'public' AND id IN (SELECT task_id FROM local_shared_tasks WHERE original_owner = ?)))");
1505
+ params.push(opts.owner, opts.owner);
1506
+ }
1370
1507
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1371
1508
 
1372
1509
  const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params) as { c: number };
@@ -1666,16 +1803,18 @@ export class SqliteStore {
1666
1803
 
1667
1804
  setClientHubConnection(conn: ClientHubConnection): void {
1668
1805
  this.db.prepare(`
1669
- INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
1670
- VALUES (1, ?, ?, ?, ?, ?, ?)
1806
+ INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status)
1807
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
1671
1808
  ON CONFLICT(id) DO UPDATE SET
1672
1809
  hub_url = excluded.hub_url,
1673
1810
  user_id = excluded.user_id,
1674
1811
  username = excluded.username,
1675
1812
  user_token = excluded.user_token,
1676
1813
  role = excluded.role,
1677
- connected_at = excluded.connected_at
1678
- `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt);
1814
+ connected_at = excluded.connected_at,
1815
+ identity_key = excluded.identity_key,
1816
+ last_known_status = excluded.last_known_status
1817
+ `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "");
1679
1818
  }
1680
1819
 
1681
1820
  getClientHubConnection(): ClientHubConnection | null {
@@ -1782,8 +1921,8 @@ export class SqliteStore {
1782
1921
 
1783
1922
  upsertHubUser(user: HubUserRecord): void {
1784
1923
  this.db.prepare(`
1785
- INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
1786
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1924
+ 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)
1925
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1787
1926
  ON CONFLICT(id) DO UPDATE SET
1788
1927
  username = excluded.username,
1789
1928
  device_name = excluded.device_name,
@@ -1791,21 +1930,32 @@ export class SqliteStore {
1791
1930
  status = excluded.status,
1792
1931
  token_hash = excluded.token_hash,
1793
1932
  created_at = excluded.created_at,
1794
- approved_at = excluded.approved_at
1795
- `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt);
1933
+ approved_at = excluded.approved_at,
1934
+ identity_key = excluded.identity_key,
1935
+ left_at = excluded.left_at,
1936
+ removed_at = excluded.removed_at,
1937
+ rejected_at = excluded.rejected_at,
1938
+ rejoin_requested_at = excluded.rejoin_requested_at
1939
+ `).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);
1796
1940
  }
1797
1941
 
1798
1942
  getHubUser(userId: string): HubUserRecord | null {
1799
1943
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
1800
1944
  if (!row) return null;
1801
- return rowToHubUser(row);
1945
+ const user = rowToHubUser(row);
1946
+ user.groups = this.getGroupsForHubUser(userId);
1947
+ return user;
1802
1948
  }
1803
1949
 
1804
1950
  listHubUsers(status?: UserStatus): HubUserRecord[] {
1805
1951
  const rows = status
1806
1952
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
1807
1953
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
1808
- return rows.map(rowToHubUser);
1954
+ return rows.map(r => {
1955
+ const user = rowToHubUser(r);
1956
+ user.groups = this.getGroupsForHubUser(r.id);
1957
+ return user;
1958
+ });
1809
1959
  }
1810
1960
 
1811
1961
  deleteHubUser(userId: string, cleanResources = false): boolean {
@@ -1813,8 +1963,21 @@ export class SqliteStore {
1813
1963
  this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1814
1964
  this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
1815
1965
  this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
1966
+ const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1967
+ return result.changes > 0;
1816
1968
  }
1817
- const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1969
+ const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
1970
+ return result.changes > 0;
1971
+ }
1972
+
1973
+ findHubUserByIdentityKey(identityKey: string): HubUserRecord | null {
1974
+ if (!identityKey) return null;
1975
+ const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey) as HubUserRow | undefined;
1976
+ return row ? rowToHubUser(row) : null;
1977
+ }
1978
+
1979
+ markHubUserLeft(userId: string): boolean {
1980
+ const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
1818
1981
  return result.changes > 0;
1819
1982
  }
1820
1983
 
@@ -1822,6 +1985,35 @@ export class SqliteStore {
1822
1985
  this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1823
1986
  }
1824
1987
 
1988
+ // ─── Hub Groups ───
1989
+
1990
+ upsertHubGroup(group: { id: string; name: string; description?: string; createdAt: number }): void {
1991
+ this.db.prepare(`
1992
+ INSERT INTO hub_groups (id, name, description, created_at)
1993
+ VALUES (?, ?, ?, ?)
1994
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description
1995
+ `).run(group.id, group.name, group.description ?? "", group.createdAt);
1996
+ }
1997
+
1998
+ addHubGroupMember(groupId: string, userId: string, joinedAt: number): void {
1999
+ this.db.prepare(`
2000
+ INSERT OR IGNORE INTO hub_group_members (group_id, user_id, joined_at)
2001
+ VALUES (?, ?, ?)
2002
+ `).run(groupId, userId, joinedAt);
2003
+ }
2004
+
2005
+ removeHubGroupMember(groupId: string, userId: string): void {
2006
+ this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
2007
+ }
2008
+
2009
+ getGroupsForHubUser(userId: string): Array<{ id: string; name: string; description: string }> {
2010
+ return this.db.prepare(`
2011
+ SELECT g.id, g.name, g.description FROM hub_groups g
2012
+ JOIN hub_group_members m ON m.group_id = g.id
2013
+ WHERE m.user_id = ?
2014
+ `).all(userId) as Array<{ id: string; name: string; description: string }>;
2015
+ }
2016
+
1825
2017
  getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
1826
2018
  const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
1827
2019
  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 }>;
@@ -1934,20 +2126,37 @@ export class SqliteStore {
1934
2126
  return out;
1935
2127
  }
1936
2128
 
2129
+ getVisibleHubSkillEmbeddings(): Array<{ skillId: string; vector: Float32Array }> {
2130
+ const rows = this.db.prepare(`
2131
+ SELECT hse.skill_id, hse.vector, hse.dimensions
2132
+ FROM hub_skill_embeddings hse
2133
+ JOIN hub_skills hs ON hs.id = hse.skill_id
2134
+ `).all() as Array<{ skill_id: string; vector: Buffer; dimensions: number }>;
2135
+ return rows.map(r => ({
2136
+ skillId: r.skill_id,
2137
+ vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
2138
+ }));
2139
+ }
2140
+
1937
2141
  searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
1938
2142
  const limit = options?.maxResults ?? 10;
1939
2143
  const userId = options?.userId ?? "";
1940
2144
  const rows = this.db.prepare(`
1941
- 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,
2145
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
2146
+ COALESCE(hg.name, '') as group_name, hu.username as owner_name,
1942
2147
  bm25(hub_chunks_fts) as rank
1943
2148
  FROM hub_chunks_fts f
1944
2149
  JOIN hub_chunks hc ON hc.rowid = f.rowid
1945
2150
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1946
2151
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
2152
+ LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1947
2153
  WHERE hub_chunks_fts MATCH ?
2154
+ AND (ht.visibility = 'public'
2155
+ OR ht.source_user_id = ?
2156
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
1948
2157
  ORDER BY rank
1949
2158
  LIMIT ?
1950
- `).all(sanitizeFtsQuery(query), limit) as HubSearchRow[];
2159
+ `).all(sanitizeFtsQuery(query), userId, userId, limit) as HubSearchRow[];
1951
2160
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1952
2161
  }
1953
2162
 
@@ -1972,7 +2181,10 @@ export class SqliteStore {
1972
2181
  FROM hub_embeddings he
1973
2182
  JOIN hub_chunks hc ON hc.id = he.chunk_id
1974
2183
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1975
- `).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
2184
+ WHERE ht.visibility = 'public'
2185
+ OR ht.source_user_id = ?
2186
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?)
2187
+ `).all(userId, userId) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
1976
2188
  return rows.map(r => ({
1977
2189
  chunkId: r.chunk_id,
1978
2190
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -1981,14 +2193,19 @@ export class SqliteStore {
1981
2193
 
1982
2194
  getVisibleHubSearchHitByChunkId(chunkId: string, userId: string): HubSearchRow | null {
1983
2195
  const row = this.db.prepare(`
1984
- 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,
2196
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
2197
+ COALESCE(hg.name, '') as group_name, hu.username as owner_name,
1985
2198
  0 as rank
1986
2199
  FROM hub_chunks hc
1987
2200
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1988
2201
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
2202
+ LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1989
2203
  WHERE hc.id = ?
2204
+ AND (ht.visibility = 'public'
2205
+ OR ht.source_user_id = ?
2206
+ OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
1990
2207
  LIMIT 1
1991
- `).get(chunkId) as HubSearchRow | undefined;
2208
+ `).get(chunkId, userId, userId) as HubSearchRow | undefined;
1992
2209
  return row ?? null;
1993
2210
  }
1994
2211
 
@@ -2004,7 +2221,7 @@ export class SqliteStore {
2004
2221
  let rows: HubSkillSearchRow[];
2005
2222
  if (sanitized) {
2006
2223
  rows = this.db.prepare(`
2007
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
2224
+ 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,
2008
2225
  bm25(hub_skills_fts) as rank
2009
2226
  FROM hub_skills_fts f
2010
2227
  JOIN hub_skills hs ON hs.rowid = f.rowid
@@ -2015,7 +2232,7 @@ export class SqliteStore {
2015
2232
  `).all(sanitized, limit) as HubSkillSearchRow[];
2016
2233
  } else {
2017
2234
  rows = this.db.prepare(`
2018
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
2235
+ 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,
2019
2236
  0 as rank
2020
2237
  FROM hub_skills hs
2021
2238
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
@@ -2030,9 +2247,9 @@ export class SqliteStore {
2030
2247
  this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
2031
2248
  }
2032
2249
 
2033
- 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 }> {
2250
+ 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 }> {
2034
2251
  const rows = this.db.prepare(`
2035
- SELECT t.*, u.username AS owner_name, NULL AS group_name,
2252
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
2036
2253
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2037
2254
  FROM hub_tasks t
2038
2255
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -2042,14 +2259,14 @@ export class SqliteStore {
2042
2259
  return rows.map(r => ({
2043
2260
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2044
2261
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
2045
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2262
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
2046
2263
  createdAt: r.created_at, updatedAt: r.updated_at,
2047
2264
  }));
2048
2265
  }
2049
2266
 
2050
- 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 }> {
2267
+ 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 }> {
2051
2268
  const rows = this.db.prepare(`
2052
- SELECT t.*, u.username AS owner_name,
2269
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status,
2053
2270
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2054
2271
  FROM hub_tasks t
2055
2272
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -2058,7 +2275,7 @@ export class SqliteStore {
2058
2275
  return rows.map(r => ({
2059
2276
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2060
2277
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
2061
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2278
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
2062
2279
  createdAt: r.created_at, updatedAt: r.updated_at,
2063
2280
  }));
2064
2281
  }
@@ -2073,9 +2290,9 @@ export class SqliteStore {
2073
2290
  return info.changes > 0;
2074
2291
  }
2075
2292
 
2076
- 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 }> {
2293
+ 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 }> {
2077
2294
  const rows = this.db.prepare(`
2078
- SELECT s.*, u.username AS owner_name, NULL AS group_name
2295
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2079
2296
  FROM hub_skills s
2080
2297
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2081
2298
  ORDER BY s.updated_at DESC
@@ -2085,14 +2302,14 @@ export class SqliteStore {
2085
2302
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2086
2303
  name: r.name, description: r.description, version: r.version,
2087
2304
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2088
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2305
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2089
2306
  createdAt: r.created_at, updatedAt: r.updated_at,
2090
2307
  }));
2091
2308
  }
2092
2309
 
2093
- 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 }> {
2310
+ 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 }> {
2094
2311
  const rows = this.db.prepare(`
2095
- SELECT s.*, u.username AS owner_name
2312
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status
2096
2313
  FROM hub_skills s
2097
2314
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2098
2315
  ORDER BY s.updated_at DESC
@@ -2101,7 +2318,7 @@ export class SqliteStore {
2101
2318
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2102
2319
  name: r.name, description: r.description, version: r.version,
2103
2320
  groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2104
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2321
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2105
2322
  createdAt: r.created_at, updatedAt: r.updated_at,
2106
2323
  }));
2107
2324
  }
@@ -2148,6 +2365,45 @@ export class SqliteStore {
2148
2365
  return info.changes > 0;
2149
2366
  }
2150
2367
 
2368
+ // ─── Team share metadata (Client role — UI only, not used for local recall / FTS) ───
2369
+
2370
+ upsertTeamSharedChunk(
2371
+ chunkId: string,
2372
+ row: { hubMemoryId?: string; visibility?: string; groupId?: string | null },
2373
+ ): void {
2374
+ const now = Date.now();
2375
+ const vis = row.visibility === "group" ? "group" : "public";
2376
+ const gid = vis === "group" ? (row.groupId ?? null) : null;
2377
+ this.db.prepare(`
2378
+ INSERT INTO team_shared_chunks (chunk_id, hub_memory_id, visibility, group_id, shared_at)
2379
+ VALUES (?, ?, ?, ?, ?)
2380
+ ON CONFLICT(chunk_id) DO UPDATE SET
2381
+ hub_memory_id = excluded.hub_memory_id,
2382
+ visibility = excluded.visibility,
2383
+ group_id = excluded.group_id,
2384
+ shared_at = excluded.shared_at
2385
+ `).run(chunkId, row.hubMemoryId ?? "", vis, gid, now);
2386
+ }
2387
+
2388
+ getTeamSharedChunk(chunkId: string): { chunkId: string; hubMemoryId: string; visibility: string; groupId: string | null; sharedAt: number } | null {
2389
+ 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) as {
2390
+ chunk_id: string; hub_memory_id: string; visibility: string; group_id: string | null; shared_at: number;
2391
+ } | undefined;
2392
+ if (!r) return null;
2393
+ return {
2394
+ chunkId: r.chunk_id,
2395
+ hubMemoryId: r.hub_memory_id,
2396
+ visibility: r.visibility,
2397
+ groupId: r.group_id,
2398
+ sharedAt: r.shared_at,
2399
+ };
2400
+ }
2401
+
2402
+ deleteTeamSharedChunk(chunkId: string): boolean {
2403
+ const info = this.db.prepare("DELETE FROM team_shared_chunks WHERE chunk_id = ?").run(chunkId);
2404
+ return info.changes > 0;
2405
+ }
2406
+
2151
2407
  // ─── Hub Notifications ───
2152
2408
 
2153
2409
  insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
@@ -2156,6 +2412,14 @@ export class SqliteStore {
2156
2412
  ).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
2157
2413
  }
2158
2414
 
2415
+ hasRecentHubNotification(userId: string, type: string, resource: string, windowMs: number = 300_000): boolean {
2416
+ const since = Date.now() - windowMs;
2417
+ const row = this.db.prepare(
2418
+ 'SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?'
2419
+ ).get(userId, type, resource, since) as { cnt: number };
2420
+ return row.cnt > 0;
2421
+ }
2422
+
2159
2423
  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 }> {
2160
2424
  const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2161
2425
  const limit = opts?.limit ?? 50;
@@ -2238,9 +2502,9 @@ export class SqliteStore {
2238
2502
  return row ?? null;
2239
2503
  }
2240
2504
 
2241
- 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; createdAt: number; updatedAt: number }> {
2505
+ 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 }> {
2242
2506
  const rows = this.db.prepare(`
2243
- SELECT m.*, u.username AS owner_name, NULL AS group_name
2507
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2244
2508
  FROM hub_memories m
2245
2509
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2246
2510
  ORDER BY m.updated_at DESC
@@ -2250,13 +2514,13 @@ export class SqliteStore {
2250
2514
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2251
2515
  role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2252
2516
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2253
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2517
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2254
2518
  }));
2255
2519
  }
2256
2520
 
2257
- 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; createdAt: number; updatedAt: number }> {
2521
+ 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 }> {
2258
2522
  const rows = this.db.prepare(`
2259
- SELECT m.*, u.username AS owner_name
2523
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status
2260
2524
  FROM hub_memories m
2261
2525
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2262
2526
  ORDER BY m.updated_at DESC
@@ -2265,7 +2529,7 @@ export class SqliteStore {
2265
2529
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2266
2530
  role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2267
2531
  groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2268
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2532
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2269
2533
  }));
2270
2534
  }
2271
2535
 
@@ -2474,6 +2738,8 @@ interface ClientHubConnection {
2474
2738
  userToken: string;
2475
2739
  role: UserRole;
2476
2740
  connectedAt: number;
2741
+ identityKey?: string;
2742
+ lastKnownStatus?: string;
2477
2743
  }
2478
2744
 
2479
2745
  interface ClientHubConnectionRow {
@@ -2483,6 +2749,8 @@ interface ClientHubConnectionRow {
2483
2749
  user_token: string;
2484
2750
  role: string;
2485
2751
  connected_at: number;
2752
+ identity_key?: string;
2753
+ last_known_status?: string;
2486
2754
  }
2487
2755
 
2488
2756
  function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnection {
@@ -2493,6 +2761,8 @@ function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnect
2493
2761
  userToken: row.user_token,
2494
2762
  role: row.role as UserRole,
2495
2763
  connectedAt: row.connected_at,
2764
+ identityKey: row.identity_key || "",
2765
+ lastKnownStatus: row.last_known_status || "",
2496
2766
  };
2497
2767
  }
2498
2768
 
@@ -2502,6 +2772,11 @@ interface HubUserRecord extends UserInfo {
2502
2772
  approvedAt: number | null;
2503
2773
  lastIp: string;
2504
2774
  lastActiveAt: number | null;
2775
+ identityKey?: string;
2776
+ leftAt?: number | null;
2777
+ removedAt?: number | null;
2778
+ rejectedAt?: number | null;
2779
+ rejoinRequestedAt?: number | null;
2505
2780
  }
2506
2781
 
2507
2782
  interface HubUserRow {
@@ -2515,6 +2790,11 @@ interface HubUserRow {
2515
2790
  approved_at: number | null;
2516
2791
  last_ip: string;
2517
2792
  last_active_at: number | null;
2793
+ identity_key?: string;
2794
+ left_at?: number | null;
2795
+ removed_at?: number | null;
2796
+ rejected_at?: number | null;
2797
+ rejoin_requested_at?: number | null;
2518
2798
  }
2519
2799
 
2520
2800
  function rowToHubUser(row: HubUserRow): HubUserRecord {
@@ -2530,6 +2810,11 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
2530
2810
  approvedAt: row.approved_at,
2531
2811
  lastIp: row.last_ip || "",
2532
2812
  lastActiveAt: row.last_active_at ?? null,
2813
+ identityKey: row.identity_key || "",
2814
+ leftAt: row.left_at ?? null,
2815
+ removedAt: row.removed_at ?? null,
2816
+ rejectedAt: row.rejected_at ?? null,
2817
+ rejoinRequestedAt: row.rejoin_requested_at ?? null,
2533
2818
  };
2534
2819
  }
2535
2820
 
@@ -2678,6 +2963,7 @@ interface HubSkillSearchRow {
2678
2963
  visibility: string;
2679
2964
  group_name: string | null;
2680
2965
  owner_name: string | null;
2966
+ owner_status: string | null;
2681
2967
  quality_score: number | null;
2682
2968
  }
2683
2969