@memtensor/memos-local-openclaw-plugin 1.0.4-beta.6 → 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 +39 -22
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +27 -7
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +29 -0
- package/dist/client/connector.d.ts.map +1 -0
- package/dist/client/connector.js +218 -0
- package/dist/client/connector.js.map +1 -0
- package/dist/client/hub.d.ts +61 -0
- package/dist/client/hub.d.ts.map +1 -0
- package/dist/client/hub.js +170 -0
- package/dist/client/hub.js.map +1 -0
- package/dist/client/skill-sync.d.ts +36 -0
- package/dist/client/skill-sync.d.ts.map +1 -0
- package/dist/client/skill-sync.js +226 -0
- package/dist/client/skill-sync.js.map +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +72 -3
- package/dist/config.js.map +1 -1
- package/dist/embedding/index.d.ts +4 -2
- package/dist/embedding/index.d.ts.map +1 -1
- package/dist/embedding/index.js +17 -1
- package/dist/embedding/index.js.map +1 -1
- package/dist/hub/auth.d.ts +19 -0
- package/dist/hub/auth.d.ts.map +1 -0
- package/dist/hub/auth.js +70 -0
- package/dist/hub/auth.js.map +1 -0
- package/dist/hub/server.d.ts +41 -0
- package/dist/hub/server.d.ts.map +1 -0
- package/dist/hub/server.js +767 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/user-manager.d.ts +31 -0
- package/dist/hub/user-manager.d.ts.map +1 -0
- package/dist/hub/user-manager.js +129 -0
- package/dist/hub/user-manager.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +10 -2
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +209 -43
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +1 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +1 -0
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.js +1 -1
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/openclaw-api.d.ts +53 -0
- package/dist/openclaw-api.d.ts.map +1 -0
- package/dist/openclaw-api.js +189 -0
- package/dist/openclaw-api.js.map +1 -0
- package/dist/recall/engine.js +1 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +4 -2
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +20 -81
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.contract.d.ts +2 -0
- package/dist/sharing/types.contract.d.ts.map +1 -0
- package/dist/sharing/types.contract.js +3 -0
- package/dist/sharing/types.contract.js.map +1 -0
- package/dist/sharing/types.d.ts +80 -0
- package/dist/sharing/types.d.ts.map +1 -0
- package/dist/sharing/types.js +3 -0
- package/dist/sharing/types.js.map +1 -0
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +2 -2
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/evolver.d.ts +0 -2
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +0 -3
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +4 -4
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/upgrader.js +1 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.js +1 -1
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts.map +1 -1
- package/dist/storage/ensure-binding.js +3 -1
- package/dist/storage/ensure-binding.js.map +1 -1
- package/dist/storage/sqlite.d.ts +329 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +909 -4
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +5 -12
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +38 -135
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory-search.d.ts +5 -2
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +50 -7
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/tools/network-memory-detail.d.ts +4 -0
- package/dist/tools/network-memory-detail.d.ts.map +1 -0
- package/dist/tools/network-memory-detail.js +34 -0
- package/dist/tools/network-memory-detail.js.map +1 -0
- package/dist/types.d.ts +49 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +3965 -459
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +51 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1564 -23
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +769 -67
- package/openclaw.plugin.json +2 -1
- package/package.json +4 -3
- package/scripts/postinstall.cjs +283 -46
- package/skill/memos-memory-guide/SKILL.md +82 -20
- package/src/capture/index.ts +27 -7
- package/src/client/connector.ts +212 -0
- package/src/client/hub.ts +207 -0
- package/src/client/skill-sync.ts +216 -0
- package/src/config.ts +94 -3
- package/src/embedding/index.ts +21 -1
- package/src/hub/auth.ts +78 -0
- package/src/hub/server.ts +754 -0
- package/src/hub/user-manager.ts +143 -0
- package/src/index.ts +13 -5
- package/src/ingest/providers/index.ts +246 -46
- package/src/ingest/providers/openai.ts +1 -1
- package/src/ingest/task-processor.ts +1 -1
- package/src/openclaw-api.ts +287 -0
- package/src/recall/engine.ts +1 -1
- package/src/shared/llm-call.ts +23 -95
- package/src/sharing/types.contract.ts +40 -0
- package/src/sharing/types.ts +102 -0
- package/src/skill/evaluator.ts +3 -2
- package/src/skill/evolver.ts +0 -5
- package/src/skill/generator.ts +6 -4
- package/src/skill/upgrader.ts +1 -1
- package/src/skill/validator.ts +1 -1
- package/src/storage/ensure-binding.ts +3 -1
- package/src/storage/sqlite.ts +1159 -4
- package/src/telemetry.ts +39 -152
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-search.ts +58 -8
- package/src/tools/network-memory-detail.ts +34 -0
- package/src/types.ts +44 -2
- package/src/viewer/html.ts +3965 -459
- package/src/viewer/server.ts +1452 -25
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Logger, MemosLocalConfig } from "../types";
|
|
2
|
+
import type { UserRole, UserStatus } from "../sharing/types";
|
|
3
|
+
import type { SqliteStore } from "../storage/sqlite";
|
|
4
|
+
import { hubRequestJson, normalizeHubUrl } from "./hub";
|
|
5
|
+
|
|
6
|
+
export interface HubSessionInfo {
|
|
7
|
+
hubUrl: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
username: string;
|
|
10
|
+
userToken: string;
|
|
11
|
+
role: UserRole;
|
|
12
|
+
connectedAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HubStatusInfo {
|
|
16
|
+
connected: boolean;
|
|
17
|
+
hubUrl?: string;
|
|
18
|
+
user: null | {
|
|
19
|
+
id: string;
|
|
20
|
+
username: string;
|
|
21
|
+
role: UserRole;
|
|
22
|
+
status: UserStatus | string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function connectToHub(store: SqliteStore, config: MemosLocalConfig, log?: Logger): Promise<HubSessionInfo> {
|
|
27
|
+
const hubAddress = config.sharing?.client?.hubAddress ?? "";
|
|
28
|
+
let userToken = config.sharing?.client?.userToken ?? "";
|
|
29
|
+
|
|
30
|
+
if (!userToken) {
|
|
31
|
+
const persisted = store.getClientHubConnection();
|
|
32
|
+
if (persisted?.userToken) userToken = persisted.userToken;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!userToken && config.sharing?.client?.teamToken) {
|
|
36
|
+
if (!log) throw new Error("hub client connection is not configured (no userToken, has teamToken but no logger for auto-join)");
|
|
37
|
+
return autoJoinHub(store, config, log);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!hubAddress || !userToken) {
|
|
41
|
+
throw new Error("hub client connection is not configured");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
45
|
+
const me = await hubRequestJson(hubUrl, userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
46
|
+
store.setClientHubConnection({
|
|
47
|
+
hubUrl,
|
|
48
|
+
userId: String(me.id),
|
|
49
|
+
username: String(me.username ?? ""),
|
|
50
|
+
userToken,
|
|
51
|
+
role: String(me.role ?? "member") as UserRole,
|
|
52
|
+
connectedAt: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
return store.getClientHubConnection()!;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig): Promise<HubStatusInfo> {
|
|
58
|
+
const conn = store.getClientHubConnection();
|
|
59
|
+
const hubAddress = conn?.hubUrl || config.sharing?.client?.hubAddress || "";
|
|
60
|
+
const userToken = conn?.userToken || config.sharing?.client?.userToken || "";
|
|
61
|
+
|
|
62
|
+
if (conn && conn.userId && (!userToken || userToken === "")) {
|
|
63
|
+
const teamToken = config.sharing?.client?.teamToken ?? "";
|
|
64
|
+
if (hubAddress && teamToken) {
|
|
65
|
+
try {
|
|
66
|
+
const result = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/join", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: JSON.stringify({ teamToken, username: conn.username || "user" }),
|
|
69
|
+
}) as any;
|
|
70
|
+
if (result.status === "pending") {
|
|
71
|
+
return {
|
|
72
|
+
connected: false,
|
|
73
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
74
|
+
user: {
|
|
75
|
+
id: conn.userId,
|
|
76
|
+
username: conn.username || "",
|
|
77
|
+
role: "member",
|
|
78
|
+
status: "pending",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (result.status === "active" && result.userToken) {
|
|
83
|
+
store.setClientHubConnection({
|
|
84
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
85
|
+
userId: String(result.userId),
|
|
86
|
+
username: conn.username || "",
|
|
87
|
+
userToken: result.userToken,
|
|
88
|
+
role: "member",
|
|
89
|
+
connectedAt: Date.now(),
|
|
90
|
+
});
|
|
91
|
+
const me = await hubRequestJson(normalizeHubUrl(hubAddress), result.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
92
|
+
return {
|
|
93
|
+
connected: true,
|
|
94
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
95
|
+
user: {
|
|
96
|
+
id: String(me.id),
|
|
97
|
+
username: String(me.username ?? ""),
|
|
98
|
+
role: String(me.role ?? "member") as UserRole,
|
|
99
|
+
status: String(me.status ?? "active"),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (result.status === "rejected") {
|
|
104
|
+
return {
|
|
105
|
+
connected: false,
|
|
106
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
107
|
+
user: {
|
|
108
|
+
id: conn.userId,
|
|
109
|
+
username: conn.username || "",
|
|
110
|
+
role: "member",
|
|
111
|
+
status: "rejected",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
} catch { /* fall through */ }
|
|
116
|
+
}
|
|
117
|
+
return { connected: false, user: null };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!hubAddress || !userToken) {
|
|
121
|
+
return { connected: false, user: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const me = await hubRequestJson(normalizeHubUrl(hubAddress), userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
126
|
+
return {
|
|
127
|
+
connected: true,
|
|
128
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
129
|
+
user: {
|
|
130
|
+
id: String(me.id),
|
|
131
|
+
username: String(me.username ?? ""),
|
|
132
|
+
role: String(me.role ?? "member") as UserRole,
|
|
133
|
+
status: String(me.status ?? "active"),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
} catch {
|
|
137
|
+
return { connected: false, user: null };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function autoJoinHub(
|
|
142
|
+
store: SqliteStore,
|
|
143
|
+
config: MemosLocalConfig,
|
|
144
|
+
log: Logger,
|
|
145
|
+
): Promise<HubSessionInfo> {
|
|
146
|
+
const hubAddress = config.sharing?.client?.hubAddress ?? "";
|
|
147
|
+
const teamToken = config.sharing?.client?.teamToken ?? "";
|
|
148
|
+
if (!hubAddress || !teamToken) {
|
|
149
|
+
throw new Error("hubAddress and teamToken are required for auto-join");
|
|
150
|
+
}
|
|
151
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
152
|
+
const osModule = typeof globalThis.process !== "undefined" ? await import("os") : null;
|
|
153
|
+
const hostname = osModule ? osModule.hostname() : "unknown";
|
|
154
|
+
const username = osModule ? osModule.userInfo().username : "user";
|
|
155
|
+
let clientIp = "";
|
|
156
|
+
if (osModule) {
|
|
157
|
+
const nets = osModule.networkInterfaces();
|
|
158
|
+
for (const name of Object.keys(nets)) {
|
|
159
|
+
for (const net of nets[name] ?? []) {
|
|
160
|
+
if (net.family === "IPv4" && !net.internal) { clientIp = net.address; break; }
|
|
161
|
+
}
|
|
162
|
+
if (clientIp) break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
log.info(`Joining Hub at ${hubUrl} as "${username}"...`);
|
|
167
|
+
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
168
|
+
method: "POST",
|
|
169
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp }),
|
|
170
|
+
}) as any;
|
|
171
|
+
|
|
172
|
+
if (result.status === "pending") {
|
|
173
|
+
log.info(`Join request submitted, awaiting admin approval. userId=${result.userId}`);
|
|
174
|
+
store.setClientHubConnection({
|
|
175
|
+
hubUrl,
|
|
176
|
+
userId: String(result.userId),
|
|
177
|
+
username,
|
|
178
|
+
userToken: "",
|
|
179
|
+
role: "member",
|
|
180
|
+
connectedAt: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
throw new PendingApprovalError(result.userId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (result.status === "rejected") {
|
|
186
|
+
throw new Error(`Join request was rejected by the Hub admin.`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!result.userToken) {
|
|
190
|
+
throw new Error(`Hub join failed: ${JSON.stringify(result)}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
log.info(`Joined Hub successfully! userId=${result.userId}`);
|
|
194
|
+
store.setClientHubConnection({
|
|
195
|
+
hubUrl,
|
|
196
|
+
userId: String(result.userId),
|
|
197
|
+
username,
|
|
198
|
+
userToken: result.userToken,
|
|
199
|
+
role: "member",
|
|
200
|
+
connectedAt: Date.now(),
|
|
201
|
+
});
|
|
202
|
+
return store.getClientHubConnection()!;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export class PendingApprovalError extends Error {
|
|
206
|
+
public readonly userId: string;
|
|
207
|
+
constructor(userId: string) {
|
|
208
|
+
super("Awaiting admin approval");
|
|
209
|
+
this.name = "PendingApprovalError";
|
|
210
|
+
this.userId = userId;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { PluginContext } from "../types";
|
|
2
|
+
import type { SqliteStore } from "../storage/sqlite";
|
|
3
|
+
import type { HubMemoryDetail, HubScope, HubSearchResult, HubSkillSearchResult } from "../sharing/types";
|
|
4
|
+
|
|
5
|
+
export interface ResolvedHubClient {
|
|
6
|
+
hubUrl: string;
|
|
7
|
+
userToken: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
username: string;
|
|
10
|
+
role: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function resolveHubClient(store: SqliteStore, ctx: PluginContext, overrides?: { hubAddress?: string; userToken?: string }): Promise<ResolvedHubClient> {
|
|
14
|
+
const persisted = store.getClientHubConnection() as any;
|
|
15
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
16
|
+
return {
|
|
17
|
+
hubUrl: normalizeHubUrl(String(persisted.hubUrl)),
|
|
18
|
+
userToken: String(persisted.userToken),
|
|
19
|
+
userId: String(persisted.userId),
|
|
20
|
+
username: String(persisted.username ?? ""),
|
|
21
|
+
role: String(persisted.role ?? "member"),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const hubAddress = overrides?.hubAddress ?? ctx.config.sharing?.client?.hubAddress ?? "";
|
|
26
|
+
const userToken = overrides?.userToken ?? ctx.config.sharing?.client?.userToken ?? "";
|
|
27
|
+
if (!hubAddress || !userToken) {
|
|
28
|
+
throw new Error("hub client connection is not configured");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
32
|
+
const me = await hubRequestJson(hubUrl, userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
hubUrl,
|
|
36
|
+
userToken,
|
|
37
|
+
userId: String(me.id),
|
|
38
|
+
username: String(me.username ?? ""),
|
|
39
|
+
role: String(me.role ?? "member"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function hubListMemories(
|
|
44
|
+
store: SqliteStore,
|
|
45
|
+
ctx: PluginContext,
|
|
46
|
+
input?: { limit?: number; hubAddress?: string; userToken?: string },
|
|
47
|
+
): Promise<{ memories: Array<any> }> {
|
|
48
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
49
|
+
const url = new URL(`${client.hubUrl}/api/v1/hub/memories`);
|
|
50
|
+
if (input?.limit != null) url.searchParams.set("limit", String(input.limit));
|
|
51
|
+
return hubRequestJson(url.origin, client.userToken, `${url.pathname}${url.search}`, {
|
|
52
|
+
method: "GET",
|
|
53
|
+
}) as Promise<{ memories: Array<any> }>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function hubListTasks(
|
|
57
|
+
store: SqliteStore,
|
|
58
|
+
ctx: PluginContext,
|
|
59
|
+
input?: { limit?: number; hubAddress?: string; userToken?: string },
|
|
60
|
+
): Promise<{ tasks: Array<any> }> {
|
|
61
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
62
|
+
const url = new URL(`${client.hubUrl}/api/v1/hub/tasks`);
|
|
63
|
+
if (input?.limit != null) url.searchParams.set("limit", String(input.limit));
|
|
64
|
+
return hubRequestJson(url.origin, client.userToken, `${url.pathname}${url.search}`, {
|
|
65
|
+
method: "GET",
|
|
66
|
+
}) as Promise<{ tasks: Array<any> }>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function hubListSkills(
|
|
70
|
+
store: SqliteStore,
|
|
71
|
+
ctx: PluginContext,
|
|
72
|
+
input?: { limit?: number; hubAddress?: string; userToken?: string },
|
|
73
|
+
): Promise<{ skills: Array<any> }> {
|
|
74
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
75
|
+
const url = new URL(`${client.hubUrl}/api/v1/hub/skills/list`);
|
|
76
|
+
if (input?.limit != null) url.searchParams.set("limit", String(input.limit));
|
|
77
|
+
return hubRequestJson(url.origin, client.userToken, `${url.pathname}${url.search}`, {
|
|
78
|
+
method: "GET",
|
|
79
|
+
}) as Promise<{ skills: Array<any> }>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function hubSearchMemories(
|
|
83
|
+
store: SqliteStore,
|
|
84
|
+
ctx: PluginContext,
|
|
85
|
+
input: { query: string; maxResults?: number; scope?: HubScope; hubAddress?: string; userToken?: string },
|
|
86
|
+
): Promise<HubSearchResult> {
|
|
87
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input.hubAddress, userToken: input.userToken });
|
|
88
|
+
return hubRequestJson(client.hubUrl, client.userToken, "/api/v1/hub/search", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
query: input.query,
|
|
92
|
+
maxResults: input.maxResults,
|
|
93
|
+
scope: input.scope,
|
|
94
|
+
}),
|
|
95
|
+
}) as Promise<HubSearchResult>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
export async function hubSearchSkills(
|
|
100
|
+
store: SqliteStore,
|
|
101
|
+
ctx: PluginContext,
|
|
102
|
+
input: { query: string; maxResults?: number; hubAddress?: string; userToken?: string },
|
|
103
|
+
): Promise<HubSkillSearchResult> {
|
|
104
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input.hubAddress, userToken: input.userToken });
|
|
105
|
+
const url = new URL(`${client.hubUrl}/api/v1/hub/skills`);
|
|
106
|
+
url.searchParams.set("query", input.query);
|
|
107
|
+
if (input.maxResults != null) url.searchParams.set("maxResults", String(input.maxResults));
|
|
108
|
+
return hubRequestJson(url.origin, client.userToken, `${url.pathname}${url.search}`, {
|
|
109
|
+
method: "GET",
|
|
110
|
+
}) as Promise<HubSkillSearchResult>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function hubGetMemoryDetail(
|
|
114
|
+
store: SqliteStore,
|
|
115
|
+
ctx: PluginContext,
|
|
116
|
+
input: { remoteHitId: string; hubAddress?: string; userToken?: string },
|
|
117
|
+
): Promise<HubMemoryDetail> {
|
|
118
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input.hubAddress, userToken: input.userToken });
|
|
119
|
+
const detail = await hubRequestJson(client.hubUrl, client.userToken, "/api/v1/hub/memory-detail", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
remoteHitId: input.remoteHitId,
|
|
123
|
+
}),
|
|
124
|
+
}) as Omit<HubMemoryDetail, "remoteHitId">;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
remoteHitId: input.remoteHitId,
|
|
128
|
+
content: String(detail.content ?? ""),
|
|
129
|
+
summary: String(detail.summary ?? ""),
|
|
130
|
+
source: {
|
|
131
|
+
ts: Number(detail.source?.ts ?? 0),
|
|
132
|
+
role: String(detail.source?.role ?? "assistant") as any,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function hubUpdateUsername(
|
|
138
|
+
store: SqliteStore,
|
|
139
|
+
ctx: PluginContext,
|
|
140
|
+
newUsername: string,
|
|
141
|
+
): Promise<{ ok: boolean; username: string; userToken: string }> {
|
|
142
|
+
const client = await resolveHubClient(store, ctx);
|
|
143
|
+
const result = await hubRequestJson(client.hubUrl, client.userToken, "/api/v1/hub/me/update-profile", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: JSON.stringify({ username: newUsername }),
|
|
146
|
+
}) as { ok: boolean; username: string; userToken: string };
|
|
147
|
+
if (result.ok && result.userToken) {
|
|
148
|
+
store.setClientHubConnection({
|
|
149
|
+
hubUrl: client.hubUrl,
|
|
150
|
+
userId: client.userId,
|
|
151
|
+
username: result.username,
|
|
152
|
+
userToken: result.userToken,
|
|
153
|
+
role: client.role as "admin" | "member",
|
|
154
|
+
connectedAt: Date.now(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let _cachedClientIp: string | null = null;
|
|
161
|
+
function getClientIp(): string {
|
|
162
|
+
if (_cachedClientIp !== null) return _cachedClientIp;
|
|
163
|
+
try {
|
|
164
|
+
const os = require("os");
|
|
165
|
+
const nets = os.networkInterfaces();
|
|
166
|
+
for (const name of Object.keys(nets)) {
|
|
167
|
+
for (const net of nets[name] ?? []) {
|
|
168
|
+
if (net.family === "IPv4" && !net.internal) { _cachedClientIp = net.address; return _cachedClientIp!; }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch { /* browser or no os module */ }
|
|
172
|
+
_cachedClientIp = "";
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function hubRequestJson(
|
|
177
|
+
hubUrl: string,
|
|
178
|
+
userToken: string,
|
|
179
|
+
route: string,
|
|
180
|
+
init: RequestInit = {},
|
|
181
|
+
): Promise<unknown> {
|
|
182
|
+
const clientIp = getClientIp();
|
|
183
|
+
const res = await fetch(`${normalizeHubUrl(hubUrl)}${route}`, {
|
|
184
|
+
...init,
|
|
185
|
+
headers: {
|
|
186
|
+
authorization: `Bearer ${userToken}`,
|
|
187
|
+
...(clientIp ? { "x-client-ip": clientIp } : {}),
|
|
188
|
+
...(init.body ? { "content-type": "application/json" } : {}),
|
|
189
|
+
...(init.headers ?? {}),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
const body = await res.text();
|
|
195
|
+
throw new Error(`hub request failed (${res.status}): ${body || res.statusText}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (res.status === 204) return null;
|
|
199
|
+
return res.json();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function normalizeHubUrl(hubAddress: string): string {
|
|
203
|
+
const trimmed = hubAddress.trim().replace(/\/+$/, "");
|
|
204
|
+
if (!trimmed) return "";
|
|
205
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
206
|
+
return `http://${trimmed}`;
|
|
207
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import type { PluginContext, Skill, SkillVersion } from "../types";
|
|
5
|
+
import type { SqliteStore } from "../storage/sqlite";
|
|
6
|
+
import type { SkillGenerateOutput } from "../types";
|
|
7
|
+
import type { SkillBundle } from "../sharing/types";
|
|
8
|
+
import { resolveHubClient, hubRequestJson } from "./hub";
|
|
9
|
+
|
|
10
|
+
export function buildSkillBundleForHub(store: SqliteStore, skillId: string): SkillBundle {
|
|
11
|
+
const skill = store.getSkill(skillId);
|
|
12
|
+
if (!skill) {
|
|
13
|
+
throw new Error(`Skill not found: ${skillId}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const latestVersion = store.getLatestSkillVersion(skillId);
|
|
17
|
+
const skillMd = readSkillMarkdown(skill.dirPath, latestVersion?.content ?? "");
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
metadata: {
|
|
21
|
+
id: skill.id,
|
|
22
|
+
name: skill.name,
|
|
23
|
+
description: skill.description,
|
|
24
|
+
version: skill.version,
|
|
25
|
+
qualityScore: skill.qualityScore,
|
|
26
|
+
},
|
|
27
|
+
bundle: {
|
|
28
|
+
skill_md: skillMd,
|
|
29
|
+
scripts: readCompanionFiles(path.join(skill.dirPath, "scripts")),
|
|
30
|
+
references: readCompanionFiles(path.join(skill.dirPath, "references")),
|
|
31
|
+
evals: readEvals(path.join(skill.dirPath, "evals", "evals.json")),
|
|
32
|
+
} satisfies SkillGenerateOutput,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readSkillMarkdown(dirPath: string, fallback: string): string {
|
|
37
|
+
const skillMdPath = path.join(dirPath, "SKILL.md");
|
|
38
|
+
if (fs.existsSync(skillMdPath)) {
|
|
39
|
+
return fs.readFileSync(skillMdPath, "utf8");
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readCompanionFiles(dirPath: string): Array<{ filename: string; content: string }> {
|
|
45
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
46
|
+
|
|
47
|
+
const out: Array<{ filename: string; content: string }> = [];
|
|
48
|
+
walkFiles(dirPath, dirPath, out);
|
|
49
|
+
return out.sort((left, right) => left.filename.localeCompare(right.filename));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function walkFiles(rootDir: string, currentDir: string, out: Array<{ filename: string; content: string }>): void {
|
|
53
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
54
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
walkFiles(rootDir, fullPath, out);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!entry.isFile()) continue;
|
|
60
|
+
out.push({
|
|
61
|
+
filename: path.relative(rootDir, fullPath).replace(/\\/g, "/"),
|
|
62
|
+
content: fs.readFileSync(fullPath, "utf8"),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readEvals(evalsPath: string): Array<{ id: number; prompt: string; expectations: string[] }> {
|
|
68
|
+
if (!fs.existsSync(evalsPath)) return [];
|
|
69
|
+
const raw = JSON.parse(fs.readFileSync(evalsPath, "utf8")) as { evals?: Array<{ id: number; prompt: string; expectations: string[] }> };
|
|
70
|
+
return Array.isArray(raw.evals) ? raw.evals : [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
export async function publishSkillBundleToHub(
|
|
75
|
+
store: SqliteStore,
|
|
76
|
+
ctx: PluginContext,
|
|
77
|
+
input: { skillId: string; visibility: "public" | "group"; groupId?: string; hubAddress?: string; userToken?: string },
|
|
78
|
+
): Promise<{ skillId: string; visibility: "public" | "group" }> {
|
|
79
|
+
const bundle = buildSkillBundleForHub(store, input.skillId);
|
|
80
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input.hubAddress, userToken: input.userToken });
|
|
81
|
+
return hubRequestJson(client.hubUrl, client.userToken, "/api/v1/hub/skills/publish", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
visibility: input.visibility,
|
|
85
|
+
groupId: input.groupId,
|
|
86
|
+
metadata: bundle.metadata,
|
|
87
|
+
bundle: bundle.bundle,
|
|
88
|
+
}),
|
|
89
|
+
}) as Promise<{ skillId: string; visibility: "public" | "group" }>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function unpublishSkillBundleFromHub(
|
|
93
|
+
store: SqliteStore,
|
|
94
|
+
ctx: PluginContext,
|
|
95
|
+
input: { skillId: string; hubAddress?: string; userToken?: string },
|
|
96
|
+
): Promise<{ ok: boolean }> {
|
|
97
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input.hubAddress, userToken: input.userToken });
|
|
98
|
+
return hubRequestJson(client.hubUrl, client.userToken, "/api/v1/hub/skills/unpublish", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
sourceSkillId: input.skillId,
|
|
102
|
+
}),
|
|
103
|
+
}) as Promise<{ ok: boolean }>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function fetchHubSkillBundle(
|
|
107
|
+
store: SqliteStore,
|
|
108
|
+
ctx: PluginContext,
|
|
109
|
+
input: { skillId: string; hubAddress?: string; userToken?: string },
|
|
110
|
+
): Promise<SkillBundle & { skillId: string }> {
|
|
111
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input.hubAddress, userToken: input.userToken });
|
|
112
|
+
return hubRequestJson(client.hubUrl, client.userToken, `/api/v1/hub/skills/${encodeURIComponent(input.skillId)}/bundle`, {
|
|
113
|
+
method: "GET",
|
|
114
|
+
}) as Promise<SkillBundle & { skillId: string }>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function restoreSkillBundleFromHub(
|
|
118
|
+
store: SqliteStore,
|
|
119
|
+
ctx: PluginContext,
|
|
120
|
+
payload: SkillBundle & { skillId?: string },
|
|
121
|
+
): { localSkillId: string; localName: string; dirPath: string } {
|
|
122
|
+
validateBundle(payload.bundle);
|
|
123
|
+
|
|
124
|
+
const skillsStoreDir = path.join(ctx.stateDir, "skills-store");
|
|
125
|
+
fs.mkdirSync(skillsStoreDir, { recursive: true });
|
|
126
|
+
|
|
127
|
+
const baseName = sanitizeName(payload.metadata.name) || `hub-skill-${(payload.skillId ?? payload.metadata.id).slice(0, 8)}`;
|
|
128
|
+
const resolvedName = resolveLocalSkillName(store, baseName, payload.skillId ?? payload.metadata.id);
|
|
129
|
+
const dirPath = path.join(skillsStoreDir, resolvedName);
|
|
130
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(dirPath, "SKILL.md"), payload.bundle.skill_md, "utf8");
|
|
132
|
+
|
|
133
|
+
writeCompanionFiles(dirPath, "scripts", payload.bundle.scripts);
|
|
134
|
+
writeCompanionFiles(dirPath, "references", payload.bundle.references);
|
|
135
|
+
if (payload.bundle.evals.length > 0) {
|
|
136
|
+
const evalDir = path.join(dirPath, "evals");
|
|
137
|
+
fs.mkdirSync(evalDir, { recursive: true });
|
|
138
|
+
fs.writeFileSync(path.join(evalDir, "evals.json"), JSON.stringify({ skill_name: payload.metadata.name, evals: payload.bundle.evals }, null, 2), "utf8");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const localSkillId = randomUUID();
|
|
143
|
+
const skill: Skill = {
|
|
144
|
+
id: localSkillId,
|
|
145
|
+
name: resolvedName,
|
|
146
|
+
description: payload.metadata.description,
|
|
147
|
+
version: payload.metadata.version,
|
|
148
|
+
status: "active",
|
|
149
|
+
tags: JSON.stringify(["hub-import"]),
|
|
150
|
+
sourceType: "manual",
|
|
151
|
+
dirPath,
|
|
152
|
+
installed: 0,
|
|
153
|
+
owner: "agent:main",
|
|
154
|
+
visibility: "private",
|
|
155
|
+
qualityScore: payload.metadata.qualityScore,
|
|
156
|
+
createdAt: now,
|
|
157
|
+
updatedAt: now,
|
|
158
|
+
};
|
|
159
|
+
const version: SkillVersion = {
|
|
160
|
+
id: randomUUID(),
|
|
161
|
+
skillId: localSkillId,
|
|
162
|
+
version: payload.metadata.version,
|
|
163
|
+
content: payload.bundle.skill_md,
|
|
164
|
+
changelog: "Imported from hub",
|
|
165
|
+
changeSummary: "Imported from hub",
|
|
166
|
+
upgradeType: "create",
|
|
167
|
+
sourceTaskId: null,
|
|
168
|
+
metrics: "{}",
|
|
169
|
+
qualityScore: payload.metadata.qualityScore,
|
|
170
|
+
createdAt: now,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
store.insertSkill(skill);
|
|
174
|
+
store.insertSkillVersion(version);
|
|
175
|
+
return { localSkillId, localName: resolvedName, dirPath };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateBundle(bundle: SkillGenerateOutput): void {
|
|
179
|
+
const allowedExtensions = new Set([".md", ".ts", ".js", ".sh", ".json", ".yaml", ".yml", ".txt"]);
|
|
180
|
+
const files = [...bundle.scripts, ...bundle.references];
|
|
181
|
+
if (Buffer.byteLength(bundle.skill_md, "utf8") > 100 * 1024) throw new Error("SKILL.md exceeds size limit");
|
|
182
|
+
if (files.length > 50) throw new Error("bundle contains too many files");
|
|
183
|
+
|
|
184
|
+
let totalBytes = Buffer.byteLength(bundle.skill_md, "utf8");
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
const name = file.filename;
|
|
187
|
+
if (!name || path.isAbsolute(name) || name.startsWith("/") || name.includes("..")) throw new Error(`unsafe filename: ${name}`);
|
|
188
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(name)) throw new Error(`invalid filename: ${name}`);
|
|
189
|
+
const ext = path.extname(name).toLowerCase();
|
|
190
|
+
if (!allowedExtensions.has(ext)) throw new Error(`unsupported file type: ${name}`);
|
|
191
|
+
const fileSize = Buffer.byteLength(file.content, "utf8");
|
|
192
|
+
if (fileSize > 512 * 1024) throw new Error(`file exceeds size limit: ${name}`);
|
|
193
|
+
totalBytes += fileSize;
|
|
194
|
+
}
|
|
195
|
+
if (totalBytes > 5 * 1024 * 1024) throw new Error("bundle exceeds size limit");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeCompanionFiles(dirPath: string, root: "scripts" | "references", files: Array<{ filename: string; content: string }>): void {
|
|
199
|
+
if (files.length === 0) return;
|
|
200
|
+
const rootDir = path.join(dirPath, root);
|
|
201
|
+
fs.mkdirSync(rootDir, { recursive: true });
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
const target = path.join(rootDir, file.filename);
|
|
204
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
205
|
+
fs.writeFileSync(target, file.content, "utf8");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function sanitizeName(input: string): string {
|
|
210
|
+
return input.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resolveLocalSkillName(store: SqliteStore, baseName: string, sourceId: string): string {
|
|
214
|
+
if (!store.getSkillByName(baseName)) return baseName;
|
|
215
|
+
return `${baseName}-hub-${sourceId.slice(0, 8)}`;
|
|
216
|
+
}
|