libp2p-mesh 2026.6.13 → 2026.6.15

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 CHANGED
@@ -225,11 +225,34 @@ If `inboundTargets` is an empty array, inbound delivery is disabled. If `inbound
225
225
  | `relayList` | `string[]` | `[]` | Multiaddrs of relays to reserve a slot on |
226
226
  | `discoverRelays` | `number` | `0` | Auto-discover this many relays via content routing |
227
227
  | `announceAddrs` | `string[]` | `[]` | Extra multiaddrs to announce on top of auto-detected ones |
228
+ | `announceLogDetail` | `"off" \| "summary" \| "payload"` | `"summary"` | Controls instance announce logging. `summary` logs peer, instance, address count, and attribute count; `payload` also logs the full announce JSON; `off` disables only the new announce summary/payload logs and keeps legacy/basic info logs. |
228
229
  | `inboundChannel` | `string` | `undefined` | OpenClaw channel used to display inbound P2P user messages, for example `"feishu"` |
229
230
  | `inboundTarget` | `string` | `undefined` | OpenClaw channel target for inbound P2P messages, for example `user:ou_xxx` or `chat:oc_xxx` |
230
231
  | `inboundTargets` | `array` | `undefined` | Optional list of receiver-owned channel targets for inbound P2P user messages. When present, it overrides `inboundChannel`/`inboundTarget`; an empty array disables inbound delivery. |
231
232
  | `deliveryAckTimeoutMs` | `number` | `15000` | Timeout for waiting on remote channel delivery ACKs |
232
233
 
234
+ ### Announce Startup and Logging
235
+
236
+ During gateway startup, `libp2p-mesh` registers the instance router handlers and direct/broadcast inbound handlers before starting the mesh node. This makes early `instance-announce` messages observable as soon as peers connect, instead of waiting until after mesh startup has already completed.
237
+
238
+ Instance announce logs are controlled by `plugins.entries["libp2p-mesh"].config.announceLogDetail`:
239
+
240
+ - `summary` is the default. It logs send/receive direction, peer ID, instance ID, multiaddr count, and public attribute count. It does not print the full announce JSON.
241
+ - `off` disables the new announce summary and payload logs. It still keeps legacy/basic info logs such as sent announce lines and instance mapping updates, along with warnings and errors.
242
+ - `payload` logs the same summary plus the full announce JSON at debug level.
243
+
244
+ Use the debug command to inspect or change this value:
245
+
246
+ ```bash
247
+ openclaw libp2p-mesh debug
248
+ ```
249
+
250
+ Full payload logging is intended for short-lived troubleshooting only. Announce payloads can include `userPublicAttributes`, peer multiaddrs, the instance pubkey, and instance identity fields. After changing the setting, restart the gateway for the new logging level to take effect:
251
+
252
+ ```bash
253
+ openclaw gateway restart
254
+ ```
255
+
233
256
  ## NAT Traversal
234
257
 
235
258
  When both peers have a routable address (same LAN, public IPs, or working port-forwarding) no extra setup is needed. The defaults above kick in automatically:
@@ -453,6 +476,18 @@ There are two sources:
453
476
  - `USER.md` tags are extracted read-only at gateway startup. The plugin never edits `USER.md`.
454
477
  - `user-profile.json` stores manually managed structured attributes such as group, project, role, skill, or a custom key.
455
478
 
479
+ By default, `USER.md` is read from:
480
+
481
+ ```text
482
+ ~/.openclaw/workspace/USER.md
483
+ ```
484
+
485
+ When `OPENCLAW_STATE_DIR` is set, the plugin reads:
486
+
487
+ ```text
488
+ $OPENCLAW_STATE_DIR/workspace/USER.md
489
+ ```
490
+
456
491
  Run the profile wizard to manage structured attributes:
457
492
 
458
493
  ```bash
@@ -616,6 +651,20 @@ Peer connection and disconnection are logged at `info` level:
616
651
 
617
652
  If these lines are missing, confirm the gateway is running with normal info logs enabled and that both instances are on the same mDNS, bootstrap, or relay network.
618
653
 
654
+ ### Instance announce routes are missing between two machines
655
+
656
+ If peers connect but sending by OpenClaw instance ID fails or `instance-peer.json` is not updated, first confirm both gateways were restarted after the latest config change. On startup, the gateway now attaches the instance router plus inbound message handlers before starting the mesh, so early announces should be handled once the peer connection appears.
657
+
658
+ For a short debug session on both computers:
659
+
660
+ 1. Run `openclaw libp2p-mesh debug`.
661
+ 2. Set `announceLogDetail` to `payload` and confirm the privacy warning.
662
+ 3. Restart both gateways with `openclaw gateway restart`.
663
+ 4. Watch for summary lines and debug lines containing full announce payload JSON.
664
+ 5. Return to `summary` or `off` with `openclaw libp2p-mesh debug`, then restart again.
665
+
666
+ Full payload logs may expose `userPublicAttributes`, multiaddrs, pubkey, and instance identity, so avoid sharing these logs outside the debugging context.
667
+
619
668
  ## Architecture
620
669
 
621
670
  ```
@@ -0,0 +1,21 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
3
+ import type { SetupPrompter } from "./setup-wizard.js";
4
+ import type { AnnounceLogDetail } from "./types.js";
5
+ type CliRootCommand = {
6
+ command(name: string): {
7
+ description(text: string): {
8
+ action(handler: () => Promise<void>): void;
9
+ };
10
+ };
11
+ };
12
+ export type DebugConfigWriter = {
13
+ saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
14
+ };
15
+ export type DebugCliDeps = {
16
+ createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
17
+ createWriter?: (api: OpenClawPluginApi) => DebugConfigWriter;
18
+ };
19
+ export declare function registerLibp2pMeshDebugCli(api: OpenClawPluginApi, deps?: DebugCliDeps): void;
20
+ export declare function registerLibp2pMeshDebugCommand(root: CliRootCommand, api: OpenClawPluginApi, ctx: OpenClawPluginCliContext, deps?: DebugCliDeps): void;
21
+ export {};
@@ -0,0 +1,54 @@
1
+ import { createReadlinePrompter, LIBP2P_MESH_CLI_REGISTRATION, } from "./setup-cli.js";
2
+ import { applyAnnounceLogDetail, getAnnounceLogDetail, } from "./setup-config.js";
3
+ import { runDebugWizard } from "./debug-wizard.js";
4
+ const DEBUG_CLI_AFTER_WRITE = {
5
+ mode: "none",
6
+ reason: "libp2p-mesh debug config updated; restart manually to apply gateway changes.",
7
+ };
8
+ export function registerLibp2pMeshDebugCli(api, deps = {}) {
9
+ api.registerCli((ctx) => {
10
+ const root = ctx.program
11
+ .command("libp2p-mesh")
12
+ .description("Configure libp2p-mesh plugin.");
13
+ registerLibp2pMeshDebugCommand(root, api, ctx, deps);
14
+ }, LIBP2P_MESH_CLI_REGISTRATION);
15
+ }
16
+ export function registerLibp2pMeshDebugCommand(root, api, ctx, deps = {}) {
17
+ root
18
+ .command("debug")
19
+ .description("Manage libp2p-mesh debug logging config.")
20
+ .action(async () => {
21
+ const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter());
22
+ const writer = deps.createWriter?.(api) ?? createOpenClawDebugConfigWriter(api);
23
+ try {
24
+ const result = await runDebugWizard({
25
+ prompter,
26
+ current: getAnnounceLogDetail(ctx.config),
27
+ writer,
28
+ });
29
+ prompter.print(result.message);
30
+ }
31
+ finally {
32
+ prompter.close?.();
33
+ }
34
+ });
35
+ }
36
+ function createOpenClawDebugConfigWriter(api) {
37
+ return {
38
+ async saveAnnounceLogDetail(detail) {
39
+ await api.runtime.config.mutateConfigFile({
40
+ afterWrite: DEBUG_CLI_AFTER_WRITE,
41
+ mutate(draft) {
42
+ const nextConfig = applyAnnounceLogDetail(draft, detail);
43
+ replaceConfig(draft, nextConfig);
44
+ },
45
+ });
46
+ },
47
+ };
48
+ }
49
+ function replaceConfig(draft, nextConfig) {
50
+ for (const key of Object.keys(draft)) {
51
+ delete draft[key];
52
+ }
53
+ Object.assign(draft, structuredClone(nextConfig));
54
+ }
@@ -0,0 +1,19 @@
1
+ import { type SetupPrompter } from "./setup-wizard.js";
2
+ import type { AnnounceLogDetail } from "./types.js";
3
+ export type DebugPromptChoice = AnnounceLogDetail | "cancel";
4
+ export type RunDebugWizardOptions = {
5
+ prompter: SetupPrompter;
6
+ current: AnnounceLogDetail;
7
+ writer: {
8
+ saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
9
+ };
10
+ };
11
+ export type DebugWizardResult = {
12
+ status: "saved";
13
+ announceLogDetail: AnnounceLogDetail;
14
+ message: string;
15
+ } | {
16
+ status: "cancelled";
17
+ message: string;
18
+ };
19
+ export declare function runDebugWizard(options: RunDebugWizardOptions): Promise<DebugWizardResult>;
@@ -0,0 +1,41 @@
1
+ import { SetupCancelledError } from "./setup-wizard.js";
2
+ const CANCELLED_MESSAGE = "Debug configuration cancelled. No changes were written.";
3
+ const SAVED_MESSAGE = "Debug config updated.\n\nRestart the gateway to apply changes:\nopenclaw gateway restart";
4
+ export async function runDebugWizard(options) {
5
+ try {
6
+ options.prompter.print(`Current announceLogDetail: ${options.current}`);
7
+ const selected = await options.prompter.select("Set announceLogDetail:", [
8
+ { label: "summary: log peer, instance, address and attribute counts", value: "summary" },
9
+ { label: "off: disable announce summary/payload logs", value: "off" },
10
+ { label: "payload: log full announce JSON", value: "payload" },
11
+ { label: "Cancel", value: "cancel" },
12
+ ]);
13
+ if (selected === "cancel") {
14
+ return cancelledResult();
15
+ }
16
+ if (selected === "payload") {
17
+ const confirmed = await options.prompter.confirm("Full announce payload logs may include userPublicAttributes, multiaddrs, pubkey and instance identity. Enable payload logs?", false);
18
+ if (!confirmed) {
19
+ return cancelledResult();
20
+ }
21
+ }
22
+ await options.writer.saveAnnounceLogDetail(selected);
23
+ return {
24
+ status: "saved",
25
+ announceLogDetail: selected,
26
+ message: SAVED_MESSAGE,
27
+ };
28
+ }
29
+ catch (error) {
30
+ if (error instanceof SetupCancelledError) {
31
+ return cancelledResult();
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+ function cancelledResult() {
37
+ return {
38
+ status: "cancelled",
39
+ message: CANCELLED_MESSAGE,
40
+ };
41
+ }
@@ -14,6 +14,17 @@ function isNonEmptyString(value) {
14
14
  function summarizeError(error) {
15
15
  return error instanceof Error ? error.message : String(error);
16
16
  }
17
+ function effectiveAnnounceLogDetail(value) {
18
+ if (value === "off" || value === "payload") {
19
+ return value;
20
+ }
21
+ return "summary";
22
+ }
23
+ function countAnnounceAttributes(payload) {
24
+ return Array.isArray(payload.userPublicAttributes)
25
+ ? payload.userPublicAttributes.length
26
+ : 0;
27
+ }
17
28
  function describeAttributeMatch(match) {
18
29
  if (match.kind === "tag") {
19
30
  return `tag ${match.value}`;
@@ -81,6 +92,7 @@ export function createInstanceRouter(options) {
81
92
  const { mesh, store, delivery } = options;
82
93
  const config = options.config ?? {};
83
94
  const logger = options.logger;
95
+ const announceLogDetail = effectiveAnnounceLogDetail(config.announceLogDetail);
84
96
  const ackTimeoutMs = config.deliveryAckTimeoutMs ?? 15000;
85
97
  const announcedPeers = new Set();
86
98
  const pendingAcks = new Map();
@@ -133,7 +145,29 @@ export function createInstanceRouter(options) {
133
145
  payload: JSON.stringify(payload),
134
146
  });
135
147
  announcedPeers.add(peerId);
136
- logger?.info?.(`[libp2p-mesh] Sent instance announce to ${peerId} (${payload.instanceId})`);
148
+ logAnnounce("Sent", peerId, payload);
149
+ }
150
+ function announceSummary(direction, peerId, payload, changed) {
151
+ const changedDetail = changed === undefined ? "" : ` changed=${changed}`;
152
+ return `[libp2p-mesh] ${direction} instance announce peer=${peerId} instance=${payload.instanceId} addrs=${payload.multiaddrs.length} attrs=${countAnnounceAttributes(payload)}${changedDetail}`;
153
+ }
154
+ function logAnnounce(direction, peerId, payload, changed) {
155
+ if (announceLogDetail === "off") {
156
+ if (direction === "Sent") {
157
+ logger?.info?.(`[libp2p-mesh] Sent instance announce to ${peerId} (${payload.instanceId})`);
158
+ }
159
+ return;
160
+ }
161
+ logger?.info?.(announceSummary(direction, peerId, payload, changed));
162
+ if (announceLogDetail !== "payload") {
163
+ return;
164
+ }
165
+ try {
166
+ logger?.debug?.(`[libp2p-mesh] ${direction} instance announce payload=${JSON.stringify(payload)}`);
167
+ }
168
+ catch {
169
+ return;
170
+ }
137
171
  }
138
172
  async function announceToConnectedPeers() {
139
173
  for (const peerId of mesh.getConnectedPeers()) {
@@ -163,9 +197,7 @@ export function createInstanceRouter(options) {
163
197
  if (result.changed) {
164
198
  logger?.info?.(`[libp2p-mesh] Instance mapping updated: ${payload.instanceId} -> ${payload.peerId}`);
165
199
  }
166
- else {
167
- logger?.debug?.(`[libp2p-mesh] Instance mapping unchanged: ${payload.instanceId}`);
168
- }
200
+ logAnnounce("Received", msg.from, payload, result.changed);
169
201
  if (!announcedPeers.has(msg.from)) {
170
202
  await announceToPeer(msg.from).catch((error) => {
171
203
  logger?.warn?.(`[libp2p-mesh] Failed to respond to announce from ${msg.from}: ${summarizeError(error)}`);
@@ -328,7 +360,10 @@ export function createInstanceRouter(options) {
328
360
  handleAck(msg);
329
361
  }
330
362
  }
331
- async function start() {
363
+ function attachHandlers() {
364
+ if (unsubs.length > 0) {
365
+ return;
366
+ }
332
367
  unsubs.push(mesh.onMessage((msg) => {
333
368
  handleMessage(msg).catch((error) => {
334
369
  logger?.error?.(`[libp2p-mesh] Instance router message error: ${summarizeError(error)}`);
@@ -339,6 +374,9 @@ export function createInstanceRouter(options) {
339
374
  logger?.warn?.(`[libp2p-mesh] Failed to announce to connected peer ${peerId}: ${summarizeError(error)}`);
340
375
  });
341
376
  }));
377
+ }
378
+ async function start() {
379
+ attachHandlers();
342
380
  await announceToConnectedPeers();
343
381
  }
344
382
  async function stop() {
@@ -505,6 +543,8 @@ export function createInstanceRouter(options) {
505
543
  };
506
544
  }
507
545
  return {
546
+ attachHandlers,
547
+ announceToConnectedPeers,
508
548
  start,
509
549
  stop,
510
550
  handleMessage,
@@ -1,2 +1,9 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import { createInstanceRouter } from "./instance-router.js";
3
+ import { createMeshNetwork } from "./mesh.js";
4
+ export type Libp2pMeshPluginDeps = {
5
+ createMeshNetwork?: typeof createMeshNetwork;
6
+ createInstanceRouter?: typeof createInstanceRouter;
7
+ };
2
8
  export declare function registerLibp2pMesh(api: OpenClawPluginApi): void;
9
+ export declare function registerLibp2pMeshWithDeps(api: OpenClawPluginApi, deps?: Libp2pMeshPluginDeps): void;
@@ -9,11 +9,15 @@ import { createUserProfileStore } from "./user-profile-store.js";
9
9
  import { buildP2PTools } from "./agent-tools.js";
10
10
  import { registerLibp2pMeshCli } from "./profile-cli.js";
11
11
  export function registerLibp2pMesh(api) {
12
+ registerLibp2pMeshWithDeps(api);
13
+ }
14
+ export function registerLibp2pMeshWithDeps(api, deps = {}) {
12
15
  registerLibp2pMeshCli(api);
13
16
  const config = api.pluginConfig;
14
17
  let unsubscribeInbound;
15
18
  let serviceStarted = false;
16
- const mesh = createMeshNetwork({
19
+ let startPromise;
20
+ const mesh = (deps.createMeshNetwork ?? createMeshNetwork)({
17
21
  config,
18
22
  logger: api.logger,
19
23
  });
@@ -32,7 +36,7 @@ export function registerLibp2pMesh(api) {
32
36
  },
33
37
  logger: api.logger,
34
38
  });
35
- const router = createInstanceRouter({
39
+ const router = (deps.createInstanceRouter ?? createInstanceRouter)({
36
40
  mesh,
37
41
  store,
38
42
  delivery,
@@ -42,6 +46,62 @@ export function registerLibp2pMesh(api) {
42
46
  userProfileStore,
43
47
  });
44
48
  const channel = createLibp2pMeshChannel(mesh);
49
+ function attachInboundHandlers() {
50
+ return mesh.onMessage((msg) => {
51
+ if (msg.type === "direct" || msg.type === "broadcast") {
52
+ const sendToChannel = async (_channelId, _target, text) => {
53
+ if (!config?.inboundChannel || !config?.inboundTarget) {
54
+ api.logger.warn?.("[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.");
55
+ return;
56
+ }
57
+ const result = await delivery.deliver({
58
+ channel: config.inboundChannel,
59
+ target: config.inboundTarget,
60
+ text,
61
+ metadata: {
62
+ fromInstanceId: msg.instanceId ?? msg.from,
63
+ fromPeerId: msg.from,
64
+ p2pMessageId: msg.id,
65
+ allowAgentAutoReply: false,
66
+ replyToInstanceId: msg.instanceId ?? msg.from,
67
+ replyTool: "p2p_send_instance_message",
68
+ },
69
+ });
70
+ if (!result.ok) {
71
+ api.logger.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`);
72
+ }
73
+ };
74
+ handleP2PInbound(msg, { logger: api.logger, sendToChannel });
75
+ }
76
+ else if (msg.type === "agent-sync") {
77
+ handleP2PInbound(msg, { logger: api.logger });
78
+ }
79
+ });
80
+ }
81
+ async function cleanupStartupFailure() {
82
+ try {
83
+ unsubscribeInbound?.();
84
+ }
85
+ catch (error) {
86
+ api.logger.warn?.(`[libp2p-mesh] Failed to clean inbound handler after startup failure: ${String(error)}`);
87
+ }
88
+ finally {
89
+ unsubscribeInbound = undefined;
90
+ }
91
+ try {
92
+ await router.stop();
93
+ }
94
+ catch (error) {
95
+ api.logger.warn?.(`[libp2p-mesh] Failed to stop router after startup failure: ${String(error)}`);
96
+ }
97
+ try {
98
+ await mesh.stop();
99
+ }
100
+ catch (error) {
101
+ api.logger.warn?.(`[libp2p-mesh] Failed to stop mesh after startup failure: ${String(error)}`);
102
+ }
103
+ serviceStarted = false;
104
+ }
45
105
  // 1. Register Service (manages libp2p node lifecycle)
46
106
  api.registerService({
47
107
  id: "libp2p-mesh",
@@ -50,56 +110,45 @@ export function registerLibp2pMesh(api) {
50
110
  api.logger.debug?.("[libp2p-mesh] Service already started; ignoring duplicate start.");
51
111
  return;
52
112
  }
53
- await mesh.start();
54
- await router.start();
55
- unsubscribeInbound = mesh.onMessage((msg) => {
56
- if (msg.type === "direct" || msg.type === "broadcast") {
57
- const sendToChannel = async (_channelId, _target, text) => {
58
- if (!config?.inboundChannel || !config?.inboundTarget) {
59
- api.logger.warn?.("[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.");
60
- return;
61
- }
62
- const result = await delivery.deliver({
63
- channel: config.inboundChannel,
64
- target: config.inboundTarget,
65
- text,
66
- metadata: {
67
- fromInstanceId: msg.instanceId ?? msg.from,
68
- fromPeerId: msg.from,
69
- p2pMessageId: msg.id,
70
- allowAgentAutoReply: false,
71
- replyToInstanceId: msg.instanceId ?? msg.from,
72
- replyTool: "p2p_send_instance_message",
73
- },
74
- });
75
- if (!result.ok) {
76
- api.logger.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`);
77
- }
78
- };
79
- handleP2PInbound(msg, { logger: api.logger, sendToChannel });
113
+ if (startPromise) {
114
+ api.logger.debug?.("[libp2p-mesh] Service start already in progress; waiting for it.");
115
+ return startPromise;
116
+ }
117
+ startPromise = (async () => {
118
+ try {
119
+ router.attachHandlers();
120
+ unsubscribeInbound = attachInboundHandlers();
121
+ await mesh.start();
122
+ await router.announceToConnectedPeers();
123
+ const identity = mesh.getInstanceIdentity();
124
+ api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
125
+ if (identity) {
126
+ api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
127
+ }
128
+ const nat = mesh.getNATStatus();
129
+ const enabledNames = Object.entries(nat.enabled)
130
+ .filter(([, on]) => on)
131
+ .map(([k]) => k);
132
+ if (enabledNames.length > 0) {
133
+ api.logger.info?.(`[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`);
134
+ }
135
+ if (nat.reservedRelays.length > 0) {
136
+ api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
137
+ }
138
+ serviceStarted = true;
80
139
  }
81
- else if (msg.type === "agent-sync") {
82
- handleP2PInbound(msg, { logger: api.logger });
140
+ catch (error) {
141
+ await cleanupStartupFailure();
142
+ throw error;
83
143
  }
84
- });
85
- const identity = mesh.getInstanceIdentity();
86
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
87
- if (identity) {
88
- api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
89
- }
90
- const nat = mesh.getNATStatus();
91
- const enabledNames = Object.entries(nat.enabled)
92
- .filter(([, on]) => on)
93
- .map(([k]) => k);
94
- if (enabledNames.length > 0) {
95
- api.logger.info?.(`[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`);
96
- }
97
- if (nat.reservedRelays.length > 0) {
98
- api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
99
- }
100
- serviceStarted = true;
144
+ finally {
145
+ startPromise = undefined;
146
+ }
147
+ })();
148
+ return startPromise;
101
149
  },
102
150
  stop: async () => {
151
+ await startPromise?.catch(() => undefined);
103
152
  unsubscribeInbound?.();
104
153
  unsubscribeInbound = undefined;
105
154
  await router.stop();
@@ -1,6 +1,7 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
3
3
  import { type SetupCliDeps } from "./setup-cli.js";
4
+ import { type DebugCliDeps } from "./debug-cli.js";
4
5
  import { type UserProfileStore } from "./user-profile-store.js";
5
6
  import type { SetupPrompter } from "./setup-wizard.js";
6
7
  type CliRootCommand = {
@@ -20,6 +21,7 @@ export type ProfileCliDeps = {
20
21
  export type Libp2pMeshCliDeps = {
21
22
  setup?: SetupCliDeps;
22
23
  profile?: ProfileCliDeps;
24
+ debug?: DebugCliDeps;
23
25
  };
24
26
  export declare function registerLibp2pMeshCli(api: OpenClawPluginApi, deps?: Libp2pMeshCliDeps): void;
25
27
  export declare function registerLibp2pMeshProfileCli(api: OpenClawPluginApi, deps?: ProfileCliDeps): void;
@@ -1,4 +1,5 @@
1
1
  import { createReadlinePrompter, LIBP2P_MESH_CLI_REGISTRATION, registerLibp2pMeshSetupCommand, } from "./setup-cli.js";
2
+ import { registerLibp2pMeshDebugCommand } from "./debug-cli.js";
2
3
  import { runProfileWizard } from "./profile-wizard.js";
3
4
  import { createUserMdAttributeSource } from "./user-md-attributes.js";
4
5
  import { createUserProfileStore } from "./user-profile-store.js";
@@ -9,6 +10,7 @@ export function registerLibp2pMeshCli(api, deps = {}) {
9
10
  .description("Configure libp2p-mesh plugin.");
10
11
  registerLibp2pMeshSetupCommand(root, api, ctx, deps.setup);
11
12
  registerLibp2pMeshProfileCommand(root, api, ctx, deps.profile);
13
+ registerLibp2pMeshDebugCommand(root, api, ctx, deps.debug);
12
14
  }, LIBP2P_MESH_CLI_REGISTRATION);
13
15
  }
14
16
  export function registerLibp2pMeshProfileCli(api, deps = {}) {
@@ -1,4 +1,4 @@
1
- import type { InboundTargetConfig, MeshConfig } from "./types.js";
1
+ import type { AnnounceLogDetail, InboundTargetConfig, MeshConfig } from "./types.js";
2
2
  export declare const LIBP2P_MESH_PLUGIN_ID = "libp2p-mesh";
3
3
  export declare const DEFAULT_DELIVERY_ACK_TIMEOUT_MS = 15000;
4
4
  export type SetupMode = "lan" | "cross-network" | "relay-node" | "tools-only";
@@ -32,11 +32,14 @@ export type AddInboundTargetResult = {
32
32
  };
33
33
  export type LegacyInboundMigrationMode = "convert" | "keep" | "replace";
34
34
  export declare function getLibp2pMeshConfig(config: OpenClawConfigLike): MeshConfig | undefined;
35
+ export declare function getAnnounceLogDetail(config: OpenClawConfigLike): AnnounceLogDetail;
36
+ export declare function normalizeAnnounceLogDetail(value: unknown): AnnounceLogDetail;
35
37
  export declare function buildNetworkConfig(mode: SetupMode, options?: {
36
38
  crossNetwork?: CrossNetworkOptions;
37
39
  relayNode?: RelayNodeOptions;
38
40
  }): MeshConfig;
39
41
  export declare function applyPluginConfig(config: OpenClawConfigLike, pluginConfig: MeshConfig): OpenClawConfigLike;
42
+ export declare function applyAnnounceLogDetail(config: OpenClawConfigLike, announceLogDetail: AnnounceLogDetail): OpenClawConfigLike;
40
43
  export declare function mergeNetworkConfig(existing: MeshConfig | undefined, networkConfig: MeshConfig): MeshConfig;
41
44
  export declare function listConfiguredChannels(config: OpenClawConfigLike): string[];
42
45
  export declare function generateInboundTargetId(channel: string, existingTargets: InboundTargetConfig[]): string;
@@ -3,6 +3,12 @@ export const DEFAULT_DELIVERY_ACK_TIMEOUT_MS = 15000;
3
3
  export function getLibp2pMeshConfig(config) {
4
4
  return config.plugins?.entries?.[LIBP2P_MESH_PLUGIN_ID]?.config;
5
5
  }
6
+ export function getAnnounceLogDetail(config) {
7
+ return normalizeAnnounceLogDetail(getLibp2pMeshConfig(config)?.announceLogDetail);
8
+ }
9
+ export function normalizeAnnounceLogDetail(value) {
10
+ return value === "off" || value === "payload" || value === "summary" ? value : "summary";
11
+ }
6
12
  export function buildNetworkConfig(mode, options) {
7
13
  switch (mode) {
8
14
  case "lan":
@@ -53,6 +59,27 @@ export function applyPluginConfig(config, pluginConfig) {
53
59
  },
54
60
  };
55
61
  }
62
+ export function applyAnnounceLogDetail(config, announceLogDetail) {
63
+ const existingEntry = config.plugins?.entries?.[LIBP2P_MESH_PLUGIN_ID];
64
+ const existingPluginConfig = existingEntry?.config ?? {};
65
+ return {
66
+ ...config,
67
+ plugins: {
68
+ ...config.plugins,
69
+ entries: {
70
+ ...config.plugins?.entries,
71
+ [LIBP2P_MESH_PLUGIN_ID]: {
72
+ ...existingEntry,
73
+ enabled: existingEntry?.enabled ?? true,
74
+ config: {
75
+ ...existingPluginConfig,
76
+ announceLogDetail,
77
+ },
78
+ },
79
+ },
80
+ },
81
+ };
82
+ }
56
83
  export function mergeNetworkConfig(existing, networkConfig) {
57
84
  if (!existing) {
58
85
  return { ...networkConfig };
@@ -180,6 +180,8 @@ export type InstanceRouterOptions = {
180
180
  };
181
181
  };
182
182
  export interface InstanceRouter {
183
+ attachHandlers(): void;
184
+ announceToConnectedPeers(): Promise<void>;
183
185
  start(): Promise<void>;
184
186
  stop(): Promise<void>;
185
187
  handleMessage(msg: P2PMessage): Promise<void>;
@@ -206,6 +208,7 @@ export interface MeshConfig {
206
208
  discovery?: "mdns" | "bootstrap" | "dht";
207
209
  bootstrapList?: string[];
208
210
  meshTopic?: string;
211
+ announceLogDetail?: AnnounceLogDetail;
209
212
  enableAgentSync?: boolean;
210
213
  enableWebSocket?: boolean;
211
214
  peerIdPath?: string;
@@ -299,6 +302,7 @@ export interface MeshNetwork {
299
302
  /** Inspect which NAT-traversal services are running and whether any relay reservations are active */
300
303
  getNATStatus(): NATTraversalStatus;
301
304
  }
305
+ export type AnnounceLogDetail = "off" | "summary" | "payload";
302
306
  export type MeshAccount = {
303
307
  accountId: string;
304
308
  configured: boolean;
@@ -6,6 +6,7 @@ export type UserMdAttributeSource = {
6
6
  warn?: (message: string) => void;
7
7
  };
8
8
  };
9
+ export declare function resolveUserMdPath(customPath?: string): string;
9
10
  export declare function extractUserMdTags(markdown: string): UserPublicAttribute[];
10
11
  export declare function createUserMdAttributeSource(options?: UserMdAttributeSource): {
11
12
  loadTags(): Promise<UserPublicAttribute[]>;
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
2
3
  import path from "node:path";
3
4
  import { normalizeAttributeValue } from "./user-attributes.js";
4
5
  const MAX_TAGS = 10;
@@ -42,6 +43,16 @@ const ENGLISH_TAG_FIELDS = new Set([
42
43
  "interest",
43
44
  "interests",
44
45
  ]);
46
+ export function resolveUserMdPath(customPath) {
47
+ if (customPath) {
48
+ return customPath;
49
+ }
50
+ const stateDir = process.env.OPENCLAW_STATE_DIR;
51
+ if (stateDir) {
52
+ return path.join(stateDir, "workspace", "USER.md");
53
+ }
54
+ return path.join(homedir(), ".openclaw", "workspace", "USER.md");
55
+ }
45
56
  function isTemplateText(value) {
46
57
  const trimmed = value.trim();
47
58
  return trimmed.length === 0 || TEMPLATE_PATTERN.test(trimmed) || /^\[[^\]]+\]$/.test(trimmed);
@@ -183,7 +194,7 @@ export function extractUserMdTags(markdown) {
183
194
  return tags;
184
195
  }
185
196
  export function createUserMdAttributeSource(options) {
186
- const filePath = options?.path ?? path.join(process.cwd(), "USER.md");
197
+ const filePath = resolveUserMdPath(options?.path);
187
198
  const logger = options?.logger;
188
199
  return {
189
200
  async loadTags() {