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

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 (153) hide show
  1. package/README.md +39 -22
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +27 -7
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/client/connector.d.ts +29 -0
  7. package/dist/client/connector.d.ts.map +1 -0
  8. package/dist/client/connector.js +218 -0
  9. package/dist/client/connector.js.map +1 -0
  10. package/dist/client/hub.d.ts +61 -0
  11. package/dist/client/hub.d.ts.map +1 -0
  12. package/dist/client/hub.js +170 -0
  13. package/dist/client/hub.js.map +1 -0
  14. package/dist/client/skill-sync.d.ts +36 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -0
  16. package/dist/client/skill-sync.js +226 -0
  17. package/dist/client/skill-sync.js.map +1 -0
  18. package/dist/config.d.ts +2 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +72 -3
  21. package/dist/config.js.map +1 -1
  22. package/dist/embedding/index.d.ts +4 -2
  23. package/dist/embedding/index.d.ts.map +1 -1
  24. package/dist/embedding/index.js +17 -1
  25. package/dist/embedding/index.js.map +1 -1
  26. package/dist/hub/auth.d.ts +19 -0
  27. package/dist/hub/auth.d.ts.map +1 -0
  28. package/dist/hub/auth.js +70 -0
  29. package/dist/hub/auth.js.map +1 -0
  30. package/dist/hub/server.d.ts +41 -0
  31. package/dist/hub/server.d.ts.map +1 -0
  32. package/dist/hub/server.js +767 -0
  33. package/dist/hub/server.js.map +1 -0
  34. package/dist/hub/user-manager.d.ts +31 -0
  35. package/dist/hub/user-manager.d.ts.map +1 -0
  36. package/dist/hub/user-manager.js +129 -0
  37. package/dist/hub/user-manager.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +8 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/ingest/providers/index.d.ts +10 -2
  43. package/dist/ingest/providers/index.d.ts.map +1 -1
  44. package/dist/ingest/providers/index.js +209 -43
  45. package/dist/ingest/providers/index.js.map +1 -1
  46. package/dist/ingest/providers/openai.d.ts +1 -0
  47. package/dist/ingest/providers/openai.d.ts.map +1 -1
  48. package/dist/ingest/providers/openai.js +1 -0
  49. package/dist/ingest/providers/openai.js.map +1 -1
  50. package/dist/ingest/task-processor.js +1 -1
  51. package/dist/ingest/task-processor.js.map +1 -1
  52. package/dist/openclaw-api.d.ts +53 -0
  53. package/dist/openclaw-api.d.ts.map +1 -0
  54. package/dist/openclaw-api.js +189 -0
  55. package/dist/openclaw-api.js.map +1 -0
  56. package/dist/recall/engine.js +1 -1
  57. package/dist/recall/engine.js.map +1 -1
  58. package/dist/shared/llm-call.d.ts +4 -2
  59. package/dist/shared/llm-call.d.ts.map +1 -1
  60. package/dist/shared/llm-call.js +20 -81
  61. package/dist/shared/llm-call.js.map +1 -1
  62. package/dist/sharing/types.contract.d.ts +2 -0
  63. package/dist/sharing/types.contract.d.ts.map +1 -0
  64. package/dist/sharing/types.contract.js +3 -0
  65. package/dist/sharing/types.contract.js.map +1 -0
  66. package/dist/sharing/types.d.ts +80 -0
  67. package/dist/sharing/types.d.ts.map +1 -0
  68. package/dist/sharing/types.js +3 -0
  69. package/dist/sharing/types.js.map +1 -0
  70. package/dist/skill/evaluator.d.ts.map +1 -1
  71. package/dist/skill/evaluator.js +2 -2
  72. package/dist/skill/evaluator.js.map +1 -1
  73. package/dist/skill/evolver.d.ts +0 -2
  74. package/dist/skill/evolver.d.ts.map +1 -1
  75. package/dist/skill/evolver.js +0 -3
  76. package/dist/skill/evolver.js.map +1 -1
  77. package/dist/skill/generator.d.ts.map +1 -1
  78. package/dist/skill/generator.js +4 -4
  79. package/dist/skill/generator.js.map +1 -1
  80. package/dist/skill/upgrader.js +1 -1
  81. package/dist/skill/upgrader.js.map +1 -1
  82. package/dist/skill/validator.js +1 -1
  83. package/dist/skill/validator.js.map +1 -1
  84. package/dist/storage/ensure-binding.d.ts.map +1 -1
  85. package/dist/storage/ensure-binding.js +3 -1
  86. package/dist/storage/ensure-binding.js.map +1 -1
  87. package/dist/storage/sqlite.d.ts +329 -1
  88. package/dist/storage/sqlite.d.ts.map +1 -1
  89. package/dist/storage/sqlite.js +909 -4
  90. package/dist/storage/sqlite.js.map +1 -1
  91. package/dist/telemetry.d.ts +5 -12
  92. package/dist/telemetry.d.ts.map +1 -1
  93. package/dist/telemetry.js +38 -135
  94. package/dist/telemetry.js.map +1 -1
  95. package/dist/tools/index.d.ts +1 -0
  96. package/dist/tools/index.d.ts.map +1 -1
  97. package/dist/tools/index.js +3 -1
  98. package/dist/tools/index.js.map +1 -1
  99. package/dist/tools/memory-search.d.ts +5 -2
  100. package/dist/tools/memory-search.d.ts.map +1 -1
  101. package/dist/tools/memory-search.js +50 -7
  102. package/dist/tools/memory-search.js.map +1 -1
  103. package/dist/tools/network-memory-detail.d.ts +4 -0
  104. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  105. package/dist/tools/network-memory-detail.js +34 -0
  106. package/dist/tools/network-memory-detail.js.map +1 -0
  107. package/dist/types.d.ts +49 -2
  108. package/dist/types.d.ts.map +1 -1
  109. package/dist/types.js.map +1 -1
  110. package/dist/viewer/html.d.ts.map +1 -1
  111. package/dist/viewer/html.js +3965 -459
  112. package/dist/viewer/html.js.map +1 -1
  113. package/dist/viewer/server.d.ts +51 -0
  114. package/dist/viewer/server.d.ts.map +1 -1
  115. package/dist/viewer/server.js +1564 -23
  116. package/dist/viewer/server.js.map +1 -1
  117. package/index.ts +769 -67
  118. package/openclaw.plugin.json +2 -1
  119. package/package.json +4 -3
  120. package/scripts/postinstall.cjs +283 -46
  121. package/skill/memos-memory-guide/SKILL.md +82 -20
  122. package/src/capture/index.ts +27 -7
  123. package/src/client/connector.ts +212 -0
  124. package/src/client/hub.ts +207 -0
  125. package/src/client/skill-sync.ts +216 -0
  126. package/src/config.ts +94 -3
  127. package/src/embedding/index.ts +21 -1
  128. package/src/hub/auth.ts +78 -0
  129. package/src/hub/server.ts +754 -0
  130. package/src/hub/user-manager.ts +143 -0
  131. package/src/index.ts +13 -5
  132. package/src/ingest/providers/index.ts +246 -46
  133. package/src/ingest/providers/openai.ts +1 -1
  134. package/src/ingest/task-processor.ts +1 -1
  135. package/src/openclaw-api.ts +287 -0
  136. package/src/recall/engine.ts +1 -1
  137. package/src/shared/llm-call.ts +23 -95
  138. package/src/sharing/types.contract.ts +40 -0
  139. package/src/sharing/types.ts +102 -0
  140. package/src/skill/evaluator.ts +3 -2
  141. package/src/skill/evolver.ts +0 -5
  142. package/src/skill/generator.ts +6 -4
  143. package/src/skill/upgrader.ts +1 -1
  144. package/src/skill/validator.ts +1 -1
  145. package/src/storage/ensure-binding.ts +3 -1
  146. package/src/storage/sqlite.ts +1159 -4
  147. package/src/telemetry.ts +39 -152
  148. package/src/tools/index.ts +1 -0
  149. package/src/tools/memory-search.ts +58 -8
  150. package/src/tools/network-memory-detail.ts +34 -0
  151. package/src/types.ts +44 -2
  152. package/src/viewer/html.ts +3965 -459
  153. package/src/viewer/server.ts +1452 -25
@@ -3,6 +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 { SharedVisibility, UserInfo, UserRole, UserStatus } from "../sharing/types";
6
7
 
7
8
  export class SqliteStore {
8
9
  private db: Database.Database;
@@ -110,6 +111,8 @@ export class SqliteStore {
110
111
  this.migrateSkillVisibility();
111
112
  this.migrateSkillEmbeddingsAndFts();
112
113
  this.migrateFtsToTrigram();
114
+ this.migrateHubTables();
115
+ this.migrateLocalSharedTasksOwner();
113
116
  this.log.debug("Database schema initialized");
114
117
  }
115
118
 
@@ -117,6 +120,16 @@ export class SqliteStore {
117
120
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
118
121
  }
119
122
 
123
+ private migrateLocalSharedTasksOwner(): void {
124
+ try {
125
+ const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
126
+ if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
127
+ this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
128
+ this.log.info("Migrated: added original_owner column to local_shared_tasks");
129
+ }
130
+ } catch { /* table may not exist yet */ }
131
+ }
132
+
120
133
  private migrateOwnerFields(): void {
121
134
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
122
135
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -514,12 +527,13 @@ export class SqliteStore {
514
527
  ).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
515
528
  }
516
529
 
517
- getToolMetrics(minutes: number): {
530
+ getToolMetrics(minutes: number, fromMs?: number, toMs?: number): {
518
531
  tools: string[];
519
532
  series: Array<{ minute: string; [tool: string]: number | string }>;
520
533
  aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
521
534
  } {
522
- const since = Date.now() - minutes * 60 * 1000;
535
+ const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
536
+ const until = toMs ?? Date.now();
523
537
 
524
538
  const rows = this.db.prepare(
525
539
  `SELECT tool_name,
@@ -527,9 +541,9 @@ export class SqliteStore {
527
541
  success,
528
542
  strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
529
543
  FROM tool_calls
530
- WHERE called_at >= ?
544
+ WHERE called_at >= ? AND called_at <= ?
531
545
  ORDER BY called_at`,
532
- ).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
546
+ ).all(since, until) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
533
547
 
534
548
  const toolSet = new Set<string>();
535
549
  const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
@@ -659,6 +673,238 @@ export class SqliteStore {
659
673
  };
660
674
  }
661
675
 
676
+
677
+ private migrateHubTables(): void {
678
+ this.db.exec(`
679
+ CREATE TABLE IF NOT EXISTS client_hub_connection (
680
+ id INTEGER PRIMARY KEY CHECK (id = 1),
681
+ hub_url TEXT NOT NULL,
682
+ user_id TEXT NOT NULL,
683
+ username TEXT NOT NULL,
684
+ user_token TEXT NOT NULL,
685
+ role TEXT NOT NULL,
686
+ connected_at INTEGER NOT NULL
687
+ );
688
+
689
+ CREATE TABLE IF NOT EXISTS local_shared_tasks (
690
+ task_id TEXT PRIMARY KEY,
691
+ hub_task_id TEXT NOT NULL,
692
+ visibility TEXT NOT NULL DEFAULT 'public',
693
+ group_id TEXT,
694
+ synced_chunks INTEGER NOT NULL DEFAULT 0,
695
+ shared_at INTEGER NOT NULL
696
+ );
697
+
698
+ CREATE TABLE IF NOT EXISTS local_shared_memories (
699
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
700
+ original_owner TEXT NOT NULL,
701
+ shared_at INTEGER NOT NULL
702
+ );
703
+
704
+ CREATE TABLE IF NOT EXISTS hub_users (
705
+ id TEXT PRIMARY KEY,
706
+ username TEXT NOT NULL UNIQUE,
707
+ device_name TEXT NOT NULL DEFAULT '',
708
+ role TEXT NOT NULL,
709
+ status TEXT NOT NULL,
710
+ token_hash TEXT NOT NULL DEFAULT '',
711
+ created_at INTEGER NOT NULL,
712
+ approved_at INTEGER,
713
+ last_ip TEXT NOT NULL DEFAULT '',
714
+ last_active_at INTEGER
715
+ );
716
+ CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
717
+ CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
718
+
719
+ CREATE TABLE IF NOT EXISTS hub_tasks (
720
+ id TEXT PRIMARY KEY,
721
+ source_task_id TEXT NOT NULL,
722
+ source_user_id TEXT NOT NULL,
723
+ title TEXT NOT NULL,
724
+ summary TEXT NOT NULL DEFAULT '',
725
+ group_id TEXT,
726
+ visibility TEXT NOT NULL,
727
+ created_at INTEGER NOT NULL,
728
+ updated_at INTEGER NOT NULL,
729
+ UNIQUE(source_user_id, source_task_id)
730
+ );
731
+ CREATE INDEX IF NOT EXISTS idx_hub_tasks_visibility ON hub_tasks(visibility);
732
+ CREATE INDEX IF NOT EXISTS idx_hub_tasks_group ON hub_tasks(group_id);
733
+
734
+ CREATE TABLE IF NOT EXISTS hub_chunks (
735
+ id TEXT PRIMARY KEY,
736
+ hub_task_id TEXT NOT NULL REFERENCES hub_tasks(id) ON DELETE CASCADE,
737
+ source_chunk_id TEXT NOT NULL,
738
+ source_user_id TEXT NOT NULL,
739
+ role TEXT NOT NULL,
740
+ content TEXT NOT NULL,
741
+ summary TEXT NOT NULL DEFAULT '',
742
+ kind TEXT NOT NULL DEFAULT 'paragraph',
743
+ created_at INTEGER NOT NULL,
744
+ UNIQUE(source_user_id, source_chunk_id)
745
+ );
746
+ CREATE INDEX IF NOT EXISTS idx_hub_chunks_task ON hub_chunks(hub_task_id);
747
+
748
+ CREATE TABLE IF NOT EXISTS hub_embeddings (
749
+ chunk_id TEXT PRIMARY KEY REFERENCES hub_chunks(id) ON DELETE CASCADE,
750
+ vector BLOB NOT NULL,
751
+ dimensions INTEGER NOT NULL,
752
+ updated_at INTEGER NOT NULL
753
+ );
754
+
755
+ CREATE VIRTUAL TABLE IF NOT EXISTS hub_chunks_fts USING fts5(
756
+ summary,
757
+ content,
758
+ content='hub_chunks',
759
+ content_rowid='rowid',
760
+ tokenize='porter unicode61'
761
+ );
762
+
763
+ CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
764
+ INSERT INTO hub_chunks_fts(rowid, summary, content)
765
+ VALUES (new.rowid, new.summary, new.content);
766
+ END;
767
+
768
+ CREATE TRIGGER IF NOT EXISTS hub_chunks_ad AFTER DELETE ON hub_chunks BEGIN
769
+ INSERT INTO hub_chunks_fts(hub_chunks_fts, rowid, summary, content)
770
+ VALUES ('delete', old.rowid, old.summary, old.content);
771
+ END;
772
+
773
+ CREATE TRIGGER IF NOT EXISTS hub_chunks_au AFTER UPDATE ON hub_chunks BEGIN
774
+ INSERT INTO hub_chunks_fts(hub_chunks_fts, rowid, summary, content)
775
+ VALUES ('delete', old.rowid, old.summary, old.content);
776
+ INSERT INTO hub_chunks_fts(rowid, summary, content)
777
+ VALUES (new.rowid, new.summary, new.content);
778
+ END;
779
+
780
+ CREATE TABLE IF NOT EXISTS hub_skills (
781
+ id TEXT PRIMARY KEY,
782
+ source_skill_id TEXT NOT NULL,
783
+ source_user_id TEXT NOT NULL,
784
+ name TEXT NOT NULL,
785
+ description TEXT NOT NULL DEFAULT '',
786
+ version INTEGER NOT NULL,
787
+ group_id TEXT,
788
+ visibility TEXT NOT NULL,
789
+ bundle TEXT NOT NULL,
790
+ quality_score REAL,
791
+ created_at INTEGER NOT NULL,
792
+ updated_at INTEGER NOT NULL,
793
+ UNIQUE(source_user_id, source_skill_id)
794
+ );
795
+ CREATE INDEX IF NOT EXISTS idx_hub_skills_visibility ON hub_skills(visibility);
796
+ CREATE INDEX IF NOT EXISTS idx_hub_skills_group ON hub_skills(group_id);
797
+
798
+ CREATE TABLE IF NOT EXISTS hub_skill_embeddings (
799
+ skill_id TEXT PRIMARY KEY REFERENCES hub_skills(id) ON DELETE CASCADE,
800
+ vector BLOB NOT NULL,
801
+ dimensions INTEGER NOT NULL,
802
+ updated_at INTEGER NOT NULL
803
+ );
804
+
805
+ CREATE VIRTUAL TABLE IF NOT EXISTS hub_skills_fts USING fts5(
806
+ name,
807
+ description,
808
+ content='hub_skills',
809
+ content_rowid='rowid',
810
+ tokenize='porter unicode61'
811
+ );
812
+
813
+ CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
814
+ INSERT INTO hub_skills_fts(rowid, name, description)
815
+ VALUES (new.rowid, new.name, new.description);
816
+ END;
817
+
818
+ CREATE TRIGGER IF NOT EXISTS hub_skills_ad AFTER DELETE ON hub_skills BEGIN
819
+ INSERT INTO hub_skills_fts(hub_skills_fts, rowid, name, description)
820
+ VALUES ('delete', old.rowid, old.name, old.description);
821
+ END;
822
+
823
+ CREATE TRIGGER IF NOT EXISTS hub_skills_au AFTER UPDATE ON hub_skills BEGIN
824
+ INSERT INTO hub_skills_fts(hub_skills_fts, rowid, name, description)
825
+ VALUES ('delete', old.rowid, old.name, old.description);
826
+ INSERT INTO hub_skills_fts(rowid, name, description)
827
+ VALUES (new.rowid, new.name, new.description);
828
+ END;
829
+
830
+ -- Independent shared memories (not tied to a task)
831
+ CREATE TABLE IF NOT EXISTS hub_memories (
832
+ id TEXT PRIMARY KEY,
833
+ source_chunk_id TEXT NOT NULL,
834
+ source_user_id TEXT NOT NULL,
835
+ role TEXT NOT NULL,
836
+ content TEXT NOT NULL,
837
+ summary TEXT NOT NULL DEFAULT '',
838
+ kind TEXT NOT NULL DEFAULT 'paragraph',
839
+ group_id TEXT,
840
+ visibility TEXT NOT NULL,
841
+ created_at INTEGER NOT NULL,
842
+ updated_at INTEGER NOT NULL,
843
+ UNIQUE(source_user_id, source_chunk_id)
844
+ );
845
+ CREATE INDEX IF NOT EXISTS idx_hub_memories_visibility ON hub_memories(visibility);
846
+ CREATE INDEX IF NOT EXISTS idx_hub_memories_group ON hub_memories(group_id);
847
+
848
+ CREATE TABLE IF NOT EXISTS hub_memory_embeddings (
849
+ memory_id TEXT PRIMARY KEY REFERENCES hub_memories(id) ON DELETE CASCADE,
850
+ vector BLOB NOT NULL,
851
+ dimensions INTEGER NOT NULL,
852
+ updated_at INTEGER NOT NULL
853
+ );
854
+
855
+ CREATE VIRTUAL TABLE IF NOT EXISTS hub_memories_fts USING fts5(
856
+ summary,
857
+ content,
858
+ content='hub_memories',
859
+ content_rowid='rowid',
860
+ tokenize='porter unicode61'
861
+ );
862
+
863
+ CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
864
+ INSERT INTO hub_memories_fts(rowid, summary, content)
865
+ VALUES (new.rowid, new.summary, new.content);
866
+ END;
867
+
868
+ CREATE TRIGGER IF NOT EXISTS hub_memories_ad AFTER DELETE ON hub_memories BEGIN
869
+ INSERT INTO hub_memories_fts(hub_memories_fts, rowid, summary, content)
870
+ VALUES ('delete', old.rowid, old.summary, old.content);
871
+ END;
872
+
873
+ CREATE TRIGGER IF NOT EXISTS hub_memories_au AFTER UPDATE ON hub_memories BEGIN
874
+ INSERT INTO hub_memories_fts(hub_memories_fts, rowid, summary, content)
875
+ VALUES ('delete', old.rowid, old.summary, old.content);
876
+ INSERT INTO hub_memories_fts(rowid, summary, content)
877
+ VALUES (new.rowid, new.summary, new.content);
878
+ END;
879
+ `);
880
+
881
+ this.db.exec(`
882
+ CREATE TABLE IF NOT EXISTS hub_notifications (
883
+ id TEXT PRIMARY KEY,
884
+ user_id TEXT NOT NULL,
885
+ type TEXT NOT NULL,
886
+ resource TEXT NOT NULL,
887
+ title TEXT NOT NULL,
888
+ message TEXT NOT NULL DEFAULT '',
889
+ read INTEGER NOT NULL DEFAULT 0,
890
+ created_at INTEGER NOT NULL
891
+ );
892
+ CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
893
+ `);
894
+
895
+ try {
896
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
897
+ if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
898
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
899
+ this.log.info("Migrated: added last_ip column to hub_users");
900
+ }
901
+ if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
902
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
903
+ this.log.info("Migrated: added last_active_at column to hub_users");
904
+ }
905
+ } catch { /* table may not exist yet */ }
906
+ }
907
+
662
908
  // ─── Write ───
663
909
 
664
910
  insertChunk(chunk: Chunk): void {
@@ -998,6 +1244,8 @@ export class SqliteStore {
998
1244
  "skill_embeddings",
999
1245
  "skill_versions",
1000
1246
  "skills",
1247
+ "local_shared_memories",
1248
+ "local_shared_tasks",
1001
1249
  "embeddings",
1002
1250
  "chunks",
1003
1251
  "tasks",
@@ -1414,6 +1662,628 @@ export class SqliteStore {
1414
1662
  .map(r => r.session_key);
1415
1663
  }
1416
1664
 
1665
+ // ─── Hub / Client connection ───
1666
+
1667
+ setClientHubConnection(conn: ClientHubConnection): void {
1668
+ this.db.prepare(`
1669
+ INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
1670
+ VALUES (1, ?, ?, ?, ?, ?, ?)
1671
+ ON CONFLICT(id) DO UPDATE SET
1672
+ hub_url = excluded.hub_url,
1673
+ user_id = excluded.user_id,
1674
+ username = excluded.username,
1675
+ user_token = excluded.user_token,
1676
+ role = excluded.role,
1677
+ connected_at = excluded.connected_at
1678
+ `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt);
1679
+ }
1680
+
1681
+ getClientHubConnection(): ClientHubConnection | null {
1682
+ const row = this.db.prepare('SELECT * FROM client_hub_connection WHERE id = 1').get() as ClientHubConnectionRow | undefined;
1683
+ return row ? rowToClientHubConnection(row) : null;
1684
+ }
1685
+
1686
+ clearClientHubConnection(): void {
1687
+ this.db.prepare('DELETE FROM client_hub_connection WHERE id = 1').run();
1688
+ }
1689
+
1690
+ // ─── Local Shared Tasks (client-side tracking) ───
1691
+
1692
+ markTaskShared(taskId: string, hubTaskId: string, syncedChunks: number, visibility: string, groupId?: string | null): void {
1693
+ this.db.prepare(`
1694
+ INSERT INTO local_shared_tasks (task_id, hub_task_id, visibility, group_id, synced_chunks, shared_at)
1695
+ VALUES (?, ?, ?, ?, ?, ?)
1696
+ ON CONFLICT(task_id) DO UPDATE SET
1697
+ hub_task_id = excluded.hub_task_id,
1698
+ visibility = excluded.visibility,
1699
+ group_id = excluded.group_id,
1700
+ synced_chunks = excluded.synced_chunks,
1701
+ shared_at = excluded.shared_at
1702
+ `).run(taskId, hubTaskId, visibility, groupId ?? null, syncedChunks, Date.now());
1703
+ }
1704
+
1705
+ unmarkTaskShared(taskId: string): void {
1706
+ this.db.prepare('DELETE FROM local_shared_tasks WHERE task_id = ?').run(taskId);
1707
+ }
1708
+
1709
+ getLocalSharedTask(taskId: string): { taskId: string; hubTaskId: string; visibility: string; groupId: string | null; syncedChunks: number; sharedAt: number } | null {
1710
+ const row = this.db.prepare('SELECT * FROM local_shared_tasks WHERE task_id = ?').get(taskId) as any;
1711
+ if (!row) return null;
1712
+ return { taskId: row.task_id, hubTaskId: row.hub_task_id, visibility: row.visibility, groupId: row.group_id, syncedChunks: row.synced_chunks, sharedAt: row.shared_at };
1713
+ }
1714
+
1715
+ listLocalSharedTasks(): Array<{ taskId: string; hubTaskId: string; visibility: string; groupId: string | null; syncedChunks: number }> {
1716
+ const rows = this.db.prepare('SELECT task_id, hub_task_id, visibility, group_id, synced_chunks FROM local_shared_tasks').all() as any[];
1717
+ return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks }));
1718
+ }
1719
+
1720
+ // ─── Local Shared Memories (client-side tracking) ───
1721
+
1722
+ markMemorySharedLocally(chunkId: string): { ok: boolean; owner?: string; originalOwner?: string; sharedAt?: number; reason?: string } {
1723
+ const chunk = this.getChunk(chunkId);
1724
+ if (!chunk) return { ok: false, reason: "not_found" };
1725
+ if (chunk.owner === "public") {
1726
+ const existing = this.getLocalSharedMemory(chunkId);
1727
+ return {
1728
+ ok: true,
1729
+ owner: "public",
1730
+ originalOwner: existing?.originalOwner ?? undefined,
1731
+ sharedAt: existing?.sharedAt ?? undefined,
1732
+ };
1733
+ }
1734
+
1735
+ const sharedAt = Date.now();
1736
+ this.db.transaction(() => {
1737
+ this.db.prepare(`
1738
+ INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
1739
+ VALUES (?, ?, ?)
1740
+ ON CONFLICT(chunk_id) DO UPDATE SET
1741
+ original_owner = excluded.original_owner,
1742
+ shared_at = excluded.shared_at
1743
+ `).run(chunkId, chunk.owner, sharedAt);
1744
+ this.updateChunk(chunkId, { owner: "public" });
1745
+ })();
1746
+
1747
+ return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
1748
+ }
1749
+
1750
+ unmarkMemorySharedLocally(chunkId: string, fallbackOwner?: string): { ok: boolean; owner?: string; originalOwner?: string; reason?: string } {
1751
+ const chunk = this.getChunk(chunkId);
1752
+ if (!chunk) return { ok: false, reason: "not_found" };
1753
+ if (chunk.owner !== "public") {
1754
+ return { ok: true, owner: chunk.owner };
1755
+ }
1756
+
1757
+ const existing = this.getLocalSharedMemory(chunkId);
1758
+ const restoreOwner = existing?.originalOwner ?? fallbackOwner;
1759
+ if (!restoreOwner || restoreOwner === "public") {
1760
+ return { ok: false, reason: "original_owner_missing" };
1761
+ }
1762
+
1763
+ this.db.transaction(() => {
1764
+ this.updateChunk(chunkId, { owner: restoreOwner });
1765
+ this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
1766
+ })();
1767
+
1768
+ return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
1769
+ }
1770
+
1771
+ getLocalSharedMemory(chunkId: string): { chunkId: string; originalOwner: string; sharedAt: number } | null {
1772
+ const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId) as any;
1773
+ if (!row) return null;
1774
+ return {
1775
+ chunkId: row.chunk_id,
1776
+ originalOwner: row.original_owner,
1777
+ sharedAt: row.shared_at,
1778
+ };
1779
+ }
1780
+
1781
+ // ─── Hub Users / Groups ───
1782
+
1783
+ upsertHubUser(user: HubUserRecord): void {
1784
+ this.db.prepare(`
1785
+ INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
1786
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1787
+ ON CONFLICT(id) DO UPDATE SET
1788
+ username = excluded.username,
1789
+ device_name = excluded.device_name,
1790
+ role = excluded.role,
1791
+ status = excluded.status,
1792
+ token_hash = excluded.token_hash,
1793
+ created_at = excluded.created_at,
1794
+ approved_at = excluded.approved_at
1795
+ `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt);
1796
+ }
1797
+
1798
+ getHubUser(userId: string): HubUserRecord | null {
1799
+ const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
1800
+ if (!row) return null;
1801
+ return rowToHubUser(row);
1802
+ }
1803
+
1804
+ listHubUsers(status?: UserStatus): HubUserRecord[] {
1805
+ const rows = status
1806
+ ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
1807
+ : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
1808
+ return rows.map(rowToHubUser);
1809
+ }
1810
+
1811
+ deleteHubUser(userId: string, cleanResources = false): boolean {
1812
+ if (cleanResources) {
1813
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1814
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
1815
+ this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
1816
+ }
1817
+ const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1818
+ return result.changes > 0;
1819
+ }
1820
+
1821
+ updateHubUserActivity(userId: string, ip: string): void {
1822
+ this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, Date.now(), userId);
1823
+ }
1824
+
1825
+ getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
1826
+ const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
1827
+ 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 }>;
1828
+ 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 }>;
1829
+ 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 }>;
1830
+ 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; }
1831
+ 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; }
1832
+ 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; }
1833
+ return result;
1834
+ }
1835
+
1836
+ // ─── Hub Shared Data ───
1837
+
1838
+ upsertHubTask(task: HubTaskRecord): void {
1839
+ this.db.prepare(`
1840
+ INSERT INTO hub_tasks (id, source_task_id, source_user_id, title, summary, group_id, visibility, created_at, updated_at)
1841
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1842
+ ON CONFLICT(source_user_id, source_task_id) DO UPDATE SET
1843
+ title = excluded.title,
1844
+ summary = excluded.summary,
1845
+ group_id = excluded.group_id,
1846
+ visibility = excluded.visibility,
1847
+ created_at = excluded.created_at,
1848
+ updated_at = excluded.updated_at
1849
+ `).run(task.id, task.sourceTaskId, task.sourceUserId, task.title, task.summary, task.groupId, task.visibility, task.createdAt, task.updatedAt);
1850
+ }
1851
+
1852
+ getHubTaskBySource(sourceUserId: string, sourceTaskId: string): HubTaskRecord | null {
1853
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId) as HubTaskRow | undefined;
1854
+ return row ? rowToHubTask(row) : null;
1855
+ }
1856
+
1857
+ getHubTaskById(taskId: string): HubTaskRecord | null {
1858
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId) as HubTaskRow | undefined;
1859
+ return row ? rowToHubTask(row) : null;
1860
+ }
1861
+
1862
+ upsertHubChunk(chunk: HubChunkUpsertInput): void {
1863
+ if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
1864
+ const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
1865
+ this.db.prepare(`
1866
+ INSERT INTO hub_chunks (id, hub_task_id, source_chunk_id, source_user_id, role, content, summary, kind, created_at)
1867
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1868
+ ON CONFLICT(source_user_id, source_chunk_id) DO UPDATE SET
1869
+ hub_task_id = excluded.hub_task_id,
1870
+ role = excluded.role,
1871
+ content = excluded.content,
1872
+ summary = excluded.summary,
1873
+ kind = excluded.kind,
1874
+ created_at = excluded.created_at
1875
+ `).run(chunk.id, taskId, chunk.sourceChunkId, chunk.sourceUserId, chunk.role, chunk.content, chunk.summary, chunk.kind, chunk.createdAt);
1876
+ }
1877
+
1878
+ getHubChunkBySource(sourceUserId: string, sourceChunkId: string): HubChunkRecord | null {
1879
+ const row = this.db.prepare('SELECT * FROM hub_chunks WHERE source_user_id = ? AND source_chunk_id = ?').get(sourceUserId, sourceChunkId) as HubChunkRow | undefined;
1880
+ return row ? rowToHubChunk(row) : null;
1881
+ }
1882
+
1883
+ deleteHubTaskBySource(sourceUserId: string, sourceTaskId: string): void {
1884
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').run(sourceUserId, sourceTaskId);
1885
+ }
1886
+
1887
+ upsertHubSkill(skill: HubSkillRecord): void {
1888
+ this.db.prepare(`
1889
+ INSERT INTO hub_skills (id, source_skill_id, source_user_id, name, description, version, group_id, visibility, bundle, quality_score, created_at, updated_at)
1890
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1891
+ ON CONFLICT(source_user_id, source_skill_id) DO UPDATE SET
1892
+ name = excluded.name,
1893
+ description = excluded.description,
1894
+ version = excluded.version,
1895
+ group_id = excluded.group_id,
1896
+ visibility = excluded.visibility,
1897
+ bundle = excluded.bundle,
1898
+ quality_score = excluded.quality_score,
1899
+ created_at = excluded.created_at,
1900
+ updated_at = excluded.updated_at
1901
+ `).run(skill.id, skill.sourceSkillId, skill.sourceUserId, skill.name, skill.description, skill.version, skill.groupId, skill.visibility, skill.bundle, skill.qualityScore, skill.createdAt, skill.updatedAt);
1902
+ }
1903
+
1904
+ getHubSkillBySource(sourceUserId: string, sourceSkillId: string): HubSkillRecord | null {
1905
+ const row = this.db.prepare('SELECT * FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').get(sourceUserId, sourceSkillId) as HubSkillRow | undefined;
1906
+ return row ? rowToHubSkill(row) : null;
1907
+ }
1908
+
1909
+ getHubSkillById(skillId: string): HubSkillRecord | null {
1910
+ const row = this.db.prepare('SELECT * FROM hub_skills WHERE id = ?').get(skillId) as HubSkillRow | undefined;
1911
+ return row ? rowToHubSkill(row) : null;
1912
+ }
1913
+
1914
+ upsertHubSkillEmbedding(skillId: string, vector: number[], sourceUserId: string, sourceSkillId: string): void {
1915
+ if (!sourceUserId || !sourceSkillId) throw new Error("sourceUserId and sourceSkillId are required for hub skill embedding upserts");
1916
+ const canonicalSkillId = this.resolveCanonicalHubSkillId(skillId, sourceUserId, sourceSkillId);
1917
+ const buf = Buffer.allocUnsafe(vector.length * 4);
1918
+ for (let i = 0; i < vector.length; i++) buf.writeFloatLE(vector[i], i * 4);
1919
+ this.db.prepare(`
1920
+ INSERT INTO hub_skill_embeddings (skill_id, vector, dimensions, updated_at)
1921
+ VALUES (?, ?, ?, ?)
1922
+ ON CONFLICT(skill_id) DO UPDATE SET
1923
+ vector = excluded.vector,
1924
+ dimensions = excluded.dimensions,
1925
+ updated_at = excluded.updated_at
1926
+ `).run(canonicalSkillId, buf, vector.length, Date.now());
1927
+ }
1928
+
1929
+ getHubSkillEmbedding(skillId: string): number[] | null {
1930
+ const row = this.db.prepare('SELECT vector, dimensions FROM hub_skill_embeddings WHERE skill_id = ?').get(skillId) as { vector: Buffer; dimensions: number } | undefined;
1931
+ if (!row) return null;
1932
+ const out: number[] = [];
1933
+ for (let i = 0; i < row.dimensions; i++) out.push(row.vector.readFloatLE(i * 4));
1934
+ return out;
1935
+ }
1936
+
1937
+ searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
1938
+ const limit = options?.maxResults ?? 10;
1939
+ const userId = options?.userId ?? "";
1940
+ const rows = this.db.prepare(`
1941
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
1942
+ bm25(hub_chunks_fts) as rank
1943
+ FROM hub_chunks_fts f
1944
+ JOIN hub_chunks hc ON hc.rowid = f.rowid
1945
+ JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1946
+ LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1947
+ WHERE hub_chunks_fts MATCH ?
1948
+ ORDER BY rank
1949
+ LIMIT ?
1950
+ `).all(sanitizeFtsQuery(query), limit) as HubSearchRow[];
1951
+ return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1952
+ }
1953
+
1954
+ upsertHubEmbedding(chunkId: string, vector: Float32Array): void {
1955
+ const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
1956
+ this.db.prepare(`
1957
+ INSERT INTO hub_embeddings (chunk_id, vector, dimensions, updated_at)
1958
+ VALUES (?, ?, ?, ?)
1959
+ ON CONFLICT(chunk_id) DO UPDATE SET vector = excluded.vector, dimensions = excluded.dimensions, updated_at = excluded.updated_at
1960
+ `).run(chunkId, buf, vector.length, Date.now());
1961
+ }
1962
+
1963
+ getHubEmbedding(chunkId: string): Float32Array | null {
1964
+ const row = this.db.prepare('SELECT vector, dimensions FROM hub_embeddings WHERE chunk_id = ?').get(chunkId) as { vector: Buffer; dimensions: number } | undefined;
1965
+ if (!row) return null;
1966
+ return new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions);
1967
+ }
1968
+
1969
+ getVisibleHubEmbeddings(userId: string): Array<{ chunkId: string; vector: Float32Array }> {
1970
+ const rows = this.db.prepare(`
1971
+ SELECT he.chunk_id, he.vector, he.dimensions
1972
+ FROM hub_embeddings he
1973
+ JOIN hub_chunks hc ON hc.id = he.chunk_id
1974
+ JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1975
+ `).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
1976
+ return rows.map(r => ({
1977
+ chunkId: r.chunk_id,
1978
+ vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
1979
+ }));
1980
+ }
1981
+
1982
+ getVisibleHubSearchHitByChunkId(chunkId: string, userId: string): HubSearchRow | null {
1983
+ const row = this.db.prepare(`
1984
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
1985
+ 0 as rank
1986
+ FROM hub_chunks hc
1987
+ JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1988
+ LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1989
+ WHERE hc.id = ?
1990
+ LIMIT 1
1991
+ `).get(chunkId) as HubSearchRow | undefined;
1992
+ return row ?? null;
1993
+ }
1994
+
1995
+ getHubChunkById(chunkId: string): HubChunkRecord | null {
1996
+ const row = this.db.prepare('SELECT * FROM hub_chunks WHERE id = ?').get(chunkId) as HubChunkRow | undefined;
1997
+ return row ? rowToHubChunk(row) : null;
1998
+ }
1999
+
2000
+ searchHubSkills(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSkillSearchRow; rank: number }> {
2001
+ const limit = options?.maxResults ?? 10;
2002
+ const userId = options?.userId ?? "";
2003
+ const sanitized = sanitizeFtsQuery(query);
2004
+ let rows: HubSkillSearchRow[];
2005
+ if (sanitized) {
2006
+ rows = this.db.prepare(`
2007
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
2008
+ bm25(hub_skills_fts) as rank
2009
+ FROM hub_skills_fts f
2010
+ JOIN hub_skills hs ON hs.rowid = f.rowid
2011
+ LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
2012
+ WHERE hub_skills_fts MATCH ?
2013
+ ORDER BY rank
2014
+ LIMIT ?
2015
+ `).all(sanitized, limit) as HubSkillSearchRow[];
2016
+ } else {
2017
+ rows = this.db.prepare(`
2018
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
2019
+ 0 as rank
2020
+ FROM hub_skills hs
2021
+ LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
2022
+ ORDER BY hs.updated_at DESC
2023
+ LIMIT ?
2024
+ `).all(limit) as HubSkillSearchRow[];
2025
+ }
2026
+ return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
2027
+ }
2028
+
2029
+ deleteHubSkillBySource(sourceUserId: string, sourceSkillId: string): void {
2030
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
2031
+ }
2032
+
2033
+ listVisibleHubTasks(userId: string, limit = 40): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; chunkCount: number; createdAt: number; updatedAt: number }> {
2034
+ const rows = this.db.prepare(`
2035
+ SELECT t.*, u.username AS owner_name, NULL AS group_name,
2036
+ (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2037
+ FROM hub_tasks t
2038
+ LEFT JOIN hub_users u ON u.id = t.source_user_id
2039
+ ORDER BY t.updated_at DESC
2040
+ LIMIT ?
2041
+ `).all(limit) as any[];
2042
+ return rows.map(r => ({
2043
+ id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2044
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
2045
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2046
+ createdAt: r.created_at, updatedAt: r.updated_at,
2047
+ }));
2048
+ }
2049
+
2050
+ listAllHubTasks(): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; chunkCount: number; createdAt: number; updatedAt: number }> {
2051
+ const rows = this.db.prepare(`
2052
+ SELECT t.*, u.username AS owner_name,
2053
+ (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2054
+ FROM hub_tasks t
2055
+ LEFT JOIN hub_users u ON u.id = t.source_user_id
2056
+ ORDER BY t.updated_at DESC
2057
+ `).all() as any[];
2058
+ return rows.map(r => ({
2059
+ id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2060
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
2061
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2062
+ createdAt: r.created_at, updatedAt: r.updated_at,
2063
+ }));
2064
+ }
2065
+
2066
+ deleteHubTaskById(taskId: string): boolean {
2067
+ const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
2068
+ return info.changes > 0;
2069
+ }
2070
+
2071
+ 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 }> {
2072
+ const rows = this.db.prepare(`
2073
+ SELECT s.*, u.username AS owner_name, NULL AS group_name
2074
+ FROM hub_skills s
2075
+ LEFT JOIN hub_users u ON u.id = s.source_user_id
2076
+ ORDER BY s.updated_at DESC
2077
+ LIMIT ?
2078
+ `).all(limit) as any[];
2079
+ return rows.map(r => ({
2080
+ id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2081
+ name: r.name, description: r.description, version: r.version,
2082
+ groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2083
+ ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2084
+ createdAt: r.created_at, updatedAt: r.updated_at,
2085
+ }));
2086
+ }
2087
+
2088
+ 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 }> {
2089
+ const rows = this.db.prepare(`
2090
+ SELECT s.*, u.username AS owner_name
2091
+ FROM hub_skills s
2092
+ LEFT JOIN hub_users u ON u.id = s.source_user_id
2093
+ ORDER BY s.updated_at DESC
2094
+ `).all() as any[];
2095
+ return rows.map(r => ({
2096
+ id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2097
+ name: r.name, description: r.description, version: r.version,
2098
+ groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2099
+ ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2100
+ createdAt: r.created_at, updatedAt: r.updated_at,
2101
+ }));
2102
+ }
2103
+
2104
+ deleteHubSkillById(skillId: string): boolean {
2105
+ const info = this.db.prepare('DELETE FROM hub_skills WHERE id = ?').run(skillId);
2106
+ return info.changes > 0;
2107
+ }
2108
+
2109
+ // ─── Hub Shared Memories (independent) ───
2110
+
2111
+ upsertHubMemory(memory: HubMemoryRecord): void {
2112
+ this.db.prepare(`
2113
+ INSERT INTO hub_memories (id, source_chunk_id, source_user_id, role, content, summary, kind, group_id, visibility, created_at, updated_at)
2114
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2115
+ ON CONFLICT(source_user_id, source_chunk_id) DO UPDATE SET
2116
+ role = excluded.role,
2117
+ content = excluded.content,
2118
+ summary = excluded.summary,
2119
+ kind = excluded.kind,
2120
+ group_id = excluded.group_id,
2121
+ visibility = excluded.visibility,
2122
+ created_at = excluded.created_at,
2123
+ updated_at = excluded.updated_at
2124
+ `).run(memory.id, memory.sourceChunkId, memory.sourceUserId, memory.role, memory.content, memory.summary, memory.kind, memory.groupId, memory.visibility, memory.createdAt, memory.updatedAt);
2125
+ }
2126
+
2127
+ getHubMemoryBySource(sourceUserId: string, sourceChunkId: string): HubMemoryRecord | null {
2128
+ const row = this.db.prepare('SELECT * FROM hub_memories WHERE source_user_id = ? AND source_chunk_id = ?').get(sourceUserId, sourceChunkId) as HubMemoryRow | undefined;
2129
+ return row ? rowToHubMemory(row) : null;
2130
+ }
2131
+
2132
+ getHubMemoryById(memoryId: string): HubMemoryRecord | null {
2133
+ const row = this.db.prepare('SELECT * FROM hub_memories WHERE id = ?').get(memoryId) as HubMemoryRow | undefined;
2134
+ return row ? rowToHubMemory(row) : null;
2135
+ }
2136
+
2137
+ deleteHubMemoryBySource(sourceUserId: string, sourceChunkId: string): void {
2138
+ this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ? AND source_chunk_id = ?').run(sourceUserId, sourceChunkId);
2139
+ }
2140
+
2141
+ deleteHubMemoryById(memoryId: string): boolean {
2142
+ const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
2143
+ return info.changes > 0;
2144
+ }
2145
+
2146
+ // ─── Hub Notifications ───
2147
+
2148
+ insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
2149
+ this.db.prepare(
2150
+ 'INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)'
2151
+ ).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
2152
+ }
2153
+
2154
+ 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 }> {
2155
+ const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2156
+ const limit = opts?.limit ?? 50;
2157
+ const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit) as any[];
2158
+ 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 }));
2159
+ }
2160
+
2161
+ countUnreadHubNotifications(userId: string): number {
2162
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId) as { cnt: number };
2163
+ return row.cnt;
2164
+ }
2165
+
2166
+ markHubNotificationsRead(userId: string, ids?: string[]): void {
2167
+ if (ids && ids.length > 0) {
2168
+ const placeholders = ids.map(() => '?').join(',');
2169
+ this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
2170
+ } else {
2171
+ this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
2172
+ }
2173
+ }
2174
+
2175
+ clearHubNotifications(userId: string): void {
2176
+ this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
2177
+ }
2178
+
2179
+ upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
2180
+ const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
2181
+ this.db.prepare(`
2182
+ INSERT INTO hub_memory_embeddings (memory_id, vector, dimensions, updated_at)
2183
+ VALUES (?, ?, ?, ?)
2184
+ ON CONFLICT(memory_id) DO UPDATE SET vector = excluded.vector, dimensions = excluded.dimensions, updated_at = excluded.updated_at
2185
+ `).run(memoryId, buf, vector.length, Date.now());
2186
+ }
2187
+
2188
+ getHubMemoryEmbedding(memoryId: string): Float32Array | null {
2189
+ const row = this.db.prepare('SELECT vector, dimensions FROM hub_memory_embeddings WHERE memory_id = ?').get(memoryId) as { vector: Buffer; dimensions: number } | undefined;
2190
+ if (!row) return null;
2191
+ return new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions);
2192
+ }
2193
+
2194
+ searchHubMemories(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubMemorySearchRow; rank: number }> {
2195
+ const limit = options?.maxResults ?? 10;
2196
+ const userId = options?.userId ?? "";
2197
+ const sanitized = sanitizeFtsQuery(query);
2198
+ if (!sanitized) return [];
2199
+ const rows = this.db.prepare(`
2200
+ SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
2201
+ bm25(hub_memories_fts) as rank
2202
+ FROM hub_memories_fts f
2203
+ JOIN hub_memories hm ON hm.rowid = f.rowid
2204
+ LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2205
+ WHERE hub_memories_fts MATCH ?
2206
+ ORDER BY rank
2207
+ LIMIT ?
2208
+ `).all(sanitized, limit) as HubMemorySearchRow[];
2209
+ return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
2210
+ }
2211
+
2212
+ getVisibleHubMemoryEmbeddings(userId: string): Array<{ memoryId: string; vector: Float32Array }> {
2213
+ const rows = this.db.prepare(`
2214
+ SELECT hme.memory_id, hme.vector, hme.dimensions
2215
+ FROM hub_memory_embeddings hme
2216
+ JOIN hub_memories hm ON hm.id = hme.memory_id
2217
+ `).all() as Array<{ memory_id: string; vector: Buffer; dimensions: number }>;
2218
+ return rows.map(r => ({
2219
+ memoryId: r.memory_id,
2220
+ vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
2221
+ }));
2222
+ }
2223
+
2224
+ getVisibleHubSearchHitByMemoryId(memoryId: string, userId: string): HubMemorySearchRow | null {
2225
+ const row = this.db.prepare(`
2226
+ SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
2227
+ 0 as rank
2228
+ FROM hub_memories hm
2229
+ LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2230
+ WHERE hm.id = ?
2231
+ LIMIT 1
2232
+ `).get(memoryId) as HubMemorySearchRow | undefined;
2233
+ return row ?? null;
2234
+ }
2235
+
2236
+ 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 }> {
2237
+ const rows = this.db.prepare(`
2238
+ SELECT m.*, u.username AS owner_name, NULL AS group_name
2239
+ FROM hub_memories m
2240
+ LEFT JOIN hub_users u ON u.id = m.source_user_id
2241
+ ORDER BY m.updated_at DESC
2242
+ LIMIT ?
2243
+ `).all(limit) as any[];
2244
+ return rows.map(r => ({
2245
+ id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2246
+ role: r.role, summary: r.summary, kind: r.kind,
2247
+ groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2248
+ ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2249
+ }));
2250
+ }
2251
+
2252
+ 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 }> {
2253
+ const rows = this.db.prepare(`
2254
+ SELECT m.*, u.username AS owner_name
2255
+ FROM hub_memories m
2256
+ LEFT JOIN hub_users u ON u.id = m.source_user_id
2257
+ ORDER BY m.updated_at DESC
2258
+ `).all() as any[];
2259
+ return rows.map(r => ({
2260
+ id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2261
+ role: r.role, summary: r.summary, kind: r.kind,
2262
+ groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
2263
+ ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2264
+ }));
2265
+ }
2266
+
2267
+ private resolveCanonicalHubTaskId(taskId: string, sourceUserId: string, sourceTaskId?: string): string {
2268
+ if (sourceTaskId) {
2269
+ const bySource = this.db.prepare('SELECT id FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId) as { id: string } | undefined;
2270
+ if (!bySource) throw new Error(`source task not found for user=${sourceUserId} sourceTaskId=${sourceTaskId}`);
2271
+ if (bySource.id != taskId) throw new Error(`mismatch between source task and hubTaskId: expected ${bySource.id}, got ${taskId}`);
2272
+ return bySource.id;
2273
+ }
2274
+ throw new Error(`source task not found for user=${sourceUserId} taskId=${taskId}`);
2275
+ }
2276
+
2277
+ private resolveCanonicalHubSkillId(skillId: string, sourceUserId?: string, sourceSkillId?: string): string {
2278
+ if (sourceUserId && sourceSkillId) {
2279
+ const bySource = this.db.prepare('SELECT id FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').get(sourceUserId, sourceSkillId) as { id: string } | undefined;
2280
+ if (!bySource) throw new Error(`source skill not found for user=${sourceUserId} sourceSkillId=${sourceSkillId}`);
2281
+ if (bySource.id != skillId) throw new Error(`mismatch between source skill and skillId: expected ${bySource.id}, got ${skillId}`);
2282
+ return bySource.id;
2283
+ }
2284
+ throw new Error(`source skill not found for skillId=${skillId}`);
2285
+ }
2286
+
1417
2287
  getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
1418
2288
  const result = new Map<string, string>();
1419
2289
  if (sessionKeys.length === 0) return result;
@@ -1591,6 +2461,291 @@ function rowToSkillVersion(row: SkillVersionRow): SkillVersion {
1591
2461
  };
1592
2462
  }
1593
2463
 
2464
+
2465
+ interface ClientHubConnection {
2466
+ hubUrl: string;
2467
+ userId: string;
2468
+ username: string;
2469
+ userToken: string;
2470
+ role: UserRole;
2471
+ connectedAt: number;
2472
+ }
2473
+
2474
+ interface ClientHubConnectionRow {
2475
+ hub_url: string;
2476
+ user_id: string;
2477
+ username: string;
2478
+ user_token: string;
2479
+ role: string;
2480
+ connected_at: number;
2481
+ }
2482
+
2483
+ function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnection {
2484
+ return {
2485
+ hubUrl: row.hub_url,
2486
+ userId: row.user_id,
2487
+ username: row.username,
2488
+ userToken: row.user_token,
2489
+ role: row.role as UserRole,
2490
+ connectedAt: row.connected_at,
2491
+ };
2492
+ }
2493
+
2494
+ interface HubUserRecord extends UserInfo {
2495
+ tokenHash: string;
2496
+ createdAt: number;
2497
+ approvedAt: number | null;
2498
+ lastIp: string;
2499
+ lastActiveAt: number | null;
2500
+ }
2501
+
2502
+ interface HubUserRow {
2503
+ id: string;
2504
+ username: string;
2505
+ device_name: string;
2506
+ role: string;
2507
+ status: string;
2508
+ token_hash: string;
2509
+ created_at: number;
2510
+ approved_at: number | null;
2511
+ last_ip: string;
2512
+ last_active_at: number | null;
2513
+ }
2514
+
2515
+ function rowToHubUser(row: HubUserRow): HubUserRecord {
2516
+ return {
2517
+ id: row.id,
2518
+ username: row.username,
2519
+ deviceName: row.device_name || undefined,
2520
+ role: row.role as UserRole,
2521
+ status: row.status as UserStatus,
2522
+ groups: [],
2523
+ tokenHash: row.token_hash,
2524
+ createdAt: row.created_at,
2525
+ approvedAt: row.approved_at,
2526
+ lastIp: row.last_ip || "",
2527
+ lastActiveAt: row.last_active_at ?? null,
2528
+ };
2529
+ }
2530
+
2531
+ interface HubTaskRecord {
2532
+ id: string;
2533
+ sourceTaskId: string;
2534
+ sourceUserId: string;
2535
+ title: string;
2536
+ summary: string;
2537
+ groupId: string | null;
2538
+ visibility: SharedVisibility;
2539
+ createdAt: number;
2540
+ updatedAt: number;
2541
+ }
2542
+
2543
+ interface HubTaskRow {
2544
+ id: string;
2545
+ source_task_id: string;
2546
+ source_user_id: string;
2547
+ title: string;
2548
+ summary: string;
2549
+ group_id: string | null;
2550
+ visibility: string;
2551
+ created_at: number;
2552
+ updated_at: number;
2553
+ }
2554
+
2555
+ function rowToHubTask(row: HubTaskRow): HubTaskRecord {
2556
+ return {
2557
+ id: row.id,
2558
+ sourceTaskId: row.source_task_id,
2559
+ sourceUserId: row.source_user_id,
2560
+ title: row.title,
2561
+ summary: row.summary,
2562
+ groupId: row.group_id,
2563
+ visibility: row.visibility as SharedVisibility,
2564
+ createdAt: row.created_at,
2565
+ updatedAt: row.updated_at,
2566
+ };
2567
+ }
2568
+
2569
+ interface HubChunkUpsertInput {
2570
+ id: string;
2571
+ hubTaskId: string;
2572
+ sourceTaskId: string;
2573
+ sourceChunkId: string;
2574
+ sourceUserId: string;
2575
+ role: Chunk["role"];
2576
+ content: string;
2577
+ summary: string;
2578
+ kind: Chunk["kind"];
2579
+ createdAt: number;
2580
+ }
2581
+
2582
+ interface HubChunkRecord {
2583
+ id: string;
2584
+ hubTaskId: string;
2585
+ sourceChunkId: string;
2586
+ sourceUserId: string;
2587
+ role: Chunk["role"];
2588
+ content: string;
2589
+ summary: string;
2590
+ kind: Chunk["kind"];
2591
+ createdAt: number;
2592
+ }
2593
+
2594
+ interface HubChunkRow {
2595
+ id: string;
2596
+ hub_task_id: string;
2597
+ source_chunk_id: string;
2598
+ source_user_id: string;
2599
+ role: string;
2600
+ content: string;
2601
+ summary: string;
2602
+ kind: string;
2603
+ created_at: number;
2604
+ }
2605
+
2606
+ function rowToHubChunk(row: HubChunkRow): HubChunkRecord {
2607
+ return {
2608
+ id: row.id,
2609
+ hubTaskId: row.hub_task_id,
2610
+ sourceChunkId: row.source_chunk_id,
2611
+ sourceUserId: row.source_user_id,
2612
+ role: row.role as Chunk["role"],
2613
+ content: row.content,
2614
+ summary: row.summary,
2615
+ kind: row.kind as Chunk["kind"],
2616
+ createdAt: row.created_at,
2617
+ };
2618
+ }
2619
+
2620
+ interface HubSkillRecord {
2621
+ id: string;
2622
+ sourceSkillId: string;
2623
+ sourceUserId: string;
2624
+ name: string;
2625
+ description: string;
2626
+ version: number;
2627
+ groupId: string | null;
2628
+ visibility: SharedVisibility;
2629
+ bundle: string;
2630
+ qualityScore: number | null;
2631
+ createdAt: number;
2632
+ updatedAt: number;
2633
+ }
2634
+
2635
+ interface HubSkillRow {
2636
+ id: string;
2637
+ source_skill_id: string;
2638
+ source_user_id: string;
2639
+ name: string;
2640
+ description: string;
2641
+ version: number;
2642
+ group_id: string | null;
2643
+ visibility: string;
2644
+ bundle: string;
2645
+ quality_score: number | null;
2646
+ created_at: number;
2647
+ updated_at: number;
2648
+ }
2649
+
2650
+ function rowToHubSkill(row: HubSkillRow): HubSkillRecord {
2651
+ return {
2652
+ id: row.id,
2653
+ sourceSkillId: row.source_skill_id,
2654
+ sourceUserId: row.source_user_id,
2655
+ name: row.name,
2656
+ description: row.description,
2657
+ version: row.version,
2658
+ groupId: row.group_id,
2659
+ visibility: row.visibility as SharedVisibility,
2660
+ bundle: row.bundle,
2661
+ qualityScore: row.quality_score,
2662
+ createdAt: row.created_at,
2663
+ updatedAt: row.updated_at,
2664
+ };
2665
+ }
2666
+
2667
+
2668
+ interface HubSkillSearchRow {
2669
+ id: string;
2670
+ name: string;
2671
+ description: string;
2672
+ version: number;
2673
+ visibility: string;
2674
+ group_name: string | null;
2675
+ owner_name: string | null;
2676
+ quality_score: number | null;
2677
+ }
2678
+
2679
+ interface HubSearchRow {
2680
+ id: string;
2681
+ content: string;
2682
+ summary: string;
2683
+ role: string;
2684
+ created_at: number;
2685
+ task_title: string | null;
2686
+ visibility: string;
2687
+ group_name: string | null;
2688
+ owner_name: string | null;
2689
+ rank: number;
2690
+ }
2691
+
2692
+ export interface HubMemoryRecord {
2693
+ id: string;
2694
+ sourceChunkId: string;
2695
+ sourceUserId: string;
2696
+ role: string;
2697
+ content: string;
2698
+ summary: string;
2699
+ kind: string;
2700
+ groupId: string | null;
2701
+ visibility: SharedVisibility;
2702
+ createdAt: number;
2703
+ updatedAt: number;
2704
+ }
2705
+
2706
+ interface HubMemoryRow {
2707
+ id: string;
2708
+ source_chunk_id: string;
2709
+ source_user_id: string;
2710
+ role: string;
2711
+ content: string;
2712
+ summary: string;
2713
+ kind: string;
2714
+ group_id: string | null;
2715
+ visibility: string;
2716
+ created_at: number;
2717
+ updated_at: number;
2718
+ }
2719
+
2720
+ function rowToHubMemory(row: HubMemoryRow): HubMemoryRecord {
2721
+ return {
2722
+ id: row.id,
2723
+ sourceChunkId: row.source_chunk_id,
2724
+ sourceUserId: row.source_user_id,
2725
+ role: row.role,
2726
+ content: row.content,
2727
+ summary: row.summary,
2728
+ kind: row.kind,
2729
+ groupId: row.group_id,
2730
+ visibility: row.visibility as SharedVisibility,
2731
+ createdAt: row.created_at,
2732
+ updatedAt: row.updated_at,
2733
+ };
2734
+ }
2735
+
2736
+ interface HubMemorySearchRow {
2737
+ id: string;
2738
+ content: string;
2739
+ summary: string;
2740
+ role: string;
2741
+ created_at: number;
2742
+ visibility: string;
2743
+ group_name: string | null;
2744
+ owner_name: string | null;
2745
+ rank: number;
2746
+ }
2747
+
2748
+
1594
2749
  function contentHash(content: string): string {
1595
2750
  return createHash("sha256").update(content).digest("hex").slice(0, 16);
1596
2751
  }