@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.
Files changed (138) hide show
  1. package/README.md +38 -21
  2. package/dist/client/connector.d.ts +26 -0
  3. package/dist/client/connector.d.ts.map +1 -0
  4. package/dist/client/connector.js +127 -0
  5. package/dist/client/connector.js.map +1 -0
  6. package/dist/client/hub.d.ts +61 -0
  7. package/dist/client/hub.d.ts.map +1 -0
  8. package/dist/client/hub.js +148 -0
  9. package/dist/client/hub.js.map +1 -0
  10. package/dist/client/skill-sync.d.ts +29 -0
  11. package/dist/client/skill-sync.d.ts.map +1 -0
  12. package/dist/client/skill-sync.js +216 -0
  13. package/dist/client/skill-sync.js.map +1 -0
  14. package/dist/config.d.ts +2 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +70 -3
  17. package/dist/config.js.map +1 -1
  18. package/dist/embedding/index.d.ts +4 -2
  19. package/dist/embedding/index.d.ts.map +1 -1
  20. package/dist/embedding/index.js +21 -4
  21. package/dist/embedding/index.js.map +1 -1
  22. package/dist/hub/auth.d.ts +19 -0
  23. package/dist/hub/auth.d.ts.map +1 -0
  24. package/dist/hub/auth.js +70 -0
  25. package/dist/hub/auth.js.map +1 -0
  26. package/dist/hub/server.d.ts +41 -0
  27. package/dist/hub/server.d.ts.map +1 -0
  28. package/dist/hub/server.js +742 -0
  29. package/dist/hub/server.js.map +1 -0
  30. package/dist/hub/user-manager.d.ts +28 -0
  31. package/dist/hub/user-manager.d.ts.map +1 -0
  32. package/dist/hub/user-manager.js +112 -0
  33. package/dist/hub/user-manager.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/ingest/providers/index.d.ts +10 -2
  39. package/dist/ingest/providers/index.d.ts.map +1 -1
  40. package/dist/ingest/providers/index.js +242 -8
  41. package/dist/ingest/providers/index.js.map +1 -1
  42. package/dist/ingest/providers/openai.d.ts +1 -0
  43. package/dist/ingest/providers/openai.d.ts.map +1 -1
  44. package/dist/ingest/providers/openai.js +1 -0
  45. package/dist/ingest/providers/openai.js.map +1 -1
  46. package/dist/ingest/task-processor.js +1 -1
  47. package/dist/ingest/task-processor.js.map +1 -1
  48. package/dist/openclaw-api.d.ts +53 -0
  49. package/dist/openclaw-api.d.ts.map +1 -0
  50. package/dist/openclaw-api.js +189 -0
  51. package/dist/openclaw-api.js.map +1 -0
  52. package/dist/recall/engine.js +2 -2
  53. package/dist/recall/engine.js.map +1 -1
  54. package/dist/shared/llm-call.d.ts +4 -1
  55. package/dist/shared/llm-call.d.ts.map +1 -1
  56. package/dist/shared/llm-call.js +15 -0
  57. package/dist/shared/llm-call.js.map +1 -1
  58. package/dist/sharing/types.contract.d.ts +2 -0
  59. package/dist/sharing/types.contract.d.ts.map +1 -0
  60. package/dist/sharing/types.contract.js +3 -0
  61. package/dist/sharing/types.contract.js.map +1 -0
  62. package/dist/sharing/types.d.ts +80 -0
  63. package/dist/sharing/types.d.ts.map +1 -0
  64. package/dist/sharing/types.js +3 -0
  65. package/dist/sharing/types.js.map +1 -0
  66. package/dist/skill/evaluator.d.ts.map +1 -1
  67. package/dist/skill/evaluator.js +2 -2
  68. package/dist/skill/evaluator.js.map +1 -1
  69. package/dist/skill/generator.d.ts.map +1 -1
  70. package/dist/skill/generator.js +4 -4
  71. package/dist/skill/generator.js.map +1 -1
  72. package/dist/skill/upgrader.js +1 -1
  73. package/dist/skill/upgrader.js.map +1 -1
  74. package/dist/skill/validator.js +1 -1
  75. package/dist/skill/validator.js.map +1 -1
  76. package/dist/storage/sqlite.d.ts +294 -0
  77. package/dist/storage/sqlite.d.ts.map +1 -1
  78. package/dist/storage/sqlite.js +902 -8
  79. package/dist/storage/sqlite.js.map +1 -1
  80. package/dist/tools/index.d.ts +1 -0
  81. package/dist/tools/index.d.ts.map +1 -1
  82. package/dist/tools/index.js +3 -1
  83. package/dist/tools/index.js.map +1 -1
  84. package/dist/tools/memory-search.d.ts +3 -2
  85. package/dist/tools/memory-search.d.ts.map +1 -1
  86. package/dist/tools/memory-search.js +48 -7
  87. package/dist/tools/memory-search.js.map +1 -1
  88. package/dist/tools/network-memory-detail.d.ts +4 -0
  89. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  90. package/dist/tools/network-memory-detail.js +34 -0
  91. package/dist/tools/network-memory-detail.js.map +1 -0
  92. package/dist/types.d.ts +47 -2
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js.map +1 -1
  95. package/dist/update-check.d.ts.map +1 -1
  96. package/dist/update-check.js +0 -1
  97. package/dist/update-check.js.map +1 -1
  98. package/dist/viewer/html.d.ts.map +1 -1
  99. package/dist/viewer/html.js +2396 -289
  100. package/dist/viewer/html.js.map +1 -1
  101. package/dist/viewer/server.d.ts +43 -0
  102. package/dist/viewer/server.d.ts.map +1 -1
  103. package/dist/viewer/server.js +1180 -33
  104. package/dist/viewer/server.js.map +1 -1
  105. package/index.ts +445 -25
  106. package/openclaw.plugin.json +2 -1
  107. package/package.json +2 -1
  108. package/scripts/postinstall.cjs +282 -45
  109. package/skill/memos-memory-guide/SKILL.md +26 -2
  110. package/src/client/connector.ts +124 -0
  111. package/src/client/hub.ts +189 -0
  112. package/src/client/skill-sync.ts +202 -0
  113. package/src/config.ts +92 -3
  114. package/src/embedding/index.ts +25 -3
  115. package/src/hub/auth.ts +78 -0
  116. package/src/hub/server.ts +734 -0
  117. package/src/hub/user-manager.ts +126 -0
  118. package/src/index.ts +7 -4
  119. package/src/ingest/providers/index.ts +279 -8
  120. package/src/ingest/providers/openai.ts +1 -1
  121. package/src/ingest/task-processor.ts +1 -1
  122. package/src/openclaw-api.ts +287 -0
  123. package/src/recall/engine.ts +2 -2
  124. package/src/shared/llm-call.ts +19 -1
  125. package/src/sharing/types.contract.ts +40 -0
  126. package/src/sharing/types.ts +102 -0
  127. package/src/skill/evaluator.ts +3 -2
  128. package/src/skill/generator.ts +6 -4
  129. package/src/skill/upgrader.ts +1 -1
  130. package/src/skill/validator.ts +1 -1
  131. package/src/storage/sqlite.ts +1167 -7
  132. package/src/tools/index.ts +1 -0
  133. package/src/tools/memory-search.ts +57 -8
  134. package/src/tools/network-memory-detail.ts +34 -0
  135. package/src/types.ts +48 -2
  136. package/src/update-check.ts +0 -1
  137. package/src/viewer/html.ts +2396 -289
  138. 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: resolveConfig(rawConfig, stateDir),
77
- log: log ?? defaultLog,
164
+ config,
165
+ log: logger,
166
+ openclawAPI,
78
167
  };
79
168
  }
@@ -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
  }
@@ -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
+ }