@memtensor/memos-local-openclaw-plugin 1.0.4-beta.10 → 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/dist/client/connector.d.ts +1 -0
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +37 -8
- package/dist/client/connector.js.map +1 -1
- package/dist/hub/server.d.ts +1 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +122 -28
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +9 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +26 -2
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +2 -0
- package/dist/recall/engine.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +56 -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/sqlite.d.ts +15 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +91 -9
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +10 -0
- 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 +44 -23
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +35 -15
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +316 -13
- package/package.json +1 -1
- package/src/client/connector.ts +41 -8
- package/src/hub/server.ts +123 -27
- package/src/hub/user-manager.ts +42 -6
- package/src/recall/engine.ts +2 -0
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +58 -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/sqlite.ts +105 -9
- package/src/types.ts +11 -0
- package/src/viewer/html.ts +44 -23
- package/src/viewer/server.ts +35 -15
package/src/hub/server.ts
CHANGED
|
@@ -169,6 +169,20 @@ export class HubServer {
|
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
private embedSkillAsync(skillId: string, name: string, description: string, sourceUserId: string, sourceSkillId: string): void {
|
|
173
|
+
const embedder = this.opts.embedder;
|
|
174
|
+
if (!embedder) return;
|
|
175
|
+
const text = `${name}: ${description}`;
|
|
176
|
+
embedder.embed([text]).then((vectors) => {
|
|
177
|
+
if (vectors[0]) {
|
|
178
|
+
this.opts.store.upsertHubSkillEmbedding(skillId, Array.from(vectors[0]), sourceUserId, sourceSkillId);
|
|
179
|
+
this.opts.log.info(`hub: embedded shared skill ${skillId}`);
|
|
180
|
+
}
|
|
181
|
+
}).catch((err) => {
|
|
182
|
+
this.opts.log.warn(`hub: embedding shared skill failed: ${err}`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
private embedMemoryAsync(memoryId: string, summary: string, content: string): void {
|
|
173
187
|
const embedder = this.opts.embedder;
|
|
174
188
|
if (!embedder) return;
|
|
@@ -205,46 +219,70 @@ export class HubServer {
|
|
|
205
219
|
|| (req.headers["x-client-ip"] as string)?.trim()
|
|
206
220
|
|| (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim()
|
|
207
221
|
|| req.socket.remoteAddress || "";
|
|
208
|
-
const
|
|
209
|
-
|
|
222
|
+
const identityKey = typeof body.identityKey === "string" ? body.identityKey.trim() : "";
|
|
223
|
+
|
|
224
|
+
let existingUser = identityKey
|
|
225
|
+
? this.userManager.findByIdentityKey(identityKey)
|
|
226
|
+
: null;
|
|
227
|
+
if (!existingUser) {
|
|
228
|
+
const existingUsers = this.opts.store.listHubUsers();
|
|
229
|
+
existingUser = existingUsers.find(u => u.username === username && u.status !== "left" && u.status !== "removed") ?? null;
|
|
230
|
+
}
|
|
231
|
+
|
|
210
232
|
if (existingUser) {
|
|
211
233
|
try { this.opts.store.updateHubUserActivity(existingUser.id, joinIp); } catch { /* best-effort */ }
|
|
234
|
+
|
|
212
235
|
if (existingUser.status === "active") {
|
|
213
236
|
const token = issueUserToken(
|
|
214
237
|
{ userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" },
|
|
215
238
|
this.authSecret,
|
|
216
239
|
);
|
|
217
240
|
this.userManager.approveUser(existingUser.id, token);
|
|
218
|
-
|
|
241
|
+
if (identityKey && !existingUser.identityKey) {
|
|
242
|
+
this.opts.store.upsertHubUser({ ...existingUser, identityKey });
|
|
243
|
+
}
|
|
244
|
+
return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token, identityKey: existingUser.identityKey || identityKey });
|
|
219
245
|
}
|
|
220
246
|
if (existingUser.status === "pending") {
|
|
221
247
|
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
222
|
-
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
248
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
223
249
|
}
|
|
224
250
|
if (existingUser.status === "rejected") {
|
|
225
251
|
if (body.reapply === true) {
|
|
226
252
|
this.userManager.resetToPending(existingUser.id);
|
|
227
253
|
this.notifyAdmins("user_join_request", "user", username, "");
|
|
228
254
|
this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
229
|
-
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
255
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
230
256
|
}
|
|
231
257
|
return this.json(res, 200, { status: "rejected", userId: existingUser.id });
|
|
232
258
|
}
|
|
233
259
|
if (existingUser.status === "removed") {
|
|
234
|
-
this.userManager.
|
|
235
|
-
this.notifyAdmins("user_join_request", "user", username, "");
|
|
236
|
-
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
237
|
-
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
260
|
+
this.userManager.rejoinUser(existingUser.id);
|
|
261
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
262
|
+
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
|
|
263
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
264
|
+
}
|
|
265
|
+
if (existingUser.status === "left") {
|
|
266
|
+
this.userManager.rejoinUser(existingUser.id);
|
|
267
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
268
|
+
this.opts.log.info(`Hub: left user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
|
|
269
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
270
|
+
}
|
|
271
|
+
if (existingUser.status === "blocked") {
|
|
272
|
+
return this.json(res, 200, { status: "blocked", userId: existingUser.id });
|
|
238
273
|
}
|
|
239
274
|
}
|
|
275
|
+
|
|
276
|
+
const generatedIdentityKey = identityKey || randomUUID();
|
|
240
277
|
const user = this.userManager.createPendingUser({
|
|
241
278
|
username,
|
|
242
279
|
deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
|
|
280
|
+
identityKey: generatedIdentityKey,
|
|
243
281
|
});
|
|
244
282
|
try { this.opts.store.updateHubUserActivity(user.id, joinIp); } catch { /* best-effort */ }
|
|
245
283
|
this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
|
|
246
284
|
this.notifyAdmins("user_join_request", "user", username, "");
|
|
247
|
-
return this.json(res, 200, { status: "pending", userId: user.id });
|
|
285
|
+
return this.json(res, 200, { status: "pending", userId: user.id, identityKey: generatedIdentityKey });
|
|
248
286
|
}
|
|
249
287
|
|
|
250
288
|
if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
|
|
@@ -262,6 +300,15 @@ export class HubServer {
|
|
|
262
300
|
if (user.status === "rejected") {
|
|
263
301
|
return this.json(res, 200, { status: "rejected" });
|
|
264
302
|
}
|
|
303
|
+
if (user.status === "blocked") {
|
|
304
|
+
return this.json(res, 200, { status: "blocked" });
|
|
305
|
+
}
|
|
306
|
+
if (user.status === "left") {
|
|
307
|
+
return this.json(res, 200, { status: "left" });
|
|
308
|
+
}
|
|
309
|
+
if (user.status === "removed") {
|
|
310
|
+
return this.json(res, 200, { status: "removed" });
|
|
311
|
+
}
|
|
265
312
|
if (user.status === "active") {
|
|
266
313
|
const token = issueUserToken(
|
|
267
314
|
{ userId: user.id, username: user.username, role: user.role, status: user.status },
|
|
@@ -286,12 +333,10 @@ export class HubServer {
|
|
|
286
333
|
}
|
|
287
334
|
|
|
288
335
|
if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
|
|
289
|
-
|
|
290
|
-
this.opts.store.updateHubUserActivity(auth.userId, "", 0);
|
|
291
|
-
} catch { /* best-effort */ }
|
|
336
|
+
this.userManager.markUserLeft(auth.userId);
|
|
292
337
|
this.knownOnlineUsers.delete(auth.userId);
|
|
293
338
|
this.notifyAdmins("user_offline", "user", auth.username, auth.userId);
|
|
294
|
-
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily`);
|
|
339
|
+
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily, status set to "left"`);
|
|
295
340
|
return this.json(res, 200, { ok: true });
|
|
296
341
|
}
|
|
297
342
|
|
|
@@ -609,20 +654,70 @@ export class HubServer {
|
|
|
609
654
|
}
|
|
610
655
|
|
|
611
656
|
if (req.method === "GET" && routePath === "/api/v1/hub/skills") {
|
|
612
|
-
const
|
|
657
|
+
const skillQuery = String(url.searchParams.get("query") || "");
|
|
658
|
+
const skillMaxResults = Number(url.searchParams.get("maxResults") || 10);
|
|
659
|
+
const ftsSkillHits = this.opts.store.searchHubSkills(skillQuery, {
|
|
613
660
|
userId: auth.userId,
|
|
614
|
-
maxResults:
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
661
|
+
maxResults: skillMaxResults * 2,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
let mergedSkillIds: string[];
|
|
665
|
+
if (this.opts.embedder && skillQuery) {
|
|
666
|
+
try {
|
|
667
|
+
const [queryVec] = await this.opts.embedder.embed([skillQuery]);
|
|
668
|
+
if (queryVec) {
|
|
669
|
+
const skillEmbs = this.opts.store.getVisibleHubSkillEmbeddings();
|
|
670
|
+
const cosineSim = (vec: Float32Array) => {
|
|
671
|
+
let dot = 0, nA = 0, nB = 0;
|
|
672
|
+
for (let i = 0; i < queryVec.length && i < vec.length; i++) {
|
|
673
|
+
dot += queryVec[i] * vec[i]; nA += queryVec[i] * queryVec[i]; nB += vec[i] * vec[i];
|
|
674
|
+
}
|
|
675
|
+
return nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
676
|
+
};
|
|
677
|
+
const vecScored = skillEmbs
|
|
678
|
+
.map(e => ({ id: e.skillId, score: cosineSim(e.vector) }))
|
|
679
|
+
.filter(e => e.score > 0.3)
|
|
680
|
+
.sort((a, b) => b.score - a.score)
|
|
681
|
+
.slice(0, skillMaxResults * 2);
|
|
682
|
+
|
|
683
|
+
const K = 60;
|
|
684
|
+
const rrfScores = new Map<string, number>();
|
|
685
|
+
ftsSkillHits.forEach(({ hit }, idx) => {
|
|
686
|
+
rrfScores.set(hit.id, (rrfScores.get(hit.id) ?? 0) + 1 / (K + idx + 1));
|
|
687
|
+
});
|
|
688
|
+
vecScored.forEach(({ id }, idx) => {
|
|
689
|
+
rrfScores.set(id, (rrfScores.get(id) ?? 0) + 1 / (K + idx + 1));
|
|
690
|
+
});
|
|
691
|
+
mergedSkillIds = [...rrfScores.entries()].sort((a, b) => b[1] - a[1]).slice(0, skillMaxResults).map(([id]) => id);
|
|
692
|
+
} else {
|
|
693
|
+
mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const ftsSkillMap = new Map(ftsSkillHits.map(({ hit }) => [hit.id, hit]));
|
|
703
|
+
const hits = mergedSkillIds.map(id => {
|
|
704
|
+
const hit = ftsSkillMap.get(id);
|
|
705
|
+
if (hit) {
|
|
706
|
+
return {
|
|
707
|
+
skillId: hit.id, name: hit.name, description: hit.description,
|
|
708
|
+
version: hit.version, visibility: hit.visibility, groupName: hit.group_name,
|
|
709
|
+
ownerName: hit.owner_name || "unknown", ownerStatus: hit.owner_status || "",
|
|
710
|
+
qualityScore: hit.quality_score,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const skill = this.opts.store.getHubSkillById(id);
|
|
714
|
+
if (!skill) return null;
|
|
715
|
+
return {
|
|
716
|
+
skillId: skill.id, name: skill.name, description: skill.description,
|
|
717
|
+
version: skill.version, visibility: skill.visibility, groupName: "",
|
|
718
|
+
ownerName: "unknown", ownerStatus: "", qualityScore: skill.qualityScore,
|
|
719
|
+
};
|
|
720
|
+
}).filter(Boolean);
|
|
626
721
|
return this.json(res, 200, { hits });
|
|
627
722
|
}
|
|
628
723
|
|
|
@@ -648,6 +743,7 @@ export class HubServer {
|
|
|
648
743
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
649
744
|
updatedAt: Date.now(),
|
|
650
745
|
});
|
|
746
|
+
this.embedSkillAsync(skillId, String(metadata.name || sourceSkillId), String(metadata.description || ""), auth.userId, sourceSkillId);
|
|
651
747
|
if (!existing) {
|
|
652
748
|
this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
|
|
653
749
|
}
|
package/src/hub/user-manager.ts
CHANGED
|
@@ -4,13 +4,24 @@ 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 & {
|
|
7
|
+
type ManagedHubUser = UserInfo & {
|
|
8
|
+
tokenHash: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
approvedAt: number | null;
|
|
11
|
+
lastIp: string;
|
|
12
|
+
lastActiveAt: number | null;
|
|
13
|
+
identityKey?: string;
|
|
14
|
+
leftAt?: number | null;
|
|
15
|
+
removedAt?: number | null;
|
|
16
|
+
rejectedAt?: number | null;
|
|
17
|
+
rejoinRequestedAt?: number | null;
|
|
18
|
+
};
|
|
8
19
|
|
|
9
20
|
export class HubUserManager {
|
|
10
21
|
constructor(private store: SqliteStore, private log: Logger) {}
|
|
11
22
|
|
|
12
|
-
createPendingUser(input: { username: string; deviceName?: string }): ManagedHubUser {
|
|
13
|
-
const user = {
|
|
23
|
+
createPendingUser(input: { username: string; deviceName?: string; identityKey?: string }): ManagedHubUser {
|
|
24
|
+
const user: ManagedHubUser = {
|
|
14
25
|
id: randomUUID(),
|
|
15
26
|
username: input.username,
|
|
16
27
|
deviceName: input.deviceName,
|
|
@@ -22,11 +33,36 @@ export class HubUserManager {
|
|
|
22
33
|
approvedAt: null,
|
|
23
34
|
lastIp: "",
|
|
24
35
|
lastActiveAt: null,
|
|
36
|
+
identityKey: input.identityKey || "",
|
|
25
37
|
};
|
|
26
38
|
this.store.upsertHubUser(user);
|
|
27
39
|
return user;
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
findByIdentityKey(identityKey: string): ManagedHubUser | null {
|
|
43
|
+
if (!identityKey) return null;
|
|
44
|
+
return this.store.findHubUserByIdentityKey(identityKey);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
markUserLeft(userId: string): boolean {
|
|
48
|
+
this.log.info(`Hub: user "${userId}" marked as left`);
|
|
49
|
+
return this.store.markHubUserLeft(userId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
rejoinUser(userId: string): ManagedHubUser | null {
|
|
53
|
+
const user = this.store.getHubUser(userId);
|
|
54
|
+
if (!user) return null;
|
|
55
|
+
const updated: ManagedHubUser = {
|
|
56
|
+
...user,
|
|
57
|
+
status: "pending" as const,
|
|
58
|
+
tokenHash: "",
|
|
59
|
+
rejoinRequestedAt: Date.now(),
|
|
60
|
+
};
|
|
61
|
+
this.store.upsertHubUser(updated);
|
|
62
|
+
this.log.info(`Hub: user "${userId}" (${user.username}) requested rejoin, previous status: ${user.status}`);
|
|
63
|
+
return updated;
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
listPendingUsers(): ManagedHubUser[] {
|
|
31
67
|
return this.store.listHubUsers("pending");
|
|
32
68
|
}
|
|
@@ -105,7 +141,7 @@ export class HubUserManager {
|
|
|
105
141
|
|
|
106
142
|
isUsernameTaken(username: string, excludeUserId?: string): boolean {
|
|
107
143
|
const users = this.store.listHubUsers();
|
|
108
|
-
return users.some(u => u.username === username && u.id !== excludeUserId);
|
|
144
|
+
return users.some(u => u.username === username && u.id !== excludeUserId && u.status !== "left" && u.status !== "removed");
|
|
109
145
|
}
|
|
110
146
|
|
|
111
147
|
updateUsername(userId: string, newUsername: string): ManagedHubUser | null {
|
|
@@ -119,10 +155,10 @@ export class HubUserManager {
|
|
|
119
155
|
rejectUser(userId: string): ManagedHubUser | null {
|
|
120
156
|
const user = this.store.getHubUser(userId);
|
|
121
157
|
if (!user) return null;
|
|
122
|
-
const updated = {
|
|
158
|
+
const updated: ManagedHubUser = {
|
|
123
159
|
...user,
|
|
124
160
|
status: "rejected" as const,
|
|
125
|
-
|
|
161
|
+
rejectedAt: Date.now(),
|
|
126
162
|
};
|
|
127
163
|
this.store.upsertHubUser(updated);
|
|
128
164
|
return updated;
|
package/src/recall/engine.ts
CHANGED
|
@@ -202,6 +202,7 @@ export class RecallEngine {
|
|
|
202
202
|
taskId: null,
|
|
203
203
|
skillId: null,
|
|
204
204
|
owner: `hub-user:${mem.sourceUserId}`,
|
|
205
|
+
origin: "hub-memory",
|
|
205
206
|
source: {
|
|
206
207
|
ts: mem.createdAt,
|
|
207
208
|
role: (mem.role || "assistant") as any,
|
|
@@ -228,6 +229,7 @@ export class RecallEngine {
|
|
|
228
229
|
score: Math.round(candidate.score * 1000) / 1000,
|
|
229
230
|
taskId: chunk.taskId,
|
|
230
231
|
skillId: chunk.skillId,
|
|
232
|
+
origin: chunk.owner === "public" ? "local-shared" : "local",
|
|
231
233
|
source: {
|
|
232
234
|
ts: chunk.createdAt,
|
|
233
235
|
role: chunk.role,
|
package/src/sharing/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
export type HubScope = "local" | "group" | "all";
|
|
13
13
|
export type SharedVisibility = "group" | "public";
|
|
14
14
|
export type UserRole = "admin" | "member";
|
|
15
|
-
export type UserStatus = "pending" | "active" | "blocked" | "rejected" | "removed";
|
|
15
|
+
export type UserStatus = "pending" | "active" | "blocked" | "rejected" | "removed" | "left";
|
|
16
16
|
|
|
17
17
|
export type { ClientModeConfig, HubModeConfig, SharingCapabilities, SharingConfig, SharingRole };
|
|
18
18
|
|
package/src/skill/evolver.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { DEFAULTS } from "../types";
|
|
|
9
9
|
import { SkillEvaluator } from "./evaluator";
|
|
10
10
|
import { SkillGenerator } from "./generator";
|
|
11
11
|
import { SkillUpgrader } from "./upgrader";
|
|
12
|
-
import { SkillInstaller } from "./installer";
|
|
12
|
+
import { SkillInstaller, type SkillInstallMode } from "./installer";
|
|
13
13
|
import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
|
|
14
14
|
|
|
15
15
|
export type SkillEvolvedCallback = (skillName: string, upgradeType: "created" | "upgraded") => void;
|
|
@@ -96,10 +96,19 @@ export class SkillEvolver {
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const preferUpgrade = this.ctx.config.skillEvolution?.preferUpgradeExisting ?? DEFAULTS.skillPreferUpgrade;
|
|
99
100
|
const relatedSkill = await this.findRelatedSkill(task);
|
|
100
101
|
|
|
101
102
|
if (relatedSkill) {
|
|
102
103
|
await this.handleExistingSkill(task, chunks, relatedSkill);
|
|
104
|
+
} else if (preferUpgrade) {
|
|
105
|
+
const nameCandidate = await this.findSkillByNameSimilarity(task);
|
|
106
|
+
if (nameCandidate) {
|
|
107
|
+
this.ctx.log.info(`SkillEvolver: preferUpgrade found name-similar skill "${nameCandidate.name}" for task "${task.title}"`);
|
|
108
|
+
await this.handleExistingSkill(task, chunks, nameCandidate);
|
|
109
|
+
} else {
|
|
110
|
+
await this.handleNewSkill(task, chunks);
|
|
111
|
+
}
|
|
103
112
|
} else {
|
|
104
113
|
await this.handleNewSkill(task, chunks);
|
|
105
114
|
}
|
|
@@ -281,7 +290,11 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
281
290
|
|
|
282
291
|
if (upgraded) {
|
|
283
292
|
this.store.linkTaskSkill(task.id, freshSkill.id, "evolved_from", freshSkill.version + 1);
|
|
284
|
-
|
|
293
|
+
if (freshSkill.installed) {
|
|
294
|
+
this.installer.syncIfInstalled(freshSkill.name);
|
|
295
|
+
} else {
|
|
296
|
+
this.autoInstallIfNeeded(freshSkill);
|
|
297
|
+
}
|
|
285
298
|
this.onSkillEvolved?.(freshSkill.name, "upgraded");
|
|
286
299
|
} else {
|
|
287
300
|
this.store.linkTaskSkill(task.id, freshSkill.id, "applied_to", freshSkill.version);
|
|
@@ -304,6 +317,13 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
304
317
|
const evalResult = await this.evaluator.evaluateCreate(task);
|
|
305
318
|
|
|
306
319
|
if (evalResult.shouldGenerate && evalResult.confidence >= minConfidence) {
|
|
320
|
+
const existingByName = this.store.getSkillByName(evalResult.suggestedName);
|
|
321
|
+
if (existingByName && (existingByName.status === "active" || existingByName.status === "draft")) {
|
|
322
|
+
this.ctx.log.info(`SkillEvolver: skill "${evalResult.suggestedName}" already exists, redirecting to upgrade instead of create`);
|
|
323
|
+
await this.handleExistingSkill(task, chunks, existingByName);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
307
327
|
this.ctx.log.info(`SkillEvolver: generating new skill "${evalResult.suggestedName}" — ${evalResult.reason}`);
|
|
308
328
|
this.store.setTaskSkillMeta(task.id, { skillStatus: "generating", skillReason: evalResult.reason });
|
|
309
329
|
|
|
@@ -313,10 +333,7 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
313
333
|
this.store.setTaskSkillMeta(task.id, { skillStatus: "generated", skillReason: evalResult.reason });
|
|
314
334
|
this.onSkillEvolved?.(skill.name, "created");
|
|
315
335
|
|
|
316
|
-
|
|
317
|
-
if (autoInstall && skill.status === "active") {
|
|
318
|
-
this.installer.install(skill.id);
|
|
319
|
-
}
|
|
336
|
+
this.autoInstallIfNeeded(skill);
|
|
320
337
|
} else {
|
|
321
338
|
const reason = evalResult.reason || `confidence不足 (${evalResult.confidence} < ${minConfidence})`;
|
|
322
339
|
this.ctx.log.debug(`SkillEvolver: task "${task.title}" not worth generating skill — ${reason}`);
|
|
@@ -331,6 +348,41 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
331
348
|
this.ctx.log.debug(`SkillEvolver: marked ${chunks.length} chunks with skill_id=${skillId}`);
|
|
332
349
|
}
|
|
333
350
|
|
|
351
|
+
private async findSkillByNameSimilarity(task: Task): Promise<Skill | null> {
|
|
352
|
+
const query = task.title.slice(0, 200);
|
|
353
|
+
const owner = task.owner ?? "agent:main";
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const ftsHits = this.store.skillFtsSearch(query, 5, "mix", owner);
|
|
357
|
+
for (const hit of ftsHits) {
|
|
358
|
+
if (hit.score < 0.5) continue;
|
|
359
|
+
const skill = this.store.getSkill(hit.skillId);
|
|
360
|
+
if (skill && (skill.status === "active" || skill.status === "draft")) {
|
|
361
|
+
return skill;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch { /* best-effort */ }
|
|
365
|
+
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private autoInstallIfNeeded(skill: Skill): void {
|
|
370
|
+
if (skill.status !== "active") return;
|
|
371
|
+
|
|
372
|
+
const explicitAutoInstall = this.ctx.config.skillEvolution?.autoInstall ?? DEFAULTS.skillAutoInstall;
|
|
373
|
+
if (explicitAutoInstall) {
|
|
374
|
+
this.installer.install(skill.id);
|
|
375
|
+
this.ctx.log.info(`SkillEvolver: auto-installed "${skill.name}" (explicit autoInstall=true)`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const manifest = SkillInstaller.buildManifest(skill.dirPath, !!skill.installed, skill.name);
|
|
380
|
+
if (manifest.installMode === "install_recommended") {
|
|
381
|
+
this.installer.install(skill.id);
|
|
382
|
+
this.ctx.log.info(`SkillEvolver: auto-installed "${skill.name}" (install_recommended: ${manifest.scriptsCount} scripts, ${Math.round(manifest.totalSize / 1024)}KB)`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
334
386
|
private readSkillContent(skill: Skill): string | null {
|
|
335
387
|
const filePath = path.join(skill.dirPath, "SKILL.md");
|
|
336
388
|
try {
|
package/src/skill/generator.ts
CHANGED
|
@@ -484,14 +484,55 @@ export class SkillGenerator {
|
|
|
484
484
|
|
|
485
485
|
private buildConversationText(chunks: Chunk[]): string {
|
|
486
486
|
const lines: string[] = [];
|
|
487
|
+
const redact = this.ctx.config.skillEvolution?.redactSensitiveInSkill ?? true;
|
|
488
|
+
|
|
487
489
|
for (const c of chunks) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
490
|
+
let roleLabel: string;
|
|
491
|
+
switch (c.role) {
|
|
492
|
+
case "user": roleLabel = "User"; break;
|
|
493
|
+
case "assistant": roleLabel = "Assistant"; break;
|
|
494
|
+
case "tool": roleLabel = "Tool"; break;
|
|
495
|
+
case "system": roleLabel = "System"; break;
|
|
496
|
+
default: continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let content = c.content;
|
|
500
|
+
if (c.role === "system") continue;
|
|
501
|
+
|
|
502
|
+
if (c.role === "tool") {
|
|
503
|
+
content = this.truncateToolOutput(content);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (redact) {
|
|
507
|
+
content = SkillGenerator.redactSensitive(content);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
lines.push(`[${roleLabel}]: ${content}`);
|
|
491
511
|
}
|
|
492
512
|
return lines.join("\n\n");
|
|
493
513
|
}
|
|
494
514
|
|
|
515
|
+
private truncateToolOutput(content: string): string {
|
|
516
|
+
const MAX_TOOL_OUTPUT = 1500;
|
|
517
|
+
if (content.length <= MAX_TOOL_OUTPUT) return content;
|
|
518
|
+
const head = content.slice(0, MAX_TOOL_OUTPUT * 0.6);
|
|
519
|
+
const tail = content.slice(-MAX_TOOL_OUTPUT * 0.3);
|
|
520
|
+
return `${head}\n... (truncated ${content.length - MAX_TOOL_OUTPUT} chars) ...\n${tail}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
static redactSensitive(text: string): string {
|
|
524
|
+
let result = text;
|
|
525
|
+
result = result.replace(/\bsk-[a-zA-Z0-9]{20,}\b/g, "sk-***REDACTED***");
|
|
526
|
+
result = result.replace(/\bBearer\s+[a-zA-Z0-9_\-.]{20,}\b/g, "Bearer ***REDACTED***");
|
|
527
|
+
result = result.replace(/\bAKIA[0-9A-Z]{16}\b/g, "AKIA***REDACTED***");
|
|
528
|
+
result = result.replace(/(api[_-]?key|secret|token|password|credential)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
529
|
+
(match, key) => `${key}="***REDACTED***"`);
|
|
530
|
+
result = result.replace(/\/Users\/[a-zA-Z0-9._-]+\//g, "/Users/****/");
|
|
531
|
+
result = result.replace(/\/home\/[a-zA-Z0-9._-]+\//g, "/home/****/");
|
|
532
|
+
result = result.replace(/C:\\Users\\[a-zA-Z0-9._-]+\\/g, "C:\\Users\\****\\");
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
|
|
495
536
|
private parseDescription(content: string): string {
|
|
496
537
|
const match = content.match(/description:\s*"([^"]+)"/);
|
|
497
538
|
if (match) return match[1];
|
|
@@ -499,6 +540,4 @@ export class SkillGenerator {
|
|
|
499
540
|
if (match2) return match2[1];
|
|
500
541
|
return "";
|
|
501
542
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
543
|
}
|
package/src/skill/installer.ts
CHANGED
|
@@ -3,6 +3,26 @@ import * as path from "path";
|
|
|
3
3
|
import type { SqliteStore } from "../storage/sqlite";
|
|
4
4
|
import type { PluginContext } from "../types";
|
|
5
5
|
|
|
6
|
+
export type SkillInstallMode = "inline" | "on_demand" | "install_recommended";
|
|
7
|
+
|
|
8
|
+
export interface CompanionFileInfo {
|
|
9
|
+
relativePath: string;
|
|
10
|
+
size: number;
|
|
11
|
+
type: "script" | "reference" | "eval" | "other";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SkillCompanionManifest {
|
|
15
|
+
hasCompanionFiles: boolean;
|
|
16
|
+
installMode: SkillInstallMode;
|
|
17
|
+
installed: boolean;
|
|
18
|
+
installedPath?: string;
|
|
19
|
+
files: CompanionFileInfo[];
|
|
20
|
+
totalSize: number;
|
|
21
|
+
scriptsCount: number;
|
|
22
|
+
referencesCount: number;
|
|
23
|
+
evalsCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
export class SkillInstaller {
|
|
7
27
|
private workspaceSkillsDir: string;
|
|
8
28
|
|
|
@@ -13,6 +33,82 @@ export class SkillInstaller {
|
|
|
13
33
|
this.workspaceSkillsDir = path.join(ctx.workspaceDir, "skills");
|
|
14
34
|
}
|
|
15
35
|
|
|
36
|
+
getCompanionManifest(skillId: string): SkillCompanionManifest | null {
|
|
37
|
+
const skill = this.store.getSkill(skillId);
|
|
38
|
+
if (!skill) return null;
|
|
39
|
+
return SkillInstaller.buildManifest(skill.dirPath, !!skill.installed, skill.name, this.workspaceSkillsDir);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static buildManifest(dirPath: string, installed: boolean, skillName: string, workspaceSkillsDir?: string): SkillCompanionManifest {
|
|
43
|
+
const files: CompanionFileInfo[] = [];
|
|
44
|
+
|
|
45
|
+
const scanDir = (subDir: string, type: CompanionFileInfo["type"]) => {
|
|
46
|
+
const fullDir = path.join(dirPath, subDir);
|
|
47
|
+
if (!fs.existsSync(fullDir)) return;
|
|
48
|
+
try {
|
|
49
|
+
for (const f of fs.readdirSync(fullDir)) {
|
|
50
|
+
const fp = path.join(fullDir, f);
|
|
51
|
+
try {
|
|
52
|
+
const stat = fs.statSync(fp);
|
|
53
|
+
if (stat.isFile()) {
|
|
54
|
+
files.push({ relativePath: `${subDir}/${f}`, size: stat.size, type });
|
|
55
|
+
}
|
|
56
|
+
} catch { /* best-effort */ }
|
|
57
|
+
}
|
|
58
|
+
} catch { /* best-effort */ }
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
scanDir("scripts", "script");
|
|
62
|
+
scanDir("references", "reference");
|
|
63
|
+
scanDir("evals", "eval");
|
|
64
|
+
|
|
65
|
+
const scriptsCount = files.filter(f => f.type === "script").length;
|
|
66
|
+
const referencesCount = files.filter(f => f.type === "reference").length;
|
|
67
|
+
const evalsCount = files.filter(f => f.type === "eval").length;
|
|
68
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
69
|
+
const hasCompanionFiles = files.filter(f => f.type !== "eval").length > 0;
|
|
70
|
+
|
|
71
|
+
let installMode: SkillInstallMode = "inline";
|
|
72
|
+
if (hasCompanionFiles) {
|
|
73
|
+
const executableScripts = files.filter(f => f.type === "script");
|
|
74
|
+
const largeFiles = files.filter(f => f.size > 5000);
|
|
75
|
+
if (executableScripts.length >= 3 || largeFiles.length >= 2 || totalSize > 20000) {
|
|
76
|
+
installMode = "install_recommended";
|
|
77
|
+
} else {
|
|
78
|
+
installMode = "on_demand";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const installedPath = installed && workspaceSkillsDir
|
|
83
|
+
? path.join(workspaceSkillsDir, skillName)
|
|
84
|
+
: undefined;
|
|
85
|
+
|
|
86
|
+
return { hasCompanionFiles, installMode, installed, installedPath, files, totalSize, scriptsCount, referencesCount, evalsCount };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
readCompanionFile(skillId: string, relativePath: string): { content: string; size: number } | { error: string } {
|
|
90
|
+
const skill = this.store.getSkill(skillId);
|
|
91
|
+
if (!skill) return { error: "Skill not found" };
|
|
92
|
+
|
|
93
|
+
const normalized = relativePath.replace(/\.\./g, "");
|
|
94
|
+
const fullPath = path.join(skill.dirPath, normalized);
|
|
95
|
+
|
|
96
|
+
if (!fullPath.startsWith(skill.dirPath)) {
|
|
97
|
+
return { error: "Path traversal not allowed" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(fullPath)) {
|
|
101
|
+
return { error: `File not found: ${relativePath}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
106
|
+
return { content, size: content.length };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { error: `Cannot read file: ${err}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
16
112
|
install(skillId: string): { installed: boolean; path: string; message: string } {
|
|
17
113
|
const skill = this.store.getSkill(skillId);
|
|
18
114
|
if (!skill) return { installed: false, path: "", message: "Skill not found" };
|
|
@@ -22,8 +118,7 @@ export class SkillInstaller {
|
|
|
22
118
|
}
|
|
23
119
|
|
|
24
120
|
const dstDir = path.join(this.workspaceSkillsDir, skill.name);
|
|
25
|
-
|
|
26
|
-
fs.cpSync(skill.dirPath, dstDir, { recursive: true });
|
|
121
|
+
this.cleanSync(skill.dirPath, dstDir);
|
|
27
122
|
this.store.updateSkill(skillId, { installed: 1 });
|
|
28
123
|
|
|
29
124
|
this.ctx.log.info(`Skill installed: "${skill.name}" v${skill.version} → ${dstDir}`);
|
|
@@ -51,9 +146,17 @@ export class SkillInstaller {
|
|
|
51
146
|
if (!skill || !skill.installed) return;
|
|
52
147
|
|
|
53
148
|
const dstDir = path.join(this.workspaceSkillsDir, skill.name);
|
|
54
|
-
if (fs.existsSync(
|
|
55
|
-
|
|
149
|
+
if (fs.existsSync(skill.dirPath)) {
|
|
150
|
+
this.cleanSync(skill.dirPath, dstDir);
|
|
56
151
|
this.ctx.log.info(`Skill synced: "${skill.name}" v${skill.version} → workspace`);
|
|
57
152
|
}
|
|
58
153
|
}
|
|
154
|
+
|
|
155
|
+
private cleanSync(srcDir: string, dstDir: string): void {
|
|
156
|
+
if (fs.existsSync(dstDir)) {
|
|
157
|
+
fs.rmSync(dstDir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
160
|
+
fs.cpSync(srcDir, dstDir, { recursive: true });
|
|
161
|
+
}
|
|
59
162
|
}
|