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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.env.example +7 -0
  2. package/README.md +111 -44
  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 +36 -2
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/client/connector.d.ts +6 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +160 -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 +2 -3
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +9 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +500 -112
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +11 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +31 -3
  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/chunker.d.ts +2 -1
  33. package/dist/ingest/chunker.d.ts.map +1 -1
  34. package/dist/ingest/chunker.js +14 -10
  35. package/dist/ingest/chunker.js.map +1 -1
  36. package/dist/ingest/providers/index.d.ts.map +1 -1
  37. package/dist/ingest/providers/index.js +37 -6
  38. package/dist/ingest/providers/index.js.map +1 -1
  39. package/dist/recall/engine.d.ts.map +1 -1
  40. package/dist/recall/engine.js +96 -1
  41. package/dist/recall/engine.js.map +1 -1
  42. package/dist/shared/llm-call.d.ts +1 -0
  43. package/dist/shared/llm-call.d.ts.map +1 -1
  44. package/dist/shared/llm-call.js +84 -9
  45. package/dist/shared/llm-call.js.map +1 -1
  46. package/dist/sharing/types.d.ts +1 -1
  47. package/dist/sharing/types.d.ts.map +1 -1
  48. package/dist/skill/evolver.d.ts +4 -0
  49. package/dist/skill/evolver.d.ts.map +1 -1
  50. package/dist/skill/evolver.js +59 -5
  51. package/dist/skill/evolver.js.map +1 -1
  52. package/dist/skill/generator.d.ts +2 -0
  53. package/dist/skill/generator.d.ts.map +1 -1
  54. package/dist/skill/generator.js +45 -3
  55. package/dist/skill/generator.js.map +1 -1
  56. package/dist/skill/installer.d.ts +26 -0
  57. package/dist/skill/installer.d.ts.map +1 -1
  58. package/dist/skill/installer.js +80 -4
  59. package/dist/skill/installer.js.map +1 -1
  60. package/dist/skill/upgrader.d.ts +2 -0
  61. package/dist/skill/upgrader.d.ts.map +1 -1
  62. package/dist/skill/upgrader.js +139 -1
  63. package/dist/skill/upgrader.js.map +1 -1
  64. package/dist/skill/validator.d.ts +3 -0
  65. package/dist/skill/validator.d.ts.map +1 -1
  66. package/dist/skill/validator.js +75 -0
  67. package/dist/skill/validator.js.map +1 -1
  68. package/dist/storage/ensure-binding.d.ts +12 -0
  69. package/dist/storage/ensure-binding.d.ts.map +1 -0
  70. package/dist/storage/ensure-binding.js +53 -0
  71. package/dist/storage/ensure-binding.js.map +1 -0
  72. package/dist/storage/sqlite.d.ts +115 -20
  73. package/dist/storage/sqlite.d.ts.map +1 -1
  74. package/dist/storage/sqlite.js +458 -110
  75. package/dist/storage/sqlite.js.map +1 -1
  76. package/dist/telemetry.d.ts +12 -5
  77. package/dist/telemetry.d.ts.map +1 -1
  78. package/dist/telemetry.js +156 -40
  79. package/dist/telemetry.js.map +1 -1
  80. package/dist/tools/memory-search.d.ts +3 -1
  81. package/dist/tools/memory-search.d.ts.map +1 -1
  82. package/dist/tools/memory-search.js +3 -1
  83. package/dist/tools/memory-search.js.map +1 -1
  84. package/dist/types.d.ts +11 -2
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js +4 -0
  87. package/dist/types.js.map +1 -1
  88. package/dist/viewer/html.d.ts.map +1 -1
  89. package/dist/viewer/html.js +2952 -910
  90. package/dist/viewer/html.js.map +1 -1
  91. package/dist/viewer/server.d.ts +39 -8
  92. package/dist/viewer/server.d.ts.map +1 -1
  93. package/dist/viewer/server.js +1198 -227
  94. package/dist/viewer/server.js.map +1 -1
  95. package/index.ts +774 -74
  96. package/openclaw.plugin.json +2 -2
  97. package/package.json +3 -2
  98. package/scripts/postinstall.cjs +1 -1
  99. package/skill/memos-memory-guide/SKILL.md +64 -26
  100. package/src/capture/index.ts +40 -1
  101. package/src/client/connector.ts +161 -28
  102. package/src/client/hub.ts +18 -0
  103. package/src/client/skill-sync.ts +14 -0
  104. package/src/config.ts +2 -3
  105. package/src/hub/server.ts +481 -107
  106. package/src/hub/user-manager.ts +48 -8
  107. package/src/index.ts +10 -2
  108. package/src/ingest/chunker.ts +19 -13
  109. package/src/ingest/providers/index.ts +41 -7
  110. package/src/recall/engine.ts +89 -1
  111. package/src/shared/llm-call.ts +99 -10
  112. package/src/sharing/types.ts +1 -1
  113. package/src/skill/evolver.ts +63 -6
  114. package/src/skill/generator.ts +44 -5
  115. package/src/skill/installer.ts +107 -4
  116. package/src/skill/upgrader.ts +139 -1
  117. package/src/skill/validator.ts +79 -0
  118. package/src/storage/ensure-binding.ts +52 -0
  119. package/src/storage/sqlite.ts +498 -137
  120. package/src/telemetry.ts +172 -41
  121. package/src/tools/memory-search.ts +2 -1
  122. package/src/types.ts +12 -2
  123. package/src/viewer/html.ts +2952 -910
  124. package/src/viewer/server.ts +1109 -212
@@ -146,11 +146,70 @@ class SqliteStore {
146
146
  this.migrateSkillEmbeddingsAndFts();
147
147
  this.migrateFtsToTrigram();
148
148
  this.migrateHubTables();
149
+ this.migrateHubFtsToTrigram();
150
+ this.migrateLocalSharedTasksOwner();
151
+ this.migrateHubUserIdentityFields();
152
+ this.migrateClientHubConnectionIdentityFields();
149
153
  this.log.debug("Database schema initialized");
150
154
  }
151
155
  migrateChunksIndexesForRecall() {
152
156
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
153
157
  }
158
+ migrateLocalSharedTasksOwner() {
159
+ try {
160
+ const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all();
161
+ if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
162
+ this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
163
+ this.log.info("Migrated: added original_owner column to local_shared_tasks");
164
+ }
165
+ }
166
+ catch { /* table may not exist yet */ }
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
+ }
154
213
  migrateOwnerFields() {
155
214
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all();
156
215
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -298,6 +357,54 @@ class SqliteStore {
298
357
  this.log.warn(`Failed to migrate skills_fts to trigram: ${err}`);
299
358
  }
300
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
+ }
301
408
  migrateTaskId() {
302
409
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
303
410
  if (!cols.some((c) => c.name === "task_id")) {
@@ -503,15 +610,16 @@ class SqliteStore {
503
610
  recordToolCall(toolName, durationMs, success) {
504
611
  this.db.prepare("INSERT INTO tool_calls (tool_name, duration_ms, success, called_at) VALUES (?, ?, ?, ?)").run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
505
612
  }
506
- getToolMetrics(minutes) {
507
- const since = Date.now() - minutes * 60 * 1000;
613
+ getToolMetrics(minutes, fromMs, toMs) {
614
+ const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
615
+ const until = toMs ?? Date.now();
508
616
  const rows = this.db.prepare(`SELECT tool_name,
509
617
  duration_ms,
510
618
  success,
511
619
  strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
512
620
  FROM tool_calls
513
- WHERE called_at >= ?
514
- ORDER BY called_at`).all(since);
621
+ WHERE called_at >= ? AND called_at <= ?
622
+ ORDER BY called_at`).all(since, until);
515
623
  const toolSet = new Set();
516
624
  const minuteMap = new Map();
517
625
  const aggMap = new Map();
@@ -644,33 +752,49 @@ class SqliteStore {
644
752
  shared_at INTEGER NOT NULL
645
753
  );
646
754
 
755
+ CREATE TABLE IF NOT EXISTS local_shared_memories (
756
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
757
+ original_owner TEXT NOT NULL,
758
+ shared_at INTEGER NOT NULL
759
+ );
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
+
647
770
  CREATE TABLE IF NOT EXISTS hub_users (
648
- id TEXT PRIMARY KEY,
649
- username TEXT NOT NULL UNIQUE,
650
- device_name TEXT NOT NULL DEFAULT '',
651
- role TEXT NOT NULL,
652
- status TEXT NOT NULL,
653
- token_hash TEXT NOT NULL DEFAULT '',
654
- created_at INTEGER NOT NULL,
655
- approved_at INTEGER
771
+ id TEXT PRIMARY KEY,
772
+ username TEXT NOT NULL UNIQUE,
773
+ device_name TEXT NOT NULL DEFAULT '',
774
+ role TEXT NOT NULL,
775
+ status TEXT NOT NULL,
776
+ token_hash TEXT NOT NULL DEFAULT '',
777
+ created_at INTEGER NOT NULL,
778
+ approved_at INTEGER,
779
+ last_ip TEXT NOT NULL DEFAULT '',
780
+ last_active_at INTEGER
656
781
  );
657
782
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
658
783
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
659
784
 
660
785
  CREATE TABLE IF NOT EXISTS hub_groups (
661
786
  id TEXT PRIMARY KEY,
662
- name TEXT NOT NULL UNIQUE,
787
+ name TEXT NOT NULL,
663
788
  description TEXT NOT NULL DEFAULT '',
664
789
  created_at INTEGER NOT NULL
665
790
  );
666
791
 
667
792
  CREATE TABLE IF NOT EXISTS hub_group_members (
668
- group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
669
- user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
670
- joined_at INTEGER NOT NULL,
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,
671
796
  PRIMARY KEY (group_id, user_id)
672
797
  );
673
- CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
674
798
 
675
799
  CREATE TABLE IF NOT EXISTS hub_tasks (
676
800
  id TEXT PRIMARY KEY,
@@ -713,7 +837,7 @@ class SqliteStore {
713
837
  content,
714
838
  content='hub_chunks',
715
839
  content_rowid='rowid',
716
- tokenize='porter unicode61'
840
+ tokenize='trigram'
717
841
  );
718
842
 
719
843
  CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
@@ -763,7 +887,7 @@ class SqliteStore {
763
887
  description,
764
888
  content='hub_skills',
765
889
  content_rowid='rowid',
766
- tokenize='porter unicode61'
890
+ tokenize='trigram'
767
891
  );
768
892
 
769
893
  CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
@@ -813,7 +937,7 @@ class SqliteStore {
813
937
  content,
814
938
  content='hub_memories',
815
939
  content_rowid='rowid',
816
- tokenize='porter unicode61'
940
+ tokenize='trigram'
817
941
  );
818
942
 
819
943
  CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
@@ -833,6 +957,31 @@ class SqliteStore {
833
957
  VALUES (new.rowid, new.summary, new.content);
834
958
  END;
835
959
  `);
960
+ this.db.exec(`
961
+ CREATE TABLE IF NOT EXISTS hub_notifications (
962
+ id TEXT PRIMARY KEY,
963
+ user_id TEXT NOT NULL,
964
+ type TEXT NOT NULL,
965
+ resource TEXT NOT NULL,
966
+ title TEXT NOT NULL,
967
+ message TEXT NOT NULL DEFAULT '',
968
+ read INTEGER NOT NULL DEFAULT 0,
969
+ created_at INTEGER NOT NULL
970
+ );
971
+ CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
972
+ `);
973
+ try {
974
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
975
+ if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
976
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
977
+ this.log.info("Migrated: added last_ip column to hub_users");
978
+ }
979
+ if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
980
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
981
+ this.log.info("Migrated: added last_active_at column to hub_users");
982
+ }
983
+ }
984
+ catch { /* table may not exist yet */ }
836
985
  }
837
986
  // ─── Write ───
838
987
  insertChunk(chunk) {
@@ -959,6 +1108,30 @@ class SqliteStore {
959
1108
  return [];
960
1109
  }
961
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
+ }
962
1135
  // ─── Vector Search ───
963
1136
  getAllEmbeddings(ownerFilter) {
964
1137
  let sql = `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
@@ -1097,6 +1270,9 @@ class SqliteStore {
1097
1270
  "skill_embeddings",
1098
1271
  "skill_versions",
1099
1272
  "skills",
1273
+ "local_shared_memories",
1274
+ "team_shared_chunks",
1275
+ "local_shared_tasks",
1100
1276
  "embeddings",
1101
1277
  "chunks",
1102
1278
  "tasks",
@@ -1208,8 +1384,8 @@ class SqliteStore {
1208
1384
  params.push(opts.status);
1209
1385
  }
1210
1386
  if (opts.owner) {
1211
- conditions.push("owner = ?");
1212
- 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);
1213
1389
  }
1214
1390
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1215
1391
  const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params);
@@ -1475,16 +1651,18 @@ class SqliteStore {
1475
1651
  // ─── Hub / Client connection ───
1476
1652
  setClientHubConnection(conn) {
1477
1653
  this.db.prepare(`
1478
- INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
1479
- 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, ?, ?, ?, ?, ?, ?, ?, ?)
1480
1656
  ON CONFLICT(id) DO UPDATE SET
1481
1657
  hub_url = excluded.hub_url,
1482
1658
  user_id = excluded.user_id,
1483
1659
  username = excluded.username,
1484
1660
  user_token = excluded.user_token,
1485
1661
  role = excluded.role,
1486
- connected_at = excluded.connected_at
1487
- `).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 ?? "");
1488
1666
  }
1489
1667
  getClientHubConnection() {
1490
1668
  const row = this.db.prepare('SELECT * FROM client_hub_connection WHERE id = 1').get();
@@ -1519,11 +1697,66 @@ class SqliteStore {
1519
1697
  const rows = this.db.prepare('SELECT task_id, hub_task_id, visibility, group_id, synced_chunks FROM local_shared_tasks').all();
1520
1698
  return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks }));
1521
1699
  }
1700
+ // ─── Local Shared Memories (client-side tracking) ───
1701
+ markMemorySharedLocally(chunkId) {
1702
+ const chunk = this.getChunk(chunkId);
1703
+ if (!chunk)
1704
+ return { ok: false, reason: "not_found" };
1705
+ if (chunk.owner === "public") {
1706
+ const existing = this.getLocalSharedMemory(chunkId);
1707
+ return {
1708
+ ok: true,
1709
+ owner: "public",
1710
+ originalOwner: existing?.originalOwner ?? undefined,
1711
+ sharedAt: existing?.sharedAt ?? undefined,
1712
+ };
1713
+ }
1714
+ const sharedAt = Date.now();
1715
+ this.db.transaction(() => {
1716
+ this.db.prepare(`
1717
+ INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
1718
+ VALUES (?, ?, ?)
1719
+ ON CONFLICT(chunk_id) DO UPDATE SET
1720
+ original_owner = excluded.original_owner,
1721
+ shared_at = excluded.shared_at
1722
+ `).run(chunkId, chunk.owner, sharedAt);
1723
+ this.updateChunk(chunkId, { owner: "public" });
1724
+ })();
1725
+ return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
1726
+ }
1727
+ unmarkMemorySharedLocally(chunkId, fallbackOwner) {
1728
+ const chunk = this.getChunk(chunkId);
1729
+ if (!chunk)
1730
+ return { ok: false, reason: "not_found" };
1731
+ if (chunk.owner !== "public") {
1732
+ return { ok: true, owner: chunk.owner };
1733
+ }
1734
+ const existing = this.getLocalSharedMemory(chunkId);
1735
+ const restoreOwner = existing?.originalOwner ?? fallbackOwner;
1736
+ if (!restoreOwner || restoreOwner === "public") {
1737
+ return { ok: false, reason: "original_owner_missing" };
1738
+ }
1739
+ this.db.transaction(() => {
1740
+ this.updateChunk(chunkId, { owner: restoreOwner });
1741
+ this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
1742
+ })();
1743
+ return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
1744
+ }
1745
+ getLocalSharedMemory(chunkId) {
1746
+ const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId);
1747
+ if (!row)
1748
+ return null;
1749
+ return {
1750
+ chunkId: row.chunk_id,
1751
+ originalOwner: row.original_owner,
1752
+ sharedAt: row.shared_at,
1753
+ };
1754
+ }
1522
1755
  // ─── Hub Users / Groups ───
1523
1756
  upsertHubUser(user) {
1524
1757
  this.db.prepare(`
1525
- INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
1526
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1527
1760
  ON CONFLICT(id) DO UPDATE SET
1528
1761
  username = excluded.username,
1529
1762
  device_name = excluded.device_name,
@@ -1531,72 +1764,101 @@ class SqliteStore {
1531
1764
  status = excluded.status,
1532
1765
  token_hash = excluded.token_hash,
1533
1766
  created_at = excluded.created_at,
1534
- approved_at = excluded.approved_at
1535
- `).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);
1536
1774
  }
1537
1775
  getHubUser(userId) {
1538
1776
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId);
1539
1777
  if (!row)
1540
1778
  return null;
1541
- return this.attachGroupsToHubUser(rowToHubUser(row));
1779
+ const user = rowToHubUser(row);
1780
+ user.groups = this.getGroupsForHubUser(userId);
1781
+ return user;
1542
1782
  }
1543
1783
  listHubUsers(status) {
1544
1784
  const rows = status
1545
1785
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status)
1546
1786
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all();
1547
- return rows.map((row) => this.attachGroupsToHubUser(rowToHubUser(row)));
1787
+ return rows.map(r => {
1788
+ const user = rowToHubUser(r);
1789
+ user.groups = this.getGroupsForHubUser(r.id);
1790
+ return user;
1791
+ });
1792
+ }
1793
+ deleteHubUser(userId, cleanResources = false) {
1794
+ if (cleanResources) {
1795
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1796
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
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;
1800
+ }
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;
1548
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);
1812
+ return result.changes > 0;
1813
+ }
1814
+ updateHubUserActivity(userId, ip, timestamp) {
1815
+ this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1816
+ }
1817
+ // ─── Hub Groups ───
1549
1818
  upsertHubGroup(group) {
1550
1819
  this.db.prepare(`
1551
1820
  INSERT INTO hub_groups (id, name, description, created_at)
1552
1821
  VALUES (?, ?, ?, ?)
1553
- ON CONFLICT(id) DO UPDATE SET
1554
- name = excluded.name,
1555
- description = excluded.description,
1556
- created_at = excluded.created_at
1557
- `).run(group.id, group.name, group.description, group.createdAt);
1558
- }
1559
- listHubGroups() {
1560
- const rows = this.db.prepare('SELECT * FROM hub_groups ORDER BY name').all();
1561
- return rows.map(rowToHubGroup);
1822
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description
1823
+ `).run(group.id, group.name, group.description ?? "", group.createdAt);
1562
1824
  }
1563
- addHubGroupMember(groupId, userId, joinedAt = Date.now()) {
1825
+ addHubGroupMember(groupId, userId, joinedAt) {
1564
1826
  this.db.prepare(`
1565
- INSERT INTO hub_group_members (group_id, user_id, joined_at)
1827
+ INSERT OR IGNORE INTO hub_group_members (group_id, user_id, joined_at)
1566
1828
  VALUES (?, ?, ?)
1567
- ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
1568
1829
  `).run(groupId, userId, joinedAt);
1569
1830
  }
1570
- getHubGroupById(groupId) {
1571
- const row = this.db.prepare('SELECT * FROM hub_groups WHERE id = ?').get(groupId);
1572
- return row ? rowToHubGroup(row) : undefined;
1573
- }
1574
- deleteHubGroup(groupId) {
1575
- const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
1576
- return result.changes > 0;
1577
- }
1578
- listHubGroupMembers(groupId) {
1579
- const rows = this.db.prepare(`
1580
- SELECT gm.user_id, hu.username, gm.joined_at
1581
- FROM hub_group_members gm
1582
- JOIN hub_users hu ON hu.id = gm.user_id
1583
- WHERE gm.group_id = ?
1584
- ORDER BY gm.joined_at
1585
- `).all(groupId);
1586
- return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
1587
- }
1588
1831
  removeHubGroupMember(groupId, userId) {
1589
1832
  this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1590
1833
  }
1591
1834
  getGroupsForHubUser(userId) {
1592
- const rows = this.db.prepare(`
1593
- SELECT g.*
1594
- FROM hub_group_members gm
1595
- JOIN hub_groups g ON g.id = gm.group_id
1596
- WHERE gm.user_id = ?
1597
- ORDER BY g.name
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 = ?
1598
1839
  `).all(userId);
1599
- return rows.map((row) => ({ id: row.id, name: row.name, description: row.description || undefined }));
1840
+ }
1841
+ getHubUserContributions() {
1842
+ const result = {};
1843
+ const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all();
1844
+ const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all();
1845
+ const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all();
1846
+ for (const r of memRows) {
1847
+ if (!result[r.source_user_id])
1848
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1849
+ result[r.source_user_id].memoryCount = r.cnt;
1850
+ }
1851
+ for (const r of taskRows) {
1852
+ if (!result[r.source_user_id])
1853
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1854
+ result[r.source_user_id].taskCount = r.cnt;
1855
+ }
1856
+ for (const r of skillRows) {
1857
+ if (!result[r.source_user_id])
1858
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1859
+ result[r.source_user_id].skillCount = r.cnt;
1860
+ }
1861
+ return result;
1600
1862
  }
1601
1863
  // ─── Hub Shared Data ───
1602
1864
  upsertHubTask(task) {
@@ -1616,6 +1878,10 @@ class SqliteStore {
1616
1878
  const row = this.db.prepare('SELECT * FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId);
1617
1879
  return row ? rowToHubTask(row) : null;
1618
1880
  }
1881
+ getHubTaskById(taskId) {
1882
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId);
1883
+ return row ? rowToHubTask(row) : null;
1884
+ }
1619
1885
  upsertHubChunk(chunk) {
1620
1886
  if (!chunk.sourceTaskId)
1621
1887
  throw new Error("sourceTaskId is required for hub chunk upserts");
@@ -1688,20 +1954,36 @@ class SqliteStore {
1688
1954
  out.push(row.vector.readFloatLE(i * 4));
1689
1955
  return out;
1690
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
+ }
1691
1968
  searchHubChunks(query, options) {
1692
1969
  const limit = options?.maxResults ?? 10;
1693
1970
  const userId = options?.userId ?? "";
1694
1971
  const rows = this.db.prepare(`
1695
- 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,
1696
1974
  bm25(hub_chunks_fts) as rank
1697
1975
  FROM hub_chunks_fts f
1698
1976
  JOIN hub_chunks hc ON hc.rowid = f.rowid
1699
1977
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1700
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
1701
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 = ?))
1702
1984
  ORDER BY rank
1703
1985
  LIMIT ?
1704
- `).all(sanitizeFtsQuery(query), limit);
1986
+ `).all(sanitizeFtsQuery(query), userId, userId, limit);
1705
1987
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1706
1988
  }
1707
1989
  upsertHubEmbedding(chunkId, vector) {
@@ -1724,7 +2006,10 @@ class SqliteStore {
1724
2006
  FROM hub_embeddings he
1725
2007
  JOIN hub_chunks hc ON hc.id = he.chunk_id
1726
2008
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1727
- `).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);
1728
2013
  return rows.map(r => ({
1729
2014
  chunkId: r.chunk_id,
1730
2015
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -1732,14 +2017,19 @@ class SqliteStore {
1732
2017
  }
1733
2018
  getVisibleHubSearchHitByChunkId(chunkId, userId) {
1734
2019
  const row = this.db.prepare(`
1735
- 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,
1736
2022
  0 as rank
1737
2023
  FROM hub_chunks hc
1738
2024
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1739
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
1740
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 = ?))
1741
2031
  LIMIT 1
1742
- `).get(chunkId);
2032
+ `).get(chunkId, userId, userId);
1743
2033
  return row ?? null;
1744
2034
  }
1745
2035
  getHubChunkById(chunkId) {
@@ -1753,7 +2043,7 @@ class SqliteStore {
1753
2043
  let rows;
1754
2044
  if (sanitized) {
1755
2045
  rows = this.db.prepare(`
1756
- 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,
1757
2047
  bm25(hub_skills_fts) as rank
1758
2048
  FROM hub_skills_fts f
1759
2049
  JOIN hub_skills hs ON hs.rowid = f.rowid
@@ -1765,7 +2055,7 @@ class SqliteStore {
1765
2055
  }
1766
2056
  else {
1767
2057
  rows = this.db.prepare(`
1768
- 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,
1769
2059
  0 as rank
1770
2060
  FROM hub_skills hs
1771
2061
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
@@ -1780,7 +2070,7 @@ class SqliteStore {
1780
2070
  }
1781
2071
  listVisibleHubTasks(userId, limit = 40) {
1782
2072
  const rows = this.db.prepare(`
1783
- 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,
1784
2074
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1785
2075
  FROM hub_tasks t
1786
2076
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -1790,33 +2080,36 @@ class SqliteStore {
1790
2080
  return rows.map(r => ({
1791
2081
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1792
2082
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1793
- 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,
1794
2084
  createdAt: r.created_at, updatedAt: r.updated_at,
1795
2085
  }));
1796
2086
  }
1797
2087
  listAllHubTasks() {
1798
2088
  const rows = this.db.prepare(`
1799
- SELECT t.*, u.username AS owner_name, g.name AS group_name,
2089
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status,
1800
2090
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1801
2091
  FROM hub_tasks t
1802
2092
  LEFT JOIN hub_users u ON u.id = t.source_user_id
1803
- LEFT JOIN hub_groups g ON g.id = t.group_id
1804
2093
  ORDER BY t.updated_at DESC
1805
2094
  `).all();
1806
2095
  return rows.map(r => ({
1807
2096
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1808
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1809
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2097
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: null,
2098
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1810
2099
  createdAt: r.created_at, updatedAt: r.updated_at,
1811
2100
  }));
1812
2101
  }
2102
+ listHubChunksByTaskId(hubTaskId) {
2103
+ const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId);
2104
+ return rows.map(rowToHubChunk);
2105
+ }
1813
2106
  deleteHubTaskById(taskId) {
1814
2107
  const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
1815
2108
  return info.changes > 0;
1816
2109
  }
1817
2110
  listVisibleHubSkills(userId, limit = 40) {
1818
2111
  const rows = this.db.prepare(`
1819
- 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
1820
2113
  FROM hub_skills s
1821
2114
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1822
2115
  ORDER BY s.updated_at DESC
@@ -1826,23 +2119,22 @@ class SqliteStore {
1826
2119
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1827
2120
  name: r.name, description: r.description, version: r.version,
1828
2121
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1829
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2122
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1830
2123
  createdAt: r.created_at, updatedAt: r.updated_at,
1831
2124
  }));
1832
2125
  }
1833
2126
  listAllHubSkills() {
1834
2127
  const rows = this.db.prepare(`
1835
- SELECT s.*, u.username AS owner_name, g.name AS group_name
2128
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status
1836
2129
  FROM hub_skills s
1837
2130
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1838
- LEFT JOIN hub_groups g ON g.id = s.group_id
1839
2131
  ORDER BY s.updated_at DESC
1840
2132
  `).all();
1841
2133
  return rows.map(r => ({
1842
2134
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1843
2135
  name: r.name, description: r.description, version: r.version,
1844
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1845
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2136
+ groupId: r.group_id, groupName: null, visibility: r.visibility,
2137
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1846
2138
  createdAt: r.created_at, updatedAt: r.updated_at,
1847
2139
  }));
1848
2140
  }
@@ -1881,6 +2173,68 @@ class SqliteStore {
1881
2173
  const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
1882
2174
  return info.changes > 0;
1883
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
+ }
2207
+ // ─── Hub Notifications ───
2208
+ insertHubNotification(n) {
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());
2210
+ }
2211
+ hasRecentHubNotification(userId, type, resource, windowMs = 300_000) {
2212
+ const since = Date.now() - windowMs;
2213
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?').get(userId, type, resource, since);
2214
+ return row.cnt > 0;
2215
+ }
2216
+ listHubNotifications(userId, opts) {
2217
+ const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2218
+ const limit = opts?.limit ?? 50;
2219
+ const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit);
2220
+ 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 }));
2221
+ }
2222
+ countUnreadHubNotifications(userId) {
2223
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId);
2224
+ return row.cnt;
2225
+ }
2226
+ markHubNotificationsRead(userId, ids) {
2227
+ if (ids && ids.length > 0) {
2228
+ const placeholders = ids.map(() => '?').join(',');
2229
+ this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
2230
+ }
2231
+ else {
2232
+ this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
2233
+ }
2234
+ }
2235
+ clearHubNotifications(userId) {
2236
+ this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
2237
+ }
1884
2238
  upsertHubMemoryEmbedding(memoryId, vector) {
1885
2239
  const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
1886
2240
  this.db.prepare(`
@@ -1937,7 +2291,7 @@ class SqliteStore {
1937
2291
  }
1938
2292
  listVisibleHubMemories(userId, limit = 40) {
1939
2293
  const rows = this.db.prepare(`
1940
- 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
1941
2295
  FROM hub_memories m
1942
2296
  LEFT JOIN hub_users u ON u.id = m.source_user_id
1943
2297
  ORDER BY m.updated_at DESC
@@ -1945,24 +2299,23 @@ class SqliteStore {
1945
2299
  `).all(limit);
1946
2300
  return rows.map(r => ({
1947
2301
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
1948
- role: r.role, summary: r.summary, kind: r.kind,
2302
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
1949
2303
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1950
- 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,
1951
2305
  }));
1952
2306
  }
1953
2307
  listAllHubMemories() {
1954
2308
  const rows = this.db.prepare(`
1955
- SELECT m.*, u.username AS owner_name, g.name AS group_name
2309
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status
1956
2310
  FROM hub_memories m
1957
2311
  LEFT JOIN hub_users u ON u.id = m.source_user_id
1958
- LEFT JOIN hub_groups g ON g.id = m.group_id
1959
2312
  ORDER BY m.updated_at DESC
1960
2313
  `).all();
1961
2314
  return rows.map(r => ({
1962
2315
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
1963
- role: r.role, summary: r.summary, kind: r.kind,
1964
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1965
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2316
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2317
+ groupId: r.group_id, groupName: null, visibility: r.visibility,
2318
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
1966
2319
  }));
1967
2320
  }
1968
2321
  resolveCanonicalHubTaskId(taskId, sourceUserId, sourceTaskId) {
@@ -1987,12 +2340,6 @@ class SqliteStore {
1987
2340
  }
1988
2341
  throw new Error(`source skill not found for skillId=${skillId}`);
1989
2342
  }
1990
- attachGroupsToHubUser(user) {
1991
- return {
1992
- ...user,
1993
- groups: this.getGroupsForHubUser(user.id),
1994
- };
1995
- }
1996
2343
  getSessionOwnerMap(sessionKeys) {
1997
2344
  const result = new Map();
1998
2345
  if (sessionKeys.length === 0)
@@ -2102,6 +2449,8 @@ function rowToClientHubConnection(row) {
2102
2449
  userToken: row.user_token,
2103
2450
  role: row.role,
2104
2451
  connectedAt: row.connected_at,
2452
+ identityKey: row.identity_key || "",
2453
+ lastKnownStatus: row.last_known_status || "",
2105
2454
  };
2106
2455
  }
2107
2456
  function rowToHubUser(row) {
@@ -2115,14 +2464,13 @@ function rowToHubUser(row) {
2115
2464
  tokenHash: row.token_hash,
2116
2465
  createdAt: row.created_at,
2117
2466
  approvedAt: row.approved_at,
2118
- };
2119
- }
2120
- function rowToHubGroup(row) {
2121
- return {
2122
- id: row.id,
2123
- name: row.name,
2124
- description: row.description,
2125
- createdAt: row.created_at,
2467
+ lastIp: row.last_ip || "",
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,
2126
2474
  };
2127
2475
  }
2128
2476
  function rowToHubTask(row) {