libp2p-mesh 2026.6.11 → 2026.6.13

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.
@@ -1,4 +1,12 @@
1
- import type { DeliveryTargetResult, InstanceRouter, MeshNetwork } from "./types.js";
1
+ import type {
2
+ DeliveryTargetResult,
3
+ InstanceRouter,
4
+ MeshNetwork,
5
+ UserAttributeMatch,
6
+ UserAttributeMessageDeliveryResult,
7
+ UserAttributeMessageTarget,
8
+ UserPublicAttribute,
9
+ } from "./types.js";
2
10
 
3
11
  function targetLabel(result: DeliveryTargetResult) {
4
12
  const name = result.id ?? `${result.channel}:${result.target}`;
@@ -20,6 +28,68 @@ function formatDeliveryResults(
20
28
  return [heading, ...lines].join("\n");
21
29
  }
22
30
 
31
+ function attributeLabel(attribute: UserPublicAttribute): string {
32
+ if (attribute.kind === "tag") {
33
+ return `tag:${attribute.value}`;
34
+ }
35
+
36
+ return `${attribute.key}:${attribute.value}`;
37
+ }
38
+
39
+ function instanceTargetLabel(target: Pick<UserAttributeMessageTarget, "instanceId" | "instanceName" | "peerId">) {
40
+ const name = target.instanceName ? ` (${target.instanceName})` : "";
41
+ return `${target.instanceId}${name} -> ${target.peerId}`;
42
+ }
43
+
44
+ function formatUserAttributeTargets(targets: UserAttributeMessageTarget[]) {
45
+ return targets.map((target) => `${instanceTargetLabel(target)} [${attributeLabel(target.matchedAttribute)}]`);
46
+ }
47
+
48
+ function formatUserAttributeResults(results: UserAttributeMessageDeliveryResult[]) {
49
+ return results.map((result) => {
50
+ let status = "已送达";
51
+ if (!result.sent) {
52
+ status = `发送失败:${result.error ?? "unknown error"}`;
53
+ } else if (!result.delivered) {
54
+ status = `投递失败:${result.error ?? "unknown error"}`;
55
+ }
56
+
57
+ return `${instanceTargetLabel(result)}:${status}`;
58
+ });
59
+ }
60
+
61
+ type SendUserAttributeToolParams = {
62
+ match?: {
63
+ kind?: unknown;
64
+ key?: unknown;
65
+ value?: unknown;
66
+ };
67
+ message?: unknown;
68
+ dryRun?: unknown;
69
+ };
70
+
71
+ function normalizeUserAttributeMatch(params: SendUserAttributeToolParams): UserAttributeMatch | string {
72
+ const match = params.match;
73
+ if (!match || typeof match !== "object") {
74
+ return "match is required.";
75
+ }
76
+
77
+ if (match.kind === "tag") {
78
+ const value = typeof match.value === "string" ? match.value.trim() : "";
79
+ return value ? { kind: "tag", value } : "match.value is required for tag matches.";
80
+ }
81
+
82
+ if (match.kind === "structured") {
83
+ const key = typeof match.key === "string" ? match.key.trim() : "";
84
+ const value = typeof match.value === "string" ? match.value.trim() : "";
85
+ return key && value
86
+ ? { kind: "structured", key, value }
87
+ : "match.key and match.value are required for structured matches.";
88
+ }
89
+
90
+ return 'match.kind must be "tag" or "structured".';
91
+ }
92
+
23
93
  export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
24
94
  return [
25
95
  {
@@ -378,5 +448,121 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
378
448
  };
379
449
  },
380
450
  },
451
+ {
452
+ name: "p2p_send_user_attribute_message",
453
+ label: "P2P Send User Attribute Message",
454
+ description:
455
+ "Send a user message to discovered OpenClaw instances matching a public user attribute. Always run a dry run with dryRun=true first and ask the user to confirm targets before group sending.",
456
+ parameters: {
457
+ type: "object" as const,
458
+ properties: {
459
+ match: {
460
+ type: "object" as const,
461
+ description:
462
+ 'User public attribute match. Use { "kind": "tag", "value": "..." } for USER.md tags or { "kind": "structured", "key": "...", "value": "..." } for profile attributes.',
463
+ oneOf: [
464
+ {
465
+ type: "object" as const,
466
+ additionalProperties: false,
467
+ properties: {
468
+ kind: { const: "tag" as const },
469
+ value: {
470
+ type: "string" as const,
471
+ description: "Tag value to match.",
472
+ },
473
+ },
474
+ required: ["kind", "value"],
475
+ },
476
+ {
477
+ type: "object" as const,
478
+ additionalProperties: false,
479
+ properties: {
480
+ kind: { const: "structured" as const },
481
+ key: {
482
+ type: "string" as const,
483
+ description: "Structured attribute key to match.",
484
+ },
485
+ value: {
486
+ type: "string" as const,
487
+ description: "Structured attribute value to match.",
488
+ },
489
+ },
490
+ required: ["kind", "key", "value"],
491
+ },
492
+ ],
493
+ },
494
+ message: {
495
+ type: "string" as const,
496
+ description: "Message content to send after dry-run target confirmation.",
497
+ },
498
+ dryRun: {
499
+ type: "boolean" as const,
500
+ description: "Preview matching instances without sending. Run this before group sending.",
501
+ },
502
+ },
503
+ required: ["match", "message"],
504
+ },
505
+ async execute(_toolCallId: string, params: SendUserAttributeToolParams) {
506
+ if (!router) {
507
+ return {
508
+ content: [{ type: "text" as const, text: "Instance router is not initialized." }],
509
+ details: { initialized: false },
510
+ isError: true,
511
+ };
512
+ }
513
+
514
+ const match = normalizeUserAttributeMatch(params);
515
+ const message = typeof params.message === "string" ? params.message.trim() : "";
516
+ if (typeof match === "string" || !message) {
517
+ const error = typeof match === "string" ? match : "message is required.";
518
+ return {
519
+ content: [{ type: "text" as const, text: error }],
520
+ details: { error },
521
+ isError: true,
522
+ };
523
+ }
524
+
525
+ const dryRun = params.dryRun === true;
526
+ const result = await router.sendUserAttributeMessage(match, message, { dryRun });
527
+ if (result.error) {
528
+ return {
529
+ content: [{ type: "text" as const, text: result.error }],
530
+ details: result,
531
+ isError: true,
532
+ };
533
+ }
534
+
535
+ if (dryRun) {
536
+ const targetLines = formatUserAttributeTargets(result.targets ?? []);
537
+ return {
538
+ content: [
539
+ {
540
+ type: "text" as const,
541
+ text: [
542
+ `Dry run matched ${result.matched} instance(s). No message was sent.`,
543
+ ...targetLines,
544
+ ].join("\n"),
545
+ },
546
+ ],
547
+ details: result,
548
+ };
549
+ }
550
+
551
+ const resultLines = formatUserAttributeResults(result.results ?? []);
552
+ return {
553
+ content: [
554
+ {
555
+ type: "text" as const,
556
+ text: [
557
+ `Matched ${result.matched} instance(s); sent ${result.sent}; delivered ${result.delivered}; failed ${result.failed}.`,
558
+ ...resultLines,
559
+ ].join("\n"),
560
+ },
561
+ ],
562
+ details: result,
563
+ isError: result.failed > 0 ? true : undefined,
564
+ };
565
+ },
566
+ },
381
567
  ];
382
568
  }
@@ -6,7 +6,9 @@ import type {
6
6
  InstancePeerRecord,
7
7
  InstancePeerStore,
8
8
  InstancePeerTable,
9
+ UserPublicAttribute,
9
10
  } from "./types.js";
11
+ import { normalizeUserPublicAttribute } from "./user-attributes.js";
10
12
 
11
13
  export interface StoreLogger {
12
14
  info?(message: string): void;
@@ -38,21 +40,50 @@ function sameStringArray(a: string[] = [], b: string[] = []): boolean {
38
40
  return a.every((value, index) => value === b[index]);
39
41
  }
40
42
 
43
+ function normalizeUserPublicAttributes(value: unknown): UserPublicAttribute[] {
44
+ if (!Array.isArray(value)) {
45
+ return [];
46
+ }
47
+
48
+ return value
49
+ .map((attribute) => normalizeUserPublicAttribute(attribute))
50
+ .filter((attribute): attribute is UserPublicAttribute => attribute !== undefined);
51
+ }
52
+
53
+ function sameUserPublicAttributes(a: UserPublicAttribute[] = [], b: UserPublicAttribute[] = []): boolean {
54
+ if (a.length !== b.length) return false;
55
+ return a.every((attribute, index) => {
56
+ const other = b[index];
57
+ return JSON.stringify(attribute) === JSON.stringify(other);
58
+ });
59
+ }
60
+
41
61
  function sameRecord(
42
62
  record: InstancePeerRecord | undefined,
43
63
  payload: InstanceAnnouncePayload,
44
64
  ): boolean {
45
65
  if (!record) return false;
46
66
 
67
+ const recordAttributes = normalizeUserPublicAttributes(record.userPublicAttributes);
68
+ const payloadAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
69
+
47
70
  return (
48
71
  record.peerId === payload.peerId &&
49
72
  record.instanceName === payload.instanceName &&
50
73
  record.pubkey === payload.pubkey &&
51
74
  sameStringArray(record.multiaddrs, payload.multiaddrs) &&
75
+ sameUserPublicAttributes(recordAttributes, payloadAttributes) &&
52
76
  record.lastAnnouncedAt === payload.announcedAt
53
77
  );
54
78
  }
55
79
 
80
+ function normalizeRecord(value: InstancePeerRecord): InstancePeerRecord {
81
+ return {
82
+ ...value,
83
+ userPublicAttributes: normalizeUserPublicAttributes(value.userPublicAttributes),
84
+ };
85
+ }
86
+
56
87
  function normalizeTable(value: unknown): InstancePeerTable {
57
88
  const candidate = value && typeof value === "object" ? value : {};
58
89
  const table = candidate as Partial<InstancePeerTable>;
@@ -61,10 +92,17 @@ function normalizeTable(value: unknown): InstancePeerTable {
61
92
  ? table.instances
62
93
  : {};
63
94
 
95
+ const normalizedInstances: Record<string, InstancePeerRecord> = {};
96
+ for (const [instanceId, record] of Object.entries(instances)) {
97
+ if (record && typeof record === "object") {
98
+ normalizedInstances[instanceId] = normalizeRecord(record as InstancePeerRecord);
99
+ }
100
+ }
101
+
64
102
  return {
65
103
  version: 1,
66
104
  updatedAt: typeof table.updatedAt === "number" ? table.updatedAt : Date.now(),
67
- instances: instances as Record<string, InstancePeerRecord>,
105
+ instances: normalizedInstances,
68
106
  };
69
107
  }
70
108
 
@@ -149,6 +187,7 @@ export function createInstancePeerStore(options?: {
149
187
  return runMutation(async () => {
150
188
  const table = await load();
151
189
  const existing = table.instances[payload.instanceId];
190
+ const userPublicAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
152
191
  const changed = !sameRecord(existing, payload);
153
192
  const record: InstancePeerRecord = {
154
193
  instanceId: payload.instanceId,
@@ -156,6 +195,7 @@ export function createInstancePeerStore(options?: {
156
195
  instanceName: payload.instanceName,
157
196
  pubkey: payload.pubkey,
158
197
  multiaddrs: payload.multiaddrs,
198
+ userPublicAttributes,
159
199
  lastAnnouncedAt: payload.announcedAt,
160
200
  lastSeenAt: Date.now(),
161
201
  source: "announce",
@@ -1,16 +1,18 @@
1
1
  import type {
2
2
  DeliveryAckPayload,
3
3
  DeliveryTargetResult,
4
- InboundDeliveryAdapter,
5
4
  InboundTargetConfig,
6
5
  InstanceAnnouncePayload,
7
- InstancePeerStore,
8
6
  InstanceRouter,
7
+ InstanceRouterOptions,
9
8
  MeshConfig,
10
- MeshNetwork,
11
9
  P2PMessage,
10
+ UserAttributeMatch,
11
+ UserAttributeMessageTarget,
12
+ UserPublicAttribute,
12
13
  UserMessagePayload,
13
14
  } from "./types.js";
15
+ import { matchesUserAttribute, mergeUserPublicAttributes } from "./user-attributes.js";
14
16
 
15
17
  export type RouterLogger = {
16
18
  info?: (message: string) => void;
@@ -48,6 +50,14 @@ function summarizeError(error: unknown): string {
48
50
  return error instanceof Error ? error.message : String(error);
49
51
  }
50
52
 
53
+ function describeAttributeMatch(match: UserAttributeMatch): string {
54
+ if (match.kind === "tag") {
55
+ return `tag ${match.value}`;
56
+ }
57
+
58
+ return `structured attribute ${match.key}=${match.value}`;
59
+ }
60
+
51
61
  type EffectiveInboundTarget = {
52
62
  id?: string;
53
63
  channel: string;
@@ -135,13 +145,7 @@ function firstAttemptedResult(
135
145
  );
136
146
  }
137
147
 
138
- export function createInstanceRouter(options: {
139
- mesh: MeshNetwork;
140
- store: InstancePeerStore;
141
- delivery: InboundDeliveryAdapter;
142
- config?: MeshConfig;
143
- logger?: RouterLogger;
144
- }): InstanceRouter {
148
+ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRouter {
145
149
  const { mesh, store, delivery } = options;
146
150
  const config = options.config ?? {};
147
151
  const logger = options.logger;
@@ -159,7 +163,26 @@ export function createInstanceRouter(options: {
159
163
  return identity.id;
160
164
  }
161
165
 
162
- function buildAnnouncePayload(): InstanceAnnouncePayload {
166
+ async function loadUserPublicAttributes(): Promise<UserPublicAttribute[]> {
167
+ const [userMdTags, profileAttributes] = await Promise.all([
168
+ options.userAttributeSource?.loadTags().catch((error) => {
169
+ logger?.warn?.(
170
+ `[libp2p-mesh] Failed to load USER.md public attributes: ${summarizeError(error)}`,
171
+ );
172
+ return [];
173
+ }) ?? Promise.resolve([]),
174
+ options.userProfileStore?.listAttributes().catch((error) => {
175
+ logger?.warn?.(
176
+ `[libp2p-mesh] Failed to load profile public attributes: ${summarizeError(error)}`,
177
+ );
178
+ return [];
179
+ }) ?? Promise.resolve([]),
180
+ ]);
181
+
182
+ return mergeUserPublicAttributes(userMdTags, profileAttributes);
183
+ }
184
+
185
+ async function buildAnnouncePayload(): Promise<InstanceAnnouncePayload> {
163
186
  const identity = mesh.getInstanceIdentity();
164
187
  if (!identity) {
165
188
  throw new Error("Local instance identity is not initialized");
@@ -171,6 +194,7 @@ export function createInstanceRouter(options: {
171
194
  instanceName: identity.name,
172
195
  multiaddrs: mesh.getMultiaddrs(),
173
196
  pubkey: identity.pubkey,
197
+ userPublicAttributes: await loadUserPublicAttributes(),
174
198
  announcedAt: Date.now(),
175
199
  };
176
200
  }
@@ -180,7 +204,7 @@ export function createInstanceRouter(options: {
180
204
  return;
181
205
  }
182
206
 
183
- const payload = buildAnnouncePayload();
207
+ const payload = await buildAnnouncePayload();
184
208
  await mesh.sendStructuredMessage(peerId, {
185
209
  id: crypto.randomUUID(),
186
210
  type: "instance-announce",
@@ -549,6 +573,90 @@ export function createInstanceRouter(options: {
549
573
  };
550
574
  }
551
575
 
576
+ async function resolveUserAttributeTargets(
577
+ match: UserAttributeMatch,
578
+ ): Promise<UserAttributeMessageTarget[]> {
579
+ const records = await store.list();
580
+ const targets: UserAttributeMessageTarget[] = [];
581
+
582
+ for (const record of records) {
583
+ const matchedAttribute = record.userPublicAttributes?.find((attribute) =>
584
+ matchesUserAttribute(attribute, match),
585
+ );
586
+ if (!matchedAttribute) {
587
+ continue;
588
+ }
589
+
590
+ targets.push({
591
+ instanceId: record.instanceId,
592
+ instanceName: record.instanceName,
593
+ peerId: record.peerId,
594
+ matchedAttribute,
595
+ });
596
+ }
597
+
598
+ return targets;
599
+ }
600
+
601
+ async function sendUserAttributeMessage(
602
+ match: UserAttributeMatch,
603
+ message: string,
604
+ sendOptions: { dryRun?: boolean } = {},
605
+ ) {
606
+ const targets = await resolveUserAttributeTargets(match);
607
+ if (targets.length === 0) {
608
+ return {
609
+ matched: 0,
610
+ sent: 0,
611
+ delivered: 0,
612
+ failed: 0,
613
+ error: `No discovered instances match ${describeAttributeMatch(match)}.`,
614
+ };
615
+ }
616
+
617
+ if (sendOptions.dryRun === true) {
618
+ return {
619
+ matched: targets.length,
620
+ sent: 0,
621
+ delivered: 0,
622
+ failed: 0,
623
+ targets,
624
+ };
625
+ }
626
+
627
+ const results = [];
628
+ for (const target of targets) {
629
+ try {
630
+ const result = await sendInstanceMessage(target.instanceId, message);
631
+ const targetResult = {
632
+ ...target,
633
+ sent: result.sent,
634
+ delivered: result.delivered,
635
+ };
636
+ if (result.error) {
637
+ results.push({ ...targetResult, error: result.error });
638
+ } else {
639
+ results.push(targetResult);
640
+ }
641
+ } catch (error) {
642
+ results.push({
643
+ ...target,
644
+ sent: false,
645
+ delivered: false,
646
+ error: summarizeError(error),
647
+ });
648
+ }
649
+ }
650
+
651
+ return {
652
+ matched: targets.length,
653
+ sent: results.filter((result) => result.sent).length,
654
+ delivered: results.filter((result) => result.delivered).length,
655
+ failed: results.filter((result) => !result.delivered).length,
656
+ results,
657
+ };
658
+ }
659
+
552
660
  return {
553
661
  start,
554
662
  stop,
@@ -557,5 +665,6 @@ export function createInstanceRouter(options: {
557
665
  listInstances,
558
666
  resolveInstance,
559
667
  sendInstanceMessage,
668
+ sendUserAttributeMessage,
560
669
  };
561
670
  }
package/src/plugin.ts CHANGED
@@ -5,13 +5,15 @@ import { createOpenClawRuntimeInboundDelivery } from "./inbound-delivery.js";
5
5
  import { createInstancePeerStore } from "./instance-peer-store.js";
6
6
  import { createInstanceRouter } from "./instance-router.js";
7
7
  import { createMeshNetwork } from "./mesh.js";
8
+ import { createUserMdAttributeSource } from "./user-md-attributes.js";
9
+ import { createUserProfileStore } from "./user-profile-store.js";
8
10
  import { buildP2PTools } from "./agent-tools.js";
9
- import { registerLibp2pMeshSetupCli } from "./setup-cli.js";
11
+ import { registerLibp2pMeshCli } from "./profile-cli.js";
10
12
  import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
11
13
  import type { MeshConfig } from "./types.js";
12
14
 
13
15
  export function registerLibp2pMesh(api: OpenClawPluginApi) {
14
- registerLibp2pMeshSetupCli(api);
16
+ registerLibp2pMeshCli(api);
15
17
 
16
18
  const config = api.pluginConfig as MeshConfig | undefined;
17
19
  let unsubscribeInbound: (() => void) | undefined;
@@ -21,9 +23,20 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
21
23
  logger: api.logger,
22
24
  });
23
25
  const store = createInstancePeerStore({ logger: api.logger });
26
+ const userAttributeSource = createUserMdAttributeSource({ logger: api.logger });
27
+ const userProfileStore = createUserProfileStore({ logger: api.logger });
24
28
  const delivery = createOpenClawRuntimeInboundDelivery({
25
29
  config: api.config,
26
- loadAdapter: api.runtime.channel.outbound.loadAdapter,
30
+ loadAdapter: async (channelId) => {
31
+ const loadAdapter = api.runtime.channel?.outbound?.loadAdapter;
32
+ if (!loadAdapter) {
33
+ api.logger.warn?.(
34
+ "[libp2p-mesh] Runtime channel outbound adapter is unavailable; inbound delivery is disabled in this context.",
35
+ );
36
+ return undefined;
37
+ }
38
+ return loadAdapter(channelId);
39
+ },
27
40
  logger: api.logger,
28
41
  });
29
42
  const router = createInstanceRouter({
@@ -32,6 +45,8 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
32
45
  delivery,
33
46
  config,
34
47
  logger: api.logger,
48
+ userAttributeSource,
49
+ userProfileStore,
35
50
  });
36
51
 
37
52
  const channel = createLibp2pMeshChannel(mesh);
@@ -0,0 +1,85 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
3
+ import {
4
+ createReadlinePrompter,
5
+ LIBP2P_MESH_CLI_REGISTRATION,
6
+ registerLibp2pMeshSetupCommand,
7
+ type ClosableSetupPrompter,
8
+ type SetupCliDeps,
9
+ } from "./setup-cli.js";
10
+ import { runProfileWizard } from "./profile-wizard.js";
11
+ import { createUserMdAttributeSource } from "./user-md-attributes.js";
12
+ import { createUserProfileStore, type UserProfileStore } from "./user-profile-store.js";
13
+ import type { SetupPrompter } from "./setup-wizard.js";
14
+
15
+ type CliRootCommand = {
16
+ command(name: string): {
17
+ description(text: string): {
18
+ action(handler: () => Promise<void>): void;
19
+ };
20
+ };
21
+ };
22
+
23
+ export type ProfileCliDeps = {
24
+ createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
25
+ createProfileStore?: (api: OpenClawPluginApi) => Pick<UserProfileStore, "listAttributes" | "replaceAttributes">;
26
+ createUserMdAttributeSource?: (api: OpenClawPluginApi) => { loadTags(): Promise<Awaited<ReturnType<UserProfileStore["listAttributes"]>>> };
27
+ };
28
+
29
+ export type Libp2pMeshCliDeps = {
30
+ setup?: SetupCliDeps;
31
+ profile?: ProfileCliDeps;
32
+ };
33
+
34
+ export function registerLibp2pMeshCli(api: OpenClawPluginApi, deps: Libp2pMeshCliDeps = {}): void {
35
+ api.registerCli((ctx) => {
36
+ const root = ctx.program
37
+ .command("libp2p-mesh")
38
+ .description("Configure libp2p-mesh plugin.");
39
+
40
+ registerLibp2pMeshSetupCommand(root, api, ctx, deps.setup);
41
+ registerLibp2pMeshProfileCommand(root, api, ctx, deps.profile);
42
+ }, LIBP2P_MESH_CLI_REGISTRATION);
43
+ }
44
+
45
+ export function registerLibp2pMeshProfileCli(api: OpenClawPluginApi, deps: ProfileCliDeps = {}): void {
46
+ api.registerCli((ctx) => {
47
+ const root = ctx.program
48
+ .command("libp2p-mesh")
49
+ .description("Configure libp2p-mesh plugin.");
50
+
51
+ registerLibp2pMeshProfileCommand(root, api, ctx, deps);
52
+ }, LIBP2P_MESH_CLI_REGISTRATION);
53
+ }
54
+
55
+ export function registerLibp2pMeshProfileCommand(
56
+ root: CliRootCommand,
57
+ api: OpenClawPluginApi,
58
+ ctx: OpenClawPluginCliContext,
59
+ deps: ProfileCliDeps = {},
60
+ ): void {
61
+ root
62
+ .command("profile")
63
+ .description("Manage libp2p-mesh public profile attributes.")
64
+ .action(async () => {
65
+ const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter()) as ClosableSetupPrompter;
66
+ const profileStore = deps.createProfileStore?.(api) ?? createUserProfileStore({ logger: api.logger });
67
+ const userMdAttributeSource = deps.createUserMdAttributeSource?.(api) ?? createUserMdAttributeSource({ logger: api.logger });
68
+
69
+ try {
70
+ const result = await runProfileWizard({
71
+ prompter,
72
+ readOnlyTags: await userMdAttributeSource.loadTags(),
73
+ profileAttributes: await profileStore.listAttributes(),
74
+ writer: {
75
+ async replaceAttributes(attributes) {
76
+ await profileStore.replaceAttributes(attributes);
77
+ },
78
+ },
79
+ });
80
+ prompter.print(result.message);
81
+ } finally {
82
+ prompter.close?.();
83
+ }
84
+ });
85
+ }