@decentchat/decentclaw 0.1.4 → 0.1.5

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 (3) hide show
  1. package/README.md +17 -1
  2. package/package.json +1 -1
  3. package/src/channel.ts +266 -1
package/README.md CHANGED
@@ -12,7 +12,23 @@ openclaw plugins install @decentchat/decentclaw
12
12
 
13
13
  ## Configure
14
14
 
15
- Add a `channels.decentchat` block to your OpenClaw config (`~/.openclaw/openclaw.json` or per-project):
15
+ The quickest way to set up is with the interactive wizard:
16
+
17
+ ```
18
+ openclaw configure
19
+ ```
20
+
21
+ Select **DecentChat** when prompted. The wizard will:
22
+
23
+ 1. Offer to generate a new 12-word seed phrase (or let you paste an existing one)
24
+ 2. Ask for a display name
25
+ 3. Ask for an invite URL to join a workspace
26
+
27
+ You can also set the seed phrase via the `DECENTCHAT_SEED_PHRASE` environment variable instead of storing it in the config file.
28
+
29
+ ### Manual configuration
30
+
31
+ If you prefer to edit the config directly, add a `channels.decentchat` block to your OpenClaw config (`~/.openclaw/openclaw.json` or per-project):
16
32
 
17
33
  ```yaml
18
34
  channels:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentchat/decentclaw",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "OpenClaw channel plugin for DecentChat — P2P encrypted chat",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/channel.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import type { ChannelPlugin } from "openclaw/plugin-sdk";
4
+ import type { ChannelPlugin, ChannelSetupWizard, ChannelSetupInput, OpenClawConfig } from "openclaw/plugin-sdk";
5
+ import { createStandardChannelSetupStatus, patchTopLevelChannelConfigSection, createTopLevelChannelDmPolicy } from "openclaw/plugin-sdk/setup";
5
6
  import { z } from "zod";
6
7
 
7
8
  import { assertCompanyBootstrapAgentInstallation, ensureCompanyBootstrapRuntime, resolveCompanyManifestPath } from "@decentchat/company-sim";
9
+ import { SeedPhraseManager } from "@decentchat/protocol";
8
10
  import { startDecentChatPeer } from "./monitor.js";
9
11
  import { getActivePeer, listActivePeerAccountIds } from "./peer-registry.js";
10
12
  import { buildDecentChatRuntimeBootstrapKey, invalidateDecentChatBootstrapKey, runDecentChatBootstrapOnce } from "./runtime.js";
@@ -532,6 +534,266 @@ export async function bootstrapDecentChatCompanySimForStartup(params: {
532
534
  });
533
535
  }
534
536
 
537
+ // ---------------------------------------------------------------------------
538
+ // Setup wizard (powers `openclaw configure`)
539
+ // ---------------------------------------------------------------------------
540
+
541
+ const CHANNEL = "decentchat";
542
+ const seedPhraseManager = new SeedPhraseManager();
543
+
544
+ function validateSeedPhrase(mnemonic: string): string | undefined {
545
+ const result = seedPhraseManager.validate(mnemonic);
546
+ if (!result.valid) return result.error ?? "Invalid seed phrase";
547
+ return undefined;
548
+ }
549
+
550
+ const decentChatSetupWizard: ChannelSetupWizard = {
551
+ channel: CHANNEL,
552
+
553
+ resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
554
+ resolveShouldPromptAccountIds: () => false,
555
+
556
+ status: createStandardChannelSetupStatus({
557
+ channelLabel: "DecentChat",
558
+ configuredLabel: "configured",
559
+ unconfiguredLabel: "needs seed phrase",
560
+ configuredHint: "configured",
561
+ unconfiguredHint: "needs seed phrase",
562
+ configuredScore: 1,
563
+ unconfiguredScore: 0,
564
+ includeStatusLine: true,
565
+ resolveConfigured: ({ cfg }) => resolveDecentChatAccount(cfg).configured,
566
+ resolveExtraStatusLines: ({ cfg }) => {
567
+ const account = resolveDecentChatAccount(cfg);
568
+ const lines: string[] = [];
569
+ if (account.alias) lines.push(`Alias: ${account.alias}`);
570
+ if (account.invites.length > 0) lines.push(`Invites: ${account.invites.length}`);
571
+ return lines;
572
+ },
573
+ }),
574
+
575
+ introNote: {
576
+ title: "DecentChat setup",
577
+ lines: [
578
+ "DecentChat is a P2P encrypted messaging network.",
579
+ "Your bot needs a 12-word BIP39 seed phrase to create its identity.",
580
+ "You can generate a new one here or paste an existing one.",
581
+ ],
582
+ },
583
+
584
+ stepOrder: "credentials-first",
585
+
586
+ prepare: async ({ cfg, accountId, prompter }) => {
587
+ const account = resolveDecentChatAccount(cfg, accountId);
588
+ if (account.configured) return;
589
+
590
+ const generateNew = await prompter.confirm({
591
+ message: "Generate a new DecentChat identity?",
592
+ initialValue: true,
593
+ });
594
+
595
+ if (generateNew) {
596
+ const { mnemonic } = seedPhraseManager.generate();
597
+ await prompter.note(
598
+ [
599
+ `Your new seed phrase:`,
600
+ ``,
601
+ ` ${mnemonic}`,
602
+ ``,
603
+ `Write this down and store it somewhere safe.`,
604
+ `This is the only way to recover your bot's identity.`,
605
+ ].join("\n"),
606
+ "New identity generated",
607
+ );
608
+ return {
609
+ credentialValues: { privateKey: mnemonic },
610
+ };
611
+ }
612
+
613
+ return;
614
+ },
615
+
616
+ credentials: [
617
+ {
618
+ inputKey: "privateKey" as keyof ChannelSetupInput,
619
+ providerHint: CHANNEL,
620
+ credentialLabel: "seed phrase",
621
+ helpTitle: "DecentChat seed phrase",
622
+ helpLines: [
623
+ "A 12-word BIP39 mnemonic that determines your bot's identity on the network.",
624
+ "All encryption keys are derived from this phrase.",
625
+ ],
626
+ envPrompt: "DECENTCHAT_SEED_PHRASE detected. Use env var?",
627
+ keepPrompt: "Seed phrase already configured. Keep it?",
628
+ inputPrompt: "DecentChat seed phrase (12 words)",
629
+ preferredEnvVar: "DECENTCHAT_SEED_PHRASE",
630
+
631
+ allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
632
+
633
+ inspect: ({ cfg, accountId }) => {
634
+ const account = resolveDecentChatAccount(cfg, accountId);
635
+ return {
636
+ accountConfigured: account.configured,
637
+ hasConfiguredValue: !!account.seedPhrase?.trim(),
638
+ resolvedValue: account.seedPhrase?.trim(),
639
+ envValue: process.env.DECENTCHAT_SEED_PHRASE?.trim(),
640
+ };
641
+ },
642
+
643
+ shouldPrompt: ({ credentialValues, state }) => {
644
+ // Skip the prompt if prepare() already generated a seed phrase
645
+ if (credentialValues.privateKey?.trim()) return false;
646
+ if (state.hasConfiguredValue) return false;
647
+ return true;
648
+ },
649
+
650
+ applyUseEnv: async ({ cfg }) =>
651
+ patchTopLevelChannelConfigSection({
652
+ cfg,
653
+ channel: CHANNEL,
654
+ enabled: true,
655
+ clearFields: ["seedPhrase"],
656
+ patch: {},
657
+ }),
658
+
659
+ applySet: async ({ cfg, resolvedValue }) =>
660
+ patchTopLevelChannelConfigSection({
661
+ cfg,
662
+ channel: CHANNEL,
663
+ enabled: true,
664
+ patch: { seedPhrase: resolvedValue },
665
+ }),
666
+ },
667
+ ],
668
+
669
+ textInputs: [
670
+ {
671
+ inputKey: "name" as keyof ChannelSetupInput,
672
+ message: "Bot display name",
673
+ placeholder: "DecentChat Bot",
674
+ required: false,
675
+ helpTitle: "Bot display name",
676
+ helpLines: ["The name other users see when your bot sends messages."],
677
+
678
+ currentValue: ({ cfg, accountId }) => {
679
+ const account = resolveDecentChatAccount(cfg, accountId);
680
+ return account.alias !== "DecentChat Bot" ? account.alias : undefined;
681
+ },
682
+
683
+ initialValue: () => "DecentChat Bot",
684
+
685
+ applySet: async ({ cfg, value }) =>
686
+ patchTopLevelChannelConfigSection({
687
+ cfg,
688
+ channel: CHANNEL,
689
+ enabled: true,
690
+ patch: { alias: value.trim() || "DecentChat Bot" },
691
+ }),
692
+ },
693
+ {
694
+ inputKey: "url" as keyof ChannelSetupInput,
695
+ message: "Invite URL to join a workspace (optional)",
696
+ placeholder: "decentchat://invite/...",
697
+ required: false,
698
+ applyEmptyValue: false,
699
+ helpTitle: "DecentChat invite URL",
700
+ helpLines: [
701
+ "Paste an invite link to automatically join a workspace on startup.",
702
+ "You can add more later in the config file.",
703
+ "Leave blank to skip.",
704
+ ],
705
+
706
+ currentValue: ({ cfg, accountId }) => {
707
+ const account = resolveDecentChatAccount(cfg, accountId);
708
+ return account.invites.length > 0 ? account.invites[0] : undefined;
709
+ },
710
+
711
+ keepPrompt: (value) => `Invite URL set (${value}). Keep it?`,
712
+
713
+ applySet: async ({ cfg, value }) => {
714
+ const trimmed = value.trim();
715
+ if (!trimmed) return cfg;
716
+ // Merge with existing invites, avoiding duplicates
717
+ const existing: string[] = (cfg as any)?.channels?.decentchat?.invites ?? [];
718
+ const merged = [...new Set([...existing, trimmed])];
719
+ return patchTopLevelChannelConfigSection({
720
+ cfg,
721
+ channel: CHANNEL,
722
+ enabled: true,
723
+ patch: { invites: merged },
724
+ });
725
+ },
726
+ },
727
+ ],
728
+
729
+ completionNote: {
730
+ title: "DecentChat ready",
731
+ lines: [
732
+ "Your bot will connect to the DecentChat P2P network on next startup.",
733
+ "Run `openclaw start` to bring it online.",
734
+ ],
735
+ },
736
+
737
+ dmPolicy: createTopLevelChannelDmPolicy({
738
+ label: "DecentChat",
739
+ channel: CHANNEL,
740
+ policyKey: `channels.${CHANNEL}.dmPolicy`,
741
+ allowFromKey: `channels.${CHANNEL}.allowFrom`,
742
+ getCurrent: (cfg) => (cfg as any)?.channels?.decentchat?.dmPolicy ?? "open",
743
+ }),
744
+
745
+ disable: (cfg) =>
746
+ patchTopLevelChannelConfigSection({
747
+ cfg,
748
+ channel: CHANNEL,
749
+ patch: { enabled: false },
750
+ }),
751
+ };
752
+
753
+ const decentChatSetupAdapter = {
754
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
755
+
756
+ validateInput: ({ input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) => {
757
+ const typedInput = input as ChannelSetupInput & { privateKey?: string };
758
+ if (!typedInput.useEnv) {
759
+ const seedPhrase = typedInput.privateKey?.trim();
760
+ if (!seedPhrase) return "DecentChat requires a seed phrase.";
761
+ const error = validateSeedPhrase(seedPhrase);
762
+ if (error) return error;
763
+ }
764
+ return null;
765
+ },
766
+
767
+ applyAccountConfig: ({ cfg, input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) => {
768
+ const typedInput = input as ChannelSetupInput & { privateKey?: string };
769
+ const patch: Record<string, unknown> = {};
770
+
771
+ if (typedInput.useEnv) {
772
+ // Clear stored seed phrase, will read from env at runtime
773
+ } else if (typedInput.privateKey?.trim()) {
774
+ patch.seedPhrase = typedInput.privateKey.trim();
775
+ }
776
+
777
+ if ((typedInput as any).name?.trim()) {
778
+ patch.alias = (typedInput as any).name.trim();
779
+ }
780
+
781
+ if ((typedInput as any).url?.trim()) {
782
+ const existing: string[] = (cfg as any)?.channels?.decentchat?.invites ?? [];
783
+ const invite = (typedInput as any).url.trim();
784
+ patch.invites = [...new Set([...existing, invite])];
785
+ }
786
+
787
+ return patchTopLevelChannelConfigSection({
788
+ cfg,
789
+ channel: CHANNEL,
790
+ enabled: true,
791
+ clearFields: typedInput.useEnv ? ["seedPhrase"] : undefined,
792
+ patch,
793
+ });
794
+ },
795
+ };
796
+
535
797
  export const decentChatPlugin: ChannelPlugin<ResolvedDecentChatAccount> = {
536
798
  id: "decentchat",
537
799
  meta: {
@@ -571,6 +833,9 @@ export const decentChatPlugin: ChannelPlugin<ResolvedDecentChatAccount> = {
571
833
  },
572
834
  },
573
835
 
836
+ setup: decentChatSetupAdapter,
837
+ setupWizard: decentChatSetupWizard,
838
+
574
839
  config: {
575
840
  listAccountIds: (cfg) => listDecentChatAccountIds(cfg),
576
841
  resolveAccount: (cfg, accountId) => resolveDecentChatAccount(cfg, accountId),