@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/viewer/server.ts
CHANGED
|
@@ -224,6 +224,11 @@ export class ViewerServer {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
if (p === "/api/memories" && req.method === "GET") this.serveMemories(res, url);
|
|
227
|
+
else if (p === "/api/memories/share-local" && req.method === "POST") this.handleMemoryLocalShare(req, res);
|
|
228
|
+
else if (p === "/api/memories/unshare-local" && req.method === "POST") this.handleMemoryLocalUnshare(req, res);
|
|
229
|
+
else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT") this.handleMemoryScope(req, res, p);
|
|
230
|
+
else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT") this.handleTaskScope(req, res, p);
|
|
231
|
+
else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT") this.handleSkillScope(req, res, p);
|
|
227
232
|
else if (p === "/api/stats") this.serveStats(res, url);
|
|
228
233
|
else if (p === "/api/metrics") this.serveMetrics(res, url);
|
|
229
234
|
else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
|
|
@@ -251,6 +256,8 @@ export class ViewerServer {
|
|
|
251
256
|
else if (p === "/api/sharing/pending-users" && req.method === "GET") this.serveSharingPendingUsers(res);
|
|
252
257
|
else if (p === "/api/sharing/approve-user" && req.method === "POST") this.handleSharingApproveUser(req, res);
|
|
253
258
|
else if (p === "/api/sharing/reject-user" && req.method === "POST") this.handleSharingRejectUser(req, res);
|
|
259
|
+
else if (p === "/api/sharing/remove-user" && req.method === "POST") this.handleSharingRemoveUser(req, res);
|
|
260
|
+
else if (p === "/api/sharing/change-role" && req.method === "POST") this.handleSharingChangeRole(req, res);
|
|
254
261
|
else if (p === "/api/sharing/retry-join" && req.method === "POST") this.handleRetryJoin(req, res);
|
|
255
262
|
else if (p === "/api/sharing/search/memories" && req.method === "POST") this.handleSharingMemorySearch(req, res);
|
|
256
263
|
else if (p === "/api/sharing/memories/list" && req.method === "GET") this.serveSharingMemoryList(res, url);
|
|
@@ -261,20 +268,17 @@ export class ViewerServer {
|
|
|
261
268
|
else if (p === "/api/sharing/tasks/share" && req.method === "POST") this.handleSharingTaskShare(req, res);
|
|
262
269
|
else if (p === "/api/sharing/tasks/unshare" && req.method === "POST") this.handleSharingTaskUnshare(req, res);
|
|
263
270
|
else if (p === "/api/sharing/update-username" && req.method === "POST") this.handleUpdateUsername(req, res);
|
|
271
|
+
else if (p === "/api/sharing/rename-user" && req.method === "POST") this.handleAdminRenameUser(req, res);
|
|
264
272
|
else if (p === "/api/sharing/test-hub" && req.method === "POST") this.handleTestHubConnection(req, res);
|
|
265
273
|
else if (p === "/api/sharing/memories/share" && req.method === "POST") this.handleSharingMemoryShare(req, res);
|
|
266
274
|
else if (p === "/api/sharing/memories/unshare" && req.method === "POST") this.handleSharingMemoryUnshare(req, res);
|
|
267
275
|
else if (p === "/api/sharing/skills/pull" && req.method === "POST") this.handleSharingSkillPull(req, res);
|
|
268
276
|
else if (p === "/api/sharing/skills/share" && req.method === "POST") this.handleSharingSkillShare(req, res);
|
|
269
277
|
else if (p === "/api/sharing/skills/unshare" && req.method === "POST") this.handleSharingSkillUnshare(req, res);
|
|
270
|
-
else if (p === "/api/sharing/groups" && req.method === "GET") this.serveSharingGroups(res);
|
|
271
|
-
else if (p === "/api/sharing/groups" && req.method === "POST") this.handleSharingGroupCreate(req, res);
|
|
272
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT") this.handleSharingGroupUpdate(req, res, p);
|
|
273
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE") this.handleSharingGroupDelete(res, p);
|
|
274
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET") this.serveSharingGroupMembers(res, p);
|
|
275
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST") this.handleSharingGroupAddMember(req, res, p);
|
|
276
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE") this.handleSharingGroupRemoveMember(req, res, p);
|
|
277
278
|
else if (p === "/api/sharing/users" && req.method === "GET") this.serveSharingUsers(res);
|
|
279
|
+
else if (p === "/api/sharing/notifications" && req.method === "GET") this.serveSharingNotifications(res, url);
|
|
280
|
+
else if (p === "/api/sharing/notifications/read" && req.method === "POST") this.handleSharingNotificationsRead(req, res);
|
|
281
|
+
else if (p === "/api/sharing/notifications/clear" && req.method === "POST") this.handleSharingNotificationsClear(req, res);
|
|
278
282
|
else if (p === "/api/admin/shared-tasks" && req.method === "GET") this.serveAdminSharedTasks(res);
|
|
279
283
|
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteTask(res, p);
|
|
280
284
|
else if (p === "/api/admin/shared-skills" && req.method === "GET") this.serveAdminSharedSkills(res);
|
|
@@ -443,11 +447,14 @@ export class ViewerServer {
|
|
|
443
447
|
|
|
444
448
|
const chunkIds = rawMemories.map((m: any) => m.id);
|
|
445
449
|
const sharingMap = new Map<string, { visibility: string; group_id: string | null }>();
|
|
450
|
+
const localShareMap = new Map<string, { original_owner: string; shared_at: number }>();
|
|
446
451
|
if (chunkIds.length > 0) {
|
|
447
452
|
try {
|
|
448
453
|
const placeholders = chunkIds.map(() => "?").join(",");
|
|
449
454
|
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ source_chunk_id: string; visibility: string; group_id: string | null }>;
|
|
450
455
|
for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
|
|
456
|
+
const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; original_owner: string; shared_at: number }>;
|
|
457
|
+
for (const r of localRows) localShareMap.set(r.chunk_id, r);
|
|
451
458
|
} catch {
|
|
452
459
|
}
|
|
453
460
|
}
|
|
@@ -458,8 +465,12 @@ export class ViewerServer {
|
|
|
458
465
|
out.merge_sources = sources;
|
|
459
466
|
}
|
|
460
467
|
const shared = sharingMap.get(m.id);
|
|
468
|
+
const localShared = localShareMap.get(m.id);
|
|
461
469
|
out.sharingVisibility = shared?.visibility ?? null;
|
|
462
470
|
out.sharingGroupId = shared?.group_id ?? null;
|
|
471
|
+
out.localSharing = out.owner === "public";
|
|
472
|
+
out.localSharingManaged = !!localShared;
|
|
473
|
+
out.localOriginalOwner = localShared?.original_owner ?? null;
|
|
463
474
|
return out;
|
|
464
475
|
});
|
|
465
476
|
|
|
@@ -477,7 +488,21 @@ export class ViewerServer {
|
|
|
477
488
|
}
|
|
478
489
|
|
|
479
490
|
private serveToolMetrics(res: http.ServerResponse, url: URL): void {
|
|
480
|
-
const
|
|
491
|
+
const fromParam = url.searchParams.get("from");
|
|
492
|
+
const toParam = url.searchParams.get("to");
|
|
493
|
+
if (fromParam) {
|
|
494
|
+
const fromMs = new Date(fromParam).getTime();
|
|
495
|
+
const toMs = toParam ? new Date(toParam).getTime() : Date.now();
|
|
496
|
+
if (isNaN(fromMs) || isNaN(toMs)) {
|
|
497
|
+
this.jsonResponse(res, { error: "Invalid date" }, 400);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
|
|
501
|
+
const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
|
|
502
|
+
this.jsonResponse(res, data);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
|
|
481
506
|
const data = this.store.getToolMetrics(minutes);
|
|
482
507
|
this.jsonResponse(res, data);
|
|
483
508
|
}
|
|
@@ -491,7 +516,8 @@ export class ViewerServer {
|
|
|
491
516
|
|
|
492
517
|
const db = (this.store as any).db;
|
|
493
518
|
const items = tasks.map((t) => {
|
|
494
|
-
const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null } | undefined;
|
|
519
|
+
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null; owner: string | null } | undefined;
|
|
520
|
+
const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id) as { visibility: string } | undefined;
|
|
495
521
|
return {
|
|
496
522
|
id: t.id,
|
|
497
523
|
sessionKey: t.sessionKey,
|
|
@@ -502,6 +528,8 @@ export class ViewerServer {
|
|
|
502
528
|
endedAt: t.endedAt,
|
|
503
529
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
504
530
|
skillStatus: meta?.skill_status ?? null,
|
|
531
|
+
owner: meta?.owner ?? "agent:main",
|
|
532
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
505
533
|
};
|
|
506
534
|
});
|
|
507
535
|
|
|
@@ -544,6 +572,7 @@ export class ViewerServer {
|
|
|
544
572
|
title: task.title,
|
|
545
573
|
summary: task.summary,
|
|
546
574
|
status: task.status,
|
|
575
|
+
owner: task.owner ?? "agent:main",
|
|
547
576
|
startedAt: task.startedAt,
|
|
548
577
|
endedAt: task.endedAt,
|
|
549
578
|
chunks: chunkItems,
|
|
@@ -552,6 +581,7 @@ export class ViewerServer {
|
|
|
552
581
|
skillLinks,
|
|
553
582
|
sharingVisibility: sharedTask?.visibility ?? null,
|
|
554
583
|
sharingGroupId: sharedTask?.group_id ?? null,
|
|
584
|
+
hubTaskId: sharedTask ? true : false,
|
|
555
585
|
});
|
|
556
586
|
}
|
|
557
587
|
|
|
@@ -727,7 +757,12 @@ export class ViewerServer {
|
|
|
727
757
|
if (visibility) {
|
|
728
758
|
skills = skills.filter(s => s.visibility === visibility);
|
|
729
759
|
}
|
|
730
|
-
this.
|
|
760
|
+
const db = (this.store as any).db;
|
|
761
|
+
const enriched = skills.map(s => {
|
|
762
|
+
const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id) as { visibility: string } | undefined;
|
|
763
|
+
return { ...s, sharingVisibility: hub?.visibility ?? null };
|
|
764
|
+
});
|
|
765
|
+
this.jsonResponse(res, { skills: enriched });
|
|
731
766
|
}
|
|
732
767
|
|
|
733
768
|
private serveSkillDetail(res: http.ServerResponse, urlPath: string): void {
|
|
@@ -1029,7 +1064,15 @@ export class ViewerServer {
|
|
|
1029
1064
|
const cleaned = chunk.role === "user" && chunk.content
|
|
1030
1065
|
? { ...chunk, content: stripInboundMetadata(chunk.content) }
|
|
1031
1066
|
: chunk;
|
|
1032
|
-
this.
|
|
1067
|
+
const localShared = this.store.getLocalSharedMemory(chunkId);
|
|
1068
|
+
this.jsonResponse(res, {
|
|
1069
|
+
memory: {
|
|
1070
|
+
...cleaned,
|
|
1071
|
+
localSharing: cleaned.owner === "public",
|
|
1072
|
+
localSharingManaged: !!localShared,
|
|
1073
|
+
localOriginalOwner: localShared?.originalOwner ?? null,
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1033
1076
|
}
|
|
1034
1077
|
|
|
1035
1078
|
private handleUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
@@ -1058,6 +1101,340 @@ export class ViewerServer {
|
|
|
1058
1101
|
else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
|
|
1059
1102
|
}
|
|
1060
1103
|
|
|
1104
|
+
private handleMemoryLocalShare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1105
|
+
this.readBody(req, (body) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const parsed = JSON.parse(body || "{}");
|
|
1108
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1109
|
+
if (!chunkId) return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1110
|
+
const result = this.store.markMemorySharedLocally(chunkId);
|
|
1111
|
+
if (!result.ok) {
|
|
1112
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1113
|
+
}
|
|
1114
|
+
this.jsonResponse(res, {
|
|
1115
|
+
ok: true,
|
|
1116
|
+
chunkId,
|
|
1117
|
+
owner: result.owner,
|
|
1118
|
+
localSharing: true,
|
|
1119
|
+
localSharingManaged: true,
|
|
1120
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1121
|
+
});
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
private handleMemoryLocalUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1129
|
+
this.readBody(req, (body) => {
|
|
1130
|
+
try {
|
|
1131
|
+
const parsed = JSON.parse(body || "{}");
|
|
1132
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1133
|
+
const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
|
|
1134
|
+
if (!chunkId) return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1135
|
+
const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1136
|
+
if (!result.ok) {
|
|
1137
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1138
|
+
}
|
|
1139
|
+
this.jsonResponse(res, {
|
|
1140
|
+
ok: true,
|
|
1141
|
+
chunkId,
|
|
1142
|
+
owner: result.owner,
|
|
1143
|
+
localSharing: false,
|
|
1144
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1145
|
+
});
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ─── Unified scope API ───
|
|
1153
|
+
|
|
1154
|
+
private handleMemoryScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1155
|
+
const chunkId = urlPath.split("/")[3];
|
|
1156
|
+
this.readBody(req, async (body) => {
|
|
1157
|
+
try {
|
|
1158
|
+
const parsed = JSON.parse(body || "{}");
|
|
1159
|
+
const scope = parsed.scope as string;
|
|
1160
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1161
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1162
|
+
}
|
|
1163
|
+
const db = (this.store as any).db;
|
|
1164
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
|
|
1165
|
+
if (!chunk) return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
|
|
1166
|
+
|
|
1167
|
+
if (chunk.dedup_status && chunk.dedup_status !== "active") {
|
|
1168
|
+
return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const isLocalShared = chunk.owner === "public";
|
|
1172
|
+
const hubMemory = this.getHubMemoryForChunk(chunkId);
|
|
1173
|
+
const isTeamShared = !!hubMemory;
|
|
1174
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1175
|
+
|
|
1176
|
+
if (scope === currentScope) {
|
|
1177
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
let hubSynced = false;
|
|
1181
|
+
|
|
1182
|
+
if (scope === "team") {
|
|
1183
|
+
if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
|
|
1184
|
+
if (!isTeamShared) {
|
|
1185
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1186
|
+
const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
|
|
1187
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
1188
|
+
method: "POST",
|
|
1189
|
+
body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
|
|
1190
|
+
});
|
|
1191
|
+
if (hubClient.userId) {
|
|
1192
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
|
|
1193
|
+
this.store.upsertHubMemory({
|
|
1194
|
+
id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
|
|
1195
|
+
sourceChunkId: chunkId, sourceUserId: hubClient.userId,
|
|
1196
|
+
role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
|
|
1197
|
+
kind: refreshedChunk.kind, groupId: null, visibility: "public",
|
|
1198
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
hubSynced = true;
|
|
1202
|
+
}
|
|
1203
|
+
} else if (scope === "local") {
|
|
1204
|
+
if (isTeamShared) {
|
|
1205
|
+
try {
|
|
1206
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1207
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1208
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1209
|
+
});
|
|
1210
|
+
if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1211
|
+
hubSynced = true;
|
|
1212
|
+
} catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
|
|
1213
|
+
}
|
|
1214
|
+
if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
|
|
1215
|
+
} else {
|
|
1216
|
+
if (isTeamShared) {
|
|
1217
|
+
try {
|
|
1218
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1219
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1220
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1221
|
+
});
|
|
1222
|
+
if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1223
|
+
hubSynced = true;
|
|
1224
|
+
} catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
|
|
1225
|
+
}
|
|
1226
|
+
if (isLocalShared) this.store.unmarkMemorySharedLocally(chunkId);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
private handleTaskScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1237
|
+
const taskId = urlPath.split("/")[3];
|
|
1238
|
+
this.readBody(req, async (body) => {
|
|
1239
|
+
try {
|
|
1240
|
+
const parsed = JSON.parse(body || "{}");
|
|
1241
|
+
const scope = parsed.scope as string;
|
|
1242
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1243
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1244
|
+
}
|
|
1245
|
+
const task = this.store.getTask(taskId);
|
|
1246
|
+
if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
|
|
1247
|
+
|
|
1248
|
+
if (scope !== "private" && task.status !== "completed") {
|
|
1249
|
+
return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const isLocalShared = task.owner === "public";
|
|
1253
|
+
const hubTask = this.getHubTaskForLocal(taskId);
|
|
1254
|
+
const isTeamShared = !!hubTask;
|
|
1255
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1256
|
+
|
|
1257
|
+
if (scope === currentScope) {
|
|
1258
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
let hubSynced = false;
|
|
1262
|
+
|
|
1263
|
+
if (scope === "local" || scope === "team") {
|
|
1264
|
+
if (!isLocalShared) {
|
|
1265
|
+
const originalOwner = task.owner;
|
|
1266
|
+
const db = (this.store as any).db;
|
|
1267
|
+
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
|
|
1268
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (scope === "team") {
|
|
1273
|
+
if (!isTeamShared) {
|
|
1274
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
1275
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1276
|
+
const refreshedTask = this.store.getTask(taskId)!;
|
|
1277
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1278
|
+
method: "POST",
|
|
1279
|
+
body: JSON.stringify({
|
|
1280
|
+
task: { id: refreshedTask.id, sourceTaskId: refreshedTask.id, title: refreshedTask.title, summary: refreshedTask.summary, groupId: null, visibility: "public", createdAt: refreshedTask.startedAt ?? Date.now(), updatedAt: refreshedTask.updatedAt ?? Date.now() },
|
|
1281
|
+
chunks: chunks.map((c) => ({ id: c.id, hubTaskId: refreshedTask.id, sourceTaskId: refreshedTask.id, sourceChunkId: c.id, role: c.role, content: c.content, summary: c.summary, kind: c.kind, groupId: null, visibility: "public", createdAt: c.createdAt ?? Date.now() })),
|
|
1282
|
+
}),
|
|
1283
|
+
});
|
|
1284
|
+
if (hubClient.userId) {
|
|
1285
|
+
const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
|
|
1286
|
+
this.store.upsertHubTask({
|
|
1287
|
+
id: (response as any)?.taskId ?? existing?.id ?? crypto.randomUUID(),
|
|
1288
|
+
sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
|
|
1289
|
+
summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
|
|
1290
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
hubSynced = true;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (scope === "local" && isTeamShared) {
|
|
1298
|
+
try {
|
|
1299
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1300
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1301
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1302
|
+
});
|
|
1303
|
+
if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1304
|
+
hubSynced = true;
|
|
1305
|
+
} catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (scope === "private") {
|
|
1309
|
+
if (isTeamShared) {
|
|
1310
|
+
try {
|
|
1311
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1312
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1313
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1314
|
+
});
|
|
1315
|
+
if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1316
|
+
hubSynced = true;
|
|
1317
|
+
} catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
|
|
1318
|
+
}
|
|
1319
|
+
if (isLocalShared) {
|
|
1320
|
+
const db = (this.store as any).db;
|
|
1321
|
+
const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId) as any;
|
|
1322
|
+
const restoreOwner = shared?.original_owner ?? task.owner;
|
|
1323
|
+
if (restoreOwner && restoreOwner !== "public") {
|
|
1324
|
+
db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
|
|
1325
|
+
}
|
|
1326
|
+
db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private handleSkillScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1338
|
+
const skillId = urlPath.split("/")[3];
|
|
1339
|
+
this.readBody(req, async (body) => {
|
|
1340
|
+
try {
|
|
1341
|
+
const parsed = JSON.parse(body || "{}");
|
|
1342
|
+
const scope = parsed.scope as string;
|
|
1343
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1344
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1345
|
+
}
|
|
1346
|
+
const skill = this.store.getSkill(skillId);
|
|
1347
|
+
if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
|
|
1348
|
+
|
|
1349
|
+
if (scope !== "private" && skill.status !== "active") {
|
|
1350
|
+
return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const isLocalShared = skill.visibility === "public";
|
|
1354
|
+
const hubSkill = this.getHubSkillForLocal(skillId);
|
|
1355
|
+
const isTeamShared = !!hubSkill;
|
|
1356
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1357
|
+
|
|
1358
|
+
if (scope === currentScope) {
|
|
1359
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
let hubSynced = false;
|
|
1363
|
+
|
|
1364
|
+
if (scope === "local" || scope === "team") {
|
|
1365
|
+
if (!isLocalShared) this.store.setSkillVisibility(skillId, "public");
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (scope === "team") {
|
|
1369
|
+
if (!isTeamShared) {
|
|
1370
|
+
const bundle = buildSkillBundleForHub(this.store, skillId);
|
|
1371
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1372
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
1373
|
+
method: "POST",
|
|
1374
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
1375
|
+
});
|
|
1376
|
+
if (hubClient.userId) {
|
|
1377
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
1378
|
+
this.store.upsertHubSkill({
|
|
1379
|
+
id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
|
|
1380
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
1381
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
1382
|
+
groupId: null, visibility: "public",
|
|
1383
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
1384
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
hubSynced = true;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if (scope === "local" && isTeamShared) {
|
|
1392
|
+
try {
|
|
1393
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1394
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1395
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1396
|
+
});
|
|
1397
|
+
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1398
|
+
hubSynced = true;
|
|
1399
|
+
} catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (scope === "private") {
|
|
1403
|
+
if (isTeamShared) {
|
|
1404
|
+
try {
|
|
1405
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1406
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1407
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1408
|
+
});
|
|
1409
|
+
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1410
|
+
hubSynced = true;
|
|
1411
|
+
} catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
|
|
1412
|
+
}
|
|
1413
|
+
if (isLocalShared) this.store.setSkillVisibility(skillId, "private");
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
private getHubMemoryForChunk(chunkId: string): any {
|
|
1424
|
+
const db = (this.store as any).db;
|
|
1425
|
+
return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
private getHubTaskForLocal(taskId: string): any {
|
|
1429
|
+
const db = (this.store as any).db;
|
|
1430
|
+
return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
private getHubSkillForLocal(skillId: string): any {
|
|
1434
|
+
const db = (this.store as any).db;
|
|
1435
|
+
return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1061
1438
|
private handleDeleteSession(res: http.ServerResponse, url: URL): void {
|
|
1062
1439
|
const key = url.searchParams.get("key");
|
|
1063
1440
|
if (!key) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Missing key" })); return; }
|
|
@@ -1157,8 +1534,7 @@ export class ViewerServer {
|
|
|
1157
1534
|
base.connection.connected = true;
|
|
1158
1535
|
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1159
1536
|
|
|
1160
|
-
|
|
1161
|
-
let adminUser: any = { username: "hub-admin", role: "admin", groups: [] };
|
|
1537
|
+
let adminUser: any = { username: "hub-admin", role: "admin" };
|
|
1162
1538
|
try {
|
|
1163
1539
|
const hub = this.resolveHubConnection();
|
|
1164
1540
|
if (hub) {
|
|
@@ -1168,7 +1544,6 @@ export class ViewerServer {
|
|
|
1168
1544
|
id: me.id,
|
|
1169
1545
|
username: me.username ?? "hub-admin",
|
|
1170
1546
|
role: me.role ?? "admin",
|
|
1171
|
-
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
1172
1547
|
};
|
|
1173
1548
|
}
|
|
1174
1549
|
}
|
|
@@ -1269,6 +1644,60 @@ export class ViewerServer {
|
|
|
1269
1644
|
});
|
|
1270
1645
|
}
|
|
1271
1646
|
|
|
1647
|
+
private handleSharingChangeRole(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1648
|
+
this.readBody(req, async (body) => {
|
|
1649
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1650
|
+
try {
|
|
1651
|
+
const parsed = JSON.parse(body || "{}");
|
|
1652
|
+
const hub = this.resolveHubConnection();
|
|
1653
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1654
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
|
|
1655
|
+
method: "POST",
|
|
1656
|
+
body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
|
|
1657
|
+
});
|
|
1658
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
private handleSharingRemoveUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1666
|
+
this.readBody(req, async (body) => {
|
|
1667
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1668
|
+
try {
|
|
1669
|
+
const parsed = JSON.parse(body || "{}");
|
|
1670
|
+
const hub = this.resolveHubConnection();
|
|
1671
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1672
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
|
|
1673
|
+
method: "POST",
|
|
1674
|
+
body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
|
|
1675
|
+
});
|
|
1676
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
private handleAdminRenameUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1684
|
+
this.readBody(req, async (body) => {
|
|
1685
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1686
|
+
try {
|
|
1687
|
+
const parsed = JSON.parse(body || "{}");
|
|
1688
|
+
const hub = this.resolveHubConnection();
|
|
1689
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1690
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
|
|
1691
|
+
method: "POST",
|
|
1692
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1693
|
+
});
|
|
1694
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1272
1701
|
private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1273
1702
|
this.readBody(req, async (_body) => {
|
|
1274
1703
|
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
@@ -1720,117 +2149,6 @@ export class ViewerServer {
|
|
|
1720
2149
|
return resolveHubClient(this.store, this.ctx);
|
|
1721
2150
|
}
|
|
1722
2151
|
|
|
1723
|
-
private extractGroupId(path: string): string {
|
|
1724
|
-
const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
|
|
1725
|
-
return m ? decodeURIComponent(m[1]) : "";
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
private async serveSharingGroups(res: http.ServerResponse): Promise<void> {
|
|
1729
|
-
const hub = this.resolveHubConnection();
|
|
1730
|
-
if (!hub) return this.jsonResponse(res, { groups: [], error: "not_configured" });
|
|
1731
|
-
try {
|
|
1732
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" }) as any;
|
|
1733
|
-
this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
|
|
1734
|
-
} catch (err) {
|
|
1735
|
-
this.jsonResponse(res, { groups: [], error: String(err) });
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
private handleSharingGroupCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1740
|
-
this.readBody(req, async (body) => {
|
|
1741
|
-
const hub = this.resolveHubConnection();
|
|
1742
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1743
|
-
try {
|
|
1744
|
-
const parsed = JSON.parse(body || "{}");
|
|
1745
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
|
|
1746
|
-
method: "POST",
|
|
1747
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1748
|
-
}) as any;
|
|
1749
|
-
this.jsonResponse(res, { ok: true, ...data });
|
|
1750
|
-
} catch (err) {
|
|
1751
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1752
|
-
}
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
private handleSharingGroupUpdate(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1757
|
-
this.readBody(req, async (body) => {
|
|
1758
|
-
const hub = this.resolveHubConnection();
|
|
1759
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1760
|
-
const groupId = this.extractGroupId(p);
|
|
1761
|
-
try {
|
|
1762
|
-
const parsed = JSON.parse(body || "{}");
|
|
1763
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
|
|
1764
|
-
method: "PUT",
|
|
1765
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1766
|
-
});
|
|
1767
|
-
this.jsonResponse(res, { ok: true });
|
|
1768
|
-
} catch (err) {
|
|
1769
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1770
|
-
}
|
|
1771
|
-
});
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
private async handleSharingGroupDelete(res: http.ServerResponse, p: string): Promise<void> {
|
|
1775
|
-
const hub = this.resolveHubConnection();
|
|
1776
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1777
|
-
const groupId = this.extractGroupId(p);
|
|
1778
|
-
try {
|
|
1779
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
|
|
1780
|
-
this.jsonResponse(res, { ok: true });
|
|
1781
|
-
} catch (err) {
|
|
1782
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
private async serveSharingGroupMembers(res: http.ServerResponse, p: string): Promise<void> {
|
|
1787
|
-
const hub = this.resolveHubConnection();
|
|
1788
|
-
if (!hub) return this.jsonResponse(res, { members: [], error: "not_configured" });
|
|
1789
|
-
const groupId = this.extractGroupId(p);
|
|
1790
|
-
try {
|
|
1791
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" }) as any;
|
|
1792
|
-
this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
|
|
1793
|
-
} catch (err) {
|
|
1794
|
-
this.jsonResponse(res, { members: [], error: String(err) });
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
private handleSharingGroupAddMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1799
|
-
this.readBody(req, async (body) => {
|
|
1800
|
-
const hub = this.resolveHubConnection();
|
|
1801
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1802
|
-
const groupId = this.extractGroupId(p);
|
|
1803
|
-
try {
|
|
1804
|
-
const parsed = JSON.parse(body || "{}");
|
|
1805
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1806
|
-
method: "POST",
|
|
1807
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1808
|
-
});
|
|
1809
|
-
this.jsonResponse(res, { ok: true });
|
|
1810
|
-
} catch (err) {
|
|
1811
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1812
|
-
}
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
private handleSharingGroupRemoveMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1817
|
-
this.readBody(req, async (body) => {
|
|
1818
|
-
const hub = this.resolveHubConnection();
|
|
1819
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1820
|
-
const groupId = this.extractGroupId(p);
|
|
1821
|
-
try {
|
|
1822
|
-
const parsed = JSON.parse(body || "{}");
|
|
1823
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1824
|
-
method: "DELETE",
|
|
1825
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1826
|
-
});
|
|
1827
|
-
this.jsonResponse(res, { ok: true });
|
|
1828
|
-
} catch (err) {
|
|
1829
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1830
|
-
}
|
|
1831
|
-
});
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
2152
|
private async serveSharingUsers(res: http.ServerResponse): Promise<void> {
|
|
1835
2153
|
const hub = this.resolveHubConnection();
|
|
1836
2154
|
if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
@@ -1849,7 +2167,14 @@ export class ViewerServer {
|
|
|
1849
2167
|
if (!hub) return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
1850
2168
|
try {
|
|
1851
2169
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" }) as any;
|
|
1852
|
-
|
|
2170
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
2171
|
+
for (const tk of tasks) {
|
|
2172
|
+
if (!tk.summary && tk.sourceTaskId) {
|
|
2173
|
+
const local = this.store.getTask(tk.sourceTaskId);
|
|
2174
|
+
if (local) { tk.summary = local.summary; tk.title = tk.title || local.title; }
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
this.jsonResponse(res, { tasks });
|
|
1853
2178
|
} catch (err) {
|
|
1854
2179
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1855
2180
|
}
|
|
@@ -1872,7 +2197,14 @@ export class ViewerServer {
|
|
|
1872
2197
|
if (!hub) return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
1873
2198
|
try {
|
|
1874
2199
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" }) as any;
|
|
1875
|
-
|
|
2200
|
+
const skills = Array.isArray(data?.skills) ? data.skills : [];
|
|
2201
|
+
for (const sk of skills) {
|
|
2202
|
+
if (!sk.description && sk.sourceSkillId) {
|
|
2203
|
+
const local = this.store.getSkill(sk.sourceSkillId);
|
|
2204
|
+
if (local) { sk.description = sk.description || local.description; sk.name = sk.name || local.name; }
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
this.jsonResponse(res, { skills });
|
|
1876
2208
|
} catch (err) {
|
|
1877
2209
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1878
2210
|
}
|
|
@@ -1895,7 +2227,14 @@ export class ViewerServer {
|
|
|
1895
2227
|
if (!hub) return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
1896
2228
|
try {
|
|
1897
2229
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" }) as any;
|
|
1898
|
-
|
|
2230
|
+
const memories = Array.isArray(data?.memories) ? data.memories : [];
|
|
2231
|
+
for (const m of memories) {
|
|
2232
|
+
if (!m.content && m.sourceChunkId) {
|
|
2233
|
+
const local = this.store.getChunk(m.sourceChunkId);
|
|
2234
|
+
if (local) { m.content = local.content; if (!m.summary && local.summary) m.summary = local.summary; }
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
this.jsonResponse(res, { memories });
|
|
1899
2238
|
} catch (err) {
|
|
1900
2239
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
1901
2240
|
}
|
|
@@ -1913,6 +2252,45 @@ export class ViewerServer {
|
|
|
1913
2252
|
}
|
|
1914
2253
|
}
|
|
1915
2254
|
|
|
2255
|
+
private async serveSharingNotifications(res: http.ServerResponse, url: URL): Promise<void> {
|
|
2256
|
+
const hub = this.resolveHubConnection();
|
|
2257
|
+
if (!hub) return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2258
|
+
try {
|
|
2259
|
+
const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
|
|
2260
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`) as any;
|
|
2261
|
+
this.jsonResponse(res, data);
|
|
2262
|
+
} catch {
|
|
2263
|
+
this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
private handleSharingNotificationsRead(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2268
|
+
const hub = this.resolveHubConnection();
|
|
2269
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2270
|
+
this.readBody(req, async (raw) => {
|
|
2271
|
+
try {
|
|
2272
|
+
const body = JSON.parse(raw || "{}");
|
|
2273
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2274
|
+
this.jsonResponse(res, { ok: true });
|
|
2275
|
+
} catch (err) {
|
|
2276
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
private handleSharingNotificationsClear(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2282
|
+
const hub = this.resolveHubConnection();
|
|
2283
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2284
|
+
this.readBody(req, async () => {
|
|
2285
|
+
try {
|
|
2286
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2287
|
+
this.jsonResponse(res, { ok: true });
|
|
2288
|
+
} catch (err) {
|
|
2289
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
|
|
1916
2294
|
private getLocalIPs(): string[] {
|
|
1917
2295
|
const nets = os.networkInterfaces();
|
|
1918
2296
|
const ips: string[] = [];
|