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