@memtensor/memos-local-openclaw-plugin 1.0.4-beta.2 → 1.0.4-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +111 -44
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +36 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +6 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +160 -26
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -3
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +9 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +500 -112
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +11 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +31 -3
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +96 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +84 -9
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +4 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +59 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +115 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +458 -110
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2952 -910
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +39 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1198 -227
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +774 -74
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +40 -1
- package/src/client/connector.ts +161 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +2 -3
- package/src/hub/server.ts +481 -107
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +99 -10
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +63 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +498 -137
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +12 -2
- package/src/viewer/html.ts +2952 -910
- package/src/viewer/server.ts +1109 -212
package/dist/storage/sqlite.js
CHANGED
|
@@ -146,11 +146,70 @@ class SqliteStore {
|
|
|
146
146
|
this.migrateSkillEmbeddingsAndFts();
|
|
147
147
|
this.migrateFtsToTrigram();
|
|
148
148
|
this.migrateHubTables();
|
|
149
|
+
this.migrateHubFtsToTrigram();
|
|
150
|
+
this.migrateLocalSharedTasksOwner();
|
|
151
|
+
this.migrateHubUserIdentityFields();
|
|
152
|
+
this.migrateClientHubConnectionIdentityFields();
|
|
149
153
|
this.log.debug("Database schema initialized");
|
|
150
154
|
}
|
|
151
155
|
migrateChunksIndexesForRecall() {
|
|
152
156
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
|
|
153
157
|
}
|
|
158
|
+
migrateLocalSharedTasksOwner() {
|
|
159
|
+
try {
|
|
160
|
+
const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all();
|
|
161
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
|
|
162
|
+
this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
|
|
163
|
+
this.log.info("Migrated: added original_owner column to local_shared_tasks");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch { /* table may not exist yet */ }
|
|
167
|
+
}
|
|
168
|
+
migrateHubUserIdentityFields() {
|
|
169
|
+
try {
|
|
170
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
|
|
171
|
+
if (cols.length === 0)
|
|
172
|
+
return;
|
|
173
|
+
if (!cols.some(c => c.name === "identity_key")) {
|
|
174
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
|
|
175
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_hub_users_identity_key ON hub_users(identity_key)");
|
|
176
|
+
this.log.info("Migrated: added identity_key to hub_users");
|
|
177
|
+
}
|
|
178
|
+
if (!cols.some(c => c.name === "left_at")) {
|
|
179
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN left_at INTEGER");
|
|
180
|
+
this.log.info("Migrated: added left_at to hub_users");
|
|
181
|
+
}
|
|
182
|
+
if (!cols.some(c => c.name === "removed_at")) {
|
|
183
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN removed_at INTEGER");
|
|
184
|
+
this.log.info("Migrated: added removed_at to hub_users");
|
|
185
|
+
}
|
|
186
|
+
if (!cols.some(c => c.name === "rejected_at")) {
|
|
187
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN rejected_at INTEGER");
|
|
188
|
+
this.log.info("Migrated: added rejected_at to hub_users");
|
|
189
|
+
}
|
|
190
|
+
if (!cols.some(c => c.name === "rejoin_requested_at")) {
|
|
191
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN rejoin_requested_at INTEGER");
|
|
192
|
+
this.log.info("Migrated: added rejoin_requested_at to hub_users");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch { /* table may not exist yet */ }
|
|
196
|
+
}
|
|
197
|
+
migrateClientHubConnectionIdentityFields() {
|
|
198
|
+
try {
|
|
199
|
+
const cols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all();
|
|
200
|
+
if (cols.length === 0)
|
|
201
|
+
return;
|
|
202
|
+
if (!cols.some(c => c.name === "identity_key")) {
|
|
203
|
+
this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
|
|
204
|
+
this.log.info("Migrated: added identity_key to client_hub_connection");
|
|
205
|
+
}
|
|
206
|
+
if (!cols.some(c => c.name === "last_known_status")) {
|
|
207
|
+
this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN last_known_status TEXT NOT NULL DEFAULT ''");
|
|
208
|
+
this.log.info("Migrated: added last_known_status to client_hub_connection");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch { /* table may not exist yet */ }
|
|
212
|
+
}
|
|
154
213
|
migrateOwnerFields() {
|
|
155
214
|
const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all();
|
|
156
215
|
if (!chunkCols.some((c) => c.name === "owner")) {
|
|
@@ -298,6 +357,54 @@ class SqliteStore {
|
|
|
298
357
|
this.log.warn(`Failed to migrate skills_fts to trigram: ${err}`);
|
|
299
358
|
}
|
|
300
359
|
}
|
|
360
|
+
migrateHubFtsToTrigram() {
|
|
361
|
+
const tables = [
|
|
362
|
+
{
|
|
363
|
+
fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
|
|
364
|
+
triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
|
|
368
|
+
triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
|
|
372
|
+
triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
for (const t of tables) {
|
|
376
|
+
try {
|
|
377
|
+
const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get();
|
|
378
|
+
if (!row || !row.sql)
|
|
379
|
+
continue;
|
|
380
|
+
if (row.sql.includes("trigram"))
|
|
381
|
+
continue;
|
|
382
|
+
this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
|
|
383
|
+
for (const tr of t.triggers)
|
|
384
|
+
this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
|
|
385
|
+
this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
|
|
386
|
+
this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
|
|
387
|
+
this.db.exec(`
|
|
388
|
+
CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
|
|
389
|
+
INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
|
|
390
|
+
END;
|
|
391
|
+
CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
|
|
392
|
+
INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
|
|
393
|
+
END;
|
|
394
|
+
CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
|
|
395
|
+
INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
|
|
396
|
+
INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
|
|
397
|
+
END
|
|
398
|
+
`);
|
|
399
|
+
this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
|
|
400
|
+
const cnt = this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get().c;
|
|
401
|
+
this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
301
408
|
migrateTaskId() {
|
|
302
409
|
const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
|
|
303
410
|
if (!cols.some((c) => c.name === "task_id")) {
|
|
@@ -503,15 +610,16 @@ class SqliteStore {
|
|
|
503
610
|
recordToolCall(toolName, durationMs, success) {
|
|
504
611
|
this.db.prepare("INSERT INTO tool_calls (tool_name, duration_ms, success, called_at) VALUES (?, ?, ?, ?)").run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
|
|
505
612
|
}
|
|
506
|
-
getToolMetrics(minutes) {
|
|
507
|
-
const since = Date.now() - minutes * 60 * 1000;
|
|
613
|
+
getToolMetrics(minutes, fromMs, toMs) {
|
|
614
|
+
const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
|
|
615
|
+
const until = toMs ?? Date.now();
|
|
508
616
|
const rows = this.db.prepare(`SELECT tool_name,
|
|
509
617
|
duration_ms,
|
|
510
618
|
success,
|
|
511
619
|
strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
|
|
512
620
|
FROM tool_calls
|
|
513
|
-
WHERE called_at >= ?
|
|
514
|
-
ORDER BY called_at`).all(since);
|
|
621
|
+
WHERE called_at >= ? AND called_at <= ?
|
|
622
|
+
ORDER BY called_at`).all(since, until);
|
|
515
623
|
const toolSet = new Set();
|
|
516
624
|
const minuteMap = new Map();
|
|
517
625
|
const aggMap = new Map();
|
|
@@ -644,33 +752,49 @@ class SqliteStore {
|
|
|
644
752
|
shared_at INTEGER NOT NULL
|
|
645
753
|
);
|
|
646
754
|
|
|
755
|
+
CREATE TABLE IF NOT EXISTS local_shared_memories (
|
|
756
|
+
chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
|
|
757
|
+
original_owner TEXT NOT NULL,
|
|
758
|
+
shared_at INTEGER NOT NULL
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
-- Client: team share UI metadata only (no hub_memories row — avoids local FTS/embed recall duplication)
|
|
762
|
+
CREATE TABLE IF NOT EXISTS team_shared_chunks (
|
|
763
|
+
chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
|
|
764
|
+
hub_memory_id TEXT NOT NULL DEFAULT '',
|
|
765
|
+
visibility TEXT NOT NULL DEFAULT 'public',
|
|
766
|
+
group_id TEXT,
|
|
767
|
+
shared_at INTEGER NOT NULL
|
|
768
|
+
);
|
|
769
|
+
|
|
647
770
|
CREATE TABLE IF NOT EXISTS hub_users (
|
|
648
|
-
id
|
|
649
|
-
username
|
|
650
|
-
device_name
|
|
651
|
-
role
|
|
652
|
-
status
|
|
653
|
-
token_hash
|
|
654
|
-
created_at
|
|
655
|
-
approved_at
|
|
771
|
+
id TEXT PRIMARY KEY,
|
|
772
|
+
username TEXT NOT NULL UNIQUE,
|
|
773
|
+
device_name TEXT NOT NULL DEFAULT '',
|
|
774
|
+
role TEXT NOT NULL,
|
|
775
|
+
status TEXT NOT NULL,
|
|
776
|
+
token_hash TEXT NOT NULL DEFAULT '',
|
|
777
|
+
created_at INTEGER NOT NULL,
|
|
778
|
+
approved_at INTEGER,
|
|
779
|
+
last_ip TEXT NOT NULL DEFAULT '',
|
|
780
|
+
last_active_at INTEGER
|
|
656
781
|
);
|
|
657
782
|
CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
|
|
658
783
|
CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
|
|
659
784
|
|
|
660
785
|
CREATE TABLE IF NOT EXISTS hub_groups (
|
|
661
786
|
id TEXT PRIMARY KEY,
|
|
662
|
-
name TEXT NOT NULL
|
|
787
|
+
name TEXT NOT NULL,
|
|
663
788
|
description TEXT NOT NULL DEFAULT '',
|
|
664
789
|
created_at INTEGER NOT NULL
|
|
665
790
|
);
|
|
666
791
|
|
|
667
792
|
CREATE TABLE IF NOT EXISTS hub_group_members (
|
|
668
|
-
group_id
|
|
669
|
-
user_id
|
|
670
|
-
joined_at
|
|
793
|
+
group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
|
|
794
|
+
user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
|
|
795
|
+
joined_at INTEGER NOT NULL,
|
|
671
796
|
PRIMARY KEY (group_id, user_id)
|
|
672
797
|
);
|
|
673
|
-
CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
|
|
674
798
|
|
|
675
799
|
CREATE TABLE IF NOT EXISTS hub_tasks (
|
|
676
800
|
id TEXT PRIMARY KEY,
|
|
@@ -713,7 +837,7 @@ class SqliteStore {
|
|
|
713
837
|
content,
|
|
714
838
|
content='hub_chunks',
|
|
715
839
|
content_rowid='rowid',
|
|
716
|
-
tokenize='
|
|
840
|
+
tokenize='trigram'
|
|
717
841
|
);
|
|
718
842
|
|
|
719
843
|
CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
|
|
@@ -763,7 +887,7 @@ class SqliteStore {
|
|
|
763
887
|
description,
|
|
764
888
|
content='hub_skills',
|
|
765
889
|
content_rowid='rowid',
|
|
766
|
-
tokenize='
|
|
890
|
+
tokenize='trigram'
|
|
767
891
|
);
|
|
768
892
|
|
|
769
893
|
CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
|
|
@@ -813,7 +937,7 @@ class SqliteStore {
|
|
|
813
937
|
content,
|
|
814
938
|
content='hub_memories',
|
|
815
939
|
content_rowid='rowid',
|
|
816
|
-
tokenize='
|
|
940
|
+
tokenize='trigram'
|
|
817
941
|
);
|
|
818
942
|
|
|
819
943
|
CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
|
|
@@ -833,6 +957,31 @@ class SqliteStore {
|
|
|
833
957
|
VALUES (new.rowid, new.summary, new.content);
|
|
834
958
|
END;
|
|
835
959
|
`);
|
|
960
|
+
this.db.exec(`
|
|
961
|
+
CREATE TABLE IF NOT EXISTS hub_notifications (
|
|
962
|
+
id TEXT PRIMARY KEY,
|
|
963
|
+
user_id TEXT NOT NULL,
|
|
964
|
+
type TEXT NOT NULL,
|
|
965
|
+
resource TEXT NOT NULL,
|
|
966
|
+
title TEXT NOT NULL,
|
|
967
|
+
message TEXT NOT NULL DEFAULT '',
|
|
968
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
969
|
+
created_at INTEGER NOT NULL
|
|
970
|
+
);
|
|
971
|
+
CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
|
|
972
|
+
`);
|
|
973
|
+
try {
|
|
974
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
|
|
975
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
|
|
976
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
|
|
977
|
+
this.log.info("Migrated: added last_ip column to hub_users");
|
|
978
|
+
}
|
|
979
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
|
|
980
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
|
|
981
|
+
this.log.info("Migrated: added last_active_at column to hub_users");
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
catch { /* table may not exist yet */ }
|
|
836
985
|
}
|
|
837
986
|
// ─── Write ───
|
|
838
987
|
insertChunk(chunk) {
|
|
@@ -959,6 +1108,30 @@ class SqliteStore {
|
|
|
959
1108
|
return [];
|
|
960
1109
|
}
|
|
961
1110
|
}
|
|
1111
|
+
hubMemoryPatternSearch(patterns, opts = {}) {
|
|
1112
|
+
if (patterns.length === 0)
|
|
1113
|
+
return [];
|
|
1114
|
+
const limit = opts.limit ?? 10;
|
|
1115
|
+
const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
|
|
1116
|
+
const params = [];
|
|
1117
|
+
for (const p of patterns) {
|
|
1118
|
+
params.push(`%${p}%`, `%${p}%`);
|
|
1119
|
+
}
|
|
1120
|
+
params.push(limit);
|
|
1121
|
+
try {
|
|
1122
|
+
const rows = this.db.prepare(`
|
|
1123
|
+
SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
|
|
1124
|
+
FROM hub_memories hm
|
|
1125
|
+
WHERE ${conditions.join(" OR ")}
|
|
1126
|
+
ORDER BY hm.created_at DESC
|
|
1127
|
+
LIMIT ?
|
|
1128
|
+
`).all(...params);
|
|
1129
|
+
return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
return [];
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
962
1135
|
// ─── Vector Search ───
|
|
963
1136
|
getAllEmbeddings(ownerFilter) {
|
|
964
1137
|
let sql = `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
|
|
@@ -1097,6 +1270,9 @@ class SqliteStore {
|
|
|
1097
1270
|
"skill_embeddings",
|
|
1098
1271
|
"skill_versions",
|
|
1099
1272
|
"skills",
|
|
1273
|
+
"local_shared_memories",
|
|
1274
|
+
"team_shared_chunks",
|
|
1275
|
+
"local_shared_tasks",
|
|
1100
1276
|
"embeddings",
|
|
1101
1277
|
"chunks",
|
|
1102
1278
|
"tasks",
|
|
@@ -1208,8 +1384,8 @@ class SqliteStore {
|
|
|
1208
1384
|
params.push(opts.status);
|
|
1209
1385
|
}
|
|
1210
1386
|
if (opts.owner) {
|
|
1211
|
-
conditions.push("owner = ?");
|
|
1212
|
-
params.push(opts.owner);
|
|
1387
|
+
conditions.push("(owner = ? OR (owner = 'public' AND id IN (SELECT task_id FROM local_shared_tasks WHERE original_owner = ?)))");
|
|
1388
|
+
params.push(opts.owner, opts.owner);
|
|
1213
1389
|
}
|
|
1214
1390
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1215
1391
|
const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params);
|
|
@@ -1475,16 +1651,18 @@ class SqliteStore {
|
|
|
1475
1651
|
// ─── Hub / Client connection ───
|
|
1476
1652
|
setClientHubConnection(conn) {
|
|
1477
1653
|
this.db.prepare(`
|
|
1478
|
-
INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
|
|
1479
|
-
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
1654
|
+
INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status)
|
|
1655
|
+
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1480
1656
|
ON CONFLICT(id) DO UPDATE SET
|
|
1481
1657
|
hub_url = excluded.hub_url,
|
|
1482
1658
|
user_id = excluded.user_id,
|
|
1483
1659
|
username = excluded.username,
|
|
1484
1660
|
user_token = excluded.user_token,
|
|
1485
1661
|
role = excluded.role,
|
|
1486
|
-
connected_at = excluded.connected_at
|
|
1487
|
-
|
|
1662
|
+
connected_at = excluded.connected_at,
|
|
1663
|
+
identity_key = excluded.identity_key,
|
|
1664
|
+
last_known_status = excluded.last_known_status
|
|
1665
|
+
`).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "");
|
|
1488
1666
|
}
|
|
1489
1667
|
getClientHubConnection() {
|
|
1490
1668
|
const row = this.db.prepare('SELECT * FROM client_hub_connection WHERE id = 1').get();
|
|
@@ -1519,11 +1697,66 @@ class SqliteStore {
|
|
|
1519
1697
|
const rows = this.db.prepare('SELECT task_id, hub_task_id, visibility, group_id, synced_chunks FROM local_shared_tasks').all();
|
|
1520
1698
|
return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks }));
|
|
1521
1699
|
}
|
|
1700
|
+
// ─── Local Shared Memories (client-side tracking) ───
|
|
1701
|
+
markMemorySharedLocally(chunkId) {
|
|
1702
|
+
const chunk = this.getChunk(chunkId);
|
|
1703
|
+
if (!chunk)
|
|
1704
|
+
return { ok: false, reason: "not_found" };
|
|
1705
|
+
if (chunk.owner === "public") {
|
|
1706
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1707
|
+
return {
|
|
1708
|
+
ok: true,
|
|
1709
|
+
owner: "public",
|
|
1710
|
+
originalOwner: existing?.originalOwner ?? undefined,
|
|
1711
|
+
sharedAt: existing?.sharedAt ?? undefined,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
const sharedAt = Date.now();
|
|
1715
|
+
this.db.transaction(() => {
|
|
1716
|
+
this.db.prepare(`
|
|
1717
|
+
INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
|
|
1718
|
+
VALUES (?, ?, ?)
|
|
1719
|
+
ON CONFLICT(chunk_id) DO UPDATE SET
|
|
1720
|
+
original_owner = excluded.original_owner,
|
|
1721
|
+
shared_at = excluded.shared_at
|
|
1722
|
+
`).run(chunkId, chunk.owner, sharedAt);
|
|
1723
|
+
this.updateChunk(chunkId, { owner: "public" });
|
|
1724
|
+
})();
|
|
1725
|
+
return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
|
|
1726
|
+
}
|
|
1727
|
+
unmarkMemorySharedLocally(chunkId, fallbackOwner) {
|
|
1728
|
+
const chunk = this.getChunk(chunkId);
|
|
1729
|
+
if (!chunk)
|
|
1730
|
+
return { ok: false, reason: "not_found" };
|
|
1731
|
+
if (chunk.owner !== "public") {
|
|
1732
|
+
return { ok: true, owner: chunk.owner };
|
|
1733
|
+
}
|
|
1734
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1735
|
+
const restoreOwner = existing?.originalOwner ?? fallbackOwner;
|
|
1736
|
+
if (!restoreOwner || restoreOwner === "public") {
|
|
1737
|
+
return { ok: false, reason: "original_owner_missing" };
|
|
1738
|
+
}
|
|
1739
|
+
this.db.transaction(() => {
|
|
1740
|
+
this.updateChunk(chunkId, { owner: restoreOwner });
|
|
1741
|
+
this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
|
|
1742
|
+
})();
|
|
1743
|
+
return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
|
|
1744
|
+
}
|
|
1745
|
+
getLocalSharedMemory(chunkId) {
|
|
1746
|
+
const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId);
|
|
1747
|
+
if (!row)
|
|
1748
|
+
return null;
|
|
1749
|
+
return {
|
|
1750
|
+
chunkId: row.chunk_id,
|
|
1751
|
+
originalOwner: row.original_owner,
|
|
1752
|
+
sharedAt: row.shared_at,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1522
1755
|
// ─── Hub Users / Groups ───
|
|
1523
1756
|
upsertHubUser(user) {
|
|
1524
1757
|
this.db.prepare(`
|
|
1525
|
-
INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
|
|
1526
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1758
|
+
INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at, identity_key, left_at, removed_at, rejected_at, rejoin_requested_at)
|
|
1759
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1527
1760
|
ON CONFLICT(id) DO UPDATE SET
|
|
1528
1761
|
username = excluded.username,
|
|
1529
1762
|
device_name = excluded.device_name,
|
|
@@ -1531,72 +1764,101 @@ class SqliteStore {
|
|
|
1531
1764
|
status = excluded.status,
|
|
1532
1765
|
token_hash = excluded.token_hash,
|
|
1533
1766
|
created_at = excluded.created_at,
|
|
1534
|
-
approved_at = excluded.approved_at
|
|
1535
|
-
|
|
1767
|
+
approved_at = excluded.approved_at,
|
|
1768
|
+
identity_key = excluded.identity_key,
|
|
1769
|
+
left_at = excluded.left_at,
|
|
1770
|
+
removed_at = excluded.removed_at,
|
|
1771
|
+
rejected_at = excluded.rejected_at,
|
|
1772
|
+
rejoin_requested_at = excluded.rejoin_requested_at
|
|
1773
|
+
`).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt, user.identityKey ?? "", user.leftAt ?? null, user.removedAt ?? null, user.rejectedAt ?? null, user.rejoinRequestedAt ?? null);
|
|
1536
1774
|
}
|
|
1537
1775
|
getHubUser(userId) {
|
|
1538
1776
|
const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId);
|
|
1539
1777
|
if (!row)
|
|
1540
1778
|
return null;
|
|
1541
|
-
|
|
1779
|
+
const user = rowToHubUser(row);
|
|
1780
|
+
user.groups = this.getGroupsForHubUser(userId);
|
|
1781
|
+
return user;
|
|
1542
1782
|
}
|
|
1543
1783
|
listHubUsers(status) {
|
|
1544
1784
|
const rows = status
|
|
1545
1785
|
? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status)
|
|
1546
1786
|
: this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all();
|
|
1547
|
-
return rows.map(
|
|
1787
|
+
return rows.map(r => {
|
|
1788
|
+
const user = rowToHubUser(r);
|
|
1789
|
+
user.groups = this.getGroupsForHubUser(r.id);
|
|
1790
|
+
return user;
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
deleteHubUser(userId, cleanResources = false) {
|
|
1794
|
+
if (cleanResources) {
|
|
1795
|
+
this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
|
|
1796
|
+
this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
|
|
1797
|
+
this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
|
|
1798
|
+
const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
|
|
1799
|
+
return result.changes > 0;
|
|
1800
|
+
}
|
|
1801
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
|
|
1802
|
+
return result.changes > 0;
|
|
1548
1803
|
}
|
|
1804
|
+
findHubUserByIdentityKey(identityKey) {
|
|
1805
|
+
if (!identityKey)
|
|
1806
|
+
return null;
|
|
1807
|
+
const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey);
|
|
1808
|
+
return row ? rowToHubUser(row) : null;
|
|
1809
|
+
}
|
|
1810
|
+
markHubUserLeft(userId) {
|
|
1811
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
|
|
1812
|
+
return result.changes > 0;
|
|
1813
|
+
}
|
|
1814
|
+
updateHubUserActivity(userId, ip, timestamp) {
|
|
1815
|
+
this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
|
|
1816
|
+
}
|
|
1817
|
+
// ─── Hub Groups ───
|
|
1549
1818
|
upsertHubGroup(group) {
|
|
1550
1819
|
this.db.prepare(`
|
|
1551
1820
|
INSERT INTO hub_groups (id, name, description, created_at)
|
|
1552
1821
|
VALUES (?, ?, ?, ?)
|
|
1553
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
1554
|
-
|
|
1555
|
-
description = excluded.description,
|
|
1556
|
-
created_at = excluded.created_at
|
|
1557
|
-
`).run(group.id, group.name, group.description, group.createdAt);
|
|
1558
|
-
}
|
|
1559
|
-
listHubGroups() {
|
|
1560
|
-
const rows = this.db.prepare('SELECT * FROM hub_groups ORDER BY name').all();
|
|
1561
|
-
return rows.map(rowToHubGroup);
|
|
1822
|
+
ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description
|
|
1823
|
+
`).run(group.id, group.name, group.description ?? "", group.createdAt);
|
|
1562
1824
|
}
|
|
1563
|
-
addHubGroupMember(groupId, userId, joinedAt
|
|
1825
|
+
addHubGroupMember(groupId, userId, joinedAt) {
|
|
1564
1826
|
this.db.prepare(`
|
|
1565
|
-
INSERT INTO hub_group_members (group_id, user_id, joined_at)
|
|
1827
|
+
INSERT OR IGNORE INTO hub_group_members (group_id, user_id, joined_at)
|
|
1566
1828
|
VALUES (?, ?, ?)
|
|
1567
|
-
ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
|
|
1568
1829
|
`).run(groupId, userId, joinedAt);
|
|
1569
1830
|
}
|
|
1570
|
-
getHubGroupById(groupId) {
|
|
1571
|
-
const row = this.db.prepare('SELECT * FROM hub_groups WHERE id = ?').get(groupId);
|
|
1572
|
-
return row ? rowToHubGroup(row) : undefined;
|
|
1573
|
-
}
|
|
1574
|
-
deleteHubGroup(groupId) {
|
|
1575
|
-
const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
|
|
1576
|
-
return result.changes > 0;
|
|
1577
|
-
}
|
|
1578
|
-
listHubGroupMembers(groupId) {
|
|
1579
|
-
const rows = this.db.prepare(`
|
|
1580
|
-
SELECT gm.user_id, hu.username, gm.joined_at
|
|
1581
|
-
FROM hub_group_members gm
|
|
1582
|
-
JOIN hub_users hu ON hu.id = gm.user_id
|
|
1583
|
-
WHERE gm.group_id = ?
|
|
1584
|
-
ORDER BY gm.joined_at
|
|
1585
|
-
`).all(groupId);
|
|
1586
|
-
return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
|
|
1587
|
-
}
|
|
1588
1831
|
removeHubGroupMember(groupId, userId) {
|
|
1589
1832
|
this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
|
|
1590
1833
|
}
|
|
1591
1834
|
getGroupsForHubUser(userId) {
|
|
1592
|
-
|
|
1593
|
-
SELECT g
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
WHERE gm.user_id = ?
|
|
1597
|
-
ORDER BY g.name
|
|
1835
|
+
return this.db.prepare(`
|
|
1836
|
+
SELECT g.id, g.name, g.description FROM hub_groups g
|
|
1837
|
+
JOIN hub_group_members m ON m.group_id = g.id
|
|
1838
|
+
WHERE m.user_id = ?
|
|
1598
1839
|
`).all(userId);
|
|
1599
|
-
|
|
1840
|
+
}
|
|
1841
|
+
getHubUserContributions() {
|
|
1842
|
+
const result = {};
|
|
1843
|
+
const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all();
|
|
1844
|
+
const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all();
|
|
1845
|
+
const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all();
|
|
1846
|
+
for (const r of memRows) {
|
|
1847
|
+
if (!result[r.source_user_id])
|
|
1848
|
+
result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
|
|
1849
|
+
result[r.source_user_id].memoryCount = r.cnt;
|
|
1850
|
+
}
|
|
1851
|
+
for (const r of taskRows) {
|
|
1852
|
+
if (!result[r.source_user_id])
|
|
1853
|
+
result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
|
|
1854
|
+
result[r.source_user_id].taskCount = r.cnt;
|
|
1855
|
+
}
|
|
1856
|
+
for (const r of skillRows) {
|
|
1857
|
+
if (!result[r.source_user_id])
|
|
1858
|
+
result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
|
|
1859
|
+
result[r.source_user_id].skillCount = r.cnt;
|
|
1860
|
+
}
|
|
1861
|
+
return result;
|
|
1600
1862
|
}
|
|
1601
1863
|
// ─── Hub Shared Data ───
|
|
1602
1864
|
upsertHubTask(task) {
|
|
@@ -1616,6 +1878,10 @@ class SqliteStore {
|
|
|
1616
1878
|
const row = this.db.prepare('SELECT * FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId);
|
|
1617
1879
|
return row ? rowToHubTask(row) : null;
|
|
1618
1880
|
}
|
|
1881
|
+
getHubTaskById(taskId) {
|
|
1882
|
+
const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId);
|
|
1883
|
+
return row ? rowToHubTask(row) : null;
|
|
1884
|
+
}
|
|
1619
1885
|
upsertHubChunk(chunk) {
|
|
1620
1886
|
if (!chunk.sourceTaskId)
|
|
1621
1887
|
throw new Error("sourceTaskId is required for hub chunk upserts");
|
|
@@ -1688,20 +1954,36 @@ class SqliteStore {
|
|
|
1688
1954
|
out.push(row.vector.readFloatLE(i * 4));
|
|
1689
1955
|
return out;
|
|
1690
1956
|
}
|
|
1957
|
+
getVisibleHubSkillEmbeddings() {
|
|
1958
|
+
const rows = this.db.prepare(`
|
|
1959
|
+
SELECT hse.skill_id, hse.vector, hse.dimensions
|
|
1960
|
+
FROM hub_skill_embeddings hse
|
|
1961
|
+
JOIN hub_skills hs ON hs.id = hse.skill_id
|
|
1962
|
+
`).all();
|
|
1963
|
+
return rows.map(r => ({
|
|
1964
|
+
skillId: r.skill_id,
|
|
1965
|
+
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
1966
|
+
}));
|
|
1967
|
+
}
|
|
1691
1968
|
searchHubChunks(query, options) {
|
|
1692
1969
|
const limit = options?.maxResults ?? 10;
|
|
1693
1970
|
const userId = options?.userId ?? "";
|
|
1694
1971
|
const rows = this.db.prepare(`
|
|
1695
|
-
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
|
|
1972
|
+
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
|
|
1973
|
+
COALESCE(hg.name, '') as group_name, hu.username as owner_name,
|
|
1696
1974
|
bm25(hub_chunks_fts) as rank
|
|
1697
1975
|
FROM hub_chunks_fts f
|
|
1698
1976
|
JOIN hub_chunks hc ON hc.rowid = f.rowid
|
|
1699
1977
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1700
1978
|
LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
|
|
1979
|
+
LEFT JOIN hub_groups hg ON hg.id = ht.group_id
|
|
1701
1980
|
WHERE hub_chunks_fts MATCH ?
|
|
1981
|
+
AND (ht.visibility = 'public'
|
|
1982
|
+
OR ht.source_user_id = ?
|
|
1983
|
+
OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
|
|
1702
1984
|
ORDER BY rank
|
|
1703
1985
|
LIMIT ?
|
|
1704
|
-
`).all(sanitizeFtsQuery(query), limit);
|
|
1986
|
+
`).all(sanitizeFtsQuery(query), userId, userId, limit);
|
|
1705
1987
|
return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
|
|
1706
1988
|
}
|
|
1707
1989
|
upsertHubEmbedding(chunkId, vector) {
|
|
@@ -1724,7 +2006,10 @@ class SqliteStore {
|
|
|
1724
2006
|
FROM hub_embeddings he
|
|
1725
2007
|
JOIN hub_chunks hc ON hc.id = he.chunk_id
|
|
1726
2008
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1727
|
-
|
|
2009
|
+
WHERE ht.visibility = 'public'
|
|
2010
|
+
OR ht.source_user_id = ?
|
|
2011
|
+
OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?)
|
|
2012
|
+
`).all(userId, userId);
|
|
1728
2013
|
return rows.map(r => ({
|
|
1729
2014
|
chunkId: r.chunk_id,
|
|
1730
2015
|
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
@@ -1732,14 +2017,19 @@ class SqliteStore {
|
|
|
1732
2017
|
}
|
|
1733
2018
|
getVisibleHubSearchHitByChunkId(chunkId, userId) {
|
|
1734
2019
|
const row = this.db.prepare(`
|
|
1735
|
-
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
|
|
2020
|
+
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
|
|
2021
|
+
COALESCE(hg.name, '') as group_name, hu.username as owner_name,
|
|
1736
2022
|
0 as rank
|
|
1737
2023
|
FROM hub_chunks hc
|
|
1738
2024
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1739
2025
|
LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
|
|
2026
|
+
LEFT JOIN hub_groups hg ON hg.id = ht.group_id
|
|
1740
2027
|
WHERE hc.id = ?
|
|
2028
|
+
AND (ht.visibility = 'public'
|
|
2029
|
+
OR ht.source_user_id = ?
|
|
2030
|
+
OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
|
|
1741
2031
|
LIMIT 1
|
|
1742
|
-
`).get(chunkId);
|
|
2032
|
+
`).get(chunkId, userId, userId);
|
|
1743
2033
|
return row ?? null;
|
|
1744
2034
|
}
|
|
1745
2035
|
getHubChunkById(chunkId) {
|
|
@@ -1753,7 +2043,7 @@ class SqliteStore {
|
|
|
1753
2043
|
let rows;
|
|
1754
2044
|
if (sanitized) {
|
|
1755
2045
|
rows = this.db.prepare(`
|
|
1756
|
-
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
|
|
2046
|
+
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
|
|
1757
2047
|
bm25(hub_skills_fts) as rank
|
|
1758
2048
|
FROM hub_skills_fts f
|
|
1759
2049
|
JOIN hub_skills hs ON hs.rowid = f.rowid
|
|
@@ -1765,7 +2055,7 @@ class SqliteStore {
|
|
|
1765
2055
|
}
|
|
1766
2056
|
else {
|
|
1767
2057
|
rows = this.db.prepare(`
|
|
1768
|
-
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
|
|
2058
|
+
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
|
|
1769
2059
|
0 as rank
|
|
1770
2060
|
FROM hub_skills hs
|
|
1771
2061
|
LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
|
|
@@ -1780,7 +2070,7 @@ class SqliteStore {
|
|
|
1780
2070
|
}
|
|
1781
2071
|
listVisibleHubTasks(userId, limit = 40) {
|
|
1782
2072
|
const rows = this.db.prepare(`
|
|
1783
|
-
SELECT t.*, u.username AS owner_name, NULL AS group_name,
|
|
2073
|
+
SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
|
|
1784
2074
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
1785
2075
|
FROM hub_tasks t
|
|
1786
2076
|
LEFT JOIN hub_users u ON u.id = t.source_user_id
|
|
@@ -1790,33 +2080,36 @@ class SqliteStore {
|
|
|
1790
2080
|
return rows.map(r => ({
|
|
1791
2081
|
id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
|
|
1792
2082
|
title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
|
|
1793
|
-
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
|
|
2083
|
+
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
|
|
1794
2084
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
1795
2085
|
}));
|
|
1796
2086
|
}
|
|
1797
2087
|
listAllHubTasks() {
|
|
1798
2088
|
const rows = this.db.prepare(`
|
|
1799
|
-
SELECT t.*, u.username AS owner_name,
|
|
2089
|
+
SELECT t.*, u.username AS owner_name, u.status AS owner_status,
|
|
1800
2090
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
1801
2091
|
FROM hub_tasks t
|
|
1802
2092
|
LEFT JOIN hub_users u ON u.id = t.source_user_id
|
|
1803
|
-
LEFT JOIN hub_groups g ON g.id = t.group_id
|
|
1804
2093
|
ORDER BY t.updated_at DESC
|
|
1805
2094
|
`).all();
|
|
1806
2095
|
return rows.map(r => ({
|
|
1807
2096
|
id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
|
|
1808
|
-
title: r.title, summary: r.summary, groupId: r.group_id, groupName:
|
|
1809
|
-
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
|
|
2097
|
+
title: r.title, summary: r.summary, groupId: r.group_id, groupName: null,
|
|
2098
|
+
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
|
|
1810
2099
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
1811
2100
|
}));
|
|
1812
2101
|
}
|
|
2102
|
+
listHubChunksByTaskId(hubTaskId) {
|
|
2103
|
+
const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId);
|
|
2104
|
+
return rows.map(rowToHubChunk);
|
|
2105
|
+
}
|
|
1813
2106
|
deleteHubTaskById(taskId) {
|
|
1814
2107
|
const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
|
|
1815
2108
|
return info.changes > 0;
|
|
1816
2109
|
}
|
|
1817
2110
|
listVisibleHubSkills(userId, limit = 40) {
|
|
1818
2111
|
const rows = this.db.prepare(`
|
|
1819
|
-
SELECT s.*, u.username AS owner_name, NULL AS group_name
|
|
2112
|
+
SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
|
|
1820
2113
|
FROM hub_skills s
|
|
1821
2114
|
LEFT JOIN hub_users u ON u.id = s.source_user_id
|
|
1822
2115
|
ORDER BY s.updated_at DESC
|
|
@@ -1826,23 +2119,22 @@ class SqliteStore {
|
|
|
1826
2119
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
1827
2120
|
name: r.name, description: r.description, version: r.version,
|
|
1828
2121
|
groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
|
|
1829
|
-
ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
|
|
2122
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
|
|
1830
2123
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
1831
2124
|
}));
|
|
1832
2125
|
}
|
|
1833
2126
|
listAllHubSkills() {
|
|
1834
2127
|
const rows = this.db.prepare(`
|
|
1835
|
-
SELECT s.*, u.username AS owner_name,
|
|
2128
|
+
SELECT s.*, u.username AS owner_name, u.status AS owner_status
|
|
1836
2129
|
FROM hub_skills s
|
|
1837
2130
|
LEFT JOIN hub_users u ON u.id = s.source_user_id
|
|
1838
|
-
LEFT JOIN hub_groups g ON g.id = s.group_id
|
|
1839
2131
|
ORDER BY s.updated_at DESC
|
|
1840
2132
|
`).all();
|
|
1841
2133
|
return rows.map(r => ({
|
|
1842
2134
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
1843
2135
|
name: r.name, description: r.description, version: r.version,
|
|
1844
|
-
groupId: r.group_id, groupName:
|
|
1845
|
-
ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
|
|
2136
|
+
groupId: r.group_id, groupName: null, visibility: r.visibility,
|
|
2137
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
|
|
1846
2138
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
1847
2139
|
}));
|
|
1848
2140
|
}
|
|
@@ -1881,6 +2173,68 @@ class SqliteStore {
|
|
|
1881
2173
|
const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
|
|
1882
2174
|
return info.changes > 0;
|
|
1883
2175
|
}
|
|
2176
|
+
// ─── Team share metadata (Client role — UI only, not used for local recall / FTS) ───
|
|
2177
|
+
upsertTeamSharedChunk(chunkId, row) {
|
|
2178
|
+
const now = Date.now();
|
|
2179
|
+
const vis = row.visibility === "group" ? "group" : "public";
|
|
2180
|
+
const gid = vis === "group" ? (row.groupId ?? null) : null;
|
|
2181
|
+
this.db.prepare(`
|
|
2182
|
+
INSERT INTO team_shared_chunks (chunk_id, hub_memory_id, visibility, group_id, shared_at)
|
|
2183
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2184
|
+
ON CONFLICT(chunk_id) DO UPDATE SET
|
|
2185
|
+
hub_memory_id = excluded.hub_memory_id,
|
|
2186
|
+
visibility = excluded.visibility,
|
|
2187
|
+
group_id = excluded.group_id,
|
|
2188
|
+
shared_at = excluded.shared_at
|
|
2189
|
+
`).run(chunkId, row.hubMemoryId ?? "", vis, gid, now);
|
|
2190
|
+
}
|
|
2191
|
+
getTeamSharedChunk(chunkId) {
|
|
2192
|
+
const r = this.db.prepare("SELECT chunk_id, hub_memory_id, visibility, group_id, shared_at FROM team_shared_chunks WHERE chunk_id = ?").get(chunkId);
|
|
2193
|
+
if (!r)
|
|
2194
|
+
return null;
|
|
2195
|
+
return {
|
|
2196
|
+
chunkId: r.chunk_id,
|
|
2197
|
+
hubMemoryId: r.hub_memory_id,
|
|
2198
|
+
visibility: r.visibility,
|
|
2199
|
+
groupId: r.group_id,
|
|
2200
|
+
sharedAt: r.shared_at,
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
deleteTeamSharedChunk(chunkId) {
|
|
2204
|
+
const info = this.db.prepare("DELETE FROM team_shared_chunks WHERE chunk_id = ?").run(chunkId);
|
|
2205
|
+
return info.changes > 0;
|
|
2206
|
+
}
|
|
2207
|
+
// ─── Hub Notifications ───
|
|
2208
|
+
insertHubNotification(n) {
|
|
2209
|
+
this.db.prepare('INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)').run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
|
|
2210
|
+
}
|
|
2211
|
+
hasRecentHubNotification(userId, type, resource, windowMs = 300_000) {
|
|
2212
|
+
const since = Date.now() - windowMs;
|
|
2213
|
+
const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?').get(userId, type, resource, since);
|
|
2214
|
+
return row.cnt > 0;
|
|
2215
|
+
}
|
|
2216
|
+
listHubNotifications(userId, opts) {
|
|
2217
|
+
const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
|
|
2218
|
+
const limit = opts?.limit ?? 50;
|
|
2219
|
+
const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit);
|
|
2220
|
+
return rows.map(r => ({ id: r.id, userId: r.user_id, type: r.type, resource: r.resource, title: r.title, message: r.message, read: !!r.read, createdAt: r.created_at }));
|
|
2221
|
+
}
|
|
2222
|
+
countUnreadHubNotifications(userId) {
|
|
2223
|
+
const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId);
|
|
2224
|
+
return row.cnt;
|
|
2225
|
+
}
|
|
2226
|
+
markHubNotificationsRead(userId, ids) {
|
|
2227
|
+
if (ids && ids.length > 0) {
|
|
2228
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
2229
|
+
this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
|
|
2230
|
+
}
|
|
2231
|
+
else {
|
|
2232
|
+
this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
clearHubNotifications(userId) {
|
|
2236
|
+
this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
|
|
2237
|
+
}
|
|
1884
2238
|
upsertHubMemoryEmbedding(memoryId, vector) {
|
|
1885
2239
|
const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
1886
2240
|
this.db.prepare(`
|
|
@@ -1937,7 +2291,7 @@ class SqliteStore {
|
|
|
1937
2291
|
}
|
|
1938
2292
|
listVisibleHubMemories(userId, limit = 40) {
|
|
1939
2293
|
const rows = this.db.prepare(`
|
|
1940
|
-
SELECT m.*, u.username AS owner_name, NULL AS group_name
|
|
2294
|
+
SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
|
|
1941
2295
|
FROM hub_memories m
|
|
1942
2296
|
LEFT JOIN hub_users u ON u.id = m.source_user_id
|
|
1943
2297
|
ORDER BY m.updated_at DESC
|
|
@@ -1945,24 +2299,23 @@ class SqliteStore {
|
|
|
1945
2299
|
`).all(limit);
|
|
1946
2300
|
return rows.map(r => ({
|
|
1947
2301
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
1948
|
-
role: r.role, summary: r.summary, kind: r.kind,
|
|
2302
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
1949
2303
|
groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
|
|
1950
|
-
ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2304
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
1951
2305
|
}));
|
|
1952
2306
|
}
|
|
1953
2307
|
listAllHubMemories() {
|
|
1954
2308
|
const rows = this.db.prepare(`
|
|
1955
|
-
SELECT m.*, u.username AS owner_name,
|
|
2309
|
+
SELECT m.*, u.username AS owner_name, u.status AS owner_status
|
|
1956
2310
|
FROM hub_memories m
|
|
1957
2311
|
LEFT JOIN hub_users u ON u.id = m.source_user_id
|
|
1958
|
-
LEFT JOIN hub_groups g ON g.id = m.group_id
|
|
1959
2312
|
ORDER BY m.updated_at DESC
|
|
1960
2313
|
`).all();
|
|
1961
2314
|
return rows.map(r => ({
|
|
1962
2315
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
1963
|
-
role: r.role, summary: r.summary, kind: r.kind,
|
|
1964
|
-
groupId: r.group_id, groupName:
|
|
1965
|
-
ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2316
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2317
|
+
groupId: r.group_id, groupName: null, visibility: r.visibility,
|
|
2318
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
1966
2319
|
}));
|
|
1967
2320
|
}
|
|
1968
2321
|
resolveCanonicalHubTaskId(taskId, sourceUserId, sourceTaskId) {
|
|
@@ -1987,12 +2340,6 @@ class SqliteStore {
|
|
|
1987
2340
|
}
|
|
1988
2341
|
throw new Error(`source skill not found for skillId=${skillId}`);
|
|
1989
2342
|
}
|
|
1990
|
-
attachGroupsToHubUser(user) {
|
|
1991
|
-
return {
|
|
1992
|
-
...user,
|
|
1993
|
-
groups: this.getGroupsForHubUser(user.id),
|
|
1994
|
-
};
|
|
1995
|
-
}
|
|
1996
2343
|
getSessionOwnerMap(sessionKeys) {
|
|
1997
2344
|
const result = new Map();
|
|
1998
2345
|
if (sessionKeys.length === 0)
|
|
@@ -2102,6 +2449,8 @@ function rowToClientHubConnection(row) {
|
|
|
2102
2449
|
userToken: row.user_token,
|
|
2103
2450
|
role: row.role,
|
|
2104
2451
|
connectedAt: row.connected_at,
|
|
2452
|
+
identityKey: row.identity_key || "",
|
|
2453
|
+
lastKnownStatus: row.last_known_status || "",
|
|
2105
2454
|
};
|
|
2106
2455
|
}
|
|
2107
2456
|
function rowToHubUser(row) {
|
|
@@ -2115,14 +2464,13 @@ function rowToHubUser(row) {
|
|
|
2115
2464
|
tokenHash: row.token_hash,
|
|
2116
2465
|
createdAt: row.created_at,
|
|
2117
2466
|
approvedAt: row.approved_at,
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
createdAt: row.created_at,
|
|
2467
|
+
lastIp: row.last_ip || "",
|
|
2468
|
+
lastActiveAt: row.last_active_at ?? null,
|
|
2469
|
+
identityKey: row.identity_key || "",
|
|
2470
|
+
leftAt: row.left_at ?? null,
|
|
2471
|
+
removedAt: row.removed_at ?? null,
|
|
2472
|
+
rejectedAt: row.rejected_at ?? null,
|
|
2473
|
+
rejoinRequestedAt: row.rejoin_requested_at ?? null,
|
|
2126
2474
|
};
|
|
2127
2475
|
}
|
|
2128
2476
|
function rowToHubTask(row) {
|