@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
@@ -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,10 @@ export class SqliteStore {
112
112
  this.migrateSkillEmbeddingsAndFts();
113
113
  this.migrateFtsToTrigram();
114
114
  this.migrateHubTables();
115
+ this.migrateHubFtsToTrigram();
116
+ this.migrateLocalSharedTasksOwner();
117
+ this.migrateHubUserIdentityFields();
118
+ this.migrateClientHubConnectionIdentityFields();
115
119
  this.log.debug("Database schema initialized");
116
120
  }
117
121
 
@@ -119,6 +123,59 @@ export class SqliteStore {
119
123
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
120
124
  }
121
125
 
126
+ private migrateLocalSharedTasksOwner(): void {
127
+ try {
128
+ const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
129
+ if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
130
+ this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
131
+ this.log.info("Migrated: added original_owner column to local_shared_tasks");
132
+ }
133
+ } catch { /* table may not exist yet */ }
134
+ }
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
+
122
179
  private migrateOwnerFields(): void {
123
180
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
124
181
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -273,6 +330,51 @@ export class SqliteStore {
273
330
  }
274
331
  }
275
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
+
276
378
  private migrateTaskId(): void {
277
379
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
278
380
  if (!cols.some((c) => c.name === "task_id")) {
@@ -516,12 +618,13 @@ export class SqliteStore {
516
618
  ).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
517
619
  }
518
620
 
519
- getToolMetrics(minutes: number): {
621
+ getToolMetrics(minutes: number, fromMs?: number, toMs?: number): {
520
622
  tools: string[];
521
623
  series: Array<{ minute: string; [tool: string]: number | string }>;
522
624
  aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
523
625
  } {
524
- const since = Date.now() - minutes * 60 * 1000;
626
+ const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
627
+ const until = toMs ?? Date.now();
525
628
 
526
629
  const rows = this.db.prepare(
527
630
  `SELECT tool_name,
@@ -529,9 +632,9 @@ export class SqliteStore {
529
632
  success,
530
633
  strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
531
634
  FROM tool_calls
532
- WHERE called_at >= ?
635
+ WHERE called_at >= ? AND called_at <= ?
533
636
  ORDER BY called_at`,
534
- ).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
637
+ ).all(since, until) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
535
638
 
536
639
  const toolSet = new Set<string>();
537
640
  const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
@@ -683,33 +786,49 @@ export class SqliteStore {
683
786
  shared_at INTEGER NOT NULL
684
787
  );
685
788
 
789
+ CREATE TABLE IF NOT EXISTS local_shared_memories (
790
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
791
+ original_owner TEXT NOT NULL,
792
+ shared_at INTEGER NOT NULL
793
+ );
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
+
686
804
  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
805
+ id TEXT PRIMARY KEY,
806
+ username TEXT NOT NULL UNIQUE,
807
+ device_name TEXT NOT NULL DEFAULT '',
808
+ role TEXT NOT NULL,
809
+ status TEXT NOT NULL,
810
+ token_hash TEXT NOT NULL DEFAULT '',
811
+ created_at INTEGER NOT NULL,
812
+ approved_at INTEGER,
813
+ last_ip TEXT NOT NULL DEFAULT '',
814
+ last_active_at INTEGER
695
815
  );
696
816
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
697
817
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
698
818
 
699
819
  CREATE TABLE IF NOT EXISTS hub_groups (
700
820
  id TEXT PRIMARY KEY,
701
- name TEXT NOT NULL UNIQUE,
821
+ name TEXT NOT NULL,
702
822
  description TEXT NOT NULL DEFAULT '',
703
823
  created_at INTEGER NOT NULL
704
824
  );
705
825
 
706
826
  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,
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,
710
830
  PRIMARY KEY (group_id, user_id)
711
831
  );
712
- CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
713
832
 
714
833
  CREATE TABLE IF NOT EXISTS hub_tasks (
715
834
  id TEXT PRIMARY KEY,
@@ -752,7 +871,7 @@ export class SqliteStore {
752
871
  content,
753
872
  content='hub_chunks',
754
873
  content_rowid='rowid',
755
- tokenize='porter unicode61'
874
+ tokenize='trigram'
756
875
  );
757
876
 
758
877
  CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
@@ -802,7 +921,7 @@ export class SqliteStore {
802
921
  description,
803
922
  content='hub_skills',
804
923
  content_rowid='rowid',
805
- tokenize='porter unicode61'
924
+ tokenize='trigram'
806
925
  );
807
926
 
808
927
  CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
@@ -852,7 +971,7 @@ export class SqliteStore {
852
971
  content,
853
972
  content='hub_memories',
854
973
  content_rowid='rowid',
855
- tokenize='porter unicode61'
974
+ tokenize='trigram'
856
975
  );
857
976
 
858
977
  CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
@@ -872,6 +991,32 @@ export class SqliteStore {
872
991
  VALUES (new.rowid, new.summary, new.content);
873
992
  END;
874
993
  `);
994
+
995
+ this.db.exec(`
996
+ CREATE TABLE IF NOT EXISTS hub_notifications (
997
+ id TEXT PRIMARY KEY,
998
+ user_id TEXT NOT NULL,
999
+ type TEXT NOT NULL,
1000
+ resource TEXT NOT NULL,
1001
+ title TEXT NOT NULL,
1002
+ message TEXT NOT NULL DEFAULT '',
1003
+ read INTEGER NOT NULL DEFAULT 0,
1004
+ created_at INTEGER NOT NULL
1005
+ );
1006
+ CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
1007
+ `);
1008
+
1009
+ try {
1010
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
1011
+ if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
1012
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
1013
+ this.log.info("Migrated: added last_ip column to hub_users");
1014
+ }
1015
+ if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
1016
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
1017
+ this.log.info("Migrated: added last_active_at column to hub_users");
1018
+ }
1019
+ } catch { /* table may not exist yet */ }
875
1020
  }
876
1021
 
877
1022
  // ─── Write ───
@@ -1047,6 +1192,25 @@ export class SqliteStore {
1047
1192
  }
1048
1193
  }
1049
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
+
1050
1214
  // ─── Vector Search ───
1051
1215
 
1052
1216
  getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
@@ -1213,6 +1377,9 @@ export class SqliteStore {
1213
1377
  "skill_embeddings",
1214
1378
  "skill_versions",
1215
1379
  "skills",
1380
+ "local_shared_memories",
1381
+ "team_shared_chunks",
1382
+ "local_shared_tasks",
1216
1383
  "embeddings",
1217
1384
  "chunks",
1218
1385
  "tasks",
@@ -1333,7 +1500,10 @@ export class SqliteStore {
1333
1500
  const conditions: string[] = [];
1334
1501
  const params: unknown[] = [];
1335
1502
  if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
1336
- 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
+ }
1337
1507
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1338
1508
 
1339
1509
  const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params) as { c: number };
@@ -1633,16 +1803,18 @@ export class SqliteStore {
1633
1803
 
1634
1804
  setClientHubConnection(conn: ClientHubConnection): void {
1635
1805
  this.db.prepare(`
1636
- INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
1637
- 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, ?, ?, ?, ?, ?, ?, ?, ?)
1638
1808
  ON CONFLICT(id) DO UPDATE SET
1639
1809
  hub_url = excluded.hub_url,
1640
1810
  user_id = excluded.user_id,
1641
1811
  username = excluded.username,
1642
1812
  user_token = excluded.user_token,
1643
1813
  role = excluded.role,
1644
- connected_at = excluded.connected_at
1645
- `).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 ?? "");
1646
1818
  }
1647
1819
 
1648
1820
  getClientHubConnection(): ClientHubConnection | null {
@@ -1684,12 +1856,73 @@ export class SqliteStore {
1684
1856
  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
1857
  }
1686
1858
 
1859
+ // ─── Local Shared Memories (client-side tracking) ───
1860
+
1861
+ markMemorySharedLocally(chunkId: string): { ok: boolean; owner?: string; originalOwner?: string; sharedAt?: number; reason?: string } {
1862
+ const chunk = this.getChunk(chunkId);
1863
+ if (!chunk) return { ok: false, reason: "not_found" };
1864
+ if (chunk.owner === "public") {
1865
+ const existing = this.getLocalSharedMemory(chunkId);
1866
+ return {
1867
+ ok: true,
1868
+ owner: "public",
1869
+ originalOwner: existing?.originalOwner ?? undefined,
1870
+ sharedAt: existing?.sharedAt ?? undefined,
1871
+ };
1872
+ }
1873
+
1874
+ const sharedAt = Date.now();
1875
+ this.db.transaction(() => {
1876
+ this.db.prepare(`
1877
+ INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
1878
+ VALUES (?, ?, ?)
1879
+ ON CONFLICT(chunk_id) DO UPDATE SET
1880
+ original_owner = excluded.original_owner,
1881
+ shared_at = excluded.shared_at
1882
+ `).run(chunkId, chunk.owner, sharedAt);
1883
+ this.updateChunk(chunkId, { owner: "public" });
1884
+ })();
1885
+
1886
+ return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
1887
+ }
1888
+
1889
+ unmarkMemorySharedLocally(chunkId: string, fallbackOwner?: string): { ok: boolean; owner?: string; originalOwner?: string; reason?: string } {
1890
+ const chunk = this.getChunk(chunkId);
1891
+ if (!chunk) return { ok: false, reason: "not_found" };
1892
+ if (chunk.owner !== "public") {
1893
+ return { ok: true, owner: chunk.owner };
1894
+ }
1895
+
1896
+ const existing = this.getLocalSharedMemory(chunkId);
1897
+ const restoreOwner = existing?.originalOwner ?? fallbackOwner;
1898
+ if (!restoreOwner || restoreOwner === "public") {
1899
+ return { ok: false, reason: "original_owner_missing" };
1900
+ }
1901
+
1902
+ this.db.transaction(() => {
1903
+ this.updateChunk(chunkId, { owner: restoreOwner });
1904
+ this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
1905
+ })();
1906
+
1907
+ return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
1908
+ }
1909
+
1910
+ getLocalSharedMemory(chunkId: string): { chunkId: string; originalOwner: string; sharedAt: number } | null {
1911
+ const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId) as any;
1912
+ if (!row) return null;
1913
+ return {
1914
+ chunkId: row.chunk_id,
1915
+ originalOwner: row.original_owner,
1916
+ sharedAt: row.shared_at,
1917
+ };
1918
+ }
1919
+
1687
1920
  // ─── Hub Users / Groups ───
1688
1921
 
1689
1922
  upsertHubUser(user: HubUserRecord): void {
1690
1923
  this.db.prepare(`
1691
- INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
1692
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1693
1926
  ON CONFLICT(id) DO UPDATE SET
1694
1927
  username = excluded.username,
1695
1928
  device_name = excluded.device_name,
@@ -1697,81 +1930,99 @@ export class SqliteStore {
1697
1930
  status = excluded.status,
1698
1931
  token_hash = excluded.token_hash,
1699
1932
  created_at = excluded.created_at,
1700
- approved_at = excluded.approved_at
1701
- `).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);
1702
1940
  }
1703
1941
 
1704
1942
  getHubUser(userId: string): HubUserRecord | null {
1705
1943
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
1706
1944
  if (!row) return null;
1707
- return this.attachGroupsToHubUser(rowToHubUser(row));
1945
+ const user = rowToHubUser(row);
1946
+ user.groups = this.getGroupsForHubUser(userId);
1947
+ return user;
1708
1948
  }
1709
1949
 
1710
1950
  listHubUsers(status?: UserStatus): HubUserRecord[] {
1711
1951
  const rows = status
1712
1952
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
1713
1953
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
1714
- return rows.map((row) => this.attachGroupsToHubUser(rowToHubUser(row)));
1954
+ return rows.map(r => {
1955
+ const user = rowToHubUser(r);
1956
+ user.groups = this.getGroupsForHubUser(r.id);
1957
+ return user;
1958
+ });
1715
1959
  }
1716
1960
 
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);
1961
+ deleteHubUser(userId: string, cleanResources = false): boolean {
1962
+ if (cleanResources) {
1963
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1964
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
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;
1968
+ }
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;
1726
1971
  }
1727
1972
 
1728
- listHubGroups(): HubGroupRecord[] {
1729
- const rows = this.db.prepare('SELECT * FROM hub_groups ORDER BY name').all() as HubGroupRow[];
1730
- return rows.map(rowToHubGroup);
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;
1731
1977
  }
1732
1978
 
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);
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);
1981
+ return result.changes > 0;
1739
1982
  }
1740
1983
 
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;
1984
+ updateHubUserActivity(userId: string, ip: string, timestamp?: number): void {
1985
+ this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1744
1986
  }
1745
1987
 
1746
- deleteHubGroup(groupId: string): boolean {
1747
- const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
1748
- return result.changes > 0;
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);
1749
1996
  }
1750
1997
 
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 }));
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);
1760
2003
  }
1761
2004
 
1762
2005
  removeHubGroupMember(groupId: string, userId: string): void {
1763
2006
  this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1764
2007
  }
1765
2008
 
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 }));
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
+
2017
+ getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
2018
+ const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
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 }>;
2020
+ 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 }>;
2021
+ 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 }>;
2022
+ 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; }
2023
+ 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; }
2024
+ 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; }
2025
+ return result;
1775
2026
  }
1776
2027
 
1777
2028
  // ─── Hub Shared Data ───
@@ -1795,6 +2046,11 @@ export class SqliteStore {
1795
2046
  return row ? rowToHubTask(row) : null;
1796
2047
  }
1797
2048
 
2049
+ getHubTaskById(taskId: string): HubTaskRecord | null {
2050
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId) as HubTaskRow | undefined;
2051
+ return row ? rowToHubTask(row) : null;
2052
+ }
2053
+
1798
2054
  upsertHubChunk(chunk: HubChunkUpsertInput): void {
1799
2055
  if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
1800
2056
  const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
@@ -1870,20 +2126,37 @@ export class SqliteStore {
1870
2126
  return out;
1871
2127
  }
1872
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
+
1873
2141
  searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
1874
2142
  const limit = options?.maxResults ?? 10;
1875
2143
  const userId = options?.userId ?? "";
1876
2144
  const rows = this.db.prepare(`
1877
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' 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,
1878
2147
  bm25(hub_chunks_fts) as rank
1879
2148
  FROM hub_chunks_fts f
1880
2149
  JOIN hub_chunks hc ON hc.rowid = f.rowid
1881
2150
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1882
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
1883
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 = ?))
1884
2157
  ORDER BY rank
1885
2158
  LIMIT ?
1886
- `).all(sanitizeFtsQuery(query), limit) as HubSearchRow[];
2159
+ `).all(sanitizeFtsQuery(query), userId, userId, limit) as HubSearchRow[];
1887
2160
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1888
2161
  }
1889
2162
 
@@ -1908,7 +2181,10 @@ export class SqliteStore {
1908
2181
  FROM hub_embeddings he
1909
2182
  JOIN hub_chunks hc ON hc.id = he.chunk_id
1910
2183
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1911
- `).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 }>;
1912
2188
  return rows.map(r => ({
1913
2189
  chunkId: r.chunk_id,
1914
2190
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -1917,14 +2193,19 @@ export class SqliteStore {
1917
2193
 
1918
2194
  getVisibleHubSearchHitByChunkId(chunkId: string, userId: string): HubSearchRow | null {
1919
2195
  const row = this.db.prepare(`
1920
- 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,
1921
2198
  0 as rank
1922
2199
  FROM hub_chunks hc
1923
2200
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1924
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
1925
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 = ?))
1926
2207
  LIMIT 1
1927
- `).get(chunkId) as HubSearchRow | undefined;
2208
+ `).get(chunkId, userId, userId) as HubSearchRow | undefined;
1928
2209
  return row ?? null;
1929
2210
  }
1930
2211
 
@@ -1940,7 +2221,7 @@ export class SqliteStore {
1940
2221
  let rows: HubSkillSearchRow[];
1941
2222
  if (sanitized) {
1942
2223
  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,
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,
1944
2225
  bm25(hub_skills_fts) as rank
1945
2226
  FROM hub_skills_fts f
1946
2227
  JOIN hub_skills hs ON hs.rowid = f.rowid
@@ -1951,7 +2232,7 @@ export class SqliteStore {
1951
2232
  `).all(sanitized, limit) as HubSkillSearchRow[];
1952
2233
  } else {
1953
2234
  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,
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,
1955
2236
  0 as rank
1956
2237
  FROM hub_skills hs
1957
2238
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
@@ -1966,9 +2247,9 @@ export class SqliteStore {
1966
2247
  this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
1967
2248
  }
1968
2249
 
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 }> {
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 }> {
1970
2251
  const rows = this.db.prepare(`
1971
- 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,
1972
2253
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1973
2254
  FROM hub_tasks t
1974
2255
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -1978,36 +2259,40 @@ export class SqliteStore {
1978
2259
  return rows.map(r => ({
1979
2260
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1980
2261
  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,
2262
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1982
2263
  createdAt: r.created_at, updatedAt: r.updated_at,
1983
2264
  }));
1984
2265
  }
1985
2266
 
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 }> {
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 }> {
1987
2268
  const rows = this.db.prepare(`
1988
- SELECT t.*, u.username AS owner_name, g.name AS group_name,
2269
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status,
1989
2270
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1990
2271
  FROM hub_tasks t
1991
2272
  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
2273
  ORDER BY t.updated_at DESC
1994
2274
  `).all() as any[];
1995
2275
  return rows.map(r => ({
1996
2276
  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,
2277
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
2278
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1999
2279
  createdAt: r.created_at, updatedAt: r.updated_at,
2000
2280
  }));
2001
2281
  }
2002
2282
 
2283
+ listHubChunksByTaskId(hubTaskId: string): HubChunkRecord[] {
2284
+ const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId) as HubChunkRow[];
2285
+ return rows.map(rowToHubChunk);
2286
+ }
2287
+
2003
2288
  deleteHubTaskById(taskId: string): boolean {
2004
2289
  const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
2005
2290
  return info.changes > 0;
2006
2291
  }
2007
2292
 
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 }> {
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 }> {
2009
2294
  const rows = this.db.prepare(`
2010
- 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
2011
2296
  FROM hub_skills s
2012
2297
  LEFT JOIN hub_users u ON u.id = s.source_user_id
2013
2298
  ORDER BY s.updated_at DESC
@@ -2017,24 +2302,23 @@ export class SqliteStore {
2017
2302
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2018
2303
  name: r.name, description: r.description, version: r.version,
2019
2304
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2020
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2305
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2021
2306
  createdAt: r.created_at, updatedAt: r.updated_at,
2022
2307
  }));
2023
2308
  }
2024
2309
 
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 }> {
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 }> {
2026
2311
  const rows = this.db.prepare(`
2027
- SELECT s.*, u.username AS owner_name, g.name AS group_name
2312
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status
2028
2313
  FROM hub_skills s
2029
2314
  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
2315
  ORDER BY s.updated_at DESC
2032
2316
  `).all() as any[];
2033
2317
  return rows.map(r => ({
2034
2318
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2035
2319
  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,
2320
+ groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2321
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
2038
2322
  createdAt: r.created_at, updatedAt: r.updated_at,
2039
2323
  }));
2040
2324
  }
@@ -2081,6 +2365,86 @@ export class SqliteStore {
2081
2365
  return info.changes > 0;
2082
2366
  }
2083
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
+
2407
+ // ─── Hub Notifications ───
2408
+
2409
+ insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
2410
+ this.db.prepare(
2411
+ 'INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)'
2412
+ ).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
2413
+ }
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
+
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 }> {
2424
+ const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2425
+ const limit = opts?.limit ?? 50;
2426
+ const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit) as any[];
2427
+ 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 }));
2428
+ }
2429
+
2430
+ countUnreadHubNotifications(userId: string): number {
2431
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId) as { cnt: number };
2432
+ return row.cnt;
2433
+ }
2434
+
2435
+ markHubNotificationsRead(userId: string, ids?: string[]): void {
2436
+ if (ids && ids.length > 0) {
2437
+ const placeholders = ids.map(() => '?').join(',');
2438
+ this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
2439
+ } else {
2440
+ this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
2441
+ }
2442
+ }
2443
+
2444
+ clearHubNotifications(userId: string): void {
2445
+ this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
2446
+ }
2447
+
2084
2448
  upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
2085
2449
  const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
2086
2450
  this.db.prepare(`
@@ -2138,9 +2502,9 @@ export class SqliteStore {
2138
2502
  return row ?? null;
2139
2503
  }
2140
2504
 
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 }> {
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 }> {
2142
2506
  const rows = this.db.prepare(`
2143
- 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
2144
2508
  FROM hub_memories m
2145
2509
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2146
2510
  ORDER BY m.updated_at DESC
@@ -2148,25 +2512,24 @@ export class SqliteStore {
2148
2512
  `).all(limit) as any[];
2149
2513
  return rows.map(r => ({
2150
2514
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2151
- role: r.role, summary: r.summary, kind: r.kind,
2515
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2152
2516
  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,
2517
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2154
2518
  }));
2155
2519
  }
2156
2520
 
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 }> {
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 }> {
2158
2522
  const rows = this.db.prepare(`
2159
- SELECT m.*, u.username AS owner_name, g.name AS group_name
2523
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status
2160
2524
  FROM hub_memories m
2161
2525
  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
2526
  ORDER BY m.updated_at DESC
2164
2527
  `).all() as any[];
2165
2528
  return rows.map(r => ({
2166
2529
  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,
2530
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2531
+ groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2532
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2170
2533
  }));
2171
2534
  }
2172
2535
 
@@ -2190,13 +2553,6 @@ export class SqliteStore {
2190
2553
  throw new Error(`source skill not found for skillId=${skillId}`);
2191
2554
  }
2192
2555
 
2193
- private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
2194
- return {
2195
- ...user,
2196
- groups: this.getGroupsForHubUser(user.id),
2197
- };
2198
- }
2199
-
2200
2556
  getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
2201
2557
  const result = new Map<string, string>();
2202
2558
  if (sessionKeys.length === 0) return result;
@@ -2382,6 +2738,8 @@ interface ClientHubConnection {
2382
2738
  userToken: string;
2383
2739
  role: UserRole;
2384
2740
  connectedAt: number;
2741
+ identityKey?: string;
2742
+ lastKnownStatus?: string;
2385
2743
  }
2386
2744
 
2387
2745
  interface ClientHubConnectionRow {
@@ -2391,6 +2749,8 @@ interface ClientHubConnectionRow {
2391
2749
  user_token: string;
2392
2750
  role: string;
2393
2751
  connected_at: number;
2752
+ identity_key?: string;
2753
+ last_known_status?: string;
2394
2754
  }
2395
2755
 
2396
2756
  function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnection {
@@ -2401,6 +2761,8 @@ function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnect
2401
2761
  userToken: row.user_token,
2402
2762
  role: row.role as UserRole,
2403
2763
  connectedAt: row.connected_at,
2764
+ identityKey: row.identity_key || "",
2765
+ lastKnownStatus: row.last_known_status || "",
2404
2766
  };
2405
2767
  }
2406
2768
 
@@ -2408,6 +2770,13 @@ interface HubUserRecord extends UserInfo {
2408
2770
  tokenHash: string;
2409
2771
  createdAt: number;
2410
2772
  approvedAt: number | null;
2773
+ lastIp: string;
2774
+ lastActiveAt: number | null;
2775
+ identityKey?: string;
2776
+ leftAt?: number | null;
2777
+ removedAt?: number | null;
2778
+ rejectedAt?: number | null;
2779
+ rejoinRequestedAt?: number | null;
2411
2780
  }
2412
2781
 
2413
2782
  interface HubUserRow {
@@ -2419,6 +2788,13 @@ interface HubUserRow {
2419
2788
  token_hash: string;
2420
2789
  created_at: number;
2421
2790
  approved_at: number | null;
2791
+ last_ip: string;
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;
2422
2798
  }
2423
2799
 
2424
2800
  function rowToHubUser(row: HubUserRow): HubUserRecord {
@@ -2432,29 +2808,13 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
2432
2808
  tokenHash: row.token_hash,
2433
2809
  createdAt: row.created_at,
2434
2810
  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,
2811
+ lastIp: row.last_ip || "",
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,
2458
2818
  };
2459
2819
  }
2460
2820
 
@@ -2603,6 +2963,7 @@ interface HubSkillSearchRow {
2603
2963
  visibility: string;
2604
2964
  group_name: string | null;
2605
2965
  owner_name: string | null;
2966
+ owner_status: string | null;
2606
2967
  quality_score: number | null;
2607
2968
  }
2608
2969