@memtensor/memos-local-openclaw-plugin 1.0.4-beta.5 → 1.0.4-beta.7
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/README.md +23 -23
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +28 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +1 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +18 -19
- 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/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +101 -81
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +2 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +5 -1
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/storage/sqlite.d.ts +54 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +185 -101
- package/dist/storage/sqlite.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/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +1619 -629
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +14 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +545 -141
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +355 -41
- package/package.json +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +29 -1
- package/src/client/connector.ts +15 -21
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/hub/server.ts +88 -74
- package/src/hub/user-manager.ts +7 -3
- package/src/index.ts +7 -2
- package/src/storage/sqlite.ts +192 -122
- package/src/tools/memory-search.ts +2 -1
- package/src/viewer/html.ts +1619 -629
- package/src/viewer/server.ts +506 -128
package/src/hub/user-manager.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "crypto";
|
|
2
|
-
import { issueUserToken } from "./auth";
|
|
2
|
+
import { issueUserToken, verifyUserToken } from "./auth";
|
|
3
3
|
import type { Logger } from "../types";
|
|
4
4
|
import type { UserInfo } from "../sharing/types";
|
|
5
5
|
import type { SqliteStore } from "../storage/sqlite";
|
|
6
6
|
|
|
7
|
-
type ManagedHubUser = UserInfo & { tokenHash: string; createdAt: number; approvedAt: number | null };
|
|
7
|
+
type ManagedHubUser = UserInfo & { tokenHash: string; createdAt: number; approvedAt: number | null; lastIp: string; lastActiveAt: number | null };
|
|
8
8
|
|
|
9
9
|
export class HubUserManager {
|
|
10
10
|
constructor(private store: SqliteStore, private log: Logger) {}
|
|
@@ -20,6 +20,8 @@ export class HubUserManager {
|
|
|
20
20
|
tokenHash: "",
|
|
21
21
|
createdAt: Date.now(),
|
|
22
22
|
approvedAt: null,
|
|
23
|
+
lastIp: "",
|
|
24
|
+
lastActiveAt: null,
|
|
23
25
|
};
|
|
24
26
|
this.store.upsertHubUser(user);
|
|
25
27
|
return user;
|
|
@@ -46,7 +48,7 @@ export class HubUserManager {
|
|
|
46
48
|
if (bootstrapUserId) {
|
|
47
49
|
const bootstrapUser = this.store.getHubUser(bootstrapUserId);
|
|
48
50
|
if (bootstrapUser && bootstrapUser.role === "admin" && bootstrapUser.status === "active") {
|
|
49
|
-
if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex")) {
|
|
51
|
+
if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex") && verifyUserToken(bootstrapToken, secret)) {
|
|
50
52
|
return { user: bootstrapUser, token: bootstrapToken };
|
|
51
53
|
}
|
|
52
54
|
const refreshedToken = issueUserToken(
|
|
@@ -88,6 +90,8 @@ export class HubUserManager {
|
|
|
88
90
|
tokenHash: "",
|
|
89
91
|
createdAt: Date.now(),
|
|
90
92
|
approvedAt: Date.now(),
|
|
93
|
+
lastIp: "",
|
|
94
|
+
lastActiveAt: null,
|
|
91
95
|
};
|
|
92
96
|
const token = issueUserToken(
|
|
93
97
|
{ userId: user.id, username: user.username, role: user.role, status: user.status },
|
package/src/index.ts
CHANGED
|
@@ -64,8 +64,10 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
|
|
|
64
64
|
const worker = new IngestWorker(store, embedder, ctx);
|
|
65
65
|
const engine = new RecallEngine(store, embedder, ctx);
|
|
66
66
|
|
|
67
|
+
const sharedState = { lastSearchTime: 0 };
|
|
68
|
+
|
|
67
69
|
const tools: ToolDefinition[] = [
|
|
68
|
-
createMemorySearchTool(engine, store, ctx),
|
|
70
|
+
createMemorySearchTool(engine, store, ctx, sharedState),
|
|
69
71
|
createMemoryTimelineTool(store),
|
|
70
72
|
createMemoryGetTool(store),
|
|
71
73
|
createNetworkMemoryDetailTool(store, ctx),
|
|
@@ -87,7 +89,10 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
|
|
|
87
89
|
const turnId = uuid();
|
|
88
90
|
const tag = ctx.config.capture?.evidenceWrapperTag ?? "STORED_MEMORY";
|
|
89
91
|
|
|
90
|
-
const
|
|
92
|
+
const userSearchTime = sharedState.lastSearchTime || 0;
|
|
93
|
+
sharedState.lastSearchTime = 0;
|
|
94
|
+
|
|
95
|
+
const captured = captureMessages(messages, session, turnId, tag, ctx.log, owner, userSearchTime);
|
|
91
96
|
if (captured.length > 0) {
|
|
92
97
|
worker.enqueue(captured);
|
|
93
98
|
}
|
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,7 @@ export class SqliteStore {
|
|
|
112
112
|
this.migrateSkillEmbeddingsAndFts();
|
|
113
113
|
this.migrateFtsToTrigram();
|
|
114
114
|
this.migrateHubTables();
|
|
115
|
+
this.migrateLocalSharedTasksOwner();
|
|
115
116
|
this.log.debug("Database schema initialized");
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -119,6 +120,16 @@ export class SqliteStore {
|
|
|
119
120
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
private migrateLocalSharedTasksOwner(): void {
|
|
124
|
+
try {
|
|
125
|
+
const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
|
|
126
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
|
|
127
|
+
this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
|
|
128
|
+
this.log.info("Migrated: added original_owner column to local_shared_tasks");
|
|
129
|
+
}
|
|
130
|
+
} catch { /* table may not exist yet */ }
|
|
131
|
+
}
|
|
132
|
+
|
|
122
133
|
private migrateOwnerFields(): void {
|
|
123
134
|
const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
124
135
|
if (!chunkCols.some((c) => c.name === "owner")) {
|
|
@@ -516,12 +527,13 @@ export class SqliteStore {
|
|
|
516
527
|
).run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
|
|
517
528
|
}
|
|
518
529
|
|
|
519
|
-
getToolMetrics(minutes: number): {
|
|
530
|
+
getToolMetrics(minutes: number, fromMs?: number, toMs?: number): {
|
|
520
531
|
tools: string[];
|
|
521
532
|
series: Array<{ minute: string; [tool: string]: number | string }>;
|
|
522
533
|
aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>;
|
|
523
534
|
} {
|
|
524
|
-
const since = Date.now() - minutes * 60 * 1000;
|
|
535
|
+
const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
|
|
536
|
+
const until = toMs ?? Date.now();
|
|
525
537
|
|
|
526
538
|
const rows = this.db.prepare(
|
|
527
539
|
`SELECT tool_name,
|
|
@@ -529,9 +541,9 @@ export class SqliteStore {
|
|
|
529
541
|
success,
|
|
530
542
|
strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
|
|
531
543
|
FROM tool_calls
|
|
532
|
-
WHERE called_at >= ?
|
|
544
|
+
WHERE called_at >= ? AND called_at <= ?
|
|
533
545
|
ORDER BY called_at`,
|
|
534
|
-
).all(since) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
|
|
546
|
+
).all(since, until) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>;
|
|
535
547
|
|
|
536
548
|
const toolSet = new Set<string>();
|
|
537
549
|
const minuteMap = new Map<string, Map<string, { total: number; count: number }>>();
|
|
@@ -683,34 +695,27 @@ export class SqliteStore {
|
|
|
683
695
|
shared_at INTEGER NOT NULL
|
|
684
696
|
);
|
|
685
697
|
|
|
698
|
+
CREATE TABLE IF NOT EXISTS local_shared_memories (
|
|
699
|
+
chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
|
|
700
|
+
original_owner TEXT NOT NULL,
|
|
701
|
+
shared_at INTEGER NOT NULL
|
|
702
|
+
);
|
|
703
|
+
|
|
686
704
|
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
|
|
705
|
+
id TEXT PRIMARY KEY,
|
|
706
|
+
username TEXT NOT NULL UNIQUE,
|
|
707
|
+
device_name TEXT NOT NULL DEFAULT '',
|
|
708
|
+
role TEXT NOT NULL,
|
|
709
|
+
status TEXT NOT NULL,
|
|
710
|
+
token_hash TEXT NOT NULL DEFAULT '',
|
|
711
|
+
created_at INTEGER NOT NULL,
|
|
712
|
+
approved_at INTEGER,
|
|
713
|
+
last_ip TEXT NOT NULL DEFAULT '',
|
|
714
|
+
last_active_at INTEGER
|
|
695
715
|
);
|
|
696
716
|
CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
|
|
697
717
|
CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
|
|
698
718
|
|
|
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
719
|
CREATE TABLE IF NOT EXISTS hub_tasks (
|
|
715
720
|
id TEXT PRIMARY KEY,
|
|
716
721
|
source_task_id TEXT NOT NULL,
|
|
@@ -872,6 +877,32 @@ export class SqliteStore {
|
|
|
872
877
|
VALUES (new.rowid, new.summary, new.content);
|
|
873
878
|
END;
|
|
874
879
|
`);
|
|
880
|
+
|
|
881
|
+
this.db.exec(`
|
|
882
|
+
CREATE TABLE IF NOT EXISTS hub_notifications (
|
|
883
|
+
id TEXT PRIMARY KEY,
|
|
884
|
+
user_id TEXT NOT NULL,
|
|
885
|
+
type TEXT NOT NULL,
|
|
886
|
+
resource TEXT NOT NULL,
|
|
887
|
+
title TEXT NOT NULL,
|
|
888
|
+
message TEXT NOT NULL DEFAULT '',
|
|
889
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
890
|
+
created_at INTEGER NOT NULL
|
|
891
|
+
);
|
|
892
|
+
CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
|
|
893
|
+
`);
|
|
894
|
+
|
|
895
|
+
try {
|
|
896
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
|
|
897
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
|
|
898
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
|
|
899
|
+
this.log.info("Migrated: added last_ip column to hub_users");
|
|
900
|
+
}
|
|
901
|
+
if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
|
|
902
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
|
|
903
|
+
this.log.info("Migrated: added last_active_at column to hub_users");
|
|
904
|
+
}
|
|
905
|
+
} catch { /* table may not exist yet */ }
|
|
875
906
|
}
|
|
876
907
|
|
|
877
908
|
// ─── Write ───
|
|
@@ -1213,6 +1244,8 @@ export class SqliteStore {
|
|
|
1213
1244
|
"skill_embeddings",
|
|
1214
1245
|
"skill_versions",
|
|
1215
1246
|
"skills",
|
|
1247
|
+
"local_shared_memories",
|
|
1248
|
+
"local_shared_tasks",
|
|
1216
1249
|
"embeddings",
|
|
1217
1250
|
"chunks",
|
|
1218
1251
|
"tasks",
|
|
@@ -1684,6 +1717,67 @@ export class SqliteStore {
|
|
|
1684
1717
|
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
1718
|
}
|
|
1686
1719
|
|
|
1720
|
+
// ─── Local Shared Memories (client-side tracking) ───
|
|
1721
|
+
|
|
1722
|
+
markMemorySharedLocally(chunkId: string): { ok: boolean; owner?: string; originalOwner?: string; sharedAt?: number; reason?: string } {
|
|
1723
|
+
const chunk = this.getChunk(chunkId);
|
|
1724
|
+
if (!chunk) return { ok: false, reason: "not_found" };
|
|
1725
|
+
if (chunk.owner === "public") {
|
|
1726
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1727
|
+
return {
|
|
1728
|
+
ok: true,
|
|
1729
|
+
owner: "public",
|
|
1730
|
+
originalOwner: existing?.originalOwner ?? undefined,
|
|
1731
|
+
sharedAt: existing?.sharedAt ?? undefined,
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const sharedAt = Date.now();
|
|
1736
|
+
this.db.transaction(() => {
|
|
1737
|
+
this.db.prepare(`
|
|
1738
|
+
INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
|
|
1739
|
+
VALUES (?, ?, ?)
|
|
1740
|
+
ON CONFLICT(chunk_id) DO UPDATE SET
|
|
1741
|
+
original_owner = excluded.original_owner,
|
|
1742
|
+
shared_at = excluded.shared_at
|
|
1743
|
+
`).run(chunkId, chunk.owner, sharedAt);
|
|
1744
|
+
this.updateChunk(chunkId, { owner: "public" });
|
|
1745
|
+
})();
|
|
1746
|
+
|
|
1747
|
+
return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
unmarkMemorySharedLocally(chunkId: string, fallbackOwner?: string): { ok: boolean; owner?: string; originalOwner?: string; reason?: string } {
|
|
1751
|
+
const chunk = this.getChunk(chunkId);
|
|
1752
|
+
if (!chunk) return { ok: false, reason: "not_found" };
|
|
1753
|
+
if (chunk.owner !== "public") {
|
|
1754
|
+
return { ok: true, owner: chunk.owner };
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const existing = this.getLocalSharedMemory(chunkId);
|
|
1758
|
+
const restoreOwner = existing?.originalOwner ?? fallbackOwner;
|
|
1759
|
+
if (!restoreOwner || restoreOwner === "public") {
|
|
1760
|
+
return { ok: false, reason: "original_owner_missing" };
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
this.db.transaction(() => {
|
|
1764
|
+
this.updateChunk(chunkId, { owner: restoreOwner });
|
|
1765
|
+
this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
|
|
1766
|
+
})();
|
|
1767
|
+
|
|
1768
|
+
return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
getLocalSharedMemory(chunkId: string): { chunkId: string; originalOwner: string; sharedAt: number } | null {
|
|
1772
|
+
const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId) as any;
|
|
1773
|
+
if (!row) return null;
|
|
1774
|
+
return {
|
|
1775
|
+
chunkId: row.chunk_id,
|
|
1776
|
+
originalOwner: row.original_owner,
|
|
1777
|
+
sharedAt: row.shared_at,
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1687
1781
|
// ─── Hub Users / Groups ───
|
|
1688
1782
|
|
|
1689
1783
|
upsertHubUser(user: HubUserRecord): void {
|
|
@@ -1704,74 +1798,39 @@ export class SqliteStore {
|
|
|
1704
1798
|
getHubUser(userId: string): HubUserRecord | null {
|
|
1705
1799
|
const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId) as HubUserRow | undefined;
|
|
1706
1800
|
if (!row) return null;
|
|
1707
|
-
return
|
|
1801
|
+
return rowToHubUser(row);
|
|
1708
1802
|
}
|
|
1709
1803
|
|
|
1710
1804
|
listHubUsers(status?: UserStatus): HubUserRecord[] {
|
|
1711
1805
|
const rows = status
|
|
1712
1806
|
? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status) as HubUserRow[]
|
|
1713
1807
|
: this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all() as HubUserRow[];
|
|
1714
|
-
return rows.map(
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
upsertHubGroup(group: HubGroupRecord): void {
|
|
1718
|
-
this.db.prepare(`
|
|
1719
|
-
INSERT INTO hub_groups (id, name, description, created_at)
|
|
1720
|
-
VALUES (?, ?, ?, ?)
|
|
1721
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
1722
|
-
name = excluded.name,
|
|
1723
|
-
description = excluded.description,
|
|
1724
|
-
created_at = excluded.created_at
|
|
1725
|
-
`).run(group.id, group.name, group.description, group.createdAt);
|
|
1808
|
+
return rows.map(rowToHubUser);
|
|
1726
1809
|
}
|
|
1727
1810
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
this.db.prepare(
|
|
1735
|
-
INSERT INTO hub_group_members (group_id, user_id, joined_at)
|
|
1736
|
-
VALUES (?, ?, ?)
|
|
1737
|
-
ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
|
|
1738
|
-
`).run(groupId, userId, joinedAt);
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
getHubGroupById(groupId: string): HubGroupRecord | undefined {
|
|
1742
|
-
const row = this.db.prepare('SELECT * FROM hub_groups WHERE id = ?').get(groupId) as HubGroupRow | undefined;
|
|
1743
|
-
return row ? rowToHubGroup(row) : undefined;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
deleteHubGroup(groupId: string): boolean {
|
|
1747
|
-
const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
|
|
1811
|
+
deleteHubUser(userId: string, cleanResources = false): boolean {
|
|
1812
|
+
if (cleanResources) {
|
|
1813
|
+
this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
|
|
1814
|
+
this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
|
|
1815
|
+
this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
|
|
1816
|
+
}
|
|
1817
|
+
const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
|
|
1748
1818
|
return result.changes > 0;
|
|
1749
1819
|
}
|
|
1750
1820
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
SELECT gm.user_id, hu.username, gm.joined_at
|
|
1754
|
-
FROM hub_group_members gm
|
|
1755
|
-
JOIN hub_users hu ON hu.id = gm.user_id
|
|
1756
|
-
WHERE gm.group_id = ?
|
|
1757
|
-
ORDER BY gm.joined_at
|
|
1758
|
-
`).all(groupId) as Array<{ user_id: string; username: string; joined_at: number }>;
|
|
1759
|
-
return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
removeHubGroupMember(groupId: string, userId: string): void {
|
|
1763
|
-
this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
|
|
1821
|
+
updateHubUserActivity(userId: string, ip: string): void {
|
|
1822
|
+
this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, Date.now(), userId);
|
|
1764
1823
|
}
|
|
1765
1824
|
|
|
1766
|
-
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
return
|
|
1825
|
+
getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
|
|
1826
|
+
const result: Record<string, { memoryCount: number; taskCount: number; skillCount: number }> = {};
|
|
1827
|
+
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 }>;
|
|
1828
|
+
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 }>;
|
|
1829
|
+
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 }>;
|
|
1830
|
+
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; }
|
|
1831
|
+
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; }
|
|
1832
|
+
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; }
|
|
1833
|
+
return result;
|
|
1775
1834
|
}
|
|
1776
1835
|
|
|
1777
1836
|
// ─── Hub Shared Data ───
|
|
@@ -1795,6 +1854,11 @@ export class SqliteStore {
|
|
|
1795
1854
|
return row ? rowToHubTask(row) : null;
|
|
1796
1855
|
}
|
|
1797
1856
|
|
|
1857
|
+
getHubTaskById(taskId: string): HubTaskRecord | null {
|
|
1858
|
+
const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId) as HubTaskRow | undefined;
|
|
1859
|
+
return row ? rowToHubTask(row) : null;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1798
1862
|
upsertHubChunk(chunk: HubChunkUpsertInput): void {
|
|
1799
1863
|
if (!chunk.sourceTaskId) throw new Error("sourceTaskId is required for hub chunk upserts");
|
|
1800
1864
|
const taskId = this.resolveCanonicalHubTaskId(chunk.hubTaskId, chunk.sourceUserId, chunk.sourceTaskId);
|
|
@@ -1985,16 +2049,15 @@ export class SqliteStore {
|
|
|
1985
2049
|
|
|
1986
2050
|
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 }> {
|
|
1987
2051
|
const rows = this.db.prepare(`
|
|
1988
|
-
SELECT t.*, u.username AS owner_name,
|
|
2052
|
+
SELECT t.*, u.username AS owner_name,
|
|
1989
2053
|
(SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
|
|
1990
2054
|
FROM hub_tasks t
|
|
1991
2055
|
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
2056
|
ORDER BY t.updated_at DESC
|
|
1994
2057
|
`).all() as any[];
|
|
1995
2058
|
return rows.map(r => ({
|
|
1996
2059
|
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:
|
|
2060
|
+
title: r.title, summary: r.summary, groupId: r.group_id, groupName: null as string | null,
|
|
1998
2061
|
visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
|
|
1999
2062
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2000
2063
|
}));
|
|
@@ -2024,16 +2087,15 @@ export class SqliteStore {
|
|
|
2024
2087
|
|
|
2025
2088
|
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 }> {
|
|
2026
2089
|
const rows = this.db.prepare(`
|
|
2027
|
-
SELECT s.*, u.username AS owner_name
|
|
2090
|
+
SELECT s.*, u.username AS owner_name
|
|
2028
2091
|
FROM hub_skills s
|
|
2029
2092
|
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
2093
|
ORDER BY s.updated_at DESC
|
|
2032
2094
|
`).all() as any[];
|
|
2033
2095
|
return rows.map(r => ({
|
|
2034
2096
|
id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
|
|
2035
2097
|
name: r.name, description: r.description, version: r.version,
|
|
2036
|
-
groupId: r.group_id, groupName:
|
|
2098
|
+
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2037
2099
|
ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
|
|
2038
2100
|
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2039
2101
|
}));
|
|
@@ -2081,6 +2143,39 @@ export class SqliteStore {
|
|
|
2081
2143
|
return info.changes > 0;
|
|
2082
2144
|
}
|
|
2083
2145
|
|
|
2146
|
+
// ─── Hub Notifications ───
|
|
2147
|
+
|
|
2148
|
+
insertHubNotification(n: { id: string; userId: string; type: string; resource: string; title: string; message?: string }): void {
|
|
2149
|
+
this.db.prepare(
|
|
2150
|
+
'INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)'
|
|
2151
|
+
).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
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 }> {
|
|
2155
|
+
const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
|
|
2156
|
+
const limit = opts?.limit ?? 50;
|
|
2157
|
+
const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit) as any[];
|
|
2158
|
+
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 }));
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
countUnreadHubNotifications(userId: string): number {
|
|
2162
|
+
const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId) as { cnt: number };
|
|
2163
|
+
return row.cnt;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
markHubNotificationsRead(userId: string, ids?: string[]): void {
|
|
2167
|
+
if (ids && ids.length > 0) {
|
|
2168
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
2169
|
+
this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
|
|
2170
|
+
} else {
|
|
2171
|
+
this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
clearHubNotifications(userId: string): void {
|
|
2176
|
+
this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2084
2179
|
upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
|
|
2085
2180
|
const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
2086
2181
|
this.db.prepare(`
|
|
@@ -2156,16 +2251,15 @@ export class SqliteStore {
|
|
|
2156
2251
|
|
|
2157
2252
|
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 }> {
|
|
2158
2253
|
const rows = this.db.prepare(`
|
|
2159
|
-
SELECT m.*, u.username AS owner_name
|
|
2254
|
+
SELECT m.*, u.username AS owner_name
|
|
2160
2255
|
FROM hub_memories m
|
|
2161
2256
|
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
2257
|
ORDER BY m.updated_at DESC
|
|
2164
2258
|
`).all() as any[];
|
|
2165
2259
|
return rows.map(r => ({
|
|
2166
2260
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
2167
2261
|
role: r.role, summary: r.summary, kind: r.kind,
|
|
2168
|
-
groupId: r.group_id, groupName:
|
|
2262
|
+
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2169
2263
|
ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2170
2264
|
}));
|
|
2171
2265
|
}
|
|
@@ -2190,13 +2284,6 @@ export class SqliteStore {
|
|
|
2190
2284
|
throw new Error(`source skill not found for skillId=${skillId}`);
|
|
2191
2285
|
}
|
|
2192
2286
|
|
|
2193
|
-
private attachGroupsToHubUser(user: HubUserRecord): HubUserRecord {
|
|
2194
|
-
return {
|
|
2195
|
-
...user,
|
|
2196
|
-
groups: this.getGroupsForHubUser(user.id),
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
2287
|
getSessionOwnerMap(sessionKeys: string[]): Map<string, string> {
|
|
2201
2288
|
const result = new Map<string, string>();
|
|
2202
2289
|
if (sessionKeys.length === 0) return result;
|
|
@@ -2408,6 +2495,8 @@ interface HubUserRecord extends UserInfo {
|
|
|
2408
2495
|
tokenHash: string;
|
|
2409
2496
|
createdAt: number;
|
|
2410
2497
|
approvedAt: number | null;
|
|
2498
|
+
lastIp: string;
|
|
2499
|
+
lastActiveAt: number | null;
|
|
2411
2500
|
}
|
|
2412
2501
|
|
|
2413
2502
|
interface HubUserRow {
|
|
@@ -2419,6 +2508,8 @@ interface HubUserRow {
|
|
|
2419
2508
|
token_hash: string;
|
|
2420
2509
|
created_at: number;
|
|
2421
2510
|
approved_at: number | null;
|
|
2511
|
+
last_ip: string;
|
|
2512
|
+
last_active_at: number | null;
|
|
2422
2513
|
}
|
|
2423
2514
|
|
|
2424
2515
|
function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
@@ -2432,29 +2523,8 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
|
2432
2523
|
tokenHash: row.token_hash,
|
|
2433
2524
|
createdAt: row.created_at,
|
|
2434
2525
|
approvedAt: row.approved_at,
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
interface HubGroupRecord {
|
|
2439
|
-
id: string;
|
|
2440
|
-
name: string;
|
|
2441
|
-
description: string;
|
|
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,
|
|
2526
|
+
lastIp: row.last_ip || "",
|
|
2527
|
+
lastActiveAt: row.last_active_at ?? null,
|
|
2458
2528
|
};
|
|
2459
2529
|
}
|
|
2460
2530
|
|
|
@@ -24,7 +24,7 @@ function emptyHubResult(scope: HubScope): HubSearchResult {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function createMemorySearchTool(engine: RecallEngine, store?: SqliteStore, ctx?: PluginContext): ToolDefinition {
|
|
27
|
+
export function createMemorySearchTool(engine: RecallEngine, store?: SqliteStore, ctx?: PluginContext, sharedState?: { lastSearchTime: number }): ToolDefinition {
|
|
28
28
|
return {
|
|
29
29
|
name: "memory_search",
|
|
30
30
|
description:
|
|
@@ -60,6 +60,7 @@ export function createMemorySearchTool(engine: RecallEngine, store?: SqliteStore
|
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
62
|
handler: async (input) => {
|
|
63
|
+
if (sharedState) sharedState.lastSearchTime = Date.now();
|
|
63
64
|
const query = (input.query as string) ?? "";
|
|
64
65
|
const maxResults = input.maxResults as number | undefined;
|
|
65
66
|
const minScore = input.minScore as number | undefined;
|