@memtensor/memos-local-openclaw-plugin 1.0.2 → 1.0.4-beta.0
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 +38 -21
- package/dist/client/connector.d.ts +26 -0
- package/dist/client/connector.d.ts.map +1 -0
- package/dist/client/connector.js +127 -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 +148 -0
- package/dist/client/hub.js.map +1 -0
- package/dist/client/skill-sync.d.ts +29 -0
- package/dist/client/skill-sync.d.ts.map +1 -0
- package/dist/client/skill-sync.js +216 -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 +70 -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 +21 -4
- 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 +742 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/user-manager.d.ts +28 -0
- package/dist/hub/user-manager.d.ts.map +1 -0
- package/dist/hub/user-manager.js +112 -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 +4 -3
- 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 +242 -8
- 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 +2 -2
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +4 -1
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +15 -0
- 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/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/sqlite.d.ts +294 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +902 -8
- package/dist/storage/sqlite.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 +3 -2
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +48 -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 +47 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +0 -1
- package/dist/update-check.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2396 -289
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +43 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1180 -33
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +445 -25
- package/openclaw.plugin.json +2 -1
- package/package.json +2 -1
- package/scripts/postinstall.cjs +282 -45
- package/skill/memos-memory-guide/SKILL.md +26 -2
- package/src/client/connector.ts +124 -0
- package/src/client/hub.ts +189 -0
- package/src/client/skill-sync.ts +202 -0
- package/src/config.ts +92 -3
- package/src/embedding/index.ts +25 -3
- package/src/hub/auth.ts +78 -0
- package/src/hub/server.ts +734 -0
- package/src/hub/user-manager.ts +126 -0
- package/src/index.ts +7 -4
- package/src/ingest/providers/index.ts +279 -8
- 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 +2 -2
- package/src/shared/llm-call.ts +19 -1
- 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/generator.ts +6 -4
- package/src/skill/upgrader.ts +1 -1
- package/src/skill/validator.ts +1 -1
- package/src/storage/sqlite.ts +1167 -7
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-search.ts +57 -8
- package/src/tools/network-memory-detail.ts +34 -0
- package/src/types.ts +48 -2
- package/src/update-check.ts +0 -1
- package/src/viewer/html.ts +2396 -289
- package/src/viewer/server.ts +1087 -34
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
export async function hubRequestJson(
|
|
161
|
+
hubUrl: string,
|
|
162
|
+
userToken: string,
|
|
163
|
+
route: string,
|
|
164
|
+
init: RequestInit = {},
|
|
165
|
+
): Promise<unknown> {
|
|
166
|
+
const res = await fetch(`${normalizeHubUrl(hubUrl)}${route}`, {
|
|
167
|
+
...init,
|
|
168
|
+
headers: {
|
|
169
|
+
authorization: `Bearer ${userToken}`,
|
|
170
|
+
...(init.body ? { "content-type": "application/json" } : {}),
|
|
171
|
+
...(init.headers ?? {}),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
const body = await res.text();
|
|
177
|
+
throw new Error(`hub request failed (${res.status}): ${body || res.statusText}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (res.status === 204) return null;
|
|
181
|
+
return res.json();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function normalizeHubUrl(hubAddress: string): string {
|
|
185
|
+
const trimmed = hubAddress.trim().replace(/\/+$/, "");
|
|
186
|
+
if (!trimmed) return "";
|
|
187
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
188
|
+
return `http://${trimmed}`;
|
|
189
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
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 fetchHubSkillBundle(
|
|
93
|
+
store: SqliteStore,
|
|
94
|
+
ctx: PluginContext,
|
|
95
|
+
input: { skillId: string; hubAddress?: string; userToken?: string },
|
|
96
|
+
): Promise<SkillBundle & { skillId: string }> {
|
|
97
|
+
const client = await resolveHubClient(store, ctx, { hubAddress: input.hubAddress, userToken: input.userToken });
|
|
98
|
+
return hubRequestJson(client.hubUrl, client.userToken, `/api/v1/hub/skills/${encodeURIComponent(input.skillId)}/bundle`, {
|
|
99
|
+
method: "GET",
|
|
100
|
+
}) as Promise<SkillBundle & { skillId: string }>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function restoreSkillBundleFromHub(
|
|
104
|
+
store: SqliteStore,
|
|
105
|
+
ctx: PluginContext,
|
|
106
|
+
payload: SkillBundle & { skillId?: string },
|
|
107
|
+
): { localSkillId: string; localName: string; dirPath: string } {
|
|
108
|
+
validateBundle(payload.bundle);
|
|
109
|
+
|
|
110
|
+
const skillsStoreDir = path.join(ctx.stateDir, "skills-store");
|
|
111
|
+
fs.mkdirSync(skillsStoreDir, { recursive: true });
|
|
112
|
+
|
|
113
|
+
const baseName = sanitizeName(payload.metadata.name) || `hub-skill-${(payload.skillId ?? payload.metadata.id).slice(0, 8)}`;
|
|
114
|
+
const resolvedName = resolveLocalSkillName(store, baseName, payload.skillId ?? payload.metadata.id);
|
|
115
|
+
const dirPath = path.join(skillsStoreDir, resolvedName);
|
|
116
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
117
|
+
fs.writeFileSync(path.join(dirPath, "SKILL.md"), payload.bundle.skill_md, "utf8");
|
|
118
|
+
|
|
119
|
+
writeCompanionFiles(dirPath, "scripts", payload.bundle.scripts);
|
|
120
|
+
writeCompanionFiles(dirPath, "references", payload.bundle.references);
|
|
121
|
+
if (payload.bundle.evals.length > 0) {
|
|
122
|
+
const evalDir = path.join(dirPath, "evals");
|
|
123
|
+
fs.mkdirSync(evalDir, { recursive: true });
|
|
124
|
+
fs.writeFileSync(path.join(evalDir, "evals.json"), JSON.stringify({ skill_name: payload.metadata.name, evals: payload.bundle.evals }, null, 2), "utf8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const localSkillId = randomUUID();
|
|
129
|
+
const skill: Skill = {
|
|
130
|
+
id: localSkillId,
|
|
131
|
+
name: resolvedName,
|
|
132
|
+
description: payload.metadata.description,
|
|
133
|
+
version: payload.metadata.version,
|
|
134
|
+
status: "active",
|
|
135
|
+
tags: JSON.stringify(["hub-import"]),
|
|
136
|
+
sourceType: "manual",
|
|
137
|
+
dirPath,
|
|
138
|
+
installed: 0,
|
|
139
|
+
owner: "agent:main",
|
|
140
|
+
visibility: "private",
|
|
141
|
+
qualityScore: payload.metadata.qualityScore,
|
|
142
|
+
createdAt: now,
|
|
143
|
+
updatedAt: now,
|
|
144
|
+
};
|
|
145
|
+
const version: SkillVersion = {
|
|
146
|
+
id: randomUUID(),
|
|
147
|
+
skillId: localSkillId,
|
|
148
|
+
version: payload.metadata.version,
|
|
149
|
+
content: payload.bundle.skill_md,
|
|
150
|
+
changelog: "Imported from hub",
|
|
151
|
+
changeSummary: "Imported from hub",
|
|
152
|
+
upgradeType: "create",
|
|
153
|
+
sourceTaskId: null,
|
|
154
|
+
metrics: "{}",
|
|
155
|
+
qualityScore: payload.metadata.qualityScore,
|
|
156
|
+
createdAt: now,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
store.insertSkill(skill);
|
|
160
|
+
store.insertSkillVersion(version);
|
|
161
|
+
return { localSkillId, localName: resolvedName, dirPath };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateBundle(bundle: SkillGenerateOutput): void {
|
|
165
|
+
const allowedExtensions = new Set([".md", ".ts", ".js", ".sh", ".json", ".yaml", ".yml", ".txt"]);
|
|
166
|
+
const files = [...bundle.scripts, ...bundle.references];
|
|
167
|
+
if (Buffer.byteLength(bundle.skill_md, "utf8") > 100 * 1024) throw new Error("SKILL.md exceeds size limit");
|
|
168
|
+
if (files.length > 50) throw new Error("bundle contains too many files");
|
|
169
|
+
|
|
170
|
+
let totalBytes = Buffer.byteLength(bundle.skill_md, "utf8");
|
|
171
|
+
for (const file of files) {
|
|
172
|
+
const name = file.filename;
|
|
173
|
+
if (!name || path.isAbsolute(name) || name.startsWith("/") || name.includes("..")) throw new Error(`unsafe filename: ${name}`);
|
|
174
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(name)) throw new Error(`invalid filename: ${name}`);
|
|
175
|
+
const ext = path.extname(name).toLowerCase();
|
|
176
|
+
if (!allowedExtensions.has(ext)) throw new Error(`unsupported file type: ${name}`);
|
|
177
|
+
const fileSize = Buffer.byteLength(file.content, "utf8");
|
|
178
|
+
if (fileSize > 512 * 1024) throw new Error(`file exceeds size limit: ${name}`);
|
|
179
|
+
totalBytes += fileSize;
|
|
180
|
+
}
|
|
181
|
+
if (totalBytes > 5 * 1024 * 1024) throw new Error("bundle exceeds size limit");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function writeCompanionFiles(dirPath: string, root: "scripts" | "references", files: Array<{ filename: string; content: string }>): void {
|
|
185
|
+
if (files.length === 0) return;
|
|
186
|
+
const rootDir = path.join(dirPath, root);
|
|
187
|
+
fs.mkdirSync(rootDir, { recursive: true });
|
|
188
|
+
for (const file of files) {
|
|
189
|
+
const target = path.join(rootDir, file.filename);
|
|
190
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
191
|
+
fs.writeFileSync(target, file.content, "utf8");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sanitizeName(input: string): string {
|
|
196
|
+
return input.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveLocalSkillName(store: SqliteStore, baseName: string, sourceId: string): string {
|
|
200
|
+
if (!store.getSkillByName(baseName)) return baseName;
|
|
201
|
+
return `${baseName}-hub-${sourceId.slice(0, 8)}`;
|
|
202
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
-
import { DEFAULTS, type MemosLocalConfig, type PluginContext, type Logger } from "./types";
|
|
2
|
+
import { DEFAULTS, type MemosLocalConfig, type PluginContext, type Logger, type EmbeddingConfig, type SummarizerConfig } from "./types";
|
|
3
|
+
import { OpenClawAPIClient, type HostModelsConfig } from "./openclaw-api";
|
|
3
4
|
|
|
4
5
|
const ENV_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
5
6
|
|
|
@@ -20,6 +21,24 @@ function deepResolveEnv<T>(obj: T): T {
|
|
|
20
21
|
return obj;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function resolveProviderFallback<T extends { provider?: string; capabilities?: unknown }>(
|
|
25
|
+
config: T | undefined,
|
|
26
|
+
fallbackProvider: T["provider"],
|
|
27
|
+
enabled: boolean,
|
|
28
|
+
): T | undefined {
|
|
29
|
+
if (!config) {
|
|
30
|
+
return enabled
|
|
31
|
+
? ({ provider: fallbackProvider } as T)
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (config.provider == null && enabled) {
|
|
36
|
+
return { ...config, provider: fallbackProvider };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateDir: string): MemosLocalConfig {
|
|
24
43
|
const cfg = deepResolveEnv(raw ?? {});
|
|
25
44
|
|
|
@@ -27,6 +46,11 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
|
|
|
27
46
|
const telemetryEnabled =
|
|
28
47
|
cfg.telemetry?.enabled ??
|
|
29
48
|
(telemetryEnvVar === "false" || telemetryEnvVar === "0" ? false : true);
|
|
49
|
+
const sharingCapabilities = {
|
|
50
|
+
hostEmbedding: cfg.sharing?.capabilities?.hostEmbedding ?? false,
|
|
51
|
+
hostCompletion: cfg.sharing?.capabilities?.hostCompletion ?? false,
|
|
52
|
+
hostSkill: cfg.sharing?.capabilities?.hostSkill ?? false,
|
|
53
|
+
};
|
|
30
54
|
|
|
31
55
|
return {
|
|
32
56
|
...cfg,
|
|
@@ -54,6 +78,61 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
|
|
|
54
78
|
posthogApiKey: cfg.telemetry?.posthogApiKey ?? process.env.POSTHOG_API_KEY ?? "",
|
|
55
79
|
posthogHost: cfg.telemetry?.posthogHost ?? process.env.POSTHOG_HOST ?? "",
|
|
56
80
|
},
|
|
81
|
+
summarizer: (() => {
|
|
82
|
+
const summarizerConfig = resolveProviderFallback<SummarizerConfig>(
|
|
83
|
+
cfg.summarizer,
|
|
84
|
+
"openclaw",
|
|
85
|
+
sharingCapabilities.hostCompletion,
|
|
86
|
+
);
|
|
87
|
+
return summarizerConfig
|
|
88
|
+
? {
|
|
89
|
+
...summarizerConfig,
|
|
90
|
+
capabilities: sharingCapabilities,
|
|
91
|
+
}
|
|
92
|
+
: undefined;
|
|
93
|
+
})(),
|
|
94
|
+
embedding: (() => {
|
|
95
|
+
const embeddingConfig = resolveProviderFallback<EmbeddingConfig>(
|
|
96
|
+
cfg.embedding,
|
|
97
|
+
"openclaw",
|
|
98
|
+
sharingCapabilities.hostEmbedding,
|
|
99
|
+
);
|
|
100
|
+
return embeddingConfig
|
|
101
|
+
? {
|
|
102
|
+
...embeddingConfig,
|
|
103
|
+
capabilities: sharingCapabilities,
|
|
104
|
+
}
|
|
105
|
+
: undefined;
|
|
106
|
+
})(),
|
|
107
|
+
skillEvolution: cfg.skillEvolution ? {
|
|
108
|
+
...cfg.skillEvolution,
|
|
109
|
+
summarizer: (() => {
|
|
110
|
+
const skSumCfg = resolveProviderFallback<SummarizerConfig>(
|
|
111
|
+
cfg.skillEvolution!.summarizer as SummarizerConfig | undefined,
|
|
112
|
+
"openclaw",
|
|
113
|
+
sharingCapabilities.hostSkill,
|
|
114
|
+
);
|
|
115
|
+
return skSumCfg
|
|
116
|
+
? { ...skSumCfg, capabilities: sharingCapabilities }
|
|
117
|
+
: undefined;
|
|
118
|
+
})(),
|
|
119
|
+
} : undefined,
|
|
120
|
+
sharing: {
|
|
121
|
+
enabled: cfg.sharing?.enabled ?? false,
|
|
122
|
+
role: cfg.sharing?.role ?? "client",
|
|
123
|
+
hub: {
|
|
124
|
+
port: cfg.sharing?.hub?.port ?? 18800,
|
|
125
|
+
teamName: cfg.sharing?.hub?.teamName ?? "",
|
|
126
|
+
teamToken: cfg.sharing?.hub?.teamToken ?? "",
|
|
127
|
+
},
|
|
128
|
+
client: {
|
|
129
|
+
hubAddress: cfg.sharing?.client?.hubAddress ?? "",
|
|
130
|
+
userToken: cfg.sharing?.client?.userToken ?? "",
|
|
131
|
+
teamToken: cfg.sharing?.client?.teamToken ?? "",
|
|
132
|
+
pendingUserId: cfg.sharing?.client?.pendingUserId ?? "",
|
|
133
|
+
},
|
|
134
|
+
capabilities: sharingCapabilities,
|
|
135
|
+
},
|
|
57
136
|
};
|
|
58
137
|
}
|
|
59
138
|
|
|
@@ -62,6 +141,7 @@ export function buildContext(
|
|
|
62
141
|
workspaceDir: string,
|
|
63
142
|
rawConfig: Partial<MemosLocalConfig> | undefined,
|
|
64
143
|
log?: Logger,
|
|
144
|
+
hostModels?: HostModelsConfig,
|
|
65
145
|
): PluginContext {
|
|
66
146
|
const defaultLog: Logger = {
|
|
67
147
|
debug: (...args) => console.debug("[memos-local]", ...args),
|
|
@@ -70,10 +150,19 @@ export function buildContext(
|
|
|
70
150
|
error: (...args) => console.error("[memos-local]", ...args),
|
|
71
151
|
};
|
|
72
152
|
|
|
153
|
+
const logger = log ?? defaultLog;
|
|
154
|
+
const config = resolveConfig(rawConfig, stateDir);
|
|
155
|
+
|
|
156
|
+
// Create OpenClawAPI instance if host capabilities are enabled
|
|
157
|
+
const openclawAPI = (config.sharing?.capabilities?.hostEmbedding || config.sharing?.capabilities?.hostCompletion || config.sharing?.capabilities?.hostSkill)
|
|
158
|
+
? new OpenClawAPIClient(logger, hostModels)
|
|
159
|
+
: undefined;
|
|
160
|
+
|
|
73
161
|
return {
|
|
74
162
|
stateDir,
|
|
75
163
|
workspaceDir,
|
|
76
|
-
config
|
|
77
|
-
log:
|
|
164
|
+
config,
|
|
165
|
+
log: logger,
|
|
166
|
+
openclawAPI,
|
|
78
167
|
};
|
|
79
168
|
}
|
package/src/embedding/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EmbeddingConfig, Logger } from "../types";
|
|
1
|
+
import type { EmbeddingConfig, Logger, OpenClawAPI } from "../types";
|
|
2
2
|
import { embedOpenAI } from "./providers/openai";
|
|
3
3
|
import { embedGemini } from "./providers/gemini";
|
|
4
4
|
import { embedCohere, embedCohereQuery } from "./providers/cohere";
|
|
@@ -11,9 +11,13 @@ export class Embedder {
|
|
|
11
11
|
constructor(
|
|
12
12
|
private cfg: EmbeddingConfig | undefined,
|
|
13
13
|
private log: Logger,
|
|
14
|
+
private openclawAPI?: OpenClawAPI,
|
|
14
15
|
) {}
|
|
15
16
|
|
|
16
17
|
get provider(): string {
|
|
18
|
+
if (this.cfg?.provider === "openclaw" && this.cfg.capabilities?.hostEmbedding !== true) {
|
|
19
|
+
return "local";
|
|
20
|
+
}
|
|
17
21
|
return this.cfg?.provider ?? "local";
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -53,11 +57,13 @@ export class Embedder {
|
|
|
53
57
|
switch (provider) {
|
|
54
58
|
case "openai":
|
|
55
59
|
case "openai_compatible":
|
|
60
|
+
case "azure_openai":
|
|
61
|
+
case "zhipu":
|
|
62
|
+
case "siliconflow":
|
|
63
|
+
case "bailian":
|
|
56
64
|
result = await embedOpenAI(texts, cfg!, this.log); break;
|
|
57
65
|
case "gemini":
|
|
58
66
|
result = await embedGemini(texts, cfg!, this.log); break;
|
|
59
|
-
case "azure_openai":
|
|
60
|
-
result = await embedOpenAI(texts, cfg!, this.log); break;
|
|
61
67
|
case "cohere":
|
|
62
68
|
result = await embedCohere(texts, cfg!, this.log); break;
|
|
63
69
|
case "mistral":
|
|
@@ -79,4 +85,20 @@ export class Embedder {
|
|
|
79
85
|
throw err;
|
|
80
86
|
}
|
|
81
87
|
}
|
|
88
|
+
|
|
89
|
+
private async embedOpenClaw(texts: string[]): Promise<number[][]> {
|
|
90
|
+
if (!this.openclawAPI) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"OpenClaw API not available. Ensure sharing.capabilities.hostEmbedding is enabled in config."
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.log.debug(`Calling OpenClaw embed API for ${texts.length} texts`);
|
|
97
|
+
const response = await this.openclawAPI.embed({
|
|
98
|
+
texts,
|
|
99
|
+
model: this.cfg?.model,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return response.embeddings;
|
|
103
|
+
}
|
|
82
104
|
}
|
package/src/hub/auth.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
2
|
+
import type { UserRole, UserStatus } from "../sharing/types";
|
|
3
|
+
|
|
4
|
+
type UserTokenPayload = {
|
|
5
|
+
userId: string;
|
|
6
|
+
username: string;
|
|
7
|
+
role: UserRole;
|
|
8
|
+
status: UserStatus;
|
|
9
|
+
exp: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function base64url(input: Buffer | string): string {
|
|
13
|
+
return Buffer.from(input)
|
|
14
|
+
.toString("base64")
|
|
15
|
+
.replace(/\+/g, "-")
|
|
16
|
+
.replace(/\//g, "_")
|
|
17
|
+
.replace(/=+$/g, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unbase64url(input: string): Buffer {
|
|
21
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((input.length + 3) % 4);
|
|
22
|
+
return Buffer.from(padded, "base64");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sign(value: string, secret: string): string {
|
|
26
|
+
return base64url(createHmac("sha256", secret).update(value).digest());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createTeamToken(secret: string): string {
|
|
30
|
+
const nonce = base64url(randomBytes(12));
|
|
31
|
+
const body = `team.${nonce}`;
|
|
32
|
+
return `${body}.${sign(body, secret)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function verifyTeamToken(token: string, secret: string): boolean {
|
|
36
|
+
const idx = token.lastIndexOf(".");
|
|
37
|
+
if (idx <= 0) return false;
|
|
38
|
+
const body = token.slice(0, idx);
|
|
39
|
+
const sig = token.slice(idx + 1);
|
|
40
|
+
const expected = sign(body, secret);
|
|
41
|
+
try {
|
|
42
|
+
return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function issueUserToken(
|
|
49
|
+
payload: { userId: string; username: string; role: UserRole; status: UserStatus },
|
|
50
|
+
secret: string,
|
|
51
|
+
ttlMs = 24 * 60 * 60 * 1000,
|
|
52
|
+
): string {
|
|
53
|
+
const full: UserTokenPayload = { ...payload, exp: Date.now() + ttlMs };
|
|
54
|
+
const body = base64url(JSON.stringify(full));
|
|
55
|
+
return `${body}.${sign(body, secret)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function verifyUserToken(token: string, secret: string): Omit<UserTokenPayload, "exp"> | null {
|
|
59
|
+
const idx = token.lastIndexOf(".");
|
|
60
|
+
if (idx <= 0) return null;
|
|
61
|
+
const body = token.slice(0, idx);
|
|
62
|
+
const sig = token.slice(idx + 1);
|
|
63
|
+
const expected = sign(body, secret);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null;
|
|
67
|
+
const parsed = JSON.parse(unbase64url(body).toString("utf8")) as UserTokenPayload;
|
|
68
|
+
if (parsed.exp < Date.now()) return null;
|
|
69
|
+
return {
|
|
70
|
+
userId: parsed.userId,
|
|
71
|
+
username: parsed.username,
|
|
72
|
+
role: parsed.role,
|
|
73
|
+
status: parsed.status,
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|