@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 1.0.4-beta.11
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 +2 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +122 -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 +0 -2
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +8 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +390 -106
- 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/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 +93 -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 +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 +89 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +374 -124
- 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 +2671 -879
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +30 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +990 -198
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +700 -56
- 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 +124 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +0 -2
- package/src/hub/server.ts +374 -97
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +86 -1
- package/src/shared/llm-call.ts +97 -9
- 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 +395 -148
- 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 +2671 -879
- package/src/viewer/server.ts +913 -182
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,34 +786,27 @@ 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
|
+
|
|
686
795
|
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
|
|
796
|
+
id TEXT PRIMARY KEY,
|
|
797
|
+
username TEXT NOT NULL UNIQUE,
|
|
798
|
+
device_name TEXT NOT NULL DEFAULT '',
|
|
799
|
+
role TEXT NOT NULL,
|
|
800
|
+
status TEXT NOT NULL,
|
|
801
|
+
token_hash TEXT NOT NULL DEFAULT '',
|
|
802
|
+
created_at INTEGER NOT NULL,
|
|
803
|
+
approved_at INTEGER,
|
|
804
|
+
last_ip TEXT NOT NULL DEFAULT '',
|
|
805
|
+
last_active_at INTEGER
|
|
695
806
|
);
|
|
696
807
|
CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
|
|
697
808
|
CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
|
|
698
809
|
|
|
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
810
|
CREATE TABLE IF NOT EXISTS hub_tasks (
|
|
715
811
|
id TEXT PRIMARY KEY,
|
|
716
812
|
source_task_id TEXT NOT NULL,
|
|
@@ -752,7 +848,7 @@ export class SqliteStore {
|
|
|
752
848
|
content,
|
|
753
849
|
content='hub_chunks',
|
|
754
850
|
content_rowid='rowid',
|
|
755
|
-
tokenize='
|
|
851
|
+
tokenize='trigram'
|
|
756
852
|
);
|
|
757
853
|
|
|
758
854
|
CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
|
|
@@ -802,7 +898,7 @@ export class SqliteStore {
|
|
|
802
898
|
description,
|
|
803
899
|
content='hub_skills',
|
|
804
900
|
content_rowid='rowid',
|
|
805
|
-
tokenize='
|
|
901
|
+
tokenize='trigram'
|
|
806
902
|
);
|
|
807
903
|
|
|
808
904
|
CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
|
|
@@ -852,7 +948,7 @@ export class SqliteStore {
|
|
|
852
948
|
content,
|
|
853
949
|
content='hub_memories',
|
|
854
950
|
content_rowid='rowid',
|
|
855
|
-
tokenize='
|
|
951
|
+
tokenize='trigram'
|
|
856
952
|
);
|
|
857
953
|
|
|
858
954
|
CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
|
|
@@ -872,6 +968,32 @@ export class SqliteStore {
|
|
|
872
968
|
VALUES (new.rowid, new.summary, new.content);
|
|
873
969
|
END;
|
|
874
970
|
`);
|
|
971
|
+
|
|
972
|
+
this.db.exec(`
|
|
973
|
+
CREATE TABLE IF NOT EXISTS hub_notifications (
|
|
974
|
+
id TEXT PRIMARY KEY,
|
|
975
|
+
user_id TEXT NOT NULL,
|
|
976
|
+
type TEXT NOT NULL,
|
|
977
|
+
resource TEXT NOT NULL,
|
|
978
|
+
title TEXT NOT NULL,
|
|
979
|
+
message TEXT NOT NULL DEFAULT '',
|
|
980
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
981
|
+
created_at INTEGER NOT NULL
|
|
982
|
+
);
|
|
983
|
+
CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
|
|
984
|
+
`);
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
|
|
988
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
|
|
989
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
|
|
990
|
+
this.log.info("Migrated: added last_ip column to hub_users");
|
|
991
|
+
}
|
|
992
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
|
|
993
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
|
|
994
|
+
this.log.info("Migrated: added last_active_at column to hub_users");
|
|
995
|
+
}
|
|
996
|
+
} catch { /* table may not exist yet */ }
|
|
875
997
|
}
|
|
876
998
|
|
|
877
999
|
// ─── Write ───
|
|
@@ -1047,6 +1169,25 @@ export class SqliteStore {
|
|
|
1047
1169
|
}
|
|
1048
1170
|
}
|
|
1049
1171
|
|
|
1172
|
+
hubMemoryPatternSearch(patterns: string[], opts: { limit?: number } = {}): Array<{ memoryId: string; content: string; role: string; createdAt: number }> {
|
|
1173
|
+
if (patterns.length === 0) return [];
|
|
1174
|
+
const limit = opts.limit ?? 10;
|
|
1175
|
+
const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
|
|
1176
|
+
const params: (string | number)[] = [];
|
|
1177
|
+
for (const p of patterns) { params.push(`%${p}%`, `%${p}%`); }
|
|
1178
|
+
params.push(limit);
|
|
1179
|
+
try {
|
|
1180
|
+
const rows = this.db.prepare(`
|
|
1181
|
+
SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
|
|
1182
|
+
FROM hub_memories hm
|
|
1183
|
+
WHERE ${conditions.join(" OR ")}
|
|
1184
|
+
ORDER BY hm.created_at DESC
|
|
1185
|
+
LIMIT ?
|
|
1186
|
+
`).all(...params) as Array<{ memory_id: string; content: string; role: string; created_at: number }>;
|
|
1187
|
+
return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
|
|
1188
|
+
} catch { return []; }
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1050
1191
|
// ─── Vector Search ───
|
|
1051
1192
|
|
|
1052
1193
|
getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
|
|
@@ -1213,6 +1354,8 @@ export class SqliteStore {
|
|
|
1213
1354
|
"skill_embeddings",
|
|
1214
1355
|
"skill_versions",
|
|
1215
1356
|
"skills",
|
|
1357
|
+
"local_shared_memories",
|
|
1358
|
+
"local_shared_tasks",
|
|
1216
1359
|
"embeddings",
|
|
1217
1360
|
"chunks",
|
|
1218
1361
|
"tasks",
|
|
@@ -1633,16 +1776,18 @@ export class SqliteStore {
|
|
|
1633
1776
|
|
|
1634
1777
|
setClientHubConnection(conn: ClientHubConnection): void {
|
|
1635
1778
|
this.db.prepare(`
|
|
1636
|
-
INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
|
|
1637
|
-
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
1779
|
+
INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status)
|
|
1780
|
+
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1638
1781
|
ON CONFLICT(id) DO UPDATE SET
|
|
1639
1782
|
hub_url = excluded.hub_url,
|
|
1640
1783
|
user_id = excluded.user_id,
|
|
1641
1784
|
username = excluded.username,
|
|
1642
1785
|
user_token = excluded.user_token,
|
|
1643
1786
|
role = excluded.role,
|
|
1644
|
-
connected_at = excluded.connected_at
|
|
1645
|
-
|
|
1787
|
+
connected_at = excluded.connected_at,
|
|
1788
|
+
identity_key = excluded.identity_key,
|
|
1789
|
+
last_known_status = excluded.last_known_status
|
|
1790
|
+
`).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "");
|
|
1646
1791
|
}
|
|
1647
1792
|
|
|
1648
1793
|
getClientHubConnection(): ClientHubConnection | null {
|
|
@@ -1684,12 +1829,73 @@ export class SqliteStore {
|
|
|
1684
1829
|
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
1830
|
}
|
|
1686
1831
|
|
|
1832
|
+
// ─── Local Shared Memories (client-side tracking) ───
|
|
1833
|
+
|
|
1834
|
+
markMemorySharedLocally(chunkId: string): { ok: boolean; owner?: string; originalOwner?: string; sharedAt?: number; reason?: string } {
|
|
1835
|
+
const chunk = this.getChunk(chunkId);
|
|
1836
|
+
if (!chunk) return { ok: false, reason: "not_found" };
|
|
1837
|
+
if (chunk.owner === "public") {
|
|
1838
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1839
|
+
return {
|
|
1840
|
+
ok: true,
|
|
1841
|
+
owner: "public",
|
|
1842
|
+
originalOwner: existing?.originalOwner ?? undefined,
|
|
1843
|
+
sharedAt: existing?.sharedAt ?? undefined,
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const sharedAt = Date.now();
|
|
1848
|
+
this.db.transaction(() => {
|
|
1849
|
+
this.db.prepare(`
|
|
1850
|
+
INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
|
|
1851
|
+
VALUES (?, ?, ?)
|
|
1852
|
+
ON CONFLICT(chunk_id) DO UPDATE SET
|
|
1853
|
+
original_owner = excluded.original_owner,
|
|
1854
|
+
shared_at = excluded.shared_at
|
|
1855
|
+
`).run(chunkId, chunk.owner, sharedAt);
|
|
1856
|
+
this.updateChunk(chunkId, { owner: "public" });
|
|
1857
|
+
})();
|
|
1858
|
+
|
|
1859
|
+
return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
unmarkMemorySharedLocally(chunkId: string, fallbackOwner?: string): { ok: boolean; owner?: string; originalOwner?: string; reason?: string } {
|
|
1863
|
+
const chunk = this.getChunk(chunkId);
|
|
1864
|
+
if (!chunk) return { ok: false, reason: "not_found" };
|
|
1865
|
+
if (chunk.owner !== "public") {
|
|
1866
|
+
return { ok: true, owner: chunk.owner };
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1870
|
+
const restoreOwner = existing?.originalOwner ?? fallbackOwner;
|
|
1871
|
+
if (!restoreOwner || restoreOwner === "public") {
|
|
1872
|
+
return { ok: false, reason: "original_owner_missing" };
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
this.db.transaction(() => {
|
|
1876
|
+
this.updateChunk(chunkId, { owner: restoreOwner });
|
|
1877
|
+
this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
|
|
1878
|
+
})();
|
|
1879
|
+
|
|
1880
|
+
return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
getLocalSharedMemory(chunkId: string): { chunkId: string; originalOwner: string; sharedAt: number } | null {
|
|
1884
|
+
const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId) as any;
|
|
1885
|
+
if (!row) return null;
|
|
1886
|
+
return {
|
|
1887
|
+
chunkId: row.chunk_id,
|
|
1888
|
+
originalOwner: row.original_owner,
|
|
1889
|
+
sharedAt: row.shared_at,
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1687
1893
|
// ─── Hub Users / Groups ───
|
|
1688
1894
|
|
|
1689
1895
|
upsertHubUser(user: HubUserRecord): void {
|
|
1690
1896
|
this.db.prepare(`
|
|
1691
|
-
INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
|
|
1692
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1897
|
+
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)
|
|
1898
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1693
1899
|
ON CONFLICT(id) DO UPDATE SET
|
|
1694
1900
|
username = excluded.username,
|
|
1695
1901
|
device_name = excluded.device_name,
|
|
@@ -1697,81 +1903,64 @@ export class SqliteStore {
|
|
|
1697
1903
|
status = excluded.status,
|
|
1698
1904
|
token_hash = excluded.token_hash,
|
|
1699
1905
|
created_at = excluded.created_at,
|
|
1700
|
-
approved_at = excluded.approved_at
|
|
1701
|
-
|
|
1906
|
+
approved_at = excluded.approved_at,
|
|
1907
|
+
identity_key = excluded.identity_key,
|
|
1908
|
+
left_at = excluded.left_at,
|
|
1909
|
+
removed_at = excluded.removed_at,
|
|
1910
|
+
rejected_at = excluded.rejected_at,
|
|
1911
|
+
rejoin_requested_at = excluded.rejoin_requested_at
|
|
1912
|
+
`).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
1913
|
}
|
|
1703
1914
|
|
|
1704
1915
|
getHubUser(userId: string): HubUserRecord | null {
|
|
1705
1916
|
const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
|
|
1706
1917
|
if (!row) return null;
|
|
1707
|
-
return
|
|
1918
|
+
return rowToHubUser(row);
|
|
1708
1919
|
}
|
|
1709
1920
|
|
|
1710
1921
|
listHubUsers(status?: UserStatus): HubUserRecord[] {
|
|
1711
1922
|
const rows = status
|
|
1712
1923
|
? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
|
|
1713
1924
|
: this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
|
|
1714
|
-
return rows.map(
|
|
1925
|
+
return rows.map(rowToHubUser);
|
|
1715
1926
|
}
|
|
1716
1927
|
|
|
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);
|
|
1928
|
+
deleteHubUser(userId: string, cleanResources = false): boolean {
|
|
1929
|
+
if (cleanResources) {
|
|
1930
|
+
this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
|
|
1931
|
+
this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
|
|
1932
|
+
this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
|
|
1933
|
+
const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
|
|
1934
|
+
return result.changes > 0;
|
|
1935
|
+
}
|
|
1936
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
|
|
1937
|
+
return result.changes > 0;
|
|
1739
1938
|
}
|
|
1740
1939
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1940
|
+
findHubUserByIdentityKey(identityKey: string): HubUserRecord | null {
|
|
1941
|
+
if (!identityKey) return null;
|
|
1942
|
+
const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey) as HubUserRow | undefined;
|
|
1943
|
+
return row ? rowToHubUser(row) : null;
|
|
1744
1944
|
}
|
|
1745
1945
|
|
|
1746
|
-
|
|
1747
|
-
const result = this.db.prepare('
|
|
1946
|
+
markHubUserLeft(userId: string): boolean {
|
|
1947
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
|
|
1748
1948
|
return result.changes > 0;
|
|
1749
1949
|
}
|
|
1750
1950
|
|
|
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 }));
|
|
1951
|
+
updateHubUserActivity(userId: string, ip: string, timestamp?: number): void {
|
|
1952
|
+
this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
|
|
1760
1953
|
}
|
|
1761
1954
|
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
WHERE gm.user_id = ?
|
|
1772
|
-
ORDER BY g.name
|
|
1773
|
-
`).all(userId) as HubGroupRow[];
|
|
1774
|
-
return rows.map((row) => ({ id: row.id, name: row.name, description: row.description || undefined }));
|
|
1955
|
+
getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
|
|
1956
|
+
const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
|
|
1957
|
+
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 }>;
|
|
1958
|
+
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 }>;
|
|
1959
|
+
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 }>;
|
|
1960
|
+
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; }
|
|
1961
|
+
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; }
|
|
1962
|
+
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; }
|
|
1963
|
+
return result;
|
|
1775
1964
|
}
|
|
1776
1965
|
|
|
1777
1966
|
// ─── Hub Shared Data ───
|
|
@@ -1795,6 +1984,11 @@ export class SqliteStore {
|
|
|
1795
1984
|
return row ? rowToHubTask(row) : null;
|
|
1796
1985
|
}
|
|
1797
1986
|
|
|
1987
|
+
getHubTaskById(taskId: string): HubTaskRecord | null {
|
|
1988
|
+
const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId) as HubTaskRow | undefined;
|
|
1989
|
+
return row ? rowToHubTask(row) : null;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1798
1992
|
upsertHubChunk(chunk: HubChunkUpsertInput): void {
|
|
1799
1993
|
if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
|
|
1800
1994
|
const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
|
|
@@ -1870,6 +2064,18 @@ export class SqliteStore {
|
|
|
1870
2064
|
return out;
|
|
1871
2065
|
}
|
|
1872
2066
|
|
|
2067
|
+
getVisibleHubSkillEmbeddings(): Array<{ skillId: string; vector: Float32Array }> {
|
|
2068
|
+
const rows = this.db.prepare(`
|
|
2069
|
+
SELECT hse.skill_id, hse.vector, hse.dimensions
|
|
2070
|
+
FROM hub_skill_embeddings hse
|
|
2071
|
+
JOIN hub_skills hs ON hs.id = hse.skill_id
|
|
2072
|
+
`).all() as Array<{ skill_id: string; vector: Buffer; dimensions: number }>;
|
|
2073
|
+
return rows.map(r => ({
|
|
2074
|
+
skillId: r.skill_id,
|
|
2075
|
+
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
2076
|
+
}));
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1873
2079
|
searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
|
|
1874
2080
|
const limit = options?.maxResults ?? 10;
|
|
1875
2081
|
const userId = options?.userId ?? "";
|
|
@@ -1940,7 +2146,7 @@ export class SqliteStore {
|
|
|
1940
2146
|
let rows: HubSkillSearchRow[];
|
|
1941
2147
|
if (sanitized) {
|
|
1942
2148
|
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,
|
|
2149
|
+
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
2150
|
bm25(hub_skills_fts) as rank
|
|
1945
2151
|
FROM hub_skills_fts f
|
|
1946
2152
|
JOIN hub_skills hs ON hs.rowid = f.rowid
|
|
@@ -1951,7 +2157,7 @@ export class SqliteStore {
|
|
|
1951
2157
|
`).all(sanitized, limit) as HubSkillSearchRow[];
|
|
1952
2158
|
} else {
|
|
1953
2159
|
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,
|
|
2160
|
+
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
2161
|
0 as rank
|
|
1956
2162
|
FROM hub_skills hs
|
|
1957
2163
|
LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
|
|
@@ -1966,9 +2172,9 @@ export class SqliteStore {
|
|
|
1966
2172
|
this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ? AND source_skill_id = ?').run(sourceUserId, sourceSkillId);
|
|
1967
2173
|
}
|
|
1968
2174
|
|
|
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 }> {
|
|
2175
|
+
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
2176
|
const rows = this.db.prepare(`
|
|
1971
|
-
SELECT t.*, u.username AS owner_name, NULL AS group_name,
|
|
2177
|
+
SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
|
|
1972
2178
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
1973
2179
|
FROM hub_tasks t
|
|
1974
2180
|
LEFT JOIN hub_users u ON u.id = t.source_user_id
|
|
@@ -1978,36 +2184,40 @@ export class SqliteStore {
|
|
|
1978
2184
|
return rows.map(r => ({
|
|
1979
2185
|
id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
|
|
1980
2186
|
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,
|
|
2187
|
+
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
|
|
1982
2188
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
1983
2189
|
}));
|
|
1984
2190
|
}
|
|
1985
2191
|
|
|
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 }> {
|
|
2192
|
+
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
2193
|
const rows = this.db.prepare(`
|
|
1988
|
-
SELECT t.*, u.username AS owner_name,
|
|
2194
|
+
SELECT t.*, u.username AS owner_name, u.status AS owner_status,
|
|
1989
2195
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
1990
2196
|
FROM hub_tasks t
|
|
1991
2197
|
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
2198
|
ORDER BY t.updated_at DESC
|
|
1994
2199
|
`).all() as any[];
|
|
1995
2200
|
return rows.map(r => ({
|
|
1996
2201
|
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,
|
|
2202
|
+
title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
|
|
2203
|
+
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
|
|
1999
2204
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2000
2205
|
}));
|
|
2001
2206
|
}
|
|
2002
2207
|
|
|
2208
|
+
listHubChunksByTaskId(hubTaskId: string): HubChunkRecord[] {
|
|
2209
|
+
const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId) as HubChunkRow[];
|
|
2210
|
+
return rows.map(rowToHubChunk);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2003
2213
|
deleteHubTaskById(taskId: string): boolean {
|
|
2004
2214
|
const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
|
|
2005
2215
|
return info.changes > 0;
|
|
2006
2216
|
}
|
|
2007
2217
|
|
|
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 }> {
|
|
2218
|
+
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
2219
|
const rows = this.db.prepare(`
|
|
2010
|
-
SELECT s.*, u.username AS owner_name, NULL AS group_name
|
|
2220
|
+
SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
|
|
2011
2221
|
FROM hub_skills s
|
|
2012
2222
|
LEFT JOIN hub_users u ON u.id = s.source_user_id
|
|
2013
2223
|
ORDER BY s.updated_at DESC
|
|
@@ -2017,24 +2227,23 @@ export class SqliteStore {
|
|
|
2017
2227
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
2018
2228
|
name: r.name, description: r.description, version: r.version,
|
|
2019
2229
|
groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
|
|
2020
|
-
ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
|
|
2230
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
|
|
2021
2231
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2022
2232
|
}));
|
|
2023
2233
|
}
|
|
2024
2234
|
|
|
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 }> {
|
|
2235
|
+
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
2236
|
const rows = this.db.prepare(`
|
|
2027
|
-
SELECT s.*, u.username AS owner_name,
|
|
2237
|
+
SELECT s.*, u.username AS owner_name, u.status AS owner_status
|
|
2028
2238
|
FROM hub_skills s
|
|
2029
2239
|
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
2240
|
ORDER BY s.updated_at DESC
|
|
2032
2241
|
`).all() as any[];
|
|
2033
2242
|
return rows.map(r => ({
|
|
2034
2243
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
2035
2244
|
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,
|
|
2245
|
+
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2246
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
|
|
2038
2247
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2039
2248
|
}));
|
|
2040
2249
|
}
|
|
@@ -2081,6 +2290,47 @@ export class SqliteStore {
|
|
|
2081
2290
|
return info.changes > 0;
|
|
2082
2291
|
}
|
|
2083
2292
|
|
|
2293
|
+
// ─── Hub Notifications ───
|
|
2294
|
+
|
|
2295
|
+
insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
|
|
2296
|
+
this.db.prepare(
|
|
2297
|
+
'INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)'
|
|
2298
|
+
).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
hasRecentHubNotification(userId: string, type: string, resource: string, windowMs: number = 300_000): boolean {
|
|
2302
|
+
const since = Date.now() - windowMs;
|
|
2303
|
+
const row = this.db.prepare(
|
|
2304
|
+
'SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?'
|
|
2305
|
+
).get(userId, type, resource, since) as { cnt: number };
|
|
2306
|
+
return row.cnt > 0;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
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 }> {
|
|
2310
|
+
const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
|
|
2311
|
+
const limit = opts?.limit ?? 50;
|
|
2312
|
+
const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit) as any[];
|
|
2313
|
+
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 }));
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
countUnreadHubNotifications(userId: string): number {
|
|
2317
|
+
const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId) as { cnt: number };
|
|
2318
|
+
return row.cnt;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
markHubNotificationsRead(userId: string, ids?: string[]): void {
|
|
2322
|
+
if (ids && ids.length > 0) {
|
|
2323
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
2324
|
+
this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
|
|
2325
|
+
} else {
|
|
2326
|
+
this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
clearHubNotifications(userId: string): void {
|
|
2331
|
+
this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2084
2334
|
upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
|
|
2085
2335
|
const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
2086
2336
|
this.db.prepare(`
|
|
@@ -2138,9 +2388,9 @@ export class SqliteStore {
|
|
|
2138
2388
|
return row ?? null;
|
|
2139
2389
|
}
|
|
2140
2390
|
|
|
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 }> {
|
|
2391
|
+
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
2392
|
const rows = this.db.prepare(`
|
|
2143
|
-
SELECT m.*, u.username AS owner_name, NULL AS group_name
|
|
2393
|
+
SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
|
|
2144
2394
|
FROM hub_memories m
|
|
2145
2395
|
LEFT JOIN hub_users u ON u.id = m.source_user_id
|
|
2146
2396
|
ORDER BY m.updated_at DESC
|
|
@@ -2148,25 +2398,24 @@ export class SqliteStore {
|
|
|
2148
2398
|
`).all(limit) as any[];
|
|
2149
2399
|
return rows.map(r => ({
|
|
2150
2400
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
2151
|
-
role: r.role, summary: r.summary, kind: r.kind,
|
|
2401
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2152
2402
|
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,
|
|
2403
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2154
2404
|
}));
|
|
2155
2405
|
}
|
|
2156
2406
|
|
|
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 }> {
|
|
2407
|
+
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
2408
|
const rows = this.db.prepare(`
|
|
2159
|
-
SELECT m.*, u.username AS owner_name,
|
|
2409
|
+
SELECT m.*, u.username AS owner_name, u.status AS owner_status
|
|
2160
2410
|
FROM hub_memories m
|
|
2161
2411
|
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
2412
|
ORDER BY m.updated_at DESC
|
|
2164
2413
|
`).all() as any[];
|
|
2165
2414
|
return rows.map(r => ({
|
|
2166
2415
|
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,
|
|
2416
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2417
|
+
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2418
|
+
ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2170
2419
|
}));
|
|
2171
2420
|
}
|
|
2172
2421
|
|
|
@@ -2190,13 +2439,6 @@ export class SqliteStore {
|
|
|
2190
2439
|
throw new Error(`source skill not found for skillId=${skillId}`);
|
|
2191
2440
|
}
|
|
2192
2441
|
|
|
2193
|
-
private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
|
|
2194
|
-
return {
|
|
2195
|
-
...user,
|
|
2196
|
-
groups: this.getGroupsForHubUser(user.id),
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
2442
|
getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
|
|
2201
2443
|
const result = new Map<string, string>();
|
|
2202
2444
|
if (sessionKeys.length === 0) return result;
|
|
@@ -2382,6 +2624,8 @@ interface ClientHubConnection {
|
|
|
2382
2624
|
userToken: string;
|
|
2383
2625
|
role: UserRole;
|
|
2384
2626
|
connectedAt: number;
|
|
2627
|
+
identityKey?: string;
|
|
2628
|
+
lastKnownStatus?: string;
|
|
2385
2629
|
}
|
|
2386
2630
|
|
|
2387
2631
|
interface ClientHubConnectionRow {
|
|
@@ -2391,6 +2635,8 @@ interface ClientHubConnectionRow {
|
|
|
2391
2635
|
user_token: string;
|
|
2392
2636
|
role: string;
|
|
2393
2637
|
connected_at: number;
|
|
2638
|
+
identity_key?: string;
|
|
2639
|
+
last_known_status?: string;
|
|
2394
2640
|
}
|
|
2395
2641
|
|
|
2396
2642
|
function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnection {
|
|
@@ -2401,6 +2647,8 @@ function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnect
|
|
|
2401
2647
|
userToken: row.user_token,
|
|
2402
2648
|
role: row.role as UserRole,
|
|
2403
2649
|
connectedAt: row.connected_at,
|
|
2650
|
+
identityKey: row.identity_key || "",
|
|
2651
|
+
lastKnownStatus: row.last_known_status || "",
|
|
2404
2652
|
};
|
|
2405
2653
|
}
|
|
2406
2654
|
|
|
@@ -2408,6 +2656,13 @@ interface HubUserRecord extends UserInfo {
|
|
|
2408
2656
|
tokenHash: string;
|
|
2409
2657
|
createdAt: number;
|
|
2410
2658
|
approvedAt: number | null;
|
|
2659
|
+
lastIp: string;
|
|
2660
|
+
lastActiveAt: number | null;
|
|
2661
|
+
identityKey?: string;
|
|
2662
|
+
leftAt?: number | null;
|
|
2663
|
+
removedAt?: number | null;
|
|
2664
|
+
rejectedAt?: number | null;
|
|
2665
|
+
rejoinRequestedAt?: number | null;
|
|
2411
2666
|
}
|
|
2412
2667
|
|
|
2413
2668
|
interface HubUserRow {
|
|
@@ -2419,6 +2674,13 @@ interface HubUserRow {
|
|
|
2419
2674
|
token_hash: string;
|
|
2420
2675
|
created_at: number;
|
|
2421
2676
|
approved_at: number | null;
|
|
2677
|
+
last_ip: string;
|
|
2678
|
+
last_active_at: number | null;
|
|
2679
|
+
identity_key?: string;
|
|
2680
|
+
left_at?: number | null;
|
|
2681
|
+
removed_at?: number | null;
|
|
2682
|
+
rejected_at?: number | null;
|
|
2683
|
+
rejoin_requested_at?: number | null;
|
|
2422
2684
|
}
|
|
2423
2685
|
|
|
2424
2686
|
function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
@@ -2432,29 +2694,13 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
|
2432
2694
|
tokenHash: row.token_hash,
|
|
2433
2695
|
createdAt: row.created_at,
|
|
2434
2696
|
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,
|
|
2697
|
+
lastIp: row.last_ip || "",
|
|
2698
|
+
lastActiveAt: row.last_active_at ?? null,
|
|
2699
|
+
identityKey: row.identity_key || "",
|
|
2700
|
+
leftAt: row.left_at ?? null,
|
|
2701
|
+
removedAt: row.removed_at ?? null,
|
|
2702
|
+
rejectedAt: row.rejected_at ?? null,
|
|
2703
|
+
rejoinRequestedAt: row.rejoin_requested_at ?? null,
|
|
2458
2704
|
};
|
|
2459
2705
|
}
|
|
2460
2706
|
|
|
@@ -2603,6 +2849,7 @@ interface HubSkillSearchRow {
|
|
|
2603
2849
|
visibility: string;
|
|
2604
2850
|
group_name: string | null;
|
|
2605
2851
|
owner_name: string | null;
|
|
2852
|
+
owner_status: string | null;
|
|
2606
2853
|
quality_score: number | null;
|
|
2607
2854
|
}
|
|
2608
2855
|
|