libp2p-mesh 2026.6.2 → 2026.6.4

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.
@@ -51,6 +51,18 @@ export interface UserMessagePayload {
51
51
  replyTool: "p2p_send_instance_message";
52
52
  };
53
53
  }
54
+ export interface InboundTargetConfig {
55
+ id?: string;
56
+ channel: string;
57
+ target: string;
58
+ }
59
+ export interface DeliveryTargetResult {
60
+ id?: string;
61
+ channel: string;
62
+ target: string;
63
+ ok: boolean;
64
+ error?: string;
65
+ }
54
66
  export interface DeliveryAckPayload {
55
67
  ackFor: string;
56
68
  ok: boolean;
@@ -58,6 +70,7 @@ export interface DeliveryAckPayload {
58
70
  inboundTarget?: string;
59
71
  deliveredAt: number;
60
72
  error?: string;
73
+ results?: DeliveryTargetResult[];
61
74
  }
62
75
  export interface InstancePeerRecord {
63
76
  instanceId: string;
@@ -120,6 +133,8 @@ export interface InstanceRouter {
120
133
  toPeerId: string;
121
134
  ackMessageId?: string;
122
135
  inboundChannel?: string;
136
+ inboundTarget?: string;
137
+ deliveryResults?: DeliveryTargetResult[];
123
138
  error?: string;
124
139
  }>;
125
140
  }
@@ -165,7 +180,8 @@ export interface MeshConfig {
165
180
  /**
166
181
  * Deprecated pre-2026.6 config keys kept so existing OpenClaw configs keep
167
182
  * validating after upgrade. Relay selection is now configured with
168
- * `relayList`; inbound display uses `inboundChannel`/`inboundTarget`.
183
+ * `relayList`; inbound display uses `inboundChannel`/`inboundTarget`, or
184
+ * `inboundTargets` when multi-target inbound delivery is enabled.
169
185
  */
170
186
  relayChannel?: string;
171
187
  relayAccountId?: string;
@@ -182,6 +198,7 @@ export interface MeshConfig {
182
198
  announceAddrs?: string[];
183
199
  inboundChannel?: string;
184
200
  inboundTarget?: string;
201
+ inboundTargets?: InboundTargetConfig[];
185
202
  deliveryAckTimeoutMs?: number;
186
203
  }
187
204
  export interface NATTraversalStatus {
@@ -0,0 +1,22 @@
1
+ export interface PromptChoice {
2
+ label: string;
3
+ value: string;
4
+ hint?: string;
5
+ }
6
+ export interface WizardPrompter {
7
+ question(prompt: string, defaultValue?: string): Promise<string>;
8
+ confirm(prompt: string, defaultValue?: boolean): Promise<boolean>;
9
+ select(prompt: string, choices: PromptChoice[]): Promise<string>;
10
+ multiline(prompt: string, helpText?: string): Promise<string[]>;
11
+ displayBox(title: string, lines: string[]): void;
12
+ displaySuccess(message: string): void;
13
+ displayError(message: string): void;
14
+ displayWarning(message: string): void;
15
+ close(): void;
16
+ }
17
+ export declare function validateMultiaddr(raw: string): string | null;
18
+ export declare function createReadlinePrompter(): WizardPrompter;
19
+ export declare function runSetupWizard(prompter: WizardPrompter, currentConfig: Record<string, unknown>, availableChannels: string[]): Promise<Record<string, unknown>>;
20
+ export declare class WizardCancelledError extends Error {
21
+ constructor();
22
+ }
@@ -0,0 +1,276 @@
1
+ import * as readline from "node:readline/promises";
2
+ import { MULTIADDR_PATTERN } from "./config-io.js";
3
+ // --- Validation ---
4
+ export function validateMultiaddr(raw) {
5
+ const trimmed = raw.trim();
6
+ if (!trimmed)
7
+ return "地址不能为空";
8
+ if (!MULTIADDR_PATTERN.test(trimmed)) {
9
+ return "多地址格式无效,必须以 /ip4/、/ip6/ 或 /dns/ 开头,如 /ip4/198.51.100.5/tcp/4001/p2p/12D3KooW...";
10
+ }
11
+ return null;
12
+ }
13
+ // --- Readline Prompter ---
14
+ export function createReadlinePrompter() {
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+ const displayWidth = 60;
20
+ function boxify(title, lines) {
21
+ const top = "┌" + "─".repeat(displayWidth - 2) + "┐";
22
+ const padTitle = "│ " + title.padEnd(displayWidth - 6) + " │";
23
+ const sep = "│" + " ".repeat(displayWidth - 2) + "│";
24
+ const bottom = "└" + "─".repeat(displayWidth - 2) + "┘";
25
+ console.log(top);
26
+ console.log(padTitle);
27
+ console.log(sep);
28
+ for (const line of lines) {
29
+ const padLine = "│ " + line.padEnd(displayWidth - 6) + " │";
30
+ console.log(padLine);
31
+ }
32
+ console.log(bottom);
33
+ }
34
+ function formatChoices(choices) {
35
+ return choices
36
+ .map((c, i) => {
37
+ const hint = c.hint ? `(${c.hint})` : "";
38
+ return ` ${i + 1}. ${c.label} ${hint}`.trimEnd();
39
+ })
40
+ .join("\n");
41
+ }
42
+ async function question(prompt, defaultValue) {
43
+ const suffix = defaultValue ? `(${defaultValue})` : "";
44
+ const answer = await rl.question(`${prompt}${suffix} → `);
45
+ return answer.trim() || defaultValue || "";
46
+ }
47
+ async function confirm(prompt, defaultValue) {
48
+ const def = defaultValue === undefined ? true : defaultValue;
49
+ const yn = def ? "Y/n" : "y/N";
50
+ const answer = await rl.question(`${prompt}(${yn})→ `);
51
+ const trimmed = answer.trim().toLowerCase();
52
+ if (trimmed === "y" || trimmed === "yes")
53
+ return true;
54
+ if (trimmed === "n" || trimmed === "no")
55
+ return false;
56
+ return def;
57
+ }
58
+ async function select(prompt, choices) {
59
+ console.log(`\n${prompt}`);
60
+ console.log(formatChoices(choices));
61
+ let answer = "";
62
+ while (true) {
63
+ const raw = await rl.question(` → `);
64
+ const num = parseInt(raw.trim(), 10);
65
+ if (num >= 1 && num <= choices.length) {
66
+ answer = choices[num - 1].value;
67
+ break;
68
+ }
69
+ console.log(` 请输入 1-${choices.length} 之间的数字。`);
70
+ }
71
+ return answer;
72
+ }
73
+ async function multiline(prompt, helpText) {
74
+ console.log(`\n${prompt}`);
75
+ if (helpText)
76
+ console.log(helpText);
77
+ const lines = [];
78
+ while (true) {
79
+ const line = await rl.question(" ");
80
+ if (!line.trim())
81
+ break;
82
+ const err = validateMultiaddr(line);
83
+ if (err) {
84
+ console.log(` ⚠ ${err}`);
85
+ continue;
86
+ }
87
+ if (lines.includes(line.trim())) {
88
+ console.log(" ⚠ 该地址已存在");
89
+ continue;
90
+ }
91
+ lines.push(line.trim());
92
+ }
93
+ if (lines.length > 0) {
94
+ console.log(` ✓ 已添加 ${lines.length} 个地址`);
95
+ }
96
+ return lines;
97
+ }
98
+ function displayBox(title, lines) {
99
+ boxify(title, lines);
100
+ }
101
+ function displaySuccess(message) {
102
+ console.log(` ✓ ${message}`);
103
+ }
104
+ function displayError(message) {
105
+ console.log(` ✗ ${message}`);
106
+ }
107
+ function displayWarning(message) {
108
+ console.log(` ⚠ ${message}`);
109
+ }
110
+ return {
111
+ question,
112
+ confirm,
113
+ select,
114
+ multiline,
115
+ displayBox,
116
+ displaySuccess,
117
+ displayError,
118
+ displayWarning,
119
+ close: () => rl.close(),
120
+ };
121
+ }
122
+ // --- Setup Wizard Logic (pure — takes prompter, returns config object) ---
123
+ export async function runSetupWizard(prompter, currentConfig, availableChannels) {
124
+ const config = { ...currentConfig };
125
+ // --- Welcome ---
126
+ prompter.displayBox("🕸️ libp2p-mesh 配置向导", [
127
+ "我们将引导你完成 P2P Mesh 网络的基础配置。",
128
+ "任何时候按 Ctrl+C 可退出,配置不会被保存。",
129
+ ]);
130
+ // =================================================================
131
+ // Layer 1: Core Path
132
+ // =================================================================
133
+ // Step 1: Discovery mode
134
+ const discovery = await prompter.select("选择节点发现方式:", [
135
+ { value: "mdns", label: "mDNS — 局域网自动发现", hint: "默认,同一 WiFi 下推荐" },
136
+ { value: "bootstrap", label: "Bootstrap — 手动指定已知节点地址", hint: "跨网络场景" },
137
+ { value: "dht", label: "DHT — Kademlia 分布式发现", hint: "需要至少一个 bootstrap 入口" },
138
+ ]);
139
+ config.discovery = discovery;
140
+ // Step 2: Bootstrap addresses (only if discovery=bootstrap or dht)
141
+ if (discovery === "bootstrap" || discovery === "dht") {
142
+ const addrs = await prompter.multiline("输入 Bootstrap 节点地址(每行一个,空行结束):", " 格式: /ip4/<IP>/tcp/<端口>/p2p/<PeerID>");
143
+ if (addrs.length > 0) {
144
+ config.bootstrapList = addrs;
145
+ }
146
+ }
147
+ // Step 3: Inbound targets
148
+ if (availableChannels.length === 0) {
149
+ prompter.displayWarning("未检测到已安装的聊天频道插件。你可以稍后在 openclaw.json 中手动配置 inboundTargets。");
150
+ }
151
+ else {
152
+ const targets = [];
153
+ let addMore = true;
154
+ while (addMore) {
155
+ const channelChoices = availableChannels.map((ch) => ({
156
+ value: ch,
157
+ label: ch,
158
+ }));
159
+ const channel = await prompter.select("选择接收 P2P 消息的聊天频道:", channelChoices);
160
+ const target = await prompter.question(`输入 ${channel} 的接收目标(如 user:ou_xxx 或 chat:oc_xxx):`);
161
+ if (target) {
162
+ targets.push({ channel, target });
163
+ }
164
+ addMore = await prompter.confirm("是否添加更多接收目标?", false);
165
+ }
166
+ if (targets.length > 0) {
167
+ if (targets.length === 1 && !currentConfig.inboundChannel && !currentConfig.inboundTarget) {
168
+ // Single target: also set legacy inboundChannel/inboundTarget for backwards compat
169
+ config.inboundChannel = targets[0].channel;
170
+ config.inboundTarget = targets[0].target;
171
+ }
172
+ config.inboundTargets = targets;
173
+ }
174
+ }
175
+ // Step 4: Preview core config and confirm
176
+ const corePreview = formatConfigPreview(config);
177
+ prompter.displayBox("即将写入以下配置:", corePreview);
178
+ const coreConfirmed = await prompter.confirm("确认写入?", true);
179
+ if (!coreConfirmed) {
180
+ prompter.displayWarning("已取消,配置未保存。");
181
+ throw new WizardCancelledError();
182
+ }
183
+ // =================================================================
184
+ // Layer 2: Advanced (optional)
185
+ // =================================================================
186
+ const wantsAdvanced = await prompter.confirm("需要在不同网络之间使用吗(跨 WiFi / 跨城市)?", false);
187
+ if (wantsAdvanced) {
188
+ // Fixed port
189
+ const wantFixedPort = await prompter.confirm("是否使用固定监听端口?(推荐跨网络场景)", false);
190
+ if (wantFixedPort) {
191
+ const port = await prompter.question("端口号", "4001");
192
+ const portNum = parseInt(port, 10);
193
+ if (!isNaN(portNum) && portNum > 0 && portNum < 65536) {
194
+ config.listenAddrs = [`/ip4/0.0.0.0/tcp/${portNum}`];
195
+ }
196
+ else {
197
+ prompter.displayWarning("端口号无效,使用默认动态端口。");
198
+ }
199
+ }
200
+ // NAT traversal
201
+ const wantNAT = await prompter.confirm("是否启用 NAT 穿透?(默认开启,推荐保留)", true);
202
+ config.enableNATTraversal = wantNAT;
203
+ // Circuit Relay
204
+ const wantRelay = await prompter.confirm("需要配置 Circuit Relay 中继节点吗?", false);
205
+ if (wantRelay) {
206
+ const relays = await prompter.multiline("输入 Relay 节点地址(每行一个,空行结束):", " 格式: /ip4/<IP>/tcp/<端口>/p2p/<PeerID>");
207
+ if (relays.length > 0) {
208
+ config.relayList = relays;
209
+ }
210
+ }
211
+ // Custom instance name
212
+ const wantName = await prompter.confirm("为此节点设置一个自定义名称吗?(用于 P2P 网络中的身份显示)", false);
213
+ if (wantName) {
214
+ const name = await prompter.question("节点名称");
215
+ if (name) {
216
+ config.instanceName = name;
217
+ }
218
+ }
219
+ // Final preview and confirm (only if advanced config changed)
220
+ if (wantFixedPort || wantRelay || wantName || !config.enableNATTraversal) {
221
+ const finalPreview = formatConfigPreview(config);
222
+ prompter.displayBox("高级配置已追加,最终预览:", finalPreview);
223
+ const finalConfirmed = await prompter.confirm("确认写入?", true);
224
+ if (!finalConfirmed) {
225
+ prompter.displayWarning("已取消高级配置,核心配置已保存。");
226
+ }
227
+ }
228
+ }
229
+ prompter.displaySuccess("配置完成。运行 openclaw gateway restart 使配置生效。");
230
+ return config;
231
+ }
232
+ export class WizardCancelledError extends Error {
233
+ constructor() {
234
+ super("Wizard cancelled by user");
235
+ this.name = "WizardCancelledError";
236
+ }
237
+ }
238
+ // --- Helpers ---
239
+ function formatConfigPreview(config) {
240
+ const lines = [];
241
+ if (config.discovery) {
242
+ lines.push(`discovery: ${config.discovery}`);
243
+ }
244
+ if (Array.isArray(config.bootstrapList) && config.bootstrapList.length > 0) {
245
+ lines.push(`bootstrapList: ${config.bootstrapList.length} 个节点`);
246
+ for (const addr of config.bootstrapList) {
247
+ lines.push(` ${addr}`);
248
+ }
249
+ }
250
+ if (Array.isArray(config.inboundTargets) && config.inboundTargets.length > 0) {
251
+ lines.push("inboundTargets:");
252
+ for (const t of config.inboundTargets) {
253
+ lines.push(` - ${t.channel} / ${t.target}`);
254
+ }
255
+ }
256
+ else if (config.inboundChannel && config.inboundTarget) {
257
+ lines.push(`inboundChannel: ${config.inboundChannel}`);
258
+ lines.push(`inboundTarget: ${config.inboundTarget}`);
259
+ }
260
+ if (Array.isArray(config.listenAddrs) && config.listenAddrs.length > 0) {
261
+ lines.push(`listenAddrs: ${config.listenAddrs.join(", ")}`);
262
+ }
263
+ if (config.enableNATTraversal !== undefined) {
264
+ lines.push(`NAT 穿透: ${config.enableNATTraversal ? "开启" : "关闭"}`);
265
+ }
266
+ if (Array.isArray(config.relayList) && config.relayList.length > 0) {
267
+ lines.push(`relayList: ${config.relayList.length} 个节点`);
268
+ }
269
+ if (config.instanceName) {
270
+ lines.push(`instanceName: ${config.instanceName}`);
271
+ }
272
+ if (lines.length === 0) {
273
+ lines.push("(无配置更改)");
274
+ }
275
+ return lines;
276
+ }
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { definePluginEntry } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/core";
3
3
  import { registerLibp2pMesh } from "./src/plugin.js";
4
+ import { registerLibp2pMeshCli } from "./src/cli.js";
4
5
 
5
6
  function createLibp2pMeshConfigSchema(): OpenClawPluginConfigSchema {
6
7
  return {
@@ -129,6 +130,29 @@ function createLibp2pMeshConfigSchema(): OpenClawPluginConfigSchema {
129
130
  type: "string",
130
131
  description: "OpenClaw channel target for inbound P2P user messages, for example user:ou_xxx or chat:oc_xxx.",
131
132
  },
133
+ inboundTargets: {
134
+ type: "array",
135
+ description: "OpenClaw channel targets used to display inbound P2P user messages. When present, this overrides inboundChannel/inboundTarget. An empty array disables inbound delivery.",
136
+ items: {
137
+ type: "object",
138
+ additionalProperties: false,
139
+ properties: {
140
+ id: {
141
+ type: "string",
142
+ description: "Optional local display name used in logs and delivery ACK output.",
143
+ },
144
+ channel: {
145
+ type: "string",
146
+ description: "OpenClaw channel used to display this inbound P2P user message, for example \"feishu\".",
147
+ },
148
+ target: {
149
+ type: "string",
150
+ description: "OpenClaw channel target for this inbound P2P user message, for example user:ou_xxx or chat:oc_xxx.",
151
+ },
152
+ },
153
+ required: ["channel", "target"],
154
+ },
155
+ },
132
156
  deliveryAckTimeoutMs: {
133
157
  type: "number",
134
158
  default: 15000,
@@ -144,5 +168,11 @@ export default definePluginEntry({
144
168
  name: "libp2p Mesh Network",
145
169
  description: "P2P network for cross-instance agent communication via libp2p.",
146
170
  configSchema: createLibp2pMeshConfigSchema(),
147
- register: registerLibp2pMesh,
171
+ register: (api) => {
172
+ registerLibp2pMesh(api);
173
+ // 5. Register CLI commands (setup wizard + config management)
174
+ api.registerCli(registerLibp2pMeshCli, {
175
+ commands: ["libp2p-mesh"],
176
+ });
177
+ },
148
178
  });
@@ -11,7 +11,54 @@
11
11
  "schema": {
12
12
  "type": "object",
13
13
  "additionalProperties": false,
14
- "properties": {}
14
+ "properties": {
15
+ "listenAddrs": {
16
+ "type": "array",
17
+ "items": { "type": "string" },
18
+ "default": ["/ip4/0.0.0.0/tcp/0"]
19
+ },
20
+ "enableWebSocket": {
21
+ "type": "boolean",
22
+ "default": false,
23
+ "description": "Enable WebSocket transport"
24
+ },
25
+ "discovery": {
26
+ "type": "string",
27
+ "enum": ["mdns", "bootstrap", "dht"],
28
+ "default": "mdns"
29
+ },
30
+ "bootstrapList": {
31
+ "type": "array",
32
+ "items": { "type": "string" }
33
+ },
34
+ "meshTopic": {
35
+ "type": "string",
36
+ "default": "openclaw-mesh"
37
+ },
38
+ "inboundTargets": {
39
+ "type": "array",
40
+ "description": "OpenClaw channel targets used to display inbound P2P user messages. When present, this overrides inboundChannel/inboundTarget. An empty array disables inbound delivery.",
41
+ "items": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "properties": {
45
+ "id": {
46
+ "type": "string",
47
+ "description": "Optional local display name used in logs and delivery ACK output."
48
+ },
49
+ "channel": {
50
+ "type": "string",
51
+ "description": "OpenClaw channel used to display this inbound P2P user message, for example \"feishu\"."
52
+ },
53
+ "target": {
54
+ "type": "string",
55
+ "description": "OpenClaw channel target for this inbound P2P user message, for example user:ou_xxx or chat:oc_xxx."
56
+ }
57
+ },
58
+ "required": ["channel", "target"]
59
+ }
60
+ }
61
+ }
15
62
  }
16
63
  }
17
64
  },
@@ -130,6 +177,29 @@
130
177
  "type": "string",
131
178
  "description": "OpenClaw channel target for inbound P2P user messages, for example user:ou_xxx or chat:oc_xxx."
132
179
  },
180
+ "inboundTargets": {
181
+ "type": "array",
182
+ "description": "OpenClaw channel targets used to display inbound P2P user messages. When present, this overrides inboundChannel/inboundTarget. An empty array disables inbound delivery.",
183
+ "items": {
184
+ "type": "object",
185
+ "additionalProperties": false,
186
+ "properties": {
187
+ "id": {
188
+ "type": "string",
189
+ "description": "Optional local display name used in logs and delivery ACK output."
190
+ },
191
+ "channel": {
192
+ "type": "string",
193
+ "description": "OpenClaw channel used to display this inbound P2P user message, for example \"feishu\"."
194
+ },
195
+ "target": {
196
+ "type": "string",
197
+ "description": "OpenClaw channel target for this inbound P2P user message, for example user:ou_xxx or chat:oc_xxx."
198
+ }
199
+ },
200
+ "required": ["channel", "target"]
201
+ }
202
+ },
133
203
  "deliveryAckTimeoutMs": {
134
204
  "type": "number",
135
205
  "default": 15000,
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.6.2",
3
+ "version": "2026.6.4",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
9
  "build": "tsc -p tsconfig.json",
10
+ "test": "node --import tsx --tSest test/*.test.ts",
10
11
  "prepack": "npm run build"
11
12
  },
12
13
  "files": [
@@ -63,7 +64,7 @@
63
64
  "systemImage": "network"
64
65
  },
65
66
  "install": {
66
- "npmSpec": "openclaw-libp2p-mesh",
67
+ "npmSpec": "libp2p-mesh",
67
68
  "defaultChoice": "npm",
68
69
  "minHostVersion": ">=2026.3.24"
69
70
  },
@@ -96,12 +97,12 @@
96
97
  "license": "MIT",
97
98
  "repository": {
98
99
  "type": "git",
99
- "url": "git+https://github.com/lilingfy/openclaw-libp2p-mesh.git"
100
+ "url": "git+https://github.com/XM-ls/openclaw-libp2p-mesh.git"
100
101
  },
101
102
  "bugs": {
102
- "url": "https://github.com/lilingfy/openclaw-libp2p-mesh/issues"
103
+ "url": "https://github.com/XM-ls/openclaw-libp2p-mesh/issues"
103
104
  },
104
- "homepage": "https://github.com/lilingfy/openclaw-libp2p-mesh#readme",
105
+ "homepage": "https://github.com/XM-ls/openclaw-libp2p-mesh#readme",
105
106
  "pnpm": {
106
107
  "onlyBuiltDependencies": [
107
108
  "@google/genai",
@@ -1,4 +1,27 @@
1
- import type { InstanceRouter, MeshNetwork } from "./types.js";
1
+ import type { DeliveryTargetResult, InstanceRouter, MeshNetwork } from "./types.js";
2
+
3
+ function targetLabel(result: DeliveryTargetResult): string {
4
+ const id = result.id?.trim();
5
+ const location = `${result.channel} / ${result.target}`;
6
+ return id ? `${id} (${location})` : location;
7
+ }
8
+
9
+ function formatDeliveryResults(
10
+ instanceId: string,
11
+ delivered: boolean,
12
+ results: DeliveryTargetResult[],
13
+ ): string {
14
+ const header = delivered
15
+ ? `发往 ${instanceId} 的消息投递结果:`
16
+ : `发往 ${instanceId} 的消息投递失败:`;
17
+ const lines = results.map((result) => {
18
+ if (result.ok) {
19
+ return `- ${targetLabel(result)}:已送达`;
20
+ }
21
+ return `- ${targetLabel(result)}:失败:${result.error ?? "unknown error"}`;
22
+ });
23
+ return [header, ...lines].join("\n");
24
+ }
2
25
 
3
26
  export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
4
27
  return [
@@ -323,6 +346,18 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
323
346
  };
324
347
  }
325
348
  const result = await router.sendInstanceMessage(instanceId, message);
349
+ if (result.deliveryResults && result.deliveryResults.length > 0) {
350
+ return {
351
+ content: [
352
+ {
353
+ type: "text" as const,
354
+ text: formatDeliveryResults(instanceId, result.delivered, result.deliveryResults),
355
+ },
356
+ ],
357
+ details: result,
358
+ isError: result.delivered ? undefined : true,
359
+ };
360
+ }
326
361
  if (!result.delivered) {
327
362
  return {
328
363
  content: [