@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/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,10 @@ export class SqliteStore {
|
|
|
112
112
|
this.migrateSkillEmbeddingsAndFts();
|
|
113
113
|
this.migrateFtsToTrigram();
|
|
114
114
|
this.migrateHubTables();
|
|
115
|
+
this.migrateHubFtsToTrigram();
|
|
116
|
+
this.migrateLocalSharedTasksOwner();
|
|
117
|
+
this.migrateHubUserIdentityFields();
|
|
118
|
+
this.migrateClientHubConnectionIdentityFields();
|
|
115
119
|
this.log.debug("Database schema initialized");
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -119,6 +123,59 @@ export class SqliteStore {
|
|
|
119
123
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
private migrateLocalSharedTasksOwner(): void {
|
|
127
|
+
try {
|
|
128
|
+
const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
|
|
129
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
|
|
130
|
+
this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
|
|
131
|
+
this.log.info("Migrated: added original_owner column to local_shared_tasks");
|
|
132
|
+
}
|
|
133
|
+
} catch { /* table may not exist yet */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private migrateHubUserIdentityFields(): void {
|
|
137
|
+
try {
|
|
138
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
|
|
139
|
+
if (cols.length === 0) return;
|
|
140
|
+
if (!cols.some(c => c.name === "identity_key")) {
|
|
141
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
|
|
142
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_hub_users_identity_key ON hub_users(identity_key)");
|
|
143
|
+
this.log.info("Migrated: added identity_key to hub_users");
|
|
144
|
+
}
|
|
145
|
+
if (!cols.some(c => c.name === "left_at")) {
|
|
146
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN left_at INTEGER");
|
|
147
|
+
this.log.info("Migrated: added left_at to hub_users");
|
|
148
|
+
}
|
|
149
|
+
if (!cols.some(c => c.name === "removed_at")) {
|
|
150
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN removed_at INTEGER");
|
|
151
|
+
this.log.info("Migrated: added removed_at to hub_users");
|
|
152
|
+
}
|
|
153
|
+
if (!cols.some(c => c.name === "rejected_at")) {
|
|
154
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN rejected_at INTEGER");
|
|
155
|
+
this.log.info("Migrated: added rejected_at to hub_users");
|
|
156
|
+
}
|
|
157
|
+
if (!cols.some(c => c.name === "rejoin_requested_at")) {
|
|
158
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN rejoin_requested_at INTEGER");
|
|
159
|
+
this.log.info("Migrated: added rejoin_requested_at to hub_users");
|
|
160
|
+
}
|
|
161
|
+
} catch { /* table may not exist yet */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private migrateClientHubConnectionIdentityFields(): void {
|
|
165
|
+
try {
|
|
166
|
+
const cols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all() as Array<{ name: string }>;
|
|
167
|
+
if (cols.length === 0) return;
|
|
168
|
+
if (!cols.some(c => c.name === "identity_key")) {
|
|
169
|
+
this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
|
|
170
|
+
this.log.info("Migrated: added identity_key to client_hub_connection");
|
|
171
|
+
}
|
|
172
|
+
if (!cols.some(c => c.name === "last_known_status")) {
|
|
173
|
+
this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN last_known_status TEXT NOT NULL DEFAULT ''");
|
|
174
|
+
this.log.info("Migrated: added last_known_status to client_hub_connection");
|
|
175
|
+
}
|
|
176
|
+
} catch { /* table may not exist yet */ }
|
|
177
|
+
}
|
|
178
|
+
|
|
122
179
|
private migrateOwnerFields(): void {
|
|
123
180
|
const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
124
181
|
if (!chunkCols.some((c) => c.name === "owner")) {
|
|
@@ -273,6 +330,51 @@ export class SqliteStore {
|
|
|
273
330
|
}
|
|
274
331
|
}
|
|
275
332
|
|
|
333
|
+
private migrateHubFtsToTrigram(): void {
|
|
334
|
+
const tables: Array<{ fts: string; source: string; columns: string; triggers: string[] }> = [
|
|
335
|
+
{
|
|
336
|
+
fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
|
|
337
|
+
triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
|
|
341
|
+
triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
|
|
345
|
+
triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
for (const t of tables) {
|
|
349
|
+
try {
|
|
350
|
+
const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get() as { sql: string } | undefined;
|
|
351
|
+
if (!row || !row.sql) continue;
|
|
352
|
+
if (row.sql.includes("trigram")) continue;
|
|
353
|
+
this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
|
|
354
|
+
for (const tr of t.triggers) this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
|
|
355
|
+
this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
|
|
356
|
+
this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
|
|
357
|
+
this.db.exec(`
|
|
358
|
+
CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
|
|
359
|
+
INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
|
|
360
|
+
END;
|
|
361
|
+
CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
|
|
362
|
+
INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
|
|
363
|
+
END;
|
|
364
|
+
CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
|
|
365
|
+
INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
|
|
366
|
+
INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
|
|
367
|
+
END
|
|
368
|
+
`);
|
|
369
|
+
this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
|
|
370
|
+
const cnt = (this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get() as { c: number }).c;
|
|
371
|
+
this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
276
378
|
private migrateTaskId(): void {
|
|
277
379
|
const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
278
380
|
if (!cols.some((c) => c.name === "task_id")) {
|
|
@@ -516,12 +618,13 @@ export class SqliteStore {
|
|
|
516
618
|
).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
|
|
517
619
|
}
|
|
518
620
|
|
|
519
|
-
getToolMetrics(minutes: number): {
|
|
621
|
+
getToolMetrics(minutes: number, fromMs?: number, toMs?: number): {
|
|
520
622
|
tools: string[];
|
|
521
623
|
series: Array<{ minute: string; [tool: string]: number | string }>;
|
|
522
624
|
aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
|
|
523
625
|
} {
|
|
524
|
-
const since = Date.now() - minutes * 60 * 1000;
|
|
626
|
+
const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
|
|
627
|
+
const until = toMs ?? Date.now();
|
|
525
628
|
|
|
526
629
|
const rows = this.db.prepare(
|
|
527
630
|
`SELECT tool_name,
|
|
@@ -529,9 +632,9 @@ export class SqliteStore {
|
|
|
529
632
|
success,
|
|
530
633
|
strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
|
|
531
634
|
FROM tool_calls
|
|
532
|
-
WHERE called_at >= ?
|
|
635
|
+
WHERE called_at >= ? AND called_at <= ?
|
|
533
636
|
ORDER BY called_at`,
|
|
534
|
-
).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
|
|
637
|
+
).all(since, until) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
|
|
535
638
|
|
|
536
639
|
const toolSet = new Set<string>();
|
|
537
640
|
const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
|
|
@@ -683,33 +786,49 @@ export class SqliteStore {
|
|
|
683
786
|
shared_at INTEGER NOT NULL
|
|
684
787
|
);
|
|
685
788
|
|
|
789
|
+
CREATE TABLE IF NOT EXISTS local_shared_memories (
|
|
790
|
+
chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
|
|
791
|
+
original_owner TEXT NOT NULL,
|
|
792
|
+
shared_at INTEGER NOT NULL
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
-- Client: team share UI metadata only (no hub_memories row — avoids local FTS/embed recall duplication)
|
|
796
|
+
CREATE TABLE IF NOT EXISTS team_shared_chunks (
|
|
797
|
+
chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
|
|
798
|
+
hub_memory_id TEXT NOT NULL DEFAULT '',
|
|
799
|
+
visibility TEXT NOT NULL DEFAULT 'public',
|
|
800
|
+
group_id TEXT,
|
|
801
|
+
shared_at INTEGER NOT NULL
|
|
802
|
+
);
|
|
803
|
+
|
|
686
804
|
CREATE TABLE IF NOT EXISTS hub_users (
|
|
687
|
-
id
|
|
688
|
-
username
|
|
689
|
-
device_name
|
|
690
|
-
role
|
|
691
|
-
status
|
|
692
|
-
token_hash
|
|
693
|
-
created_at
|
|
694
|
-
approved_at
|
|
805
|
+
id TEXT PRIMARY KEY,
|
|
806
|
+
username TEXT NOT NULL UNIQUE,
|
|
807
|
+
device_name TEXT NOT NULL DEFAULT '',
|
|
808
|
+
role TEXT NOT NULL,
|
|
809
|
+
status TEXT NOT NULL,
|
|
810
|
+
token_hash TEXT NOT NULL DEFAULT '',
|
|
811
|
+
created_at INTEGER NOT NULL,
|
|
812
|
+
approved_at INTEGER,
|
|
813
|
+
last_ip TEXT NOT NULL DEFAULT '',
|
|
814
|
+
last_active_at INTEGER
|
|
695
815
|
);
|
|
696
816
|
CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
|
|
697
817
|
CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
|
|
698
818
|
|
|
699
819
|
CREATE TABLE IF NOT EXISTS hub_groups (
|
|
700
820
|
id TEXT PRIMARY KEY,
|
|
701
|
-
name TEXT NOT NULL
|
|
821
|
+
name TEXT NOT NULL,
|
|
702
822
|
description TEXT NOT NULL DEFAULT '',
|
|
703
823
|
created_at INTEGER NOT NULL
|
|
704
824
|
);
|
|
705
825
|
|
|
706
826
|
CREATE TABLE IF NOT EXISTS hub_group_members (
|
|
707
|
-
group_id
|
|
708
|
-
user_id
|
|
709
|
-
joined_at
|
|
827
|
+
group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
|
|
828
|
+
user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
|
|
829
|
+
joined_at INTEGER NOT NULL,
|
|
710
830
|
PRIMARY KEY (group_id, user_id)
|
|
711
831
|
);
|
|
712
|
-
CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
|
|
713
832
|
|
|
714
833
|
CREATE TABLE IF NOT EXISTS hub_tasks (
|
|
715
834
|
id TEXT PRIMARY KEY,
|
|
@@ -752,7 +871,7 @@ export class SqliteStore {
|
|
|
752
871
|
content,
|
|
753
872
|
content='hub_chunks',
|
|
754
873
|
content_rowid='rowid',
|
|
755
|
-
tokenize='
|
|
874
|
+
tokenize='trigram'
|
|
756
875
|
);
|
|
757
876
|
|
|
758
877
|
CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
|
|
@@ -802,7 +921,7 @@ export class SqliteStore {
|
|
|
802
921
|
description,
|
|
803
922
|
content='hub_skills',
|
|
804
923
|
content_rowid='rowid',
|
|
805
|
-
tokenize='
|
|
924
|
+
tokenize='trigram'
|
|
806
925
|
);
|
|
807
926
|
|
|
808
927
|
CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
|
|
@@ -852,7 +971,7 @@ export class SqliteStore {
|
|
|
852
971
|
content,
|
|
853
972
|
content='hub_memories',
|
|
854
973
|
content_rowid='rowid',
|
|
855
|
-
tokenize='
|
|
974
|
+
tokenize='trigram'
|
|
856
975
|
);
|
|
857
976
|
|
|
858
977
|
CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
|
|
@@ -872,6 +991,32 @@ export class SqliteStore {
|
|
|
872
991
|
VALUES (new.rowid, new.summary, new.content);
|
|
873
992
|
END;
|
|
874
993
|
`);
|
|
994
|
+
|
|
995
|
+
this.db.exec(`
|
|
996
|
+
CREATE TABLE IF NOT EXISTS hub_notifications (
|
|
997
|
+
id TEXT PRIMARY KEY,
|
|
998
|
+
user_id TEXT NOT NULL,
|
|
999
|
+
type TEXT NOT NULL,
|
|
1000
|
+
resource TEXT NOT NULL,
|
|
1001
|
+
title TEXT NOT NULL,
|
|
1002
|
+
message TEXT NOT NULL DEFAULT '',
|
|
1003
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
1004
|
+
created_at INTEGER NOT NULL
|
|
1005
|
+
);
|
|
1006
|
+
CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
|
|
1007
|
+
`);
|
|
1008
|
+
|
|
1009
|
+
try {
|
|
1010
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
|
|
1011
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
|
|
1012
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
|
|
1013
|
+
this.log.info("Migrated: added last_ip column to hub_users");
|
|
1014
|
+
}
|
|
1015
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
|
|
1016
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
|
|
1017
|
+
this.log.info("Migrated: added last_active_at column to hub_users");
|
|
1018
|
+
}
|
|
1019
|
+
} catch { /* table may not exist yet */ }
|
|
875
1020
|
}
|
|
876
1021
|
|
|
877
1022
|
// ─── Write ───
|
|
@@ -1047,6 +1192,25 @@ export class SqliteStore {
|
|
|
1047
1192
|
}
|
|
1048
1193
|
}
|
|
1049
1194
|
|
|
1195
|
+
hubMemoryPatternSearch(patterns: string[], opts: { limit?: number } = {}): Array<{ memoryId: string; content: string; role: string; createdAt: number }> {
|
|
1196
|
+
if (patterns.length === 0) return [];
|
|
1197
|
+
const limit = opts.limit ?? 10;
|
|
1198
|
+
const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
|
|
1199
|
+
const params: (string | number)[] = [];
|
|
1200
|
+
for (const p of patterns) { params.push(`%${p}%`, `%${p}%`); }
|
|
1201
|
+
params.push(limit);
|
|
1202
|
+
try {
|
|
1203
|
+
const rows = this.db.prepare(`
|
|
1204
|
+
SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
|
|
1205
|
+
FROM hub_memories hm
|
|
1206
|
+
WHERE ${conditions.join(" OR ")}
|
|
1207
|
+
ORDER BY hm.created_at DESC
|
|
1208
|
+
LIMIT ?
|
|
1209
|
+
`).all(...params) as Array<{ memory_id: string; content: string; role: string; created_at: number }>;
|
|
1210
|
+
return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
|
|
1211
|
+
} catch { return []; }
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1050
1214
|
// ─── Vector Search ───
|
|
1051
1215
|
|
|
1052
1216
|
getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
|
|
@@ -1213,6 +1377,9 @@ export class SqliteStore {
|
|
|
1213
1377
|
"skill_embeddings",
|
|
1214
1378
|
"skill_versions",
|
|
1215
1379
|
"skills",
|
|
1380
|
+
"local_shared_memories",
|
|
1381
|
+
"team_shared_chunks",
|
|
1382
|
+
"local_shared_tasks",
|
|
1216
1383
|
"embeddings",
|
|
1217
1384
|
"chunks",
|
|
1218
1385
|
"tasks",
|
|
@@ -1333,7 +1500,10 @@ export class SqliteStore {
|
|
|
1333
1500
|
const conditions: string[] = [];
|
|
1334
1501
|
const params: unknown[] = [];
|
|
1335
1502
|
if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
|
|
1336
|
-
if (opts.owner) {
|
|
1503
|
+
if (opts.owner) {
|
|
1504
|
+
conditions.push("(owner = ? OR (owner = 'public' AND id IN (SELECT task_id FROM local_shared_tasks WHERE original_owner = ?)))");
|
|
1505
|
+
params.push(opts.owner, opts.owner);
|
|
1506
|
+
}
|
|
1337
1507
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1338
1508
|
|
|
1339
1509
|
const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params) as { c: number };
|
|
@@ -1633,16 +1803,18 @@ export class SqliteStore {
|
|
|
1633
1803
|
|
|
1634
1804
|
setClientHubConnection(conn: ClientHubConnection): void {
|
|
1635
1805
|
this.db.prepare(`
|
|
1636
|
-
INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
|
|
1637
|
-
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
1806
|
+
INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status)
|
|
1807
|
+
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1638
1808
|
ON CONFLICT(id) DO UPDATE SET
|
|
1639
1809
|
hub_url = excluded.hub_url,
|
|
1640
1810
|
user_id = excluded.user_id,
|
|
1641
1811
|
username = excluded.username,
|
|
1642
1812
|
user_token = excluded.user_token,
|
|
1643
1813
|
role = excluded.role,
|
|
1644
|
-
connected_at = excluded.connected_at
|
|
1645
|
-
|
|
1814
|
+
connected_at = excluded.connected_at,
|
|
1815
|
+
identity_key = excluded.identity_key,
|
|
1816
|
+
last_known_status = excluded.last_known_status
|
|
1817
|
+
`).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "");
|
|
1646
1818
|
}
|
|
1647
1819
|
|
|
1648
1820
|
getClientHubConnection(): ClientHubConnection | null {
|
|
@@ -1684,12 +1856,73 @@ export class SqliteStore {
|
|
|
1684
1856
|
return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks }));
|
|
1685
1857
|
}
|
|
1686
1858
|
|
|
1859
|
+
// ─── Local Shared Memories (client-side tracking) ───
|
|
1860
|
+
|
|
1861
|
+
markMemorySharedLocally(chunkId: string): { ok: boolean; owner?: string; originalOwner?: string; sharedAt?: number; reason?: string } {
|
|
1862
|
+
const chunk = this.getChunk(chunkId);
|
|
1863
|
+
if (!chunk) return { ok: false, reason: "not_found" };
|
|
1864
|
+
if (chunk.owner === "public") {
|
|
1865
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1866
|
+
return {
|
|
1867
|
+
ok: true,
|
|
1868
|
+
owner: "public",
|
|
1869
|
+
originalOwner: existing?.originalOwner ?? undefined,
|
|
1870
|
+
sharedAt: existing?.sharedAt ?? undefined,
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
const sharedAt = Date.now();
|
|
1875
|
+
this.db.transaction(() => {
|
|
1876
|
+
this.db.prepare(`
|
|
1877
|
+
INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
|
|
1878
|
+
VALUES (?, ?, ?)
|
|
1879
|
+
ON CONFLICT(chunk_id) DO UPDATE SET
|
|
1880
|
+
original_owner = excluded.original_owner,
|
|
1881
|
+
shared_at = excluded.shared_at
|
|
1882
|
+
`).run(chunkId, chunk.owner, sharedAt);
|
|
1883
|
+
this.updateChunk(chunkId, { owner: "public" });
|
|
1884
|
+
})();
|
|
1885
|
+
|
|
1886
|
+
return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
unmarkMemorySharedLocally(chunkId: string, fallbackOwner?: string): { ok: boolean; owner?: string; originalOwner?: string; reason?: string } {
|
|
1890
|
+
const chunk = this.getChunk(chunkId);
|
|
1891
|
+
if (!chunk) return { ok: false, reason: "not_found" };
|
|
1892
|
+
if (chunk.owner !== "public") {
|
|
1893
|
+
return { ok: true, owner: chunk.owner };
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1897
|
+
const restoreOwner = existing?.originalOwner ?? fallbackOwner;
|
|
1898
|
+
if (!restoreOwner || restoreOwner === "public") {
|
|
1899
|
+
return { ok: false, reason: "original_owner_missing" };
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
this.db.transaction(() => {
|
|
1903
|
+
this.updateChunk(chunkId, { owner: restoreOwner });
|
|
1904
|
+
this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
|
|
1905
|
+
})();
|
|
1906
|
+
|
|
1907
|
+
return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
getLocalSharedMemory(chunkId: string): { chunkId: string; originalOwner: string; sharedAt: number } | null {
|
|
1911
|
+
const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId) as any;
|
|
1912
|
+
if (!row) return null;
|
|
1913
|
+
return {
|
|
1914
|
+
chunkId: row.chunk_id,
|
|
1915
|
+
originalOwner: row.original_owner,
|
|
1916
|
+
sharedAt: row.shared_at,
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1687
1920
|
// ─── Hub Users / Groups ───
|
|
1688
1921
|
|
|
1689
1922
|
upsertHubUser(user: HubUserRecord): void {
|
|
1690
1923
|
this.db.prepare(`
|
|
1691
|
-
INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
|
|
1692
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1924
|
+
INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at, identity_key, left_at, removed_at, rejected_at, rejoin_requested_at)
|
|
1925
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1693
1926
|
ON CONFLICT(id) DO UPDATE SET
|
|
1694
1927
|
username = excluded.username,
|
|
1695
1928
|
device_name = excluded.device_name,
|
|
@@ -1697,81 +1930,99 @@ export class SqliteStore {
|
|
|
1697
1930
|
status = excluded.status,
|
|
1698
1931
|
token_hash = excluded.token_hash,
|
|
1699
1932
|
created_at = excluded.created_at,
|
|
1700
|
-
approved_at = excluded.approved_at
|
|
1701
|
-
|
|
1933
|
+
approved_at = excluded.approved_at,
|
|
1934
|
+
identity_key = excluded.identity_key,
|
|
1935
|
+
left_at = excluded.left_at,
|
|
1936
|
+
removed_at = excluded.removed_at,
|
|
1937
|
+
rejected_at = excluded.rejected_at,
|
|
1938
|
+
rejoin_requested_at = excluded.rejoin_requested_at
|
|
1939
|
+
`).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt, user.identityKey ?? "", user.leftAt ?? null, user.removedAt ?? null, user.rejectedAt ?? null, user.rejoinRequestedAt ?? null);
|
|
1702
1940
|
}
|
|
1703
1941
|
|
|
1704
1942
|
getHubUser(userId: string): HubUserRecord | null {
|
|
1705
1943
|
const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
|
|
1706
1944
|
if (!row) return null;
|
|
1707
|
-
|
|
1945
|
+
const user = rowToHubUser(row);
|
|
1946
|
+
user.groups = this.getGroupsForHubUser(userId);
|
|
1947
|
+
return user;
|
|
1708
1948
|
}
|
|
1709
1949
|
|
|
1710
1950
|
listHubUsers(status?: UserStatus): HubUserRecord[] {
|
|
1711
1951
|
const rows = status
|
|
1712
1952
|
? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
|
|
1713
1953
|
: this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
|
|
1714
|
-
return rows.map(
|
|
1954
|
+
return rows.map(r => {
|
|
1955
|
+
const user = rowToHubUser(r);
|
|
1956
|
+
user.groups = this.getGroupsForHubUser(r.id);
|
|
1957
|
+
return user;
|
|
1958
|
+
});
|
|
1715
1959
|
}
|
|
1716
1960
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1961
|
+
deleteHubUser(userId: string, cleanResources = false): boolean {
|
|
1962
|
+
if (cleanResources) {
|
|
1963
|
+
this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
|
|
1964
|
+
this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
|
|
1965
|
+
this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
|
|
1966
|
+
const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
|
|
1967
|
+
return result.changes > 0;
|
|
1968
|
+
}
|
|
1969
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
|
|
1970
|
+
return result.changes > 0;
|
|
1726
1971
|
}
|
|
1727
1972
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1973
|
+
findHubUserByIdentityKey(identityKey: string): HubUserRecord | null {
|
|
1974
|
+
if (!identityKey) return null;
|
|
1975
|
+
const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey) as HubUserRow | undefined;
|
|
1976
|
+
return row ? rowToHubUser(row) : null;
|
|
1731
1977
|
}
|
|
1732
1978
|
|
|
1733
|
-
|
|
1734
|
-
this.db.prepare(
|
|
1735
|
-
|
|
1736
|
-
VALUES (?, ?, ?)
|
|
1737
|
-
ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
|
|
1738
|
-
`).run(groupId, userId, joinedAt);
|
|
1979
|
+
markHubUserLeft(userId: string): boolean {
|
|
1980
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
|
|
1981
|
+
return result.changes > 0;
|
|
1739
1982
|
}
|
|
1740
1983
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
return row ? rowToHubGroup(row) : undefined;
|
|
1984
|
+
updateHubUserActivity(userId: string, ip: string, timestamp?: number): void {
|
|
1985
|
+
this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
|
|
1744
1986
|
}
|
|
1745
1987
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1988
|
+
// ─── Hub Groups ───
|
|
1989
|
+
|
|
1990
|
+
upsertHubGroup(group: { id: string; name: string; description?: string; createdAt: number }): void {
|
|
1991
|
+
this.db.prepare(`
|
|
1992
|
+
INSERT INTO hub_groups (id, name, description, created_at)
|
|
1993
|
+
VALUES (?, ?, ?, ?)
|
|
1994
|
+
ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description
|
|
1995
|
+
`).run(group.id, group.name, group.description ?? "", group.createdAt);
|
|
1749
1996
|
}
|
|
1750
1997
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
WHERE gm.group_id = ?
|
|
1757
|
-
ORDER BY gm.joined_at
|
|
1758
|
-
`).all(groupId) as Array<{ user_id: string; username: string; joined_at: number }>;
|
|
1759
|
-
return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
|
|
1998
|
+
addHubGroupMember(groupId: string, userId: string, joinedAt: number): void {
|
|
1999
|
+
this.db.prepare(`
|
|
2000
|
+
INSERT OR IGNORE INTO hub_group_members (group_id, user_id, joined_at)
|
|
2001
|
+
VALUES (?, ?, ?)
|
|
2002
|
+
`).run(groupId, userId, joinedAt);
|
|
1760
2003
|
}
|
|
1761
2004
|
|
|
1762
2005
|
removeHubGroupMember(groupId: string, userId: string): void {
|
|
1763
2006
|
this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
|
|
1764
2007
|
}
|
|
1765
2008
|
|
|
1766
|
-
getGroupsForHubUser(userId: string):
|
|
1767
|
-
|
|
1768
|
-
SELECT g
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2009
|
+
getGroupsForHubUser(userId: string): Array<{ id: string; name: string; description: string }> {
|
|
2010
|
+
return this.db.prepare(`
|
|
2011
|
+
SELECT g.id, g.name, g.description FROM hub_groups g
|
|
2012
|
+
JOIN hub_group_members m ON m.group_id = g.id
|
|
2013
|
+
WHERE m.user_id = ?
|
|
2014
|
+
`).all(userId) as Array<{ id: string; name: string; description: string }>;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
|
|
2018
|
+
const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
|
|
2019
|
+
const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
|
|
2020
|
+
const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
|
|
2021
|
+
const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all() as Array<{ source_user_id: string; cnt: number }>;
|
|
2022
|
+
for (const r of memRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].memoryCount = r.cnt; }
|
|
2023
|
+
for (const r of taskRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].taskCount = r.cnt; }
|
|
2024
|
+
for (const r of skillRows) { if (!result[r.source_user_id]) result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 }; result[r.source_user_id].skillCount = r.cnt; }
|
|
2025
|
+
return result;
|
|
1775
2026
|
}
|
|
1776
2027
|
|
|
1777
2028
|
// ─── Hub Shared Data ───
|
|
@@ -1795,6 +2046,11 @@ export class SqliteStore {
|
|
|
1795
2046
|
return row ? rowToHubTask(row) : null;
|
|
1796
2047
|
}
|
|
1797
2048
|
|
|
2049
|
+
getHubTaskById(taskId: string): HubTaskRecord | null {
|
|
2050
|
+
const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId) as HubTaskRow | undefined;
|
|
2051
|
+
return row ? rowToHubTask(row) : null;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
1798
2054
|
upsertHubChunk(chunk: HubChunkUpsertInput): void {
|
|
1799
2055
|
if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
|
|
1800
2056
|
const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
|
|
@@ -1870,20 +2126,37 @@ export class SqliteStore {
|
|
|
1870
2126
|
return out;
|
|
1871
2127
|
}
|
|
1872
2128
|
|
|
2129
|
+
getVisibleHubSkillEmbeddings(): Array<{ skillId: string; vector: Float32Array }> {
|
|
2130
|
+
const rows = this.db.prepare(`
|
|
2131
|
+
SELECT hse.skill_id, hse.vector, hse.dimensions
|
|
2132
|
+
FROM hub_skill_embeddings hse
|
|
2133
|
+
JOIN hub_skills hs ON hs.id = hse.skill_id
|
|
2134
|
+
`).all() as Array<{ skill_id: string; vector: Buffer; dimensions: number }>;
|
|
2135
|
+
return rows.map(r => ({
|
|
2136
|
+
skillId: r.skill_id,
|
|
2137
|
+
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
2138
|
+
}));
|
|
2139
|
+
}
|
|
2140
|
+
|
|
1873
2141
|
searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
|
|
1874
2142
|
const limit = options?.maxResults ?? 10;
|
|
1875
2143
|
const userId = options?.userId ?? "";
|
|
1876
2144
|
const rows = this.db.prepare(`
|
|
1877
|
-
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
|
|
2145
|
+
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
|
|
2146
|
+
COALESCE(hg.name, '') as group_name, hu.username as owner_name,
|
|
1878
2147
|
bm25(hub_chunks_fts) as rank
|
|
1879
2148
|
FROM hub_chunks_fts f
|
|
1880
2149
|
JOIN hub_chunks hc ON hc.rowid = f.rowid
|
|
1881
2150
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1882
2151
|
LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
|
|
2152
|
+
LEFT JOIN hub_groups hg ON hg.id = ht.group_id
|
|
1883
2153
|
WHERE hub_chunks_fts MATCH ?
|
|
2154
|
+
AND (ht.visibility = 'public'
|
|
2155
|
+
OR ht.source_user_id = ?
|
|
2156
|
+
OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
|
|
1884
2157
|
ORDER BY rank
|
|
1885
2158
|
LIMIT ?
|
|
1886
|
-
`).all(sanitizeFtsQuery(query), limit) as HubSearchRow[];
|
|
2159
|
+
`).all(sanitizeFtsQuery(query), userId, userId, limit) as HubSearchRow[];
|
|
1887
2160
|
return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
|
|
1888
2161
|
}
|
|
1889
2162
|
|
|
@@ -1908,7 +2181,10 @@ export class SqliteStore {
|
|
|
1908
2181
|
FROM hub_embeddings he
|
|
1909
2182
|
JOIN hub_chunks hc ON hc.id = he.chunk_id
|
|
1910
2183
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1911
|
-
|
|
2184
|
+
WHERE ht.visibility = 'public'
|
|
2185
|
+
OR ht.source_user_id = ?
|
|
2186
|
+
OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?)
|
|
2187
|
+
`).all(userId, userId) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
|
|
1912
2188
|
return rows.map(r => ({
|
|
1913
2189
|
chunkId: r.chunk_id,
|
|
1914
2190
|
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
@@ -1917,14 +2193,19 @@ export class SqliteStore {
|
|
|
1917
2193
|
|
|
1918
2194
|
getVisibleHubSearchHitByChunkId(chunkId: string, userId: string): HubSearchRow | null {
|
|
1919
2195
|
const row = this.db.prepare(`
|
|
1920
|
-
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
|
|
2196
|
+
SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility,
|
|
2197
|
+
COALESCE(hg.name, '') as group_name, hu.username as owner_name,
|
|
1921
2198
|
0 as rank
|
|
1922
2199
|
FROM hub_chunks hc
|
|
1923
2200
|
JOIN hub_tasks ht ON ht.id = hc.hub_task_id
|
|
1924
2201
|
LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
|
|
2202
|
+
LEFT JOIN hub_groups hg ON hg.id = ht.group_id
|
|
1925
2203
|
WHERE hc.id = ?
|
|
2204
|
+
AND (ht.visibility = 'public'
|
|
2205
|
+
OR ht.source_user_id = ?
|
|
2206
|
+
OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = ht.group_id AND gm.user_id = ?))
|
|
1926
2207
|
LIMIT 1
|
|
1927
|
-
`).get(chunkId) as HubSearchRow | undefined;
|
|
2208
|
+
`).get(chunkId, userId, userId) as HubSearchRow | undefined;
|
|
1928
2209
|
return row ?? null;
|
|
1929
2210
|
}
|
|
1930
2211
|
|
|
@@ -1940,7 +2221,7 @@ export class SqliteStore {
|
|
|
1940
2221
|
let rows: HubSkillSearchRow[];
|
|
1941
2222
|
if (sanitized) {
|
|
1942
2223
|
rows = this.db.prepare(`
|
|
1943
|
-
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
|
|
2224
|
+
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
|
|
1944
2225
|
bm25(hub_skills_fts) as rank
|
|
1945
2226
|
FROM hub_skills_fts f
|
|
1946
2227
|
JOIN hub_skills hs ON hs.rowid = f.rowid
|
|
@@ -1951,7 +2232,7 @@ export class SqliteStore {
|
|
|
1951
2232
|
`).all(sanitized, limit) as HubSkillSearchRow[];
|
|
1952
2233
|
} else {
|
|
1953
2234
|
rows = this.db.prepare(`
|
|
1954
|
-
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hs.quality_score,
|
|
2235
|
+
SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
|
|
1955
2236
|
0 as rank
|
|
1956
2237
|
FROM hub_skills hs
|
|
1957
2238
|
LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
|
|
@@ -1966,9 +2247,9 @@ export class SqliteStore {
|
|
|
1966
2247
|
this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
|
|
1967
2248
|
}
|
|
1968
2249
|
|
|
1969
|
-
listVisibleHubTasks(userId: string, limit = 40): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; chunkCount: number; createdAt: number; updatedAt: number }> {
|
|
2250
|
+
listVisibleHubTasks(userId: string, limit = 40): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; chunkCount: number; createdAt: number; updatedAt: number }> {
|
|
1970
2251
|
const rows = this.db.prepare(`
|
|
1971
|
-
SELECT t.*, u.username AS owner_name, NULL AS group_name,
|
|
2252
|
+
SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
|
|
1972
2253
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
1973
2254
|
FROM hub_tasks t
|
|
1974
2255
|
LEFT JOIN hub_users u ON u.id = t.source_user_id
|
|
@@ -1978,36 +2259,40 @@ export class SqliteStore {
|
|
|
1978
2259
|
return rows.map(r => ({
|
|
1979
2260
|
id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
|
|
1980
2261
|
title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
|
|
1981
|
-
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
|
|
2262
|
+
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
|
|
1982
2263
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
1983
2264
|
}));
|
|
1984
2265
|
}
|
|
1985
2266
|
|
|
1986
|
-
listAllHubTasks(): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; chunkCount: number; createdAt: number; updatedAt: number }> {
|
|
2267
|
+
listAllHubTasks(): Array<{ id: string; sourceTaskId: string; sourceUserId: string; title: string; summary: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; chunkCount: number; createdAt: number; updatedAt: number }> {
|
|
1987
2268
|
const rows = this.db.prepare(`
|
|
1988
|
-
SELECT t.*, u.username AS owner_name,
|
|
2269
|
+
SELECT t.*, u.username AS owner_name, u.status AS owner_status,
|
|
1989
2270
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
1990
2271
|
FROM hub_tasks t
|
|
1991
2272
|
LEFT JOIN hub_users u ON u.id = t.source_user_id
|
|
1992
|
-
LEFT JOIN hub_groups g ON g.id = t.group_id
|
|
1993
2273
|
ORDER BY t.updated_at DESC
|
|
1994
2274
|
`).all() as any[];
|
|
1995
2275
|
return rows.map(r => ({
|
|
1996
2276
|
id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
|
|
1997
|
-
title: r.title, summary: r.summary, groupId: r.group_id, groupName:
|
|
1998
|
-
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
|
|
2277
|
+
title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
|
|
2278
|
+
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
|
|
1999
2279
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2000
2280
|
}));
|
|
2001
2281
|
}
|
|
2002
2282
|
|
|
2283
|
+
listHubChunksByTaskId(hubTaskId: string): HubChunkRecord[] {
|
|
2284
|
+
const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId) as HubChunkRow[];
|
|
2285
|
+
return rows.map(rowToHubChunk);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2003
2288
|
deleteHubTaskById(taskId: string): boolean {
|
|
2004
2289
|
const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
|
|
2005
2290
|
return info.changes > 0;
|
|
2006
2291
|
}
|
|
2007
2292
|
|
|
2008
|
-
listVisibleHubSkills(userId: string, limit = 40): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
|
|
2293
|
+
listVisibleHubSkills(userId: string, limit = 40): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
|
|
2009
2294
|
const rows = this.db.prepare(`
|
|
2010
|
-
SELECT s.*, u.username AS owner_name, NULL AS group_name
|
|
2295
|
+
SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
|
|
2011
2296
|
FROM hub_skills s
|
|
2012
2297
|
LEFT JOIN hub_users u ON u.id = s.source_user_id
|
|
2013
2298
|
ORDER BY s.updated_at DESC
|
|
@@ -2017,24 +2302,23 @@ export class SqliteStore {
|
|
|
2017
2302
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
2018
2303
|
name: r.name, description: r.description, version: r.version,
|
|
2019
2304
|
groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
|
|
2020
|
-
ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
|
|
2305
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
|
|
2021
2306
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2022
2307
|
}));
|
|
2023
2308
|
}
|
|
2024
2309
|
|
|
2025
|
-
listAllHubSkills(): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
|
|
2310
|
+
listAllHubSkills(): Array<{ id: string; sourceSkillId: string; sourceUserId: string; name: string; description: string; version: number; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; qualityScore: number | null; createdAt: number; updatedAt: number }> {
|
|
2026
2311
|
const rows = this.db.prepare(`
|
|
2027
|
-
SELECT s.*, u.username AS owner_name,
|
|
2312
|
+
SELECT s.*, u.username AS owner_name, u.status AS owner_status
|
|
2028
2313
|
FROM hub_skills s
|
|
2029
2314
|
LEFT JOIN hub_users u ON u.id = s.source_user_id
|
|
2030
|
-
LEFT JOIN hub_groups g ON g.id = s.group_id
|
|
2031
2315
|
ORDER BY s.updated_at DESC
|
|
2032
2316
|
`).all() as any[];
|
|
2033
2317
|
return rows.map(r => ({
|
|
2034
2318
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
2035
2319
|
name: r.name, description: r.description, version: r.version,
|
|
2036
|
-
groupId: r.group_id, groupName:
|
|
2037
|
-
ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
|
|
2320
|
+
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2321
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
|
|
2038
2322
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2039
2323
|
}));
|
|
2040
2324
|
}
|
|
@@ -2081,6 +2365,86 @@ export class SqliteStore {
|
|
|
2081
2365
|
return info.changes > 0;
|
|
2082
2366
|
}
|
|
2083
2367
|
|
|
2368
|
+
// ─── Team share metadata (Client role — UI only, not used for local recall / FTS) ───
|
|
2369
|
+
|
|
2370
|
+
upsertTeamSharedChunk(
|
|
2371
|
+
chunkId: string,
|
|
2372
|
+
row: { hubMemoryId?: string; visibility?: string; groupId?: string | null },
|
|
2373
|
+
): void {
|
|
2374
|
+
const now = Date.now();
|
|
2375
|
+
const vis = row.visibility === "group" ? "group" : "public";
|
|
2376
|
+
const gid = vis === "group" ? (row.groupId ?? null) : null;
|
|
2377
|
+
this.db.prepare(`
|
|
2378
|
+
INSERT INTO team_shared_chunks (chunk_id, hub_memory_id, visibility, group_id, shared_at)
|
|
2379
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2380
|
+
ON CONFLICT(chunk_id) DO UPDATE SET
|
|
2381
|
+
hub_memory_id = excluded.hub_memory_id,
|
|
2382
|
+
visibility = excluded.visibility,
|
|
2383
|
+
group_id = excluded.group_id,
|
|
2384
|
+
shared_at = excluded.shared_at
|
|
2385
|
+
`).run(chunkId, row.hubMemoryId ?? "", vis, gid, now);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
getTeamSharedChunk(chunkId: string): { chunkId: string; hubMemoryId: string; visibility: string; groupId: string | null; sharedAt: number } | null {
|
|
2389
|
+
const r = this.db.prepare("SELECT chunk_id, hub_memory_id, visibility, group_id, shared_at FROM team_shared_chunks WHERE chunk_id = ?").get(chunkId) as {
|
|
2390
|
+
chunk_id: string; hub_memory_id: string; visibility: string; group_id: string | null; shared_at: number;
|
|
2391
|
+
} | undefined;
|
|
2392
|
+
if (!r) return null;
|
|
2393
|
+
return {
|
|
2394
|
+
chunkId: r.chunk_id,
|
|
2395
|
+
hubMemoryId: r.hub_memory_id,
|
|
2396
|
+
visibility: r.visibility,
|
|
2397
|
+
groupId: r.group_id,
|
|
2398
|
+
sharedAt: r.shared_at,
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
deleteTeamSharedChunk(chunkId: string): boolean {
|
|
2403
|
+
const info = this.db.prepare("DELETE FROM team_shared_chunks WHERE chunk_id = ?").run(chunkId);
|
|
2404
|
+
return info.changes > 0;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// ─── Hub Notifications ───
|
|
2408
|
+
|
|
2409
|
+
insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
|
|
2410
|
+
this.db.prepare(
|
|
2411
|
+
'INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)'
|
|
2412
|
+
).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
hasRecentHubNotification(userId: string, type: string, resource: string, windowMs: number = 300_000): boolean {
|
|
2416
|
+
const since = Date.now() - windowMs;
|
|
2417
|
+
const row = this.db.prepare(
|
|
2418
|
+
'SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?'
|
|
2419
|
+
).get(userId, type, resource, since) as { cnt: number };
|
|
2420
|
+
return row.cnt > 0;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
listHubNotifications(userId: string, opts?: { unreadOnly?: boolean; limit?: number }): Array<{ id: string; userId: string; type: string; resource: string; title: string; message: string; read: boolean; createdAt: number }> {
|
|
2424
|
+
const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
|
|
2425
|
+
const limit = opts?.limit ?? 50;
|
|
2426
|
+
const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit) as any[];
|
|
2427
|
+
return rows.map(r => ({ id: r.id, userId: r.user_id, type: r.type, resource: r.resource, title: r.title, message: r.message, read: !!r.read, createdAt: r.created_at }));
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
countUnreadHubNotifications(userId: string): number {
|
|
2431
|
+
const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId) as { cnt: number };
|
|
2432
|
+
return row.cnt;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
markHubNotificationsRead(userId: string, ids?: string[]): void {
|
|
2436
|
+
if (ids && ids.length > 0) {
|
|
2437
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
2438
|
+
this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
|
|
2439
|
+
} else {
|
|
2440
|
+
this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
clearHubNotifications(userId: string): void {
|
|
2445
|
+
this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2084
2448
|
upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
|
|
2085
2449
|
const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
2086
2450
|
this.db.prepare(`
|
|
@@ -2138,9 +2502,9 @@ export class SqliteStore {
|
|
|
2138
2502
|
return row ?? null;
|
|
2139
2503
|
}
|
|
2140
2504
|
|
|
2141
|
-
listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
|
|
2505
|
+
listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; createdAt: number; updatedAt: number }> {
|
|
2142
2506
|
const rows = this.db.prepare(`
|
|
2143
|
-
SELECT m.*, u.username AS owner_name, NULL AS group_name
|
|
2507
|
+
SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
|
|
2144
2508
|
FROM hub_memories m
|
|
2145
2509
|
LEFT JOIN hub_users u ON u.id = m.source_user_id
|
|
2146
2510
|
ORDER BY m.updated_at DESC
|
|
@@ -2148,25 +2512,24 @@ export class SqliteStore {
|
|
|
2148
2512
|
`).all(limit) as any[];
|
|
2149
2513
|
return rows.map(r => ({
|
|
2150
2514
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
2151
|
-
role: r.role, summary: r.summary, kind: r.kind,
|
|
2515
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2152
2516
|
groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
|
|
2153
|
-
ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2517
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2154
2518
|
}));
|
|
2155
2519
|
}
|
|
2156
2520
|
|
|
2157
|
-
listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
|
|
2521
|
+
listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; ownerStatus: string; createdAt: number; updatedAt: number }> {
|
|
2158
2522
|
const rows = this.db.prepare(`
|
|
2159
|
-
SELECT m.*, u.username AS owner_name,
|
|
2523
|
+
SELECT m.*, u.username AS owner_name, u.status AS owner_status
|
|
2160
2524
|
FROM hub_memories m
|
|
2161
2525
|
LEFT JOIN hub_users u ON u.id = m.source_user_id
|
|
2162
|
-
LEFT JOIN hub_groups g ON g.id = m.group_id
|
|
2163
2526
|
ORDER BY m.updated_at DESC
|
|
2164
2527
|
`).all() as any[];
|
|
2165
2528
|
return rows.map(r => ({
|
|
2166
2529
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
2167
|
-
role: r.role, summary: r.summary, kind: r.kind,
|
|
2168
|
-
groupId: r.group_id, groupName:
|
|
2169
|
-
ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2530
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2531
|
+
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2532
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2170
2533
|
}));
|
|
2171
2534
|
}
|
|
2172
2535
|
|
|
@@ -2190,13 +2553,6 @@ export class SqliteStore {
|
|
|
2190
2553
|
throw new Error(`source skill not found for skillId=${skillId}`);
|
|
2191
2554
|
}
|
|
2192
2555
|
|
|
2193
|
-
private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
|
|
2194
|
-
return {
|
|
2195
|
-
...user,
|
|
2196
|
-
groups: this.getGroupsForHubUser(user.id),
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
2556
|
getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
|
|
2201
2557
|
const result = new Map<string, string>();
|
|
2202
2558
|
if (sessionKeys.length === 0) return result;
|
|
@@ -2382,6 +2738,8 @@ interface ClientHubConnection {
|
|
|
2382
2738
|
userToken: string;
|
|
2383
2739
|
role: UserRole;
|
|
2384
2740
|
connectedAt: number;
|
|
2741
|
+
identityKey?: string;
|
|
2742
|
+
lastKnownStatus?: string;
|
|
2385
2743
|
}
|
|
2386
2744
|
|
|
2387
2745
|
interface ClientHubConnectionRow {
|
|
@@ -2391,6 +2749,8 @@ interface ClientHubConnectionRow {
|
|
|
2391
2749
|
user_token: string;
|
|
2392
2750
|
role: string;
|
|
2393
2751
|
connected_at: number;
|
|
2752
|
+
identity_key?: string;
|
|
2753
|
+
last_known_status?: string;
|
|
2394
2754
|
}
|
|
2395
2755
|
|
|
2396
2756
|
function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnection {
|
|
@@ -2401,6 +2761,8 @@ function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnect
|
|
|
2401
2761
|
userToken: row.user_token,
|
|
2402
2762
|
role: row.role as UserRole,
|
|
2403
2763
|
connectedAt: row.connected_at,
|
|
2764
|
+
identityKey: row.identity_key || "",
|
|
2765
|
+
lastKnownStatus: row.last_known_status || "",
|
|
2404
2766
|
};
|
|
2405
2767
|
}
|
|
2406
2768
|
|
|
@@ -2408,6 +2770,13 @@ interface HubUserRecord extends UserInfo {
|
|
|
2408
2770
|
tokenHash: string;
|
|
2409
2771
|
createdAt: number;
|
|
2410
2772
|
approvedAt: number | null;
|
|
2773
|
+
lastIp: string;
|
|
2774
|
+
lastActiveAt: number | null;
|
|
2775
|
+
identityKey?: string;
|
|
2776
|
+
leftAt?: number | null;
|
|
2777
|
+
removedAt?: number | null;
|
|
2778
|
+
rejectedAt?: number | null;
|
|
2779
|
+
rejoinRequestedAt?: number | null;
|
|
2411
2780
|
}
|
|
2412
2781
|
|
|
2413
2782
|
interface HubUserRow {
|
|
@@ -2419,6 +2788,13 @@ interface HubUserRow {
|
|
|
2419
2788
|
token_hash: string;
|
|
2420
2789
|
created_at: number;
|
|
2421
2790
|
approved_at: number | null;
|
|
2791
|
+
last_ip: string;
|
|
2792
|
+
last_active_at: number | null;
|
|
2793
|
+
identity_key?: string;
|
|
2794
|
+
left_at?: number | null;
|
|
2795
|
+
removed_at?: number | null;
|
|
2796
|
+
rejected_at?: number | null;
|
|
2797
|
+
rejoin_requested_at?: number | null;
|
|
2422
2798
|
}
|
|
2423
2799
|
|
|
2424
2800
|
function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
@@ -2432,29 +2808,13 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
|
2432
2808
|
tokenHash: row.token_hash,
|
|
2433
2809
|
createdAt: row.created_at,
|
|
2434
2810
|
approvedAt: row.approved_at,
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
createdAt: number;
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
interface HubGroupRow {
|
|
2446
|
-
id: string;
|
|
2447
|
-
name: string;
|
|
2448
|
-
description: string;
|
|
2449
|
-
created_at: number;
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
function rowToHubGroup(row: HubGroupRow): HubGroupRecord {
|
|
2453
|
-
return {
|
|
2454
|
-
id: row.id,
|
|
2455
|
-
name: row.name,
|
|
2456
|
-
description: row.description,
|
|
2457
|
-
createdAt: row.created_at,
|
|
2811
|
+
lastIp: row.last_ip || "",
|
|
2812
|
+
lastActiveAt: row.last_active_at ?? null,
|
|
2813
|
+
identityKey: row.identity_key || "",
|
|
2814
|
+
leftAt: row.left_at ?? null,
|
|
2815
|
+
removedAt: row.removed_at ?? null,
|
|
2816
|
+
rejectedAt: row.rejected_at ?? null,
|
|
2817
|
+
rejoinRequestedAt: row.rejoin_requested_at ?? null,
|
|
2458
2818
|
};
|
|
2459
2819
|
}
|
|
2460
2820
|
|
|
@@ -2603,6 +2963,7 @@ interface HubSkillSearchRow {
|
|
|
2603
2963
|
visibility: string;
|
|
2604
2964
|
group_name: string | null;
|
|
2605
2965
|
owner_name: string | null;
|
|
2966
|
+
owner_status: string | null;
|
|
2606
2967
|
quality_score: number | null;
|
|
2607
2968
|
}
|
|
2608
2969
|
|