@memtensor/memos-local-openclaw-plugin 1.0.2 → 1.0.4-beta.0

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 (138) hide show
  1. package/README.md +38 -21
  2. package/dist/client/connector.d.ts +26 -0
  3. package/dist/client/connector.d.ts.map +1 -0
  4. package/dist/client/connector.js +127 -0
  5. package/dist/client/connector.js.map +1 -0
  6. package/dist/client/hub.d.ts +61 -0
  7. package/dist/client/hub.d.ts.map +1 -0
  8. package/dist/client/hub.js +148 -0
  9. package/dist/client/hub.js.map +1 -0
  10. package/dist/client/skill-sync.d.ts +29 -0
  11. package/dist/client/skill-sync.d.ts.map +1 -0
  12. package/dist/client/skill-sync.js +216 -0
  13. package/dist/client/skill-sync.js.map +1 -0
  14. package/dist/config.d.ts +2 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +70 -3
  17. package/dist/config.js.map +1 -1
  18. package/dist/embedding/index.d.ts +4 -2
  19. package/dist/embedding/index.d.ts.map +1 -1
  20. package/dist/embedding/index.js +21 -4
  21. package/dist/embedding/index.js.map +1 -1
  22. package/dist/hub/auth.d.ts +19 -0
  23. package/dist/hub/auth.d.ts.map +1 -0
  24. package/dist/hub/auth.js +70 -0
  25. package/dist/hub/auth.js.map +1 -0
  26. package/dist/hub/server.d.ts +41 -0
  27. package/dist/hub/server.d.ts.map +1 -0
  28. package/dist/hub/server.js +742 -0
  29. package/dist/hub/server.js.map +1 -0
  30. package/dist/hub/user-manager.d.ts +28 -0
  31. package/dist/hub/user-manager.d.ts.map +1 -0
  32. package/dist/hub/user-manager.js +112 -0
  33. package/dist/hub/user-manager.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/ingest/providers/index.d.ts +10 -2
  39. package/dist/ingest/providers/index.d.ts.map +1 -1
  40. package/dist/ingest/providers/index.js +242 -8
  41. package/dist/ingest/providers/index.js.map +1 -1
  42. package/dist/ingest/providers/openai.d.ts +1 -0
  43. package/dist/ingest/providers/openai.d.ts.map +1 -1
  44. package/dist/ingest/providers/openai.js +1 -0
  45. package/dist/ingest/providers/openai.js.map +1 -1
  46. package/dist/ingest/task-processor.js +1 -1
  47. package/dist/ingest/task-processor.js.map +1 -1
  48. package/dist/openclaw-api.d.ts +53 -0
  49. package/dist/openclaw-api.d.ts.map +1 -0
  50. package/dist/openclaw-api.js +189 -0
  51. package/dist/openclaw-api.js.map +1 -0
  52. package/dist/recall/engine.js +2 -2
  53. package/dist/recall/engine.js.map +1 -1
  54. package/dist/shared/llm-call.d.ts +4 -1
  55. package/dist/shared/llm-call.d.ts.map +1 -1
  56. package/dist/shared/llm-call.js +15 -0
  57. package/dist/shared/llm-call.js.map +1 -1
  58. package/dist/sharing/types.contract.d.ts +2 -0
  59. package/dist/sharing/types.contract.d.ts.map +1 -0
  60. package/dist/sharing/types.contract.js +3 -0
  61. package/dist/sharing/types.contract.js.map +1 -0
  62. package/dist/sharing/types.d.ts +80 -0
  63. package/dist/sharing/types.d.ts.map +1 -0
  64. package/dist/sharing/types.js +3 -0
  65. package/dist/sharing/types.js.map +1 -0
  66. package/dist/skill/evaluator.d.ts.map +1 -1
  67. package/dist/skill/evaluator.js +2 -2
  68. package/dist/skill/evaluator.js.map +1 -1
  69. package/dist/skill/generator.d.ts.map +1 -1
  70. package/dist/skill/generator.js +4 -4
  71. package/dist/skill/generator.js.map +1 -1
  72. package/dist/skill/upgrader.js +1 -1
  73. package/dist/skill/upgrader.js.map +1 -1
  74. package/dist/skill/validator.js +1 -1
  75. package/dist/skill/validator.js.map +1 -1
  76. package/dist/storage/sqlite.d.ts +294 -0
  77. package/dist/storage/sqlite.d.ts.map +1 -1
  78. package/dist/storage/sqlite.js +902 -8
  79. package/dist/storage/sqlite.js.map +1 -1
  80. package/dist/tools/index.d.ts +1 -0
  81. package/dist/tools/index.d.ts.map +1 -1
  82. package/dist/tools/index.js +3 -1
  83. package/dist/tools/index.js.map +1 -1
  84. package/dist/tools/memory-search.d.ts +3 -2
  85. package/dist/tools/memory-search.d.ts.map +1 -1
  86. package/dist/tools/memory-search.js +48 -7
  87. package/dist/tools/memory-search.js.map +1 -1
  88. package/dist/tools/network-memory-detail.d.ts +4 -0
  89. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  90. package/dist/tools/network-memory-detail.js +34 -0
  91. package/dist/tools/network-memory-detail.js.map +1 -0
  92. package/dist/types.d.ts +47 -2
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js.map +1 -1
  95. package/dist/update-check.d.ts.map +1 -1
  96. package/dist/update-check.js +0 -1
  97. package/dist/update-check.js.map +1 -1
  98. package/dist/viewer/html.d.ts.map +1 -1
  99. package/dist/viewer/html.js +2396 -289
  100. package/dist/viewer/html.js.map +1 -1
  101. package/dist/viewer/server.d.ts +43 -0
  102. package/dist/viewer/server.d.ts.map +1 -1
  103. package/dist/viewer/server.js +1180 -33
  104. package/dist/viewer/server.js.map +1 -1
  105. package/index.ts +445 -25
  106. package/openclaw.plugin.json +2 -1
  107. package/package.json +2 -1
  108. package/scripts/postinstall.cjs +282 -45
  109. package/skill/memos-memory-guide/SKILL.md +26 -2
  110. package/src/client/connector.ts +124 -0
  111. package/src/client/hub.ts +189 -0
  112. package/src/client/skill-sync.ts +202 -0
  113. package/src/config.ts +92 -3
  114. package/src/embedding/index.ts +25 -3
  115. package/src/hub/auth.ts +78 -0
  116. package/src/hub/server.ts +734 -0
  117. package/src/hub/user-manager.ts +126 -0
  118. package/src/index.ts +7 -4
  119. package/src/ingest/providers/index.ts +279 -8
  120. package/src/ingest/providers/openai.ts +1 -1
  121. package/src/ingest/task-processor.ts +1 -1
  122. package/src/openclaw-api.ts +287 -0
  123. package/src/recall/engine.ts +2 -2
  124. package/src/shared/llm-call.ts +19 -1
  125. package/src/sharing/types.contract.ts +40 -0
  126. package/src/sharing/types.ts +102 -0
  127. package/src/skill/evaluator.ts +3 -2
  128. package/src/skill/generator.ts +6 -4
  129. package/src/skill/upgrader.ts +1 -1
  130. package/src/skill/validator.ts +1 -1
  131. package/src/storage/sqlite.ts +1167 -7
  132. package/src/tools/index.ts +1 -0
  133. package/src/tools/memory-search.ts +57 -8
  134. package/src/tools/network-memory-detail.ts +34 -0
  135. package/src/types.ts +48 -2
  136. package/src/update-check.ts +0 -1
  137. package/src/viewer/html.ts +2396 -289
  138. package/src/viewer/server.ts +1087 -34
@@ -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 { GroupInfo, SharedVisibility, UserInfo, UserRole, UserStatus } from "../sharing/types";
6
7
 
7
8
  export class SqliteStore {
8
9
  private db: Database.Database;
@@ -110,6 +111,7 @@ export class SqliteStore {
110
111
  this.migrateSkillVisibility();
111
112
  this.migrateSkillEmbeddingsAndFts();
112
113
  this.migrateFtsToTrigram();
114
+ this.migrateHubTables();
113
115
  this.log.debug("Database schema initialized");
114
116
  }
115
117
 
@@ -659,6 +661,219 @@ export class SqliteStore {
659
661
  };
660
662
  }
661
663
 
664
+
665
+ private migrateHubTables(): void {
666
+ this.db.exec(`
667
+ CREATE TABLE IF NOT EXISTS client_hub_connection (
668
+ id INTEGER PRIMARY KEY CHECK (id = 1),
669
+ hub_url TEXT NOT NULL,
670
+ user_id TEXT NOT NULL,
671
+ username TEXT NOT NULL,
672
+ user_token TEXT NOT NULL,
673
+ role TEXT NOT NULL,
674
+ connected_at INTEGER NOT NULL
675
+ );
676
+
677
+ CREATE TABLE IF NOT EXISTS local_shared_tasks (
678
+ task_id TEXT PRIMARY KEY,
679
+ hub_task_id TEXT NOT NULL,
680
+ visibility TEXT NOT NULL DEFAULT 'public',
681
+ group_id TEXT,
682
+ synced_chunks INTEGER NOT NULL DEFAULT 0,
683
+ shared_at INTEGER NOT NULL
684
+ );
685
+
686
+ 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
695
+ );
696
+ CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
697
+ CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
698
+
699
+ CREATE TABLE IF NOT EXISTS hub_groups (
700
+ id TEXT PRIMARY KEY,
701
+ name TEXT NOT NULL UNIQUE,
702
+ description TEXT NOT NULL DEFAULT '',
703
+ created_at INTEGER NOT NULL
704
+ );
705
+
706
+ CREATE TABLE IF NOT EXISTS hub_group_members (
707
+ group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
708
+ user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
709
+ joined_at INTEGER NOT NULL,
710
+ PRIMARY KEY (group_id, user_id)
711
+ );
712
+ CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
713
+
714
+ CREATE TABLE IF NOT EXISTS hub_tasks (
715
+ id TEXT PRIMARY KEY,
716
+ source_task_id TEXT NOT NULL,
717
+ source_user_id TEXT NOT NULL,
718
+ title TEXT NOT NULL,
719
+ summary TEXT NOT NULL DEFAULT '',
720
+ group_id TEXT,
721
+ visibility TEXT NOT NULL,
722
+ created_at INTEGER NOT NULL,
723
+ updated_at INTEGER NOT NULL,
724
+ UNIQUE(source_user_id, source_task_id)
725
+ );
726
+ CREATE INDEX IF NOT EXISTS idx_hub_tasks_visibility ON hub_tasks(visibility);
727
+ CREATE INDEX IF NOT EXISTS idx_hub_tasks_group ON hub_tasks(group_id);
728
+
729
+ CREATE TABLE IF NOT EXISTS hub_chunks (
730
+ id TEXT PRIMARY KEY,
731
+ hub_task_id TEXT NOT NULL REFERENCES hub_tasks(id) ON DELETE CASCADE,
732
+ source_chunk_id TEXT NOT NULL,
733
+ source_user_id TEXT NOT NULL,
734
+ role TEXT NOT NULL,
735
+ content TEXT NOT NULL,
736
+ summary TEXT NOT NULL DEFAULT '',
737
+ kind TEXT NOT NULL DEFAULT 'paragraph',
738
+ created_at INTEGER NOT NULL,
739
+ UNIQUE(source_user_id, source_chunk_id)
740
+ );
741
+ CREATE INDEX IF NOT EXISTS idx_hub_chunks_task ON hub_chunks(hub_task_id);
742
+
743
+ CREATE TABLE IF NOT EXISTS hub_embeddings (
744
+ chunk_id TEXT PRIMARY KEY REFERENCES hub_chunks(id) ON DELETE CASCADE,
745
+ vector BLOB NOT NULL,
746
+ dimensions INTEGER NOT NULL,
747
+ updated_at INTEGER NOT NULL
748
+ );
749
+
750
+ CREATE VIRTUAL TABLE IF NOT EXISTS hub_chunks_fts USING fts5(
751
+ summary,
752
+ content,
753
+ content='hub_chunks',
754
+ content_rowid='rowid',
755
+ tokenize='porter unicode61'
756
+ );
757
+
758
+ CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
759
+ INSERT INTO hub_chunks_fts(rowid, summary, content)
760
+ VALUES (new.rowid, new.summary, new.content);
761
+ END;
762
+
763
+ CREATE TRIGGER IF NOT EXISTS hub_chunks_ad AFTER DELETE ON hub_chunks BEGIN
764
+ INSERT INTO hub_chunks_fts(hub_chunks_fts, rowid, summary, content)
765
+ VALUES ('delete', old.rowid, old.summary, old.content);
766
+ END;
767
+
768
+ CREATE TRIGGER IF NOT EXISTS hub_chunks_au AFTER UPDATE 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
+ INSERT INTO hub_chunks_fts(rowid, summary, content)
772
+ VALUES (new.rowid, new.summary, new.content);
773
+ END;
774
+
775
+ CREATE TABLE IF NOT EXISTS hub_skills (
776
+ id TEXT PRIMARY KEY,
777
+ source_skill_id TEXT NOT NULL,
778
+ source_user_id TEXT NOT NULL,
779
+ name TEXT NOT NULL,
780
+ description TEXT NOT NULL DEFAULT '',
781
+ version INTEGER NOT NULL,
782
+ group_id TEXT,
783
+ visibility TEXT NOT NULL,
784
+ bundle TEXT NOT NULL,
785
+ quality_score REAL,
786
+ created_at INTEGER NOT NULL,
787
+ updated_at INTEGER NOT NULL,
788
+ UNIQUE(source_user_id, source_skill_id)
789
+ );
790
+ CREATE INDEX IF NOT EXISTS idx_hub_skills_visibility ON hub_skills(visibility);
791
+ CREATE INDEX IF NOT EXISTS idx_hub_skills_group ON hub_skills(group_id);
792
+
793
+ CREATE TABLE IF NOT EXISTS hub_skill_embeddings (
794
+ skill_id TEXT PRIMARY KEY REFERENCES hub_skills(id) ON DELETE CASCADE,
795
+ vector BLOB NOT NULL,
796
+ dimensions INTEGER NOT NULL,
797
+ updated_at INTEGER NOT NULL
798
+ );
799
+
800
+ CREATE VIRTUAL TABLE IF NOT EXISTS hub_skills_fts USING fts5(
801
+ name,
802
+ description,
803
+ content='hub_skills',
804
+ content_rowid='rowid',
805
+ tokenize='porter unicode61'
806
+ );
807
+
808
+ CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
809
+ INSERT INTO hub_skills_fts(rowid, name, description)
810
+ VALUES (new.rowid, new.name, new.description);
811
+ END;
812
+
813
+ CREATE TRIGGER IF NOT EXISTS hub_skills_ad AFTER DELETE ON hub_skills BEGIN
814
+ INSERT INTO hub_skills_fts(hub_skills_fts, rowid, name, description)
815
+ VALUES ('delete', old.rowid, old.name, old.description);
816
+ END;
817
+
818
+ CREATE TRIGGER IF NOT EXISTS hub_skills_au AFTER UPDATE 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
+ INSERT INTO hub_skills_fts(rowid, name, description)
822
+ VALUES (new.rowid, new.name, new.description);
823
+ END;
824
+
825
+ -- Independent shared memories (not tied to a task)
826
+ CREATE TABLE IF NOT EXISTS hub_memories (
827
+ id TEXT PRIMARY KEY,
828
+ source_chunk_id TEXT NOT NULL,
829
+ source_user_id TEXT NOT NULL,
830
+ role TEXT NOT NULL,
831
+ content TEXT NOT NULL,
832
+ summary TEXT NOT NULL DEFAULT '',
833
+ kind TEXT NOT NULL DEFAULT 'paragraph',
834
+ group_id TEXT,
835
+ visibility TEXT NOT NULL,
836
+ created_at INTEGER NOT NULL,
837
+ updated_at INTEGER NOT NULL,
838
+ UNIQUE(source_user_id, source_chunk_id)
839
+ );
840
+ CREATE INDEX IF NOT EXISTS idx_hub_memories_visibility ON hub_memories(visibility);
841
+ CREATE INDEX IF NOT EXISTS idx_hub_memories_group ON hub_memories(group_id);
842
+
843
+ CREATE TABLE IF NOT EXISTS hub_memory_embeddings (
844
+ memory_id TEXT PRIMARY KEY REFERENCES hub_memories(id) ON DELETE CASCADE,
845
+ vector BLOB NOT NULL,
846
+ dimensions INTEGER NOT NULL,
847
+ updated_at INTEGER NOT NULL
848
+ );
849
+
850
+ CREATE VIRTUAL TABLE IF NOT EXISTS hub_memories_fts USING fts5(
851
+ summary,
852
+ content,
853
+ content='hub_memories',
854
+ content_rowid='rowid',
855
+ tokenize='porter unicode61'
856
+ );
857
+
858
+ CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
859
+ INSERT INTO hub_memories_fts(rowid, summary, content)
860
+ VALUES (new.rowid, new.summary, new.content);
861
+ END;
862
+
863
+ CREATE TRIGGER IF NOT EXISTS hub_memories_ad AFTER DELETE ON hub_memories BEGIN
864
+ INSERT INTO hub_memories_fts(hub_memories_fts, rowid, summary, content)
865
+ VALUES ('delete', old.rowid, old.summary, old.content);
866
+ END;
867
+
868
+ CREATE TRIGGER IF NOT EXISTS hub_memories_au AFTER UPDATE 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
+ INSERT INTO hub_memories_fts(rowid, summary, content)
872
+ VALUES (new.rowid, new.summary, new.content);
873
+ END;
874
+ `);
875
+ }
876
+
662
877
  // ─── Write ───
663
878
 
664
879
  insertChunk(chunk: Chunk): void {
@@ -1414,6 +1629,648 @@ export class SqliteStore {
1414
1629
  .map(r => r.session_key);
1415
1630
  }
1416
1631
 
1632
+ // ─── Hub / Client connection ───
1633
+
1634
+ setClientHubConnection(conn: ClientHubConnection): void {
1635
+ this.db.prepare(`
1636
+ INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
1637
+ VALUES (1, ?, ?, ?, ?, ?, ?)
1638
+ ON CONFLICT(id) DO UPDATE SET
1639
+ hub_url = excluded.hub_url,
1640
+ user_id = excluded.user_id,
1641
+ username = excluded.username,
1642
+ user_token = excluded.user_token,
1643
+ role = excluded.role,
1644
+ connected_at = excluded.connected_at
1645
+ `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt);
1646
+ }
1647
+
1648
+ getClientHubConnection(): ClientHubConnection | null {
1649
+ const row = this.db.prepare('SELECT * FROM client_hub_connection WHERE id = 1').get() as ClientHubConnectionRow | undefined;
1650
+ return row ? rowToClientHubConnection(row) : null;
1651
+ }
1652
+
1653
+ clearClientHubConnection(): void {
1654
+ this.db.prepare('DELETE FROM client_hub_connection WHERE id = 1').run();
1655
+ }
1656
+
1657
+ // ─── Local Shared Tasks (client-side tracking) ───
1658
+
1659
+ markTaskShared(taskId: string, hubTaskId: string, syncedChunks: number, visibility: string, groupId?: string | null): void {
1660
+ this.db.prepare(`
1661
+ INSERT INTO local_shared_tasks (task_id, hub_task_id, visibility, group_id, synced_chunks, shared_at)
1662
+ VALUES (?, ?, ?, ?, ?, ?)
1663
+ ON CONFLICT(task_id) DO UPDATE SET
1664
+ hub_task_id = excluded.hub_task_id,
1665
+ visibility = excluded.visibility,
1666
+ group_id = excluded.group_id,
1667
+ synced_chunks = excluded.synced_chunks,
1668
+ shared_at = excluded.shared_at
1669
+ `).run(taskId, hubTaskId, visibility, groupId ?? null, syncedChunks, Date.now());
1670
+ }
1671
+
1672
+ unmarkTaskShared(taskId: string): void {
1673
+ this.db.prepare('DELETE FROM local_shared_tasks WHERE task_id = ?').run(taskId);
1674
+ }
1675
+
1676
+ getLocalSharedTask(taskId: string): { taskId: string; hubTaskId: string; visibility: string; groupId: string | null; syncedChunks: number; sharedAt: number } | null {
1677
+ const row = this.db.prepare('SELECT * FROM local_shared_tasks WHERE task_id = ?').get(taskId) as any;
1678
+ if (!row) return null;
1679
+ 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 };
1680
+ }
1681
+
1682
+ listLocalSharedTasks(): Array<{ taskId: string; hubTaskId: string; visibility: string; groupId: string | null; syncedChunks: number }> {
1683
+ const rows = this.db.prepare('SELECT task_id, hub_task_id, visibility, group_id, synced_chunks FROM local_shared_tasks').all() as any[];
1684
+ 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
+ }
1686
+
1687
+ // ─── Hub Users / Groups ───
1688
+
1689
+ upsertHubUser(user: HubUserRecord): void {
1690
+ this.db.prepare(`
1691
+ INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
1692
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1693
+ ON CONFLICT(id) DO UPDATE SET
1694
+ username = excluded.username,
1695
+ device_name = excluded.device_name,
1696
+ role = excluded.role,
1697
+ status = excluded.status,
1698
+ token_hash = excluded.token_hash,
1699
+ 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);
1702
+ }
1703
+
1704
+ getHubUser(userId: string): HubUserRecord | null {
1705
+ const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
1706
+ if (!row) return null;
1707
+ return this.attachGroupsToHubUser(rowToHubUser(row));
1708
+ }
1709
+
1710
+ listHubUsers(status?: UserStatus): HubUserRecord[] {
1711
+ const rows = status
1712
+ ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
1713
+ : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
1714
+ return rows.map((row) => this.attachGroupsToHubUser(rowToHubUser(row)));
1715
+ }
1716
+
1717
+ upsertHubGroup(group: HubGroupRecord): void {
1718
+ this.db.prepare(`
1719
+ INSERT INTO hub_groups (id, name, description, created_at)
1720
+ VALUES (?, ?, ?, ?)
1721
+ ON CONFLICT(id) DO UPDATE SET
1722
+ name = excluded.name,
1723
+ description = excluded.description,
1724
+ created_at = excluded.created_at
1725
+ `).run(group.id, group.name, group.description, group.createdAt);
1726
+ }
1727
+
1728
+ listHubGroups(): HubGroupRecord[] {
1729
+ const rows = this.db.prepare('SELECT * FROM hub_groups ORDER BY name').all() as HubGroupRow[];
1730
+ return rows.map(rowToHubGroup);
1731
+ }
1732
+
1733
+ addHubGroupMember(groupId: string, userId: string, joinedAt = Date.now()): void {
1734
+ this.db.prepare(`
1735
+ INSERT INTO hub_group_members (group_id, user_id, joined_at)
1736
+ VALUES (?, ?, ?)
1737
+ ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
1738
+ `).run(groupId, userId, joinedAt);
1739
+ }
1740
+
1741
+ getHubGroupById(groupId: string): HubGroupRecord | undefined {
1742
+ const row = this.db.prepare('SELECT * FROM hub_groups WHERE id = ?').get(groupId) as HubGroupRow | undefined;
1743
+ return row ? rowToHubGroup(row) : undefined;
1744
+ }
1745
+
1746
+ deleteHubGroup(groupId: string): boolean {
1747
+ const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
1748
+ return result.changes > 0;
1749
+ }
1750
+
1751
+ listHubGroupMembers(groupId: string): Array<{ userId: string; username: string; joinedAt: number }> {
1752
+ const rows = this.db.prepare(`
1753
+ SELECT gm.user_id, hu.username, gm.joined_at
1754
+ FROM hub_group_members gm
1755
+ JOIN hub_users hu ON hu.id = gm.user_id
1756
+ WHERE gm.group_id = ?
1757
+ ORDER BY gm.joined_at
1758
+ `).all(groupId) as Array<{ user_id: string; username: string; joined_at: number }>;
1759
+ return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
1760
+ }
1761
+
1762
+ removeHubGroupMember(groupId: string, userId: string): void {
1763
+ this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1764
+ }
1765
+
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 }));
1775
+ }
1776
+
1777
+ // ─── Hub Shared Data ───
1778
+
1779
+ upsertHubTask(task: HubTaskRecord): void {
1780
+ this.db.prepare(`
1781
+ INSERT INTO hub_tasks (id, source_task_id, source_user_id, title, summary, group_id, visibility, created_at, updated_at)
1782
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1783
+ ON CONFLICT(source_user_id, source_task_id) DO UPDATE SET
1784
+ title = excluded.title,
1785
+ summary = excluded.summary,
1786
+ group_id = excluded.group_id,
1787
+ visibility = excluded.visibility,
1788
+ created_at = excluded.created_at,
1789
+ updated_at = excluded.updated_at
1790
+ `).run(task.id, task.sourceTaskId, task.sourceUserId, task.title, task.summary, task.groupId, task.visibility, task.createdAt, task.updatedAt);
1791
+ }
1792
+
1793
+ getHubTaskBySource(sourceUserId: string, sourceTaskId: string): HubTaskRecord | null {
1794
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId) as HubTaskRow | undefined;
1795
+ return row ? rowToHubTask(row) : null;
1796
+ }
1797
+
1798
+ upsertHubChunk(chunk: HubChunkUpsertInput): void {
1799
+ if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
1800
+ const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
1801
+ this.db.prepare(`
1802
+ INSERT INTO hub_chunks (id, hub_task_id, source_chunk_id, source_user_id, role, content, summary, kind, created_at)
1803
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1804
+ ON CONFLICT(source_user_id, source_chunk_id) DO UPDATE SET
1805
+ hub_task_id = excluded.hub_task_id,
1806
+ role = excluded.role,
1807
+ content = excluded.content,
1808
+ summary = excluded.summary,
1809
+ kind = excluded.kind,
1810
+ created_at = excluded.created_at
1811
+ `).run(chunk.id, taskId, chunk.sourceChunkId, chunk.sourceUserId, chunk.role, chunk.content, chunk.summary, chunk.kind, chunk.createdAt);
1812
+ }
1813
+
1814
+ getHubChunkBySource(sourceUserId: string, sourceChunkId: string): HubChunkRecord | null {
1815
+ const row = this.db.prepare('SELECT * FROM hub_chunks WHERE source_user_id = ? AND source_chunk_id = ?').get(sourceUserId, sourceChunkId) as HubChunkRow | undefined;
1816
+ return row ? rowToHubChunk(row) : null;
1817
+ }
1818
+
1819
+ deleteHubTaskBySource(sourceUserId: string, sourceTaskId: string): void {
1820
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').run(sourceUserId, sourceTaskId);
1821
+ }
1822
+
1823
+ upsertHubSkill(skill: HubSkillRecord): void {
1824
+ this.db.prepare(`
1825
+ INSERT INTO hub_skills (id, source_skill_id, source_user_id, name, description, version, group_id, visibility, bundle, quality_score, created_at, updated_at)
1826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1827
+ ON CONFLICT(source_user_id, source_skill_id) DO UPDATE SET
1828
+ name = excluded.name,
1829
+ description = excluded.description,
1830
+ version = excluded.version,
1831
+ group_id = excluded.group_id,
1832
+ visibility = excluded.visibility,
1833
+ bundle = excluded.bundle,
1834
+ quality_score = excluded.quality_score,
1835
+ created_at = excluded.created_at,
1836
+ updated_at = excluded.updated_at
1837
+ `).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);
1838
+ }
1839
+
1840
+ getHubSkillBySource(sourceUserId: string, sourceSkillId: string): HubSkillRecord | null {
1841
+ const row = this.db.prepare('SELECT * FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').get(sourceUserId, sourceSkillId) as HubSkillRow | undefined;
1842
+ return row ? rowToHubSkill(row) : null;
1843
+ }
1844
+
1845
+ getHubSkillById(skillId: string): HubSkillRecord | null {
1846
+ const row = this.db.prepare('SELECT * FROM hub_skills WHERE id = ?').get(skillId) as HubSkillRow | undefined;
1847
+ return row ? rowToHubSkill(row) : null;
1848
+ }
1849
+
1850
+ upsertHubSkillEmbedding(skillId: string, vector: number[], sourceUserId: string, sourceSkillId: string): void {
1851
+ if (!sourceUserId || !sourceSkillId) throw new Error("sourceUserId and sourceSkillId are required for hub skill embedding upserts");
1852
+ const canonicalSkillId = this.resolveCanonicalHubSkillId(skillId, sourceUserId, sourceSkillId);
1853
+ const buf = Buffer.allocUnsafe(vector.length * 4);
1854
+ for (let i = 0; i < vector.length; i++) buf.writeFloatLE(vector[i], i * 4);
1855
+ this.db.prepare(`
1856
+ INSERT INTO hub_skill_embeddings (skill_id, vector, dimensions, updated_at)
1857
+ VALUES (?, ?, ?, ?)
1858
+ ON CONFLICT(skill_id) DO UPDATE SET
1859
+ vector = excluded.vector,
1860
+ dimensions = excluded.dimensions,
1861
+ updated_at = excluded.updated_at
1862
+ `).run(canonicalSkillId, buf, vector.length, Date.now());
1863
+ }
1864
+
1865
+ getHubSkillEmbedding(skillId: string): number[] | null {
1866
+ const row = this.db.prepare('SELECT vector, dimensions FROM hub_skill_embeddings WHERE skill_id = ?').get(skillId) as { vector: Buffer; dimensions: number } | undefined;
1867
+ if (!row) return null;
1868
+ const out: number[] = [];
1869
+ for (let i = 0; i < row.dimensions; i++) out.push(row.vector.readFloatLE(i * 4));
1870
+ return out;
1871
+ }
1872
+
1873
+ searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
1874
+ const limit = options?.maxResults ?? 10;
1875
+ const userId = options?.userId ?? "";
1876
+ 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, hg.name as group_name, hu.username as owner_name,
1878
+ bm25(hub_chunks_fts) as rank
1879
+ FROM hub_chunks_fts f
1880
+ JOIN hub_chunks hc ON hc.rowid = f.rowid
1881
+ JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1882
+ LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1883
+ LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1884
+ WHERE hub_chunks_fts MATCH ?
1885
+ AND (
1886
+ ht.visibility = 'public'
1887
+ OR EXISTS (
1888
+ SELECT 1 FROM hub_group_members gm
1889
+ WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1890
+ )
1891
+ )
1892
+ ORDER BY rank
1893
+ LIMIT ?
1894
+ `).all(sanitizeFtsQuery(query), userId, limit) as HubSearchRow[];
1895
+ return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1896
+ }
1897
+
1898
+ upsertHubEmbedding(chunkId: string, vector: Float32Array): void {
1899
+ const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
1900
+ this.db.prepare(`
1901
+ INSERT INTO hub_embeddings (chunk_id, vector, dimensions, updated_at)
1902
+ VALUES (?, ?, ?, ?)
1903
+ ON CONFLICT(chunk_id) DO UPDATE SET vector = excluded.vector, dimensions = excluded.dimensions, updated_at = excluded.updated_at
1904
+ `).run(chunkId, buf, vector.length, Date.now());
1905
+ }
1906
+
1907
+ getHubEmbedding(chunkId: string): Float32Array | null {
1908
+ const row = this.db.prepare('SELECT vector, dimensions FROM hub_embeddings WHERE chunk_id = ?').get(chunkId) as { vector: Buffer; dimensions: number } | undefined;
1909
+ if (!row) return null;
1910
+ return new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions);
1911
+ }
1912
+
1913
+ getVisibleHubEmbeddings(userId: string): Array<{ chunkId: string; vector: Float32Array }> {
1914
+ const rows = this.db.prepare(`
1915
+ SELECT he.chunk_id, he.vector, he.dimensions
1916
+ FROM hub_embeddings he
1917
+ JOIN hub_chunks hc ON hc.id = he.chunk_id
1918
+ JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1919
+ WHERE ht.visibility = 'public'
1920
+ OR EXISTS (
1921
+ SELECT 1 FROM hub_group_members gm
1922
+ WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1923
+ )
1924
+ `).all(userId) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
1925
+ return rows.map(r => ({
1926
+ chunkId: r.chunk_id,
1927
+ vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
1928
+ }));
1929
+ }
1930
+
1931
+ getVisibleHubSearchHitByChunkId(chunkId: string, userId: string): HubSearchRow | null {
1932
+ const row = this.db.prepare(`
1933
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, hg.name as group_name, hu.username as owner_name,
1934
+ 0 as rank
1935
+ FROM hub_chunks hc
1936
+ JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1937
+ LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1938
+ LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1939
+ WHERE hc.id = ?
1940
+ AND (
1941
+ ht.visibility = 'public'
1942
+ OR EXISTS (
1943
+ SELECT 1 FROM hub_group_members gm
1944
+ WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1945
+ )
1946
+ )
1947
+ LIMIT 1
1948
+ `).get(chunkId, userId) as HubSearchRow | undefined;
1949
+ return row ?? null;
1950
+ }
1951
+
1952
+ getHubChunkById(chunkId: string): HubChunkRecord | null {
1953
+ const row = this.db.prepare('SELECT * FROM hub_chunks WHERE id = ?').get(chunkId) as HubChunkRow | undefined;
1954
+ return row ? rowToHubChunk(row) : null;
1955
+ }
1956
+
1957
+ searchHubSkills(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSkillSearchRow; rank: number }> {
1958
+ const limit = options?.maxResults ?? 10;
1959
+ const userId = options?.userId ?? "";
1960
+ const sanitized = sanitizeFtsQuery(query);
1961
+ let rows: HubSkillSearchRow[];
1962
+ if (sanitized) {
1963
+ rows = this.db.prepare(`
1964
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, hg.name AS group_name, hu.username AS owner_name, hs.quality_score,
1965
+ bm25(hub_skills_fts) as rank
1966
+ FROM hub_skills_fts f
1967
+ JOIN hub_skills hs ON hs.rowid = f.rowid
1968
+ LEFT JOIN hub_groups hg ON hg.id = hs.group_id
1969
+ LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
1970
+ WHERE hub_skills_fts MATCH ?
1971
+ AND (
1972
+ hs.visibility = 'public'
1973
+ OR EXISTS (
1974
+ SELECT 1 FROM hub_group_members gm
1975
+ WHERE gm.group_id = hs.group_id AND gm.user_id = ?
1976
+ )
1977
+ )
1978
+ ORDER BY rank
1979
+ LIMIT ?
1980
+ `).all(sanitized, userId, limit) as HubSkillSearchRow[];
1981
+ } else {
1982
+ rows = this.db.prepare(`
1983
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, hg.name AS group_name, hu.username AS owner_name, hs.quality_score,
1984
+ 0 as rank
1985
+ FROM hub_skills hs
1986
+ LEFT JOIN hub_groups hg ON hg.id = hs.group_id
1987
+ LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
1988
+ WHERE hs.visibility = 'public'
1989
+ OR EXISTS (
1990
+ SELECT 1 FROM hub_group_members gm
1991
+ WHERE gm.group_id = hs.group_id AND gm.user_id = ?
1992
+ )
1993
+ ORDER BY hs.updated_at DESC
1994
+ LIMIT ?
1995
+ `).all(userId, limit) as HubSkillSearchRow[];
1996
+ }
1997
+ return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1998
+ }
1999
+
2000
+ deleteHubSkillBySource(sourceUserId: string, sourceSkillId: string): void {
2001
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
2002
+ }
2003
+
2004
+ 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 }> {
2005
+ const rows = this.db.prepare(`
2006
+ SELECT t.*, u.username AS owner_name, g.name AS group_name,
2007
+ (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2008
+ FROM hub_tasks t
2009
+ LEFT JOIN hub_users u ON u.id = t.source_user_id
2010
+ LEFT JOIN hub_groups g ON g.id = t.group_id
2011
+ WHERE t.visibility = 'public'
2012
+ OR EXISTS (
2013
+ SELECT 1 FROM hub_group_members gm
2014
+ WHERE gm.group_id = t.group_id AND gm.user_id = ?
2015
+ )
2016
+ ORDER BY t.updated_at DESC
2017
+ LIMIT ?
2018
+ `).all(userId, limit) as any[];
2019
+ return rows.map(r => ({
2020
+ id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2021
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
2022
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2023
+ createdAt: r.created_at, updatedAt: r.updated_at,
2024
+ }));
2025
+ }
2026
+
2027
+ 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 }> {
2028
+ const rows = this.db.prepare(`
2029
+ SELECT t.*, u.username AS owner_name, g.name AS group_name,
2030
+ (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
2031
+ FROM hub_tasks t
2032
+ LEFT JOIN hub_users u ON u.id = t.source_user_id
2033
+ LEFT JOIN hub_groups g ON g.id = t.group_id
2034
+ ORDER BY t.updated_at DESC
2035
+ `).all() as any[];
2036
+ return rows.map(r => ({
2037
+ id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
2038
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
2039
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
2040
+ createdAt: r.created_at, updatedAt: r.updated_at,
2041
+ }));
2042
+ }
2043
+
2044
+ deleteHubTaskById(taskId: string): boolean {
2045
+ const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
2046
+ return info.changes > 0;
2047
+ }
2048
+
2049
+ 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 }> {
2050
+ const rows = this.db.prepare(`
2051
+ SELECT s.*, u.username AS owner_name, g.name AS group_name
2052
+ FROM hub_skills s
2053
+ LEFT JOIN hub_users u ON u.id = s.source_user_id
2054
+ LEFT JOIN hub_groups g ON g.id = s.group_id
2055
+ WHERE s.visibility = 'public'
2056
+ OR EXISTS (
2057
+ SELECT 1 FROM hub_group_members gm
2058
+ WHERE gm.group_id = s.group_id AND gm.user_id = ?
2059
+ )
2060
+ ORDER BY s.updated_at DESC
2061
+ LIMIT ?
2062
+ `).all(userId, limit) as any[];
2063
+ return rows.map(r => ({
2064
+ id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2065
+ name: r.name, description: r.description, version: r.version,
2066
+ groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2067
+ ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2068
+ createdAt: r.created_at, updatedAt: r.updated_at,
2069
+ }));
2070
+ }
2071
+
2072
+ 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 }> {
2073
+ const rows = this.db.prepare(`
2074
+ SELECT s.*, u.username AS owner_name, g.name AS group_name
2075
+ FROM hub_skills s
2076
+ LEFT JOIN hub_users u ON u.id = s.source_user_id
2077
+ LEFT JOIN hub_groups g ON g.id = s.group_id
2078
+ ORDER BY s.updated_at DESC
2079
+ `).all() as any[];
2080
+ return rows.map(r => ({
2081
+ id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
2082
+ name: r.name, description: r.description, version: r.version,
2083
+ groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2084
+ ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
2085
+ createdAt: r.created_at, updatedAt: r.updated_at,
2086
+ }));
2087
+ }
2088
+
2089
+ deleteHubSkillById(skillId: string): boolean {
2090
+ const info = this.db.prepare('DELETE FROM hub_skills WHERE id = ?').run(skillId);
2091
+ return info.changes > 0;
2092
+ }
2093
+
2094
+ // ─── Hub Shared Memories (independent) ───
2095
+
2096
+ upsertHubMemory(memory: HubMemoryRecord): void {
2097
+ this.db.prepare(`
2098
+ INSERT INTO hub_memories (id, source_chunk_id, source_user_id, role, content, summary, kind, group_id, visibility, created_at, updated_at)
2099
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2100
+ ON CONFLICT(source_user_id, source_chunk_id) DO UPDATE SET
2101
+ role = excluded.role,
2102
+ content = excluded.content,
2103
+ summary = excluded.summary,
2104
+ kind = excluded.kind,
2105
+ group_id = excluded.group_id,
2106
+ visibility = excluded.visibility,
2107
+ created_at = excluded.created_at,
2108
+ updated_at = excluded.updated_at
2109
+ `).run(memory.id, memory.sourceChunkId, memory.sourceUserId, memory.role, memory.content, memory.summary, memory.kind, memory.groupId, memory.visibility, memory.createdAt, memory.updatedAt);
2110
+ }
2111
+
2112
+ getHubMemoryBySource(sourceUserId: string, sourceChunkId: string): HubMemoryRecord | null {
2113
+ const row = this.db.prepare('SELECT * FROM hub_memories WHERE source_user_id = ? AND source_chunk_id = ?').get(sourceUserId, sourceChunkId) as HubMemoryRow | undefined;
2114
+ return row ? rowToHubMemory(row) : null;
2115
+ }
2116
+
2117
+ getHubMemoryById(memoryId: string): HubMemoryRecord | null {
2118
+ const row = this.db.prepare('SELECT * FROM hub_memories WHERE id = ?').get(memoryId) as HubMemoryRow | undefined;
2119
+ return row ? rowToHubMemory(row) : null;
2120
+ }
2121
+
2122
+ deleteHubMemoryBySource(sourceUserId: string, sourceChunkId: string): void {
2123
+ this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ? AND source_chunk_id = ?').run(sourceUserId, sourceChunkId);
2124
+ }
2125
+
2126
+ deleteHubMemoryById(memoryId: string): boolean {
2127
+ const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
2128
+ return info.changes > 0;
2129
+ }
2130
+
2131
+ upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
2132
+ const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
2133
+ this.db.prepare(`
2134
+ INSERT INTO hub_memory_embeddings (memory_id, vector, dimensions, updated_at)
2135
+ VALUES (?, ?, ?, ?)
2136
+ ON CONFLICT(memory_id) DO UPDATE SET vector = excluded.vector, dimensions = excluded.dimensions, updated_at = excluded.updated_at
2137
+ `).run(memoryId, buf, vector.length, Date.now());
2138
+ }
2139
+
2140
+ getHubMemoryEmbedding(memoryId: string): Float32Array | null {
2141
+ const row = this.db.prepare('SELECT vector, dimensions FROM hub_memory_embeddings WHERE memory_id = ?').get(memoryId) as { vector: Buffer; dimensions: number } | undefined;
2142
+ if (!row) return null;
2143
+ return new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions);
2144
+ }
2145
+
2146
+ searchHubMemories(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubMemorySearchRow; rank: number }> {
2147
+ const limit = options?.maxResults ?? 10;
2148
+ const userId = options?.userId ?? "";
2149
+ const sanitized = sanitizeFtsQuery(query);
2150
+ if (!sanitized) return [];
2151
+ const rows = this.db.prepare(`
2152
+ SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, hg.name as group_name, hu.username as owner_name,
2153
+ bm25(hub_memories_fts) as rank
2154
+ FROM hub_memories_fts f
2155
+ JOIN hub_memories hm ON hm.rowid = f.rowid
2156
+ LEFT JOIN hub_groups hg ON hg.id = hm.group_id
2157
+ LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2158
+ WHERE hub_memories_fts MATCH ?
2159
+ AND (
2160
+ hm.visibility = 'public'
2161
+ OR EXISTS (
2162
+ SELECT 1 FROM hub_group_members gm
2163
+ WHERE gm.group_id = hm.group_id AND gm.user_id = ?
2164
+ )
2165
+ )
2166
+ ORDER BY rank
2167
+ LIMIT ?
2168
+ `).all(sanitized, userId, limit) as HubMemorySearchRow[];
2169
+ return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
2170
+ }
2171
+
2172
+ getVisibleHubMemoryEmbeddings(userId: string): Array<{ memoryId: string; vector: Float32Array }> {
2173
+ const rows = this.db.prepare(`
2174
+ SELECT hme.memory_id, hme.vector, hme.dimensions
2175
+ FROM hub_memory_embeddings hme
2176
+ JOIN hub_memories hm ON hm.id = hme.memory_id
2177
+ WHERE hm.visibility = 'public'
2178
+ OR EXISTS (
2179
+ SELECT 1 FROM hub_group_members gm
2180
+ WHERE gm.group_id = hm.group_id AND gm.user_id = ?
2181
+ )
2182
+ `).all(userId) as Array<{ memory_id: string; vector: Buffer; dimensions: number }>;
2183
+ return rows.map(r => ({
2184
+ memoryId: r.memory_id,
2185
+ vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
2186
+ }));
2187
+ }
2188
+
2189
+ getVisibleHubSearchHitByMemoryId(memoryId: string, userId: string): HubMemorySearchRow | null {
2190
+ const row = this.db.prepare(`
2191
+ SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, hg.name as group_name, hu.username as owner_name,
2192
+ 0 as rank
2193
+ FROM hub_memories hm
2194
+ LEFT JOIN hub_groups hg ON hg.id = hm.group_id
2195
+ LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2196
+ WHERE hm.id = ?
2197
+ AND (
2198
+ hm.visibility = 'public'
2199
+ OR EXISTS (
2200
+ SELECT 1 FROM hub_group_members gm
2201
+ WHERE gm.group_id = hm.group_id AND gm.user_id = ?
2202
+ )
2203
+ )
2204
+ LIMIT 1
2205
+ `).get(memoryId, userId) as HubMemorySearchRow | undefined;
2206
+ return row ?? null;
2207
+ }
2208
+
2209
+ 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 }> {
2210
+ const rows = this.db.prepare(`
2211
+ SELECT m.*, u.username AS owner_name, g.name AS group_name
2212
+ FROM hub_memories m
2213
+ LEFT JOIN hub_users u ON u.id = m.source_user_id
2214
+ LEFT JOIN hub_groups g ON g.id = m.group_id
2215
+ WHERE m.visibility = 'public'
2216
+ OR EXISTS (
2217
+ SELECT 1 FROM hub_group_members gm
2218
+ WHERE gm.group_id = m.group_id AND gm.user_id = ?
2219
+ )
2220
+ ORDER BY m.updated_at DESC
2221
+ LIMIT ?
2222
+ `).all(userId, limit) as any[];
2223
+ return rows.map(r => ({
2224
+ id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2225
+ role: r.role, summary: r.summary, kind: r.kind,
2226
+ groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2227
+ ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2228
+ }));
2229
+ }
2230
+
2231
+ 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 }> {
2232
+ const rows = this.db.prepare(`
2233
+ SELECT m.*, u.username AS owner_name, g.name AS group_name
2234
+ FROM hub_memories m
2235
+ LEFT JOIN hub_users u ON u.id = m.source_user_id
2236
+ LEFT JOIN hub_groups g ON g.id = m.group_id
2237
+ ORDER BY m.updated_at DESC
2238
+ `).all() as any[];
2239
+ return rows.map(r => ({
2240
+ id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2241
+ role: r.role, summary: r.summary, kind: r.kind,
2242
+ groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2243
+ ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2244
+ }));
2245
+ }
2246
+
2247
+ private resolveCanonicalHubTaskId(taskId: string, sourceUserId: string, sourceTaskId?: string): string {
2248
+ if (sourceTaskId) {
2249
+ 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;
2250
+ if (!bySource) throw new Error(`source task not found for user=${sourceUserId} sourceTaskId=${sourceTaskId}`);
2251
+ if (bySource.id != taskId) throw new Error(`mismatch between source task and hubTaskId: expected ${bySource.id}, got ${taskId}`);
2252
+ return bySource.id;
2253
+ }
2254
+ throw new Error(`source task not found for user=${sourceUserId} taskId=${taskId}`);
2255
+ }
2256
+
2257
+ private resolveCanonicalHubSkillId(skillId: string, sourceUserId?: string, sourceSkillId?: string): string {
2258
+ if (sourceUserId && sourceSkillId) {
2259
+ 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;
2260
+ if (!bySource) throw new Error(`source skill not found for user=${sourceUserId} sourceSkillId=${sourceSkillId}`);
2261
+ if (bySource.id != skillId) throw new Error(`mismatch between source skill and skillId: expected ${bySource.id}, got ${skillId}`);
2262
+ return bySource.id;
2263
+ }
2264
+ throw new Error(`source skill not found for skillId=${skillId}`);
2265
+ }
2266
+
2267
+ private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
2268
+ return {
2269
+ ...user,
2270
+ groups: this.getGroupsForHubUser(user.id),
2271
+ };
2272
+ }
2273
+
1417
2274
  getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
1418
2275
  const result = new Map<string, string>();
1419
2276
  if (sessionKeys.length === 0) return result;
@@ -1438,13 +2295,14 @@ export class SqliteStore {
1438
2295
  * with implicit AND (space-separated) for safe querying.
1439
2296
  */
1440
2297
  function sanitizeFtsQuery(raw: string): string {
1441
- // With trigram tokenizer, the query string is matched as a substring.
1442
- // Clean special chars but keep the text as-is (trigram needs >= 3 chars).
1443
- const cleaned = raw
1444
- .replace(/[."""(){}[\]*:^~!@#$%&\\/<>,;'`??。,!、:""''()【】《》]/g, " ")
1445
- .trim();
1446
- if (cleaned.length < 3) return "";
1447
- return cleaned;
2298
+ const tokens = raw
2299
+ .replace(/[."""(){}[\]*:^~!@#$%&\\/<>,;'`-]/g, " ")
2300
+ .split(/\s+/)
2301
+ .map((t) => t.trim().replace(/^-+|-+$/g, ""))
2302
+ .filter((t) => t.length > 1)
2303
+ .filter((t) => !FTS_RESERVED.has(t.toUpperCase()));
2304
+
2305
+ return tokens.join(" ");
1448
2306
  }
1449
2307
 
1450
2308
  const FTS_RESERVED = new Set(["AND", "OR", "NOT", "NEAR"]);
@@ -1590,6 +2448,308 @@ function rowToSkillVersion(row: SkillVersionRow): SkillVersion {
1590
2448
  };
1591
2449
  }
1592
2450
 
2451
+
2452
+ interface ClientHubConnection {
2453
+ hubUrl: string;
2454
+ userId: string;
2455
+ username: string;
2456
+ userToken: string;
2457
+ role: UserRole;
2458
+ connectedAt: number;
2459
+ }
2460
+
2461
+ interface ClientHubConnectionRow {
2462
+ hub_url: string;
2463
+ user_id: string;
2464
+ username: string;
2465
+ user_token: string;
2466
+ role: string;
2467
+ connected_at: number;
2468
+ }
2469
+
2470
+ function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnection {
2471
+ return {
2472
+ hubUrl: row.hub_url,
2473
+ userId: row.user_id,
2474
+ username: row.username,
2475
+ userToken: row.user_token,
2476
+ role: row.role as UserRole,
2477
+ connectedAt: row.connected_at,
2478
+ };
2479
+ }
2480
+
2481
+ interface HubUserRecord extends UserInfo {
2482
+ tokenHash: string;
2483
+ createdAt: number;
2484
+ approvedAt: number | null;
2485
+ }
2486
+
2487
+ interface HubUserRow {
2488
+ id: string;
2489
+ username: string;
2490
+ device_name: string;
2491
+ role: string;
2492
+ status: string;
2493
+ token_hash: string;
2494
+ created_at: number;
2495
+ approved_at: number | null;
2496
+ }
2497
+
2498
+ function rowToHubUser(row: HubUserRow): HubUserRecord {
2499
+ return {
2500
+ id: row.id,
2501
+ username: row.username,
2502
+ deviceName: row.device_name || undefined,
2503
+ role: row.role as UserRole,
2504
+ status: row.status as UserStatus,
2505
+ groups: [],
2506
+ tokenHash: row.token_hash,
2507
+ createdAt: row.created_at,
2508
+ approvedAt: row.approved_at,
2509
+ };
2510
+ }
2511
+
2512
+ interface HubGroupRecord {
2513
+ id: string;
2514
+ name: string;
2515
+ description: string;
2516
+ createdAt: number;
2517
+ }
2518
+
2519
+ interface HubGroupRow {
2520
+ id: string;
2521
+ name: string;
2522
+ description: string;
2523
+ created_at: number;
2524
+ }
2525
+
2526
+ function rowToHubGroup(row: HubGroupRow): HubGroupRecord {
2527
+ return {
2528
+ id: row.id,
2529
+ name: row.name,
2530
+ description: row.description,
2531
+ createdAt: row.created_at,
2532
+ };
2533
+ }
2534
+
2535
+ interface HubTaskRecord {
2536
+ id: string;
2537
+ sourceTaskId: string;
2538
+ sourceUserId: string;
2539
+ title: string;
2540
+ summary: string;
2541
+ groupId: string | null;
2542
+ visibility: SharedVisibility;
2543
+ createdAt: number;
2544
+ updatedAt: number;
2545
+ }
2546
+
2547
+ interface HubTaskRow {
2548
+ id: string;
2549
+ source_task_id: string;
2550
+ source_user_id: string;
2551
+ title: string;
2552
+ summary: string;
2553
+ group_id: string | null;
2554
+ visibility: string;
2555
+ created_at: number;
2556
+ updated_at: number;
2557
+ }
2558
+
2559
+ function rowToHubTask(row: HubTaskRow): HubTaskRecord {
2560
+ return {
2561
+ id: row.id,
2562
+ sourceTaskId: row.source_task_id,
2563
+ sourceUserId: row.source_user_id,
2564
+ title: row.title,
2565
+ summary: row.summary,
2566
+ groupId: row.group_id,
2567
+ visibility: row.visibility as SharedVisibility,
2568
+ createdAt: row.created_at,
2569
+ updatedAt: row.updated_at,
2570
+ };
2571
+ }
2572
+
2573
+ interface HubChunkUpsertInput {
2574
+ id: string;
2575
+ hubTaskId: string;
2576
+ sourceTaskId: string;
2577
+ sourceChunkId: string;
2578
+ sourceUserId: string;
2579
+ role: Chunk["role"];
2580
+ content: string;
2581
+ summary: string;
2582
+ kind: Chunk["kind"];
2583
+ createdAt: number;
2584
+ }
2585
+
2586
+ interface HubChunkRecord {
2587
+ id: string;
2588
+ hubTaskId: string;
2589
+ sourceChunkId: string;
2590
+ sourceUserId: string;
2591
+ role: Chunk["role"];
2592
+ content: string;
2593
+ summary: string;
2594
+ kind: Chunk["kind"];
2595
+ createdAt: number;
2596
+ }
2597
+
2598
+ interface HubChunkRow {
2599
+ id: string;
2600
+ hub_task_id: string;
2601
+ source_chunk_id: string;
2602
+ source_user_id: string;
2603
+ role: string;
2604
+ content: string;
2605
+ summary: string;
2606
+ kind: string;
2607
+ created_at: number;
2608
+ }
2609
+
2610
+ function rowToHubChunk(row: HubChunkRow): HubChunkRecord {
2611
+ return {
2612
+ id: row.id,
2613
+ hubTaskId: row.hub_task_id,
2614
+ sourceChunkId: row.source_chunk_id,
2615
+ sourceUserId: row.source_user_id,
2616
+ role: row.role as Chunk["role"],
2617
+ content: row.content,
2618
+ summary: row.summary,
2619
+ kind: row.kind as Chunk["kind"],
2620
+ createdAt: row.created_at,
2621
+ };
2622
+ }
2623
+
2624
+ interface HubSkillRecord {
2625
+ id: string;
2626
+ sourceSkillId: string;
2627
+ sourceUserId: string;
2628
+ name: string;
2629
+ description: string;
2630
+ version: number;
2631
+ groupId: string | null;
2632
+ visibility: SharedVisibility;
2633
+ bundle: string;
2634
+ qualityScore: number | null;
2635
+ createdAt: number;
2636
+ updatedAt: number;
2637
+ }
2638
+
2639
+ interface HubSkillRow {
2640
+ id: string;
2641
+ source_skill_id: string;
2642
+ source_user_id: string;
2643
+ name: string;
2644
+ description: string;
2645
+ version: number;
2646
+ group_id: string | null;
2647
+ visibility: string;
2648
+ bundle: string;
2649
+ quality_score: number | null;
2650
+ created_at: number;
2651
+ updated_at: number;
2652
+ }
2653
+
2654
+ function rowToHubSkill(row: HubSkillRow): HubSkillRecord {
2655
+ return {
2656
+ id: row.id,
2657
+ sourceSkillId: row.source_skill_id,
2658
+ sourceUserId: row.source_user_id,
2659
+ name: row.name,
2660
+ description: row.description,
2661
+ version: row.version,
2662
+ groupId: row.group_id,
2663
+ visibility: row.visibility as SharedVisibility,
2664
+ bundle: row.bundle,
2665
+ qualityScore: row.quality_score,
2666
+ createdAt: row.created_at,
2667
+ updatedAt: row.updated_at,
2668
+ };
2669
+ }
2670
+
2671
+
2672
+ interface HubSkillSearchRow {
2673
+ id: string;
2674
+ name: string;
2675
+ description: string;
2676
+ version: number;
2677
+ visibility: string;
2678
+ group_name: string | null;
2679
+ owner_name: string | null;
2680
+ quality_score: number | null;
2681
+ }
2682
+
2683
+ interface HubSearchRow {
2684
+ id: string;
2685
+ content: string;
2686
+ summary: string;
2687
+ role: string;
2688
+ created_at: number;
2689
+ task_title: string | null;
2690
+ visibility: string;
2691
+ group_name: string | null;
2692
+ owner_name: string | null;
2693
+ rank: number;
2694
+ }
2695
+
2696
+ export interface HubMemoryRecord {
2697
+ id: string;
2698
+ sourceChunkId: string;
2699
+ sourceUserId: string;
2700
+ role: string;
2701
+ content: string;
2702
+ summary: string;
2703
+ kind: string;
2704
+ groupId: string | null;
2705
+ visibility: SharedVisibility;
2706
+ createdAt: number;
2707
+ updatedAt: number;
2708
+ }
2709
+
2710
+ interface HubMemoryRow {
2711
+ id: string;
2712
+ source_chunk_id: string;
2713
+ source_user_id: string;
2714
+ role: string;
2715
+ content: string;
2716
+ summary: string;
2717
+ kind: string;
2718
+ group_id: string | null;
2719
+ visibility: string;
2720
+ created_at: number;
2721
+ updated_at: number;
2722
+ }
2723
+
2724
+ function rowToHubMemory(row: HubMemoryRow): HubMemoryRecord {
2725
+ return {
2726
+ id: row.id,
2727
+ sourceChunkId: row.source_chunk_id,
2728
+ sourceUserId: row.source_user_id,
2729
+ role: row.role,
2730
+ content: row.content,
2731
+ summary: row.summary,
2732
+ kind: row.kind,
2733
+ groupId: row.group_id,
2734
+ visibility: row.visibility as SharedVisibility,
2735
+ createdAt: row.created_at,
2736
+ updatedAt: row.updated_at,
2737
+ };
2738
+ }
2739
+
2740
+ interface HubMemorySearchRow {
2741
+ id: string;
2742
+ content: string;
2743
+ summary: string;
2744
+ role: string;
2745
+ created_at: number;
2746
+ visibility: string;
2747
+ group_name: string | null;
2748
+ owner_name: string | null;
2749
+ rank: number;
2750
+ }
2751
+
2752
+
1593
2753
  function contentHash(content: string): string {
1594
2754
  return createHash("sha256").update(content).digest("hex").slice(0, 16);
1595
2755
  }