@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 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 +1 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +93 -26
  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 +0 -2
  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 +277 -87
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +2 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +5 -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 +286 -118
  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 +2660 -889
  69. package/dist/viewer/html.js.map +1 -1
  70. package/dist/viewer/server.d.ts +30 -8
  71. package/dist/viewer/server.d.ts.map +1 -1
  72. package/dist/viewer/server.js +965 -193
  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 +91 -28
  81. package/src/client/hub.ts +18 -0
  82. package/src/client/skill-sync.ts +14 -0
  83. package/src/config.ts +0 -2
  84. package/src/hub/server.ts +259 -78
  85. package/src/hub/user-manager.ts +7 -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 +295 -144
  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 +2660 -889
  98. package/src/viewer/server.ts +888 -177
@@ -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);
@@ -1940,7 +2071,7 @@ export class SqliteStore {
1940
2071
  let rows: HubSkillSearchRow[];
1941
2072
  if (sanitized) {
1942
2073
  rows = this.db.prepare(`
1943
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' 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,
1944
2075
  bm25(hub_skills_fts) as rank
1945
2076
  FROM hub_skills_fts f
1946
2077
  JOIN hub_skills hs ON hs.rowid = f.rowid
@@ -1951,7 +2082,7 @@ export class SqliteStore {
1951
2082
  `).all(sanitized, limit) as HubSkillSearchRow[];
1952
2083
  } else {
1953
2084
  rows = this.db.prepare(`
1954
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' 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,
1955
2086
  0 as rank
1956
2087
  FROM hub_skills hs
1957
2088
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
@@ -1966,9 +2097,9 @@ export class SqliteStore {
1966
2097
  this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
1967
2098
  }
1968
2099
 
1969
- 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 }> {
1970
2101
  const rows = this.db.prepare(`
1971
- SELECT t.*, u.username AS owner_name, NULL AS group_name,
2102
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
1972
2103
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1973
2104
  FROM hub_tasks t
1974
2105
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -1978,36 +2109,40 @@ export class SqliteStore {
1978
2109
  return rows.map(r => ({
1979
2110
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1980
2111
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1981
- 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,
1982
2113
  createdAt: r.created_at, updatedAt: r.updated_at,
1983
2114
  }));
1984
2115
  }
1985
2116
 
1986
- 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 }> {
1987
2118
  const rows = this.db.prepare(`
1988
- 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,
1989
2120
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1990
2121
  FROM hub_tasks t
1991
2122
  LEFT JOIN hub_users u ON u.id = t.source_user_id
1992
- LEFT JOIN hub_groups g ON g.id = t.group_id
1993
2123
  ORDER BY t.updated_at DESC
1994
2124
  `).all() as any[];
1995
2125
  return rows.map(r => ({
1996
2126
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1997
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1998
- 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,
1999
2129
  createdAt: r.created_at, updatedAt: r.updated_at,
2000
2130
  }));
2001
2131
  }
2002
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
+
2003
2138
  deleteHubTaskById(taskId: string): boolean {
2004
2139
  const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
2005
2140
  return info.changes > 0;
2006
2141
  }
2007
2142
 
2008
- 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 }> {
2009
2144
  const rows = this.db.prepare(`
2010
- SELECT s.*, u.username AS owner_name, NULL AS group_name
2145
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2011
2146
  FROM hub_skills s
2012
2147
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2013
2148
  ORDER BY s.updated_at DESC
@@ -2017,24 +2152,23 @@ export class SqliteStore {
2017
2152
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2018
2153
  name: r.name, description: r.description, version: r.version,
2019
2154
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2020
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2155
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2021
2156
  createdAt: r.created_at, updatedAt: r.updated_at,
2022
2157
  }));
2023
2158
  }
2024
2159
 
2025
- 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 }> {
2026
2161
  const rows = this.db.prepare(`
2027
- 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
2028
2163
  FROM hub_skills s
2029
2164
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2030
- LEFT JOIN hub_groups g ON g.id = s.group_id
2031
2165
  ORDER BY s.updated_at DESC
2032
2166
  `).all() as any[];
2033
2167
  return rows.map(r => ({
2034
2168
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2035
2169
  name: r.name, description: r.description, version: r.version,
2036
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2037
- 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,
2038
2172
  createdAt: r.created_at, updatedAt: r.updated_at,
2039
2173
  }));
2040
2174
  }
@@ -2081,6 +2215,47 @@ export class SqliteStore {
2081
2215
  return info.changes > 0;
2082
2216
  }
2083
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
+
2084
2259
  upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
2085
2260
  const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
2086
2261
  this.db.prepare(`
@@ -2138,9 +2313,9 @@ export class SqliteStore {
2138
2313
  return row ?? null;
2139
2314
  }
2140
2315
 
2141
- 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 }> {
2142
2317
  const rows = this.db.prepare(`
2143
- SELECT m.*, u.username AS owner_name, NULL AS group_name
2318
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2144
2319
  FROM hub_memories m
2145
2320
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2146
2321
  ORDER BY m.updated_at DESC
@@ -2148,25 +2323,24 @@ export class SqliteStore {
2148
2323
  `).all(limit) as any[];
2149
2324
  return rows.map(r => ({
2150
2325
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2151
- role: r.role, summary: r.summary, kind: r.kind,
2326
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2152
2327
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2153
- 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,
2154
2329
  }));
2155
2330
  }
2156
2331
 
2157
- 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 }> {
2158
2333
  const rows = this.db.prepare(`
2159
- 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
2160
2335
  FROM hub_memories m
2161
2336
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2162
- LEFT JOIN hub_groups g ON g.id = m.group_id
2163
2337
  ORDER BY m.updated_at DESC
2164
2338
  `).all() as any[];
2165
2339
  return rows.map(r => ({
2166
2340
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2167
- role: r.role, summary: r.summary, kind: r.kind,
2168
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2169
- 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,
2170
2344
  }));
2171
2345
  }
2172
2346
 
@@ -2190,13 +2364,6 @@ export class SqliteStore {
2190
2364
  throw new Error(`source skill not found for skillId=${skillId}`);
2191
2365
  }
2192
2366
 
2193
- private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
2194
- return {
2195
- ...user,
2196
- groups: this.getGroupsForHubUser(user.id),
2197
- };
2198
- }
2199
-
2200
2367
  getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
2201
2368
  const result = new Map<string, string>();
2202
2369
  if (sessionKeys.length === 0) return result;
@@ -2408,6 +2575,8 @@ interface HubUserRecord extends UserInfo {
2408
2575
  tokenHash: string;
2409
2576
  createdAt: number;
2410
2577
  approvedAt: number | null;
2578
+ lastIp: string;
2579
+ lastActiveAt: number | null;
2411
2580
  }
2412
2581
 
2413
2582
  interface HubUserRow {
@@ -2419,6 +2588,8 @@ interface HubUserRow {
2419
2588
  token_hash: string;
2420
2589
  created_at: number;
2421
2590
  approved_at: number | null;
2591
+ last_ip: string;
2592
+ last_active_at: number | null;
2422
2593
  }
2423
2594
 
2424
2595
  function rowToHubUser(row: HubUserRow): HubUserRecord {
@@ -2432,29 +2603,8 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
2432
2603
  tokenHash: row.token_hash,
2433
2604
  createdAt: row.created_at,
2434
2605
  approvedAt: row.approved_at,
2435
- };
2436
- }
2437
-
2438
- interface HubGroupRecord {
2439
- id: string;
2440
- name: string;
2441
- description: string;
2442
- createdAt: number;
2443
- }
2444
-
2445
- interface HubGroupRow {
2446
- id: string;
2447
- name: string;
2448
- description: string;
2449
- created_at: number;
2450
- }
2451
-
2452
- function rowToHubGroup(row: HubGroupRow): HubGroupRecord {
2453
- return {
2454
- id: row.id,
2455
- name: row.name,
2456
- description: row.description,
2457
- createdAt: row.created_at,
2606
+ lastIp: row.last_ip || "",
2607
+ lastActiveAt: row.last_active_at ?? null,
2458
2608
  };
2459
2609
  }
2460
2610
 
@@ -2603,6 +2753,7 @@ interface HubSkillSearchRow {
2603
2753
  visibility: string;
2604
2754
  group_name: string | null;
2605
2755
  owner_name: string | null;
2756
+ owner_status: string | null;
2606
2757
  quality_score: number | null;
2607
2758
  }
2608
2759