libp2p-mesh 2026.6.19 → 2026.6.21
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 +227 -5
- package/dist/src/agent-tools.d.ts +34 -14
- package/dist/src/agent-tools.js +99 -14
- package/dist/src/instance-peer-store.js +6 -2
- package/dist/src/instance-router.js +137 -21
- package/dist/src/labels-wizard.d.ts +21 -0
- package/dist/src/labels-wizard.js +168 -0
- package/dist/src/peer-label-store.d.ts +11 -0
- package/dist/src/peer-label-store.js +172 -0
- package/dist/src/plugin.js +13 -4
- package/dist/src/profile-cli.d.ts +9 -0
- package/dist/src/profile-cli.js +33 -0
- package/dist/src/prompt-config.js +35 -9
- package/dist/src/types.d.ts +44 -7
- package/dist/src/user-md-agent-attributes.d.ts +24 -0
- package/dist/src/user-md-agent-attributes.js +180 -0
- package/package.json +1 -1
- package/src/agent-tools.ts +144 -21
- package/src/instance-peer-store.ts +7 -2
- package/src/instance-router.ts +194 -24
- package/src/labels-wizard.ts +221 -0
- package/src/peer-label-store.ts +224 -0
- package/src/plugin.ts +14 -5
- package/src/profile-cli.ts +48 -0
- package/src/prompt-config.ts +35 -9
- package/src/types.ts +50 -5
- package/src/user-md-agent-attributes.ts +243 -0
package/src/types.ts
CHANGED
|
@@ -102,10 +102,51 @@ export type UserPublicAttribute =
|
|
|
102
102
|
source: "profile";
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
export type UserMdAttributeSource = {
|
|
106
|
+
loadTags(): Promise<UserPublicAttribute[]>;
|
|
107
|
+
refreshTags?(): Promise<UserPublicAttribute[]>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type LocalPeerLabel = {
|
|
111
|
+
key: string;
|
|
112
|
+
value: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type LocalPeerLabelAttribute = {
|
|
116
|
+
kind: "structured";
|
|
117
|
+
key: string;
|
|
118
|
+
value: string;
|
|
119
|
+
label: string;
|
|
120
|
+
source: "local";
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type PeerLabelsFile = {
|
|
124
|
+
version: 1;
|
|
125
|
+
updatedAt: number;
|
|
126
|
+
peers: Record<string, { labels: LocalPeerLabel[] }>;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type PeerLabelStore = {
|
|
130
|
+
load(): Promise<PeerLabelsFile>;
|
|
131
|
+
save(file: PeerLabelsFile): Promise<PeerLabelsFile>;
|
|
132
|
+
listRawLabels(instanceId: string): Promise<LocalPeerLabel[]>;
|
|
133
|
+
listLabels(instanceId: string): Promise<LocalPeerLabelAttribute[]>;
|
|
134
|
+
replaceLabels(instanceId: string, labels: LocalPeerLabel[]): Promise<PeerLabelsFile>;
|
|
135
|
+
};
|
|
136
|
+
|
|
105
137
|
export type UserAttributeMatch =
|
|
106
138
|
| { kind: "tag"; value: string }
|
|
107
139
|
| { kind: "structured"; key: string; value: string };
|
|
108
140
|
|
|
141
|
+
export type UserAttributeMatchScope = "public" | "local" | "all";
|
|
142
|
+
|
|
143
|
+
export type UserAttributeMatchSource = "public" | "local" | "all";
|
|
144
|
+
|
|
145
|
+
export type UserAttributeMessageOptions = {
|
|
146
|
+
dryRun?: boolean;
|
|
147
|
+
scope?: UserAttributeMatchScope;
|
|
148
|
+
};
|
|
149
|
+
|
|
109
150
|
export interface InstancePeerRecord {
|
|
110
151
|
instanceId: string;
|
|
111
152
|
peerId: string;
|
|
@@ -139,7 +180,8 @@ export type UserAttributeMessageTarget = {
|
|
|
139
180
|
instanceId: string;
|
|
140
181
|
instanceName?: string;
|
|
141
182
|
peerId: string;
|
|
142
|
-
matchedAttribute: UserPublicAttribute;
|
|
183
|
+
matchedAttribute: UserPublicAttribute | LocalPeerLabelAttribute;
|
|
184
|
+
matchSource: UserAttributeMatchSource;
|
|
143
185
|
};
|
|
144
186
|
|
|
145
187
|
export type UserAttributeMessageDeliveryResult = UserAttributeMessageTarget & {
|
|
@@ -149,6 +191,7 @@ export type UserAttributeMessageDeliveryResult = UserAttributeMessageTarget & {
|
|
|
149
191
|
};
|
|
150
192
|
|
|
151
193
|
export type UserAttributeMessageResult = {
|
|
194
|
+
scope: UserAttributeMatchScope;
|
|
152
195
|
matched: number;
|
|
153
196
|
sent: number;
|
|
154
197
|
delivered: number;
|
|
@@ -194,12 +237,13 @@ export type InstanceRouterOptions = {
|
|
|
194
237
|
warn?: (message: string) => void;
|
|
195
238
|
error?: (message: string) => void;
|
|
196
239
|
};
|
|
197
|
-
userAttributeSource?:
|
|
198
|
-
loadTags(): Promise<UserPublicAttribute[]>;
|
|
199
|
-
};
|
|
240
|
+
userAttributeSource?: UserMdAttributeSource;
|
|
200
241
|
userProfileStore?: {
|
|
201
242
|
listAttributes(): Promise<UserPublicAttribute[]>;
|
|
202
243
|
};
|
|
244
|
+
peerLabelStore?: {
|
|
245
|
+
listLabels(instanceId: string): Promise<LocalPeerLabelAttribute[]>;
|
|
246
|
+
};
|
|
203
247
|
};
|
|
204
248
|
|
|
205
249
|
export interface InstanceRouter {
|
|
@@ -209,6 +253,7 @@ export interface InstanceRouter {
|
|
|
209
253
|
stop(): Promise<void>;
|
|
210
254
|
handleMessage(msg: P2PMessage): Promise<void>;
|
|
211
255
|
announceToPeer(peerId: string): Promise<void>;
|
|
256
|
+
refreshPublicAttributes(): Promise<void>;
|
|
212
257
|
listInstances(): Promise<InstancePeerRecord[]>;
|
|
213
258
|
resolveInstance(instanceId: string): Promise<InstancePeerRecord | undefined>;
|
|
214
259
|
sendInstanceMessage(instanceId: string, message: string): Promise<{
|
|
@@ -225,7 +270,7 @@ export interface InstanceRouter {
|
|
|
225
270
|
sendUserAttributeMessage(
|
|
226
271
|
match: UserAttributeMatch,
|
|
227
272
|
message: string,
|
|
228
|
-
options?:
|
|
273
|
+
options?: UserAttributeMessageOptions,
|
|
229
274
|
): Promise<UserAttributeMessageResult>;
|
|
230
275
|
}
|
|
231
276
|
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { UserMdAttributeSource, UserPublicAttribute } from "./types.js";
|
|
7
|
+
import { normalizeAttributeValue } from "./user-attributes.js";
|
|
8
|
+
import { resolveUserMdPath } from "./user-md-attributes.js";
|
|
9
|
+
|
|
10
|
+
const CACHE_VERSION = 1;
|
|
11
|
+
const MAX_AGENT_TAGS = 10;
|
|
12
|
+
const MAX_AGENT_TAG_LENGTH = 40;
|
|
13
|
+
|
|
14
|
+
type Logger = {
|
|
15
|
+
debug?: (message: string) => void;
|
|
16
|
+
warn?: (message: string) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type UserMdAttributeCacheFile = {
|
|
20
|
+
version: 1;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
userMdHash: string;
|
|
23
|
+
attributes: UserPublicAttribute[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type UserMdAttributeExtractionUnavailable = {
|
|
27
|
+
unavailable: true;
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type UserMdAttributeExtractor = {
|
|
32
|
+
extract(request: {
|
|
33
|
+
markdown: string;
|
|
34
|
+
sourcePath: string;
|
|
35
|
+
}): Promise<UserPublicAttribute[] | UserMdAttributeExtractionUnavailable>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const defaultExtractor: UserMdAttributeExtractor = {
|
|
39
|
+
async extract() {
|
|
40
|
+
return {
|
|
41
|
+
unavailable: true,
|
|
42
|
+
reason: "USER.md agent extraction is unavailable in this runtime",
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function resolveUserMdAttributeCachePath(customPath?: string): string {
|
|
48
|
+
if (customPath) {
|
|
49
|
+
return customPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR;
|
|
53
|
+
if (stateDir) {
|
|
54
|
+
return path.join(stateDir, "libp2p", "user-md-attributes-cache.json");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return path.join(homedir(), ".openclaw", "libp2p", "user-md-attributes-cache.json");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
61
|
+
return typeof value === "object" && value !== null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeWhitespace(value: string): string {
|
|
65
|
+
return value.trim().replace(/\s+/g, " ");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function looksLikeSentence(value: string): boolean {
|
|
69
|
+
return /[。.!?!?]/.test(value) || value.split(/\s+/).filter(Boolean).length > 4;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeAgentTag(value: unknown): UserPublicAttribute | undefined {
|
|
73
|
+
if (!isRecord(value) || value.kind !== "tag" || value.source !== "USER.md") {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value.value !== "string" || typeof value.label !== "string") {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tagValue = normalizeWhitespace(value.value);
|
|
82
|
+
const label = normalizeWhitespace(value.label);
|
|
83
|
+
if (!tagValue || !label || tagValue.length > MAX_AGENT_TAG_LENGTH || looksLikeSentence(tagValue)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
kind: "tag",
|
|
89
|
+
value: tagValue,
|
|
90
|
+
label,
|
|
91
|
+
source: "USER.md",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateExtractedUserMdTags(value: unknown): UserPublicAttribute[] {
|
|
96
|
+
if (!Array.isArray(value)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const attributes: UserPublicAttribute[] = [];
|
|
101
|
+
const seen = new Set<string>();
|
|
102
|
+
|
|
103
|
+
for (const rawAttribute of value) {
|
|
104
|
+
const attribute = normalizeAgentTag(rawAttribute);
|
|
105
|
+
if (!attribute) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const key = normalizeAttributeValue(attribute.value);
|
|
110
|
+
if (seen.has(key)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
seen.add(key);
|
|
115
|
+
attributes.push(attribute);
|
|
116
|
+
if (attributes.length >= MAX_AGENT_TAGS) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return attributes;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hashMarkdown(markdown: string): string {
|
|
125
|
+
return createHash("sha256").update(markdown, "utf8").digest("hex");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeCacheFile(value: unknown): UserMdAttributeCacheFile | undefined {
|
|
129
|
+
if (!isRecord(value) || value.version !== CACHE_VERSION || typeof value.userMdHash !== "string") {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const attributes = validateExtractedUserMdTags(value.attributes);
|
|
134
|
+
if (!Array.isArray(value.attributes) || attributes.length !== value.attributes.length) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
version: CACHE_VERSION,
|
|
140
|
+
updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : Date.now(),
|
|
141
|
+
userMdHash: value.userMdHash,
|
|
142
|
+
attributes,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function readCache(cachePath: string): Promise<UserMdAttributeCacheFile | undefined> {
|
|
147
|
+
try {
|
|
148
|
+
return normalizeCacheFile(JSON.parse(await readFile(cachePath, "utf8")));
|
|
149
|
+
} catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function writeCache(cachePath: string, userMdHash: string, attributes: UserPublicAttribute[]): Promise<void> {
|
|
155
|
+
const cache: UserMdAttributeCacheFile = {
|
|
156
|
+
version: CACHE_VERSION,
|
|
157
|
+
updatedAt: Date.now(),
|
|
158
|
+
userMdHash,
|
|
159
|
+
attributes: validateExtractedUserMdTags(attributes),
|
|
160
|
+
};
|
|
161
|
+
const dir = path.dirname(cachePath);
|
|
162
|
+
const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
163
|
+
|
|
164
|
+
await mkdir(dir, { recursive: true });
|
|
165
|
+
try {
|
|
166
|
+
await writeFile(tmpPath, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
|
|
167
|
+
await rename(tmpPath, cachePath);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await rm(tmpPath, { force: true }).catch(() => undefined);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isExtractionUnavailable(value: unknown): value is UserMdAttributeExtractionUnavailable {
|
|
175
|
+
return isRecord(value) && value.unavailable === true && typeof value.reason === "string";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createUserMdAgentAttributeSource(options?: {
|
|
179
|
+
path?: string;
|
|
180
|
+
cachePath?: string;
|
|
181
|
+
extractor?: UserMdAttributeExtractor;
|
|
182
|
+
logger?: Logger;
|
|
183
|
+
}): UserMdAttributeSource {
|
|
184
|
+
const filePath = resolveUserMdPath(options?.path);
|
|
185
|
+
const cachePath = resolveUserMdAttributeCachePath(options?.cachePath);
|
|
186
|
+
const extractor = options?.extractor ?? defaultExtractor;
|
|
187
|
+
const logger = options?.logger;
|
|
188
|
+
|
|
189
|
+
async function readUserMd(): Promise<{ markdown: string; userMdHash: string } | undefined> {
|
|
190
|
+
try {
|
|
191
|
+
const markdown = await readFile(filePath, "utf8");
|
|
192
|
+
return { markdown, userMdHash: hashMarkdown(markdown) };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
logger?.warn?.(`[libp2p-mesh] Failed to read USER.md at ${filePath}: ${(error as Error).message}`);
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function loadCachedTagsForCurrentHash(): Promise<UserPublicAttribute[]> {
|
|
204
|
+
const userMd = await readUserMd();
|
|
205
|
+
if (!userMd) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const cache = await readCache(cachePath);
|
|
210
|
+
return cache?.userMdHash === userMd.userMdHash ? [...cache.attributes] : [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
async loadTags(): Promise<UserPublicAttribute[]> {
|
|
215
|
+
return loadCachedTagsForCurrentHash();
|
|
216
|
+
},
|
|
217
|
+
async refreshTags(): Promise<UserPublicAttribute[]> {
|
|
218
|
+
const userMd = await readUserMd();
|
|
219
|
+
if (!userMd) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const cache = await readCache(cachePath);
|
|
224
|
+
if (cache?.userMdHash === userMd.userMdHash) {
|
|
225
|
+
return [...cache.attributes];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const extracted = await extractor.extract({
|
|
229
|
+
markdown: userMd.markdown,
|
|
230
|
+
sourcePath: filePath,
|
|
231
|
+
});
|
|
232
|
+
if (isExtractionUnavailable(extracted)) {
|
|
233
|
+
logger?.warn?.(`[libp2p-mesh] USER.md agent extraction unavailable: ${extracted.reason}`);
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const attributes = validateExtractedUserMdTags(extracted);
|
|
238
|
+
await writeCache(cachePath, userMd.userMdHash, attributes);
|
|
239
|
+
logger?.debug?.(`[libp2p-mesh] Saved USER.md agent attribute cache to ${cachePath}`);
|
|
240
|
+
return attributes;
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|