@memtensor/memos-local-openclaw-plugin 1.0.4-beta.0 → 1.0.4-beta.10
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 +24 -24
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +34 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +5 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +173 -14
- 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 +9 -11
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +7 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +301 -106
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +3 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +18 -1
- 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/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 +91 -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 +82 -8
- 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 +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +3 -0
- package/dist/skill/evolver.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 +74 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +301 -207
- 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 +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2991 -1041
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +32 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1122 -261
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +384 -43
- package/openclaw.plugin.json +1 -1
- 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 +37 -1
- package/src/client/connector.ts +173 -16
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +9 -11
- package/src/hub/server.ts +285 -98
- package/src/hub/user-manager.ts +20 -3
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +84 -1
- package/src/shared/llm-call.ts +97 -9
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +5 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +310 -233
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +1 -2
- package/src/viewer/html.ts +2991 -1041
- package/src/viewer/server.ts +984 -190
package/src/storage/sqlite.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from "crypto";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVisibility, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
|
|
6
|
-
import type {
|
|
6
|
+
import type { SharedVisibility, UserInfo, UserRole, UserStatus } from "../sharing/types";
|
|
7
7
|
|
|
8
8
|
export class SqliteStore {
|
|
9
9
|
private db: Database.Database;
|
|
@@ -112,6 +112,8 @@ export class SqliteStore {
|
|
|
112
112
|
this.migrateSkillEmbeddingsAndFts();
|
|
113
113
|
this.migrateFtsToTrigram();
|
|
114
114
|
this.migrateHubTables();
|
|
115
|
+
this.migrateHubFtsToTrigram();
|
|
116
|
+
this.migrateLocalSharedTasksOwner();
|
|
115
117
|
this.log.debug("Database schema initialized");
|
|
116
118
|
}
|
|
117
119
|
|
|
@@ -119,6 +121,16 @@ export class SqliteStore {
|
|
|
119
121
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
private migrateLocalSharedTasksOwner(): void {
|
|
125
|
+
try {
|
|
126
|
+
const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
|
|
127
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
|
|
128
|
+
this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
|
|
129
|
+
this.log.info("Migrated: added original_owner column to local_shared_tasks");
|
|
130
|
+
}
|
|
131
|
+
} catch { /* table may not exist yet */ }
|
|
132
|
+
}
|
|
133
|
+
|
|
122
134
|
private migrateOwnerFields(): void {
|
|
123
135
|
const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
124
136
|
if (!chunkCols.some((c) => c.name === "owner")) {
|
|
@@ -273,6 +285,51 @@ export class SqliteStore {
|
|
|
273
285
|
}
|
|
274
286
|
}
|
|
275
287
|
|
|
288
|
+
private migrateHubFtsToTrigram(): void {
|
|
289
|
+
const tables: Array<{ fts: string; source: string; columns: string; triggers: string[] }> = [
|
|
290
|
+
{
|
|
291
|
+
fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
|
|
292
|
+
triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
|
|
296
|
+
triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
|
|
300
|
+
triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
for (const t of tables) {
|
|
304
|
+
try {
|
|
305
|
+
const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get() as { sql: string } | undefined;
|
|
306
|
+
if (!row || !row.sql) continue;
|
|
307
|
+
if (row.sql.includes("trigram")) continue;
|
|
308
|
+
this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
|
|
309
|
+
for (const tr of t.triggers) this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
|
|
310
|
+
this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
|
|
311
|
+
this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
|
|
312
|
+
this.db.exec(`
|
|
313
|
+
CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
|
|
314
|
+
INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
|
|
315
|
+
END;
|
|
316
|
+
CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
|
|
317
|
+
INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
|
|
318
|
+
END;
|
|
319
|
+
CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
|
|
320
|
+
INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
|
|
321
|
+
INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
|
|
322
|
+
END
|
|
323
|
+
`);
|
|
324
|
+
this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
|
|
325
|
+
const cnt = (this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get() as { c: number }).c;
|
|
326
|
+
this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
276
333
|
private migrateTaskId(): void {
|
|
277
334
|
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
278
335
|
if (!cols.some((c) => c.name === "task_id")) {
|
|
@@ -516,12 +573,13 @@ export class SqliteStore {
|
|
|
516
573
|
).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
|
|
517
574
|
}
|
|
518
575
|
|
|
519
|
-
getToolMetrics(minutes: number): {
|
|
576
|
+
getToolMetrics(minutes: number, fromMs?: number, toMs?: number): {
|
|
520
577
|
tools: string[];
|
|
521
578
|
series: Array<{ minute: string; [tool: string]: number | string }>;
|
|
522
579
|
aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
|
|
523
580
|
} {
|
|
524
|
-
const since = Date.now() - minutes * 60 * 1000;
|
|
581
|
+
const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
|
|
582
|
+
const until = toMs ?? Date.now();
|
|
525
583
|
|
|
526
584
|
const rows = this.db.prepare(
|
|
527
585
|
`SELECT tool_name,
|
|
@@ -529,9 +587,9 @@ export class SqliteStore {
|
|
|
529
587
|
success,
|
|
530
588
|
strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
|
|
531
589
|
FROM tool_calls
|
|
532
|
-
WHERE called_at >= ?
|
|
590
|
+
WHERE called_at >= ? AND called_at <= ?
|
|
533
591
|
ORDER BY called_at`,
|
|
534
|
-
).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
|
|
592
|
+
).all(since, until) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
|
|
535
593
|
|
|
536
594
|
const toolSet = new Set<string>();
|
|
537
595
|
const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
|
|
@@ -683,34 +741,27 @@ export class SqliteStore {
|
|
|
683
741
|
shared_at INTEGER NOT NULL
|
|
684
742
|
);
|
|
685
743
|
|
|
744
|
+
CREATE TABLE IF NOT EXISTS local_shared_memories (
|
|
745
|
+
chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
|
|
746
|
+
original_owner TEXT NOT NULL,
|
|
747
|
+
shared_at INTEGER NOT NULL
|
|
748
|
+
);
|
|
749
|
+
|
|
686
750
|
CREATE TABLE IF NOT EXISTS hub_users (
|
|
687
|
-
id
|
|
688
|
-
username
|
|
689
|
-
device_name
|
|
690
|
-
role
|
|
691
|
-
status
|
|
692
|
-
token_hash
|
|
693
|
-
created_at
|
|
694
|
-
approved_at
|
|
751
|
+
id TEXT PRIMARY KEY,
|
|
752
|
+
username TEXT NOT NULL UNIQUE,
|
|
753
|
+
device_name TEXT NOT NULL DEFAULT '',
|
|
754
|
+
role TEXT NOT NULL,
|
|
755
|
+
status TEXT NOT NULL,
|
|
756
|
+
token_hash TEXT NOT NULL DEFAULT '',
|
|
757
|
+
created_at INTEGER NOT NULL,
|
|
758
|
+
approved_at INTEGER,
|
|
759
|
+
last_ip TEXT NOT NULL DEFAULT '',
|
|
760
|
+
last_active_at INTEGER
|
|
695
761
|
);
|
|
696
762
|
CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
|
|
697
763
|
CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
|
|
698
764
|
|
|
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
765
|
CREATE TABLE IF NOT EXISTS hub_tasks (
|
|
715
766
|
id TEXT PRIMARY KEY,
|
|
716
767
|
source_task_id TEXT NOT NULL,
|
|
@@ -752,7 +803,7 @@ export class SqliteStore {
|
|
|
752
803
|
content,
|
|
753
804
|
content='hub_chunks',
|
|
754
805
|
content_rowid='rowid',
|
|
755
|
-
tokenize='
|
|
806
|
+
tokenize='trigram'
|
|
756
807
|
);
|
|
757
808
|
|
|
758
809
|
CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
|
|
@@ -802,7 +853,7 @@ export class SqliteStore {
|
|
|
802
853
|
description,
|
|
803
854
|
content='hub_skills',
|
|
804
855
|
content_rowid='rowid',
|
|
805
|
-
tokenize='
|
|
856
|
+
tokenize='trigram'
|
|
806
857
|
);
|
|
807
858
|
|
|
808
859
|
CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
|
|
@@ -852,7 +903,7 @@ export class SqliteStore {
|
|
|
852
903
|
content,
|
|
853
904
|
content='hub_memories',
|
|
854
905
|
content_rowid='rowid',
|
|
855
|
-
tokenize='
|
|
906
|
+
tokenize='trigram'
|
|
856
907
|
);
|
|
857
908
|
|
|
858
909
|
CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
|
|
@@ -872,6 +923,32 @@ export class SqliteStore {
|
|
|
872
923
|
VALUES (new.rowid, new.summary, new.content);
|
|
873
924
|
END;
|
|
874
925
|
`);
|
|
926
|
+
|
|
927
|
+
this.db.exec(`
|
|
928
|
+
CREATE TABLE IF NOT EXISTS hub_notifications (
|
|
929
|
+
id TEXT PRIMARY KEY,
|
|
930
|
+
user_id TEXT NOT NULL,
|
|
931
|
+
type TEXT NOT NULL,
|
|
932
|
+
resource TEXT NOT NULL,
|
|
933
|
+
title TEXT NOT NULL,
|
|
934
|
+
message TEXT NOT NULL DEFAULT '',
|
|
935
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
936
|
+
created_at INTEGER NOT NULL
|
|
937
|
+
);
|
|
938
|
+
CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
|
|
939
|
+
`);
|
|
940
|
+
|
|
941
|
+
try {
|
|
942
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
|
|
943
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
|
|
944
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
|
|
945
|
+
this.log.info("Migrated: added last_ip column to hub_users");
|
|
946
|
+
}
|
|
947
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
|
|
948
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
|
|
949
|
+
this.log.info("Migrated: added last_active_at column to hub_users");
|
|
950
|
+
}
|
|
951
|
+
} catch { /* table may not exist yet */ }
|
|
875
952
|
}
|
|
876
953
|
|
|
877
954
|
// ─── Write ───
|
|
@@ -1047,6 +1124,25 @@ export class SqliteStore {
|
|
|
1047
1124
|
}
|
|
1048
1125
|
}
|
|
1049
1126
|
|
|
1127
|
+
hubMemoryPatternSearch(patterns: string[], opts: { limit?: number } = {}): Array<{ memoryId: string; content: string; role: string; createdAt: number }> {
|
|
1128
|
+
if (patterns.length === 0) return [];
|
|
1129
|
+
const limit = opts.limit ?? 10;
|
|
1130
|
+
const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
|
|
1131
|
+
const params: (string | number)[] = [];
|
|
1132
|
+
for (const p of patterns) { params.push(`%${p}%`, `%${p}%`); }
|
|
1133
|
+
params.push(limit);
|
|
1134
|
+
try {
|
|
1135
|
+
const rows = this.db.prepare(`
|
|
1136
|
+
SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
|
|
1137
|
+
FROM hub_memories hm
|
|
1138
|
+
WHERE ${conditions.join(" OR ")}
|
|
1139
|
+
ORDER BY hm.created_at DESC
|
|
1140
|
+
LIMIT ?
|
|
1141
|
+
`).all(...params) as Array<{ memory_id: string; content: string; role: string; created_at: number }>;
|
|
1142
|
+
return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
|
|
1143
|
+
} catch { return []; }
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1050
1146
|
// ─── Vector Search ───
|
|
1051
1147
|
|
|
1052
1148
|
getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
|
|
@@ -1213,6 +1309,8 @@ export class SqliteStore {
|
|
|
1213
1309
|
"skill_embeddings",
|
|
1214
1310
|
"skill_versions",
|
|
1215
1311
|
"skills",
|
|
1312
|
+
"local_shared_memories",
|
|
1313
|
+
"local_shared_tasks",
|
|
1216
1314
|
"embeddings",
|
|
1217
1315
|
"chunks",
|
|
1218
1316
|
"tasks",
|
|
@@ -1684,6 +1782,67 @@ export class SqliteStore {
|
|
|
1684
1782
|
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
1783
|
}
|
|
1686
1784
|
|
|
1785
|
+
// ─── Local Shared Memories (client-side tracking) ───
|
|
1786
|
+
|
|
1787
|
+
markMemorySharedLocally(chunkId: string): { ok: boolean; owner?: string; originalOwner?: string; sharedAt?: number; reason?: string } {
|
|
1788
|
+
const chunk = this.getChunk(chunkId);
|
|
1789
|
+
if (!chunk) return { ok: false, reason: "not_found" };
|
|
1790
|
+
if (chunk.owner === "public") {
|
|
1791
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1792
|
+
return {
|
|
1793
|
+
ok: true,
|
|
1794
|
+
owner: "public",
|
|
1795
|
+
originalOwner: existing?.originalOwner ?? undefined,
|
|
1796
|
+
sharedAt: existing?.sharedAt ?? undefined,
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const sharedAt = Date.now();
|
|
1801
|
+
this.db.transaction(() => {
|
|
1802
|
+
this.db.prepare(`
|
|
1803
|
+
INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
|
|
1804
|
+
VALUES (?, ?, ?)
|
|
1805
|
+
ON CONFLICT(chunk_id) DO UPDATE SET
|
|
1806
|
+
original_owner = excluded.original_owner,
|
|
1807
|
+
shared_at = excluded.shared_at
|
|
1808
|
+
`).run(chunkId, chunk.owner, sharedAt);
|
|
1809
|
+
this.updateChunk(chunkId, { owner: "public" });
|
|
1810
|
+
})();
|
|
1811
|
+
|
|
1812
|
+
return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
unmarkMemorySharedLocally(chunkId: string, fallbackOwner?: string): { ok: boolean; owner?: string; originalOwner?: string; reason?: string } {
|
|
1816
|
+
const chunk = this.getChunk(chunkId);
|
|
1817
|
+
if (!chunk) return { ok: false, reason: "not_found" };
|
|
1818
|
+
if (chunk.owner !== "public") {
|
|
1819
|
+
return { ok: true, owner: chunk.owner };
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1823
|
+
const restoreOwner = existing?.originalOwner ?? fallbackOwner;
|
|
1824
|
+
if (!restoreOwner || restoreOwner === "public") {
|
|
1825
|
+
return { ok: false, reason: "original_owner_missing" };
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
this.db.transaction(() => {
|
|
1829
|
+
this.updateChunk(chunkId, { owner: restoreOwner });
|
|
1830
|
+
this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
|
|
1831
|
+
})();
|
|
1832
|
+
|
|
1833
|
+
return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
getLocalSharedMemory(chunkId: string): { chunkId: string; originalOwner: string; sharedAt: number } | null {
|
|
1837
|
+
const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId) as any;
|
|
1838
|
+
if (!row) return null;
|
|
1839
|
+
return {
|
|
1840
|
+
chunkId: row.chunk_id,
|
|
1841
|
+
originalOwner: row.original_owner,
|
|
1842
|
+
sharedAt: row.shared_at,
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1687
1846
|
// ─── Hub Users / Groups ───
|
|
1688
1847
|
|
|
1689
1848
|
upsertHubUser(user: HubUserRecord): void {
|
|
@@ -1704,74 +1863,41 @@ export class SqliteStore {
|
|
|
1704
1863
|
getHubUser(userId: string): HubUserRecord | null {
|
|
1705
1864
|
const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
|
|
1706
1865
|
if (!row) return null;
|
|
1707
|
-
return
|
|
1866
|
+
return rowToHubUser(row);
|
|
1708
1867
|
}
|
|
1709
1868
|
|
|
1710
1869
|
listHubUsers(status?: UserStatus): HubUserRecord[] {
|
|
1711
1870
|
const rows = status
|
|
1712
1871
|
? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
|
|
1713
1872
|
: this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
|
|
1714
|
-
return rows.map(
|
|
1873
|
+
return rows.map(rowToHubUser);
|
|
1715
1874
|
}
|
|
1716
1875
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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);
|
|
1876
|
+
deleteHubUser(userId: string, cleanResources = false): boolean {
|
|
1877
|
+
if (cleanResources) {
|
|
1878
|
+
this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
|
|
1879
|
+
this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
|
|
1880
|
+
this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
|
|
1881
|
+
const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
|
|
1882
|
+
return result.changes > 0;
|
|
1883
|
+
}
|
|
1884
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '' WHERE id = ?").run(userId);
|
|
1748
1885
|
return result.changes > 0;
|
|
1749
1886
|
}
|
|
1750
1887
|
|
|
1751
|
-
|
|
1752
|
-
|
|
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);
|
|
1888
|
+
updateHubUserActivity(userId: string, ip: string, timestamp?: number): void {
|
|
1889
|
+
this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
|
|
1764
1890
|
}
|
|
1765
1891
|
|
|
1766
|
-
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
return
|
|
1892
|
+
getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
|
|
1893
|
+
const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
|
|
1894
|
+
const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
|
|
1895
|
+
const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
|
|
1896
|
+
const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
|
|
1897
|
+
for (const r of memRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].memoryCount = r.cnt; }
|
|
1898
|
+
for (const r of taskRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].taskCount = r.cnt; }
|
|
1899
|
+
for (const r of skillRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].skillCount = r.cnt; }
|
|
1900
|
+
return result;
|
|
1775
1901
|
}
|
|
1776
1902
|
|
|
1777
1903
|
// ─── Hub Shared Data ───
|
|
@@ -1795,6 +1921,11 @@ export class SqliteStore {
|
|
|
1795
1921
|
return row ? rowToHubTask(row) : null;
|
|
1796
1922
|
}
|
|
1797
1923
|
|
|
1924
|
+
getHubTaskById(taskId: string): HubTaskRecord | null {
|
|
1925
|
+
const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId) as HubTaskRow | undefined;
|
|
1926
|
+
return row ? rowToHubTask(row) : null;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1798
1929
|
upsertHubChunk(chunk: HubChunkUpsertInput): void {
|
|
1799
1930
|
if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
|
|
1800
1931
|
const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
|
|
@@ -1874,24 +2005,16 @@ export class SqliteStore {
|
|
|
1874
2005
|
const limit = options?.maxResults ?? 10;
|
|
1875
2006
|
const userId = options?.userId ?? "";
|
|
1876
2007
|
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,
|
|
2008
|
+
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
|
|
1878
2009
|
bm25(hub_chunks_fts) as rank
|
|
1879
2010
|
FROM hub_chunks_fts f
|
|
1880
2011
|
JOIN hub_chunks hc ON hc.rowid = f.rowid
|
|
1881
2012
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1882
|
-
LEFT JOIN hub_groups hg ON hg.id = ht.group_id
|
|
1883
2013
|
LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
|
|
1884
2014
|
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
2015
|
ORDER BY rank
|
|
1893
2016
|
LIMIT ?
|
|
1894
|
-
`).all(sanitizeFtsQuery(query),
|
|
2017
|
+
`).all(sanitizeFtsQuery(query), limit) as HubSearchRow[];
|
|
1895
2018
|
return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
|
|
1896
2019
|
}
|
|
1897
2020
|
|
|
@@ -1916,12 +2039,7 @@ export class SqliteStore {
|
|
|
1916
2039
|
FROM hub_embeddings he
|
|
1917
2040
|
JOIN hub_chunks hc ON hc.id = he.chunk_id
|
|
1918
2041
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1919
|
-
|
|
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 }>;
|
|
2042
|
+
`).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
|
|
1925
2043
|
return rows.map(r => ({
|
|
1926
2044
|
chunkId: r.chunk_id,
|
|
1927
2045
|
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
@@ -1930,22 +2048,14 @@ export class SqliteStore {
|
|
|
1930
2048
|
|
|
1931
2049
|
getVisibleHubSearchHitByChunkId(chunkId: string, userId: string): HubSearchRow | null {
|
|
1932
2050
|
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,
|
|
2051
|
+
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
|
|
1934
2052
|
0 as rank
|
|
1935
2053
|
FROM hub_chunks hc
|
|
1936
2054
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1937
|
-
LEFT JOIN hub_groups hg ON hg.id = ht.group_id
|
|
1938
2055
|
LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
|
|
1939
2056
|
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
2057
|
LIMIT 1
|
|
1948
|
-
`).get(chunkId
|
|
2058
|
+
`).get(chunkId) as HubSearchRow | undefined;
|
|
1949
2059
|
return row ?? null;
|
|
1950
2060
|
}
|
|
1951
2061
|
|
|
@@ -1961,38 +2071,24 @@ export class SqliteStore {
|
|
|
1961
2071
|
let rows: HubSkillSearchRow[];
|
|
1962
2072
|
if (sanitized) {
|
|
1963
2073
|
rows = this.db.prepare(`
|
|
1964
|
-
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility,
|
|
2074
|
+
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,
|
|
1965
2075
|
bm25(hub_skills_fts) as rank
|
|
1966
2076
|
FROM hub_skills_fts f
|
|
1967
2077
|
JOIN hub_skills hs ON hs.rowid = f.rowid
|
|
1968
|
-
LEFT JOIN hub_groups hg ON hg.id = hs.group_id
|
|
1969
2078
|
LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
|
|
1970
2079
|
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
2080
|
ORDER BY rank
|
|
1979
2081
|
LIMIT ?
|
|
1980
|
-
`).all(sanitized,
|
|
2082
|
+
`).all(sanitized, limit) as HubSkillSearchRow[];
|
|
1981
2083
|
} else {
|
|
1982
2084
|
rows = this.db.prepare(`
|
|
1983
|
-
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility,
|
|
2085
|
+
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,
|
|
1984
2086
|
0 as rank
|
|
1985
2087
|
FROM hub_skills hs
|
|
1986
|
-
LEFT JOIN hub_groups hg ON hg.id = hs.group_id
|
|
1987
2088
|
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
2089
|
ORDER BY hs.updated_at DESC
|
|
1994
2090
|
LIMIT ?
|
|
1995
|
-
`).all(
|
|
2091
|
+
`).all(limit) as HubSkillSearchRow[];
|
|
1996
2092
|
}
|
|
1997
2093
|
return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
|
|
1998
2094
|
}
|
|
@@ -2001,87 +2097,78 @@ export class SqliteStore {
|
|
|
2001
2097
|
this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
|
|
2002
2098
|
}
|
|
2003
2099
|
|
|
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 }> {
|
|
2100
|
+
listVisibleHubTasks(userId: string, limit = 40): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; chunkCount: number; createdAt: number; updatedAt: number }> {
|
|
2005
2101
|
const rows = this.db.prepare(`
|
|
2006
|
-
SELECT t.*, u.username AS owner_name,
|
|
2102
|
+
SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
|
|
2007
2103
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
2008
2104
|
FROM hub_tasks t
|
|
2009
2105
|
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
2106
|
ORDER BY t.updated_at DESC
|
|
2017
2107
|
LIMIT ?
|
|
2018
|
-
`).all(
|
|
2108
|
+
`).all(limit) as any[];
|
|
2019
2109
|
return rows.map(r => ({
|
|
2020
2110
|
id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
|
|
2021
2111
|
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,
|
|
2112
|
+
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
|
|
2023
2113
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2024
2114
|
}));
|
|
2025
2115
|
}
|
|
2026
2116
|
|
|
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 }> {
|
|
2117
|
+
listAllHubTasks(): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; chunkCount: number; createdAt: number; updatedAt: number }> {
|
|
2028
2118
|
const rows = this.db.prepare(`
|
|
2029
|
-
SELECT t.*, u.username AS owner_name,
|
|
2119
|
+
SELECT t.*, u.username AS owner_name, u.status AS owner_status,
|
|
2030
2120
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
2031
2121
|
FROM hub_tasks t
|
|
2032
2122
|
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
2123
|
ORDER BY t.updated_at DESC
|
|
2035
2124
|
`).all() as any[];
|
|
2036
2125
|
return rows.map(r => ({
|
|
2037
2126
|
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:
|
|
2039
|
-
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
|
|
2127
|
+
title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
|
|
2128
|
+
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
|
|
2040
2129
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2041
2130
|
}));
|
|
2042
2131
|
}
|
|
2043
2132
|
|
|
2133
|
+
listHubChunksByTaskId(hubTaskId: string): HubChunkRecord[] {
|
|
2134
|
+
const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId) as HubChunkRow[];
|
|
2135
|
+
return rows.map(rowToHubChunk);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2044
2138
|
deleteHubTaskById(taskId: string): boolean {
|
|
2045
2139
|
const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
|
|
2046
2140
|
return info.changes > 0;
|
|
2047
2141
|
}
|
|
2048
2142
|
|
|
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 }> {
|
|
2143
|
+
listVisibleHubSkills(userId: string, limit = 40): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
|
|
2050
2144
|
const rows = this.db.prepare(`
|
|
2051
|
-
SELECT s.*, u.username AS owner_name,
|
|
2145
|
+
SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
|
|
2052
2146
|
FROM hub_skills s
|
|
2053
2147
|
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
2148
|
ORDER BY s.updated_at DESC
|
|
2061
2149
|
LIMIT ?
|
|
2062
|
-
`).all(
|
|
2150
|
+
`).all(limit) as any[];
|
|
2063
2151
|
return rows.map(r => ({
|
|
2064
2152
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
2065
2153
|
name: r.name, description: r.description, version: r.version,
|
|
2066
2154
|
groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
|
|
2067
|
-
ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
|
|
2155
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
|
|
2068
2156
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2069
2157
|
}));
|
|
2070
2158
|
}
|
|
2071
2159
|
|
|
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 }> {
|
|
2160
|
+
listAllHubSkills(): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
|
|
2073
2161
|
const rows = this.db.prepare(`
|
|
2074
|
-
SELECT s.*, u.username AS owner_name,
|
|
2162
|
+
SELECT s.*, u.username AS owner_name, u.status AS owner_status
|
|
2075
2163
|
FROM hub_skills s
|
|
2076
2164
|
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
2165
|
ORDER BY s.updated_at DESC
|
|
2079
2166
|
`).all() as any[];
|
|
2080
2167
|
return rows.map(r => ({
|
|
2081
2168
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
2082
2169
|
name: r.name, description: r.description, version: r.version,
|
|
2083
|
-
groupId: r.group_id, groupName:
|
|
2084
|
-
ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
|
|
2170
|
+
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2171
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
|
|
2085
2172
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2086
2173
|
}));
|
|
2087
2174
|
}
|
|
@@ -2128,6 +2215,47 @@ export class SqliteStore {
|
|
|
2128
2215
|
return info.changes > 0;
|
|
2129
2216
|
}
|
|
2130
2217
|
|
|
2218
|
+
// ─── Hub Notifications ───
|
|
2219
|
+
|
|
2220
|
+
insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
|
|
2221
|
+
this.db.prepare(
|
|
2222
|
+
'INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)'
|
|
2223
|
+
).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
hasRecentHubNotification(userId: string, type: string, resource: string, windowMs: number = 300_000): boolean {
|
|
2227
|
+
const since = Date.now() - windowMs;
|
|
2228
|
+
const row = this.db.prepare(
|
|
2229
|
+
'SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?'
|
|
2230
|
+
).get(userId, type, resource, since) as { cnt: number };
|
|
2231
|
+
return row.cnt > 0;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
listHubNotifications(userId: string, opts?: { unreadOnly?: boolean; limit?: number }): Array<{ id: string; userId: string; type: string; resource: string; title: string; message: string; read: boolean; createdAt: number }> {
|
|
2235
|
+
const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
|
|
2236
|
+
const limit = opts?.limit ?? 50;
|
|
2237
|
+
const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit) as any[];
|
|
2238
|
+
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 }));
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
countUnreadHubNotifications(userId: string): number {
|
|
2242
|
+
const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId) as { cnt: number };
|
|
2243
|
+
return row.cnt;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
markHubNotificationsRead(userId: string, ids?: string[]): void {
|
|
2247
|
+
if (ids && ids.length > 0) {
|
|
2248
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
2249
|
+
this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
|
|
2250
|
+
} else {
|
|
2251
|
+
this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
clearHubNotifications(userId: string): void {
|
|
2256
|
+
this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2131
2259
|
upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
|
|
2132
2260
|
const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
2133
2261
|
this.db.prepare(`
|
|
@@ -2149,23 +2277,15 @@ export class SqliteStore {
|
|
|
2149
2277
|
const sanitized = sanitizeFtsQuery(query);
|
|
2150
2278
|
if (!sanitized) return [];
|
|
2151
2279
|
const rows = this.db.prepare(`
|
|
2152
|
-
SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility,
|
|
2280
|
+
SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
|
|
2153
2281
|
bm25(hub_memories_fts) as rank
|
|
2154
2282
|
FROM hub_memories_fts f
|
|
2155
2283
|
JOIN hub_memories hm ON hm.rowid = f.rowid
|
|
2156
|
-
LEFT JOIN hub_groups hg ON hg.id = hm.group_id
|
|
2157
2284
|
LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
|
|
2158
2285
|
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
2286
|
ORDER BY rank
|
|
2167
2287
|
LIMIT ?
|
|
2168
|
-
`).all(sanitized,
|
|
2288
|
+
`).all(sanitized, limit) as HubMemorySearchRow[];
|
|
2169
2289
|
return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
|
|
2170
2290
|
}
|
|
2171
2291
|
|
|
@@ -2174,12 +2294,7 @@ export class SqliteStore {
|
|
|
2174
2294
|
SELECT hme.memory_id, hme.vector, hme.dimensions
|
|
2175
2295
|
FROM hub_memory_embeddings hme
|
|
2176
2296
|
JOIN hub_memories hm ON hm.id = hme.memory_id
|
|
2177
|
-
|
|
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 }>;
|
|
2297
|
+
`).all() as Array<{ memory_id: string; vector: Buffer; dimensions: number }>;
|
|
2183
2298
|
return rows.map(r => ({
|
|
2184
2299
|
memoryId: r.memory_id,
|
|
2185
2300
|
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
@@ -2188,59 +2303,44 @@ export class SqliteStore {
|
|
|
2188
2303
|
|
|
2189
2304
|
getVisibleHubSearchHitByMemoryId(memoryId: string, userId: string): HubMemorySearchRow | null {
|
|
2190
2305
|
const row = this.db.prepare(`
|
|
2191
|
-
SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility,
|
|
2306
|
+
SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
|
|
2192
2307
|
0 as rank
|
|
2193
2308
|
FROM hub_memories hm
|
|
2194
|
-
LEFT JOIN hub_groups hg ON hg.id = hm.group_id
|
|
2195
2309
|
LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
|
|
2196
2310
|
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
2311
|
LIMIT 1
|
|
2205
|
-
`).get(memoryId
|
|
2312
|
+
`).get(memoryId) as HubMemorySearchRow | undefined;
|
|
2206
2313
|
return row ?? null;
|
|
2207
2314
|
}
|
|
2208
2315
|
|
|
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 }> {
|
|
2316
|
+
listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; createdAt: number; updatedAt: number }> {
|
|
2210
2317
|
const rows = this.db.prepare(`
|
|
2211
|
-
SELECT m.*, u.username AS owner_name,
|
|
2318
|
+
SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
|
|
2212
2319
|
FROM hub_memories m
|
|
2213
2320
|
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
2321
|
ORDER BY m.updated_at DESC
|
|
2221
2322
|
LIMIT ?
|
|
2222
|
-
`).all(
|
|
2323
|
+
`).all(limit) as any[];
|
|
2223
2324
|
return rows.map(r => ({
|
|
2224
2325
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
2225
|
-
role: r.role, summary: r.summary, kind: r.kind,
|
|
2326
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2226
2327
|
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,
|
|
2328
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2228
2329
|
}));
|
|
2229
2330
|
}
|
|
2230
2331
|
|
|
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 }> {
|
|
2332
|
+
listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; createdAt: number; updatedAt: number }> {
|
|
2232
2333
|
const rows = this.db.prepare(`
|
|
2233
|
-
SELECT m.*, u.username AS owner_name,
|
|
2334
|
+
SELECT m.*, u.username AS owner_name, u.status AS owner_status
|
|
2234
2335
|
FROM hub_memories m
|
|
2235
2336
|
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
2337
|
ORDER BY m.updated_at DESC
|
|
2238
2338
|
`).all() as any[];
|
|
2239
2339
|
return rows.map(r => ({
|
|
2240
2340
|
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:
|
|
2243
|
-
ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2341
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2342
|
+
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2343
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2244
2344
|
}));
|
|
2245
2345
|
}
|
|
2246
2346
|
|
|
@@ -2264,13 +2364,6 @@ export class SqliteStore {
|
|
|
2264
2364
|
throw new Error(`source skill not found for skillId=${skillId}`);
|
|
2265
2365
|
}
|
|
2266
2366
|
|
|
2267
|
-
private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
|
|
2268
|
-
return {
|
|
2269
|
-
...user,
|
|
2270
|
-
groups: this.getGroupsForHubUser(user.id),
|
|
2271
|
-
};
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
2367
|
getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
|
|
2275
2368
|
const result = new Map<string, string>();
|
|
2276
2369
|
if (sessionKeys.length === 0) return result;
|
|
@@ -2482,6 +2575,8 @@ interface HubUserRecord extends UserInfo {
|
|
|
2482
2575
|
tokenHash: string;
|
|
2483
2576
|
createdAt: number;
|
|
2484
2577
|
approvedAt: number | null;
|
|
2578
|
+
lastIp: string;
|
|
2579
|
+
lastActiveAt: number | null;
|
|
2485
2580
|
}
|
|
2486
2581
|
|
|
2487
2582
|
interface HubUserRow {
|
|
@@ -2493,6 +2588,8 @@ interface HubUserRow {
|
|
|
2493
2588
|
token_hash: string;
|
|
2494
2589
|
created_at: number;
|
|
2495
2590
|
approved_at: number | null;
|
|
2591
|
+
last_ip: string;
|
|
2592
|
+
last_active_at: number | null;
|
|
2496
2593
|
}
|
|
2497
2594
|
|
|
2498
2595
|
function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
@@ -2506,29 +2603,8 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
|
2506
2603
|
tokenHash: row.token_hash,
|
|
2507
2604
|
createdAt: row.created_at,
|
|
2508
2605
|
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,
|
|
2606
|
+
lastIp: row.last_ip || "",
|
|
2607
|
+
lastActiveAt: row.last_active_at ?? null,
|
|
2532
2608
|
};
|
|
2533
2609
|
}
|
|
2534
2610
|
|
|
@@ -2677,6 +2753,7 @@ interface HubSkillSearchRow {
|
|
|
2677
2753
|
visibility: string;
|
|
2678
2754
|
group_name: string | null;
|
|
2679
2755
|
owner_name: string | null;
|
|
2756
|
+
owner_status: string | null;
|
|
2680
2757
|
quality_score: number | null;
|
|
2681
2758
|
}
|
|
2682
2759
|
|