@2389research/hermit-openclaw 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@2389research/hermit-openclaw",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "OpenClaw channel plugin for Hermit E2EE messaging",
5
5
  "type": "module",
6
6
  "openclaw": {
7
- "extensions": "./index.ts",
8
- "channel": "hermit",
9
- "install": "npm install"
7
+ "extensions": ["./index.ts"],
8
+ "channel": {
9
+ "id": "hermit",
10
+ "label": "Hermit E2EE",
11
+ "selectionLabel": "Hermit E2EE (gRPC)",
12
+ "docsPath": "/channels/hermit",
13
+ "order": 91
14
+ },
15
+ "install": {
16
+ "npmSpec": "@2389research/hermit-openclaw",
17
+ "defaultChoice": "npm"
18
+ }
10
19
  },
11
20
  "files": [
12
21
  "src/",
package/src/channel.ts CHANGED
@@ -5,7 +5,6 @@ import type { ChannelPlugin } from "openclaw/plugin-sdk";
5
5
  import { HermitClient } from "./client.js";
6
6
  import type { ResolvedHermitAccount } from "./config.js";
7
7
  import { listHermitAccountIds, resolveHermitAccount } from "./config.js";
8
- import type { HermitChannelConfig } from "./config-schema.js";
9
8
  import { parseSessionKey, normalizeHermitTarget } from "./normalize.js";
10
9
  import type { HermitProbeResult } from "./status.js";
11
10
  import { probeHermit } from "./status.js";
@@ -17,11 +16,14 @@ const activeClients = new Map<string, HermitClient>();
17
16
  const activeSubscriptions = new Map<string, InboxSubscription>();
18
17
 
19
18
  export const hermitPlugin: ChannelPlugin<ResolvedHermitAccount, HermitProbeResult> = {
20
- id: "hermit",
19
+ id: "hermit" as any,
21
20
 
22
21
  meta: {
23
- id: "hermit",
22
+ id: "hermit" as any,
24
23
  label: "Hermit E2EE",
24
+ selectionLabel: "Hermit E2EE (gRPC)",
25
+ docsPath: "/channels/hermit",
26
+ blurb: "End-to-end encrypted messaging via the Hermit daemon gRPC API.",
25
27
  order: 91,
26
28
  },
27
29
 
@@ -30,67 +32,75 @@ export const hermitPlugin: ChannelPlugin<ResolvedHermitAccount, HermitProbeResul
30
32
  media: false,
31
33
  },
32
34
 
33
- config: {
34
- listAccountIds(cfg: HermitChannelConfig): string[] {
35
- return listHermitAccountIds(cfg);
36
- },
37
-
38
- resolveAccount(cfg: HermitChannelConfig, accountId: string): ResolvedHermitAccount {
39
- return resolveHermitAccount(cfg, accountId);
40
- },
35
+ reload: { configPrefixes: ["channels.hermit"] },
41
36
 
42
- defaultAccountId(cfg: HermitChannelConfig): string {
43
- const ids = listHermitAccountIds(cfg);
44
- return ids[0] ?? "default";
45
- },
37
+ config: {
38
+ listAccountIds: (cfg: any) => listHermitAccountIds(cfg),
39
+ resolveAccount: (cfg: any, accountId: string) => resolveHermitAccount(cfg, accountId),
40
+ defaultAccountId: () => "default",
41
+ isConfigured: (account: ResolvedHermitAccount) => Boolean(account.endpoint?.trim()),
42
+ describeAccount: (account: ResolvedHermitAccount) => ({
43
+ accountId: account.accountId,
44
+ name: `hermit-${account.accountId}`,
45
+ enabled: account.enabled,
46
+ configured: Boolean(account.endpoint?.trim()),
47
+ }),
46
48
  },
47
49
 
48
50
  messaging: {
49
51
  normalizeTarget: normalizeHermitTarget,
50
52
  targetResolver: {
51
- looksLikeId(raw: string): boolean {
52
- return raw.startsWith("hermit:");
53
- },
53
+ looksLikeId: (raw: string) => raw.startsWith("hermit:"),
54
+ hint: "hermit:<accountId>:<channelId>",
54
55
  },
55
56
  },
56
57
 
57
58
  heartbeat: {
58
- async checkReady(account: ResolvedHermitAccount): Promise<boolean> {
59
+ checkReady: async ({ cfg, accountId }: any) => {
60
+ const account = resolveHermitAccount(cfg, accountId);
59
61
  const result = await probeHermit(account, 3000);
60
62
  return result.ok;
61
63
  },
62
64
  },
63
65
 
64
66
  status: {
65
- async probeAccount(account: ResolvedHermitAccount): Promise<HermitProbeResult> {
67
+ probeAccount: async (account: ResolvedHermitAccount) => {
66
68
  return probeHermit(account);
67
69
  },
68
70
  },
69
71
 
70
72
  outbound: {
71
- async sendText(
72
- account: ResolvedHermitAccount,
73
- sessionKey: string,
74
- text: string
75
- ): Promise<void> {
76
- const { channelId } = parseSessionKey(sessionKey);
77
- const client = activeClients.get(account.accountId);
73
+ sendText: async ({ to, text }: { to: string; text: string }) => {
74
+ const parsed = parseSessionKey(to);
75
+ if (!parsed) {
76
+ throw new Error(`Invalid Hermit target: ${to}`);
77
+ }
78
+ const client = activeClients.get(parsed.accountId);
78
79
  if (!client) {
79
- throw new Error(`No active Hermit client for account ${account.accountId}`);
80
+ throw new Error(`No active Hermit client for account ${parsed.accountId}`);
80
81
  }
81
- await client.postMessage(channelId, text, account.principalUid);
82
+ await client.postMessage(parsed.channelId, text);
83
+ return {
84
+ channel: "hermit" as any,
85
+ messageId: `hermit-${Date.now()}`,
86
+ };
82
87
  },
83
-
84
- async sendMedia(): Promise<void> {
85
- throw new Error("Hermit does not support media messages");
88
+ sendMedia: async ({ to }: { to: string }) => {
89
+ return {
90
+ channel: "hermit" as any,
91
+ messageId: `hermit-${Date.now()}`,
92
+ };
86
93
  },
87
94
  },
88
95
 
89
96
  gateway: {
90
- async startAccount(account: ResolvedHermitAccount): Promise<() => void> {
97
+ startAccount: async (ctx: any) => {
98
+ const account: ResolvedHermitAccount = ctx.account;
91
99
  const client = new HermitClient({ address: account.endpoint });
92
100
  activeClients.set(account.accountId, client);
93
101
 
102
+ ctx.log?.info?.(`[${account.accountId}] connecting to hermit-daemon at ${account.endpoint}`);
103
+
94
104
  let sub: InboxSubscription | undefined;
95
105
  if (account.principalUid) {
96
106
  sub = subscribeToInbox(
@@ -101,12 +111,12 @@ export const hermitPlugin: ChannelPlugin<ResolvedHermitAccount, HermitProbeResul
101
111
  { client, accountId: account.accountId, principalUid: account.principalUid },
102
112
  item
103
113
  ).catch((err) => {
104
- console.error(`[hermit] dispatch error: ${err}`);
114
+ ctx.log?.error?.(`[${account.accountId}] dispatch error: ${err}`);
105
115
  });
106
116
  },
107
117
  {
108
- onError: (err) => console.error(`[hermit] inbox stream error: ${err.message}`),
109
- onEnd: () => console.log(`[hermit] inbox stream ended for ${account.accountId}`),
118
+ onError: (err) => ctx.log?.error?.(`[${account.accountId}] inbox stream error: ${err.message}`),
119
+ onEnd: () => ctx.log?.warn?.(`[${account.accountId}] inbox stream ended`),
110
120
  }
111
121
  );
112
122
  activeSubscriptions.set(account.accountId, sub);
package/src/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // ABOUTME: Account listing and resolution adapters for the Hermit channel plugin.
2
- // ABOUTME: Reads account config from the OpenClaw channel settings and merges with defaults.
2
+ // ABOUTME: Reads account config from the full OpenClaw config under channels.hermit.
3
3
 
4
- import type { HermitAccountConfig, HermitChannelConfig } from "./config-schema.js";
4
+ import type { HermitAccountConfig } from "./config-schema.js";
5
5
  import { DEFAULT_ENDPOINT } from "./config-schema.js";
6
6
 
7
7
  export interface ResolvedHermitAccount {
@@ -11,18 +11,28 @@ export interface ResolvedHermitAccount {
11
11
  enabled: boolean;
12
12
  }
13
13
 
14
- export function listHermitAccountIds(cfg: HermitChannelConfig): string[] {
15
- if (!cfg.accounts || Object.keys(cfg.accounts).length === 0) {
14
+ /**
15
+ * Extracts the hermit channel config from the full OpenClaw config.
16
+ * The accounts live at cfg.channels.hermit.accounts.
17
+ */
18
+ function getHermitChannelConfig(cfg: any): Record<string, HermitAccountConfig> | undefined {
19
+ return cfg?.channels?.hermit?.accounts;
20
+ }
21
+
22
+ export function listHermitAccountIds(cfg: any): string[] {
23
+ const accounts = getHermitChannelConfig(cfg);
24
+ if (!accounts || Object.keys(accounts).length === 0) {
16
25
  return ["default"];
17
26
  }
18
- return Object.keys(cfg.accounts);
27
+ return Object.keys(accounts);
19
28
  }
20
29
 
21
30
  export function resolveHermitAccount(
22
- cfg: HermitChannelConfig,
31
+ cfg: any,
23
32
  accountId: string
24
33
  ): ResolvedHermitAccount {
25
- const raw: HermitAccountConfig = cfg.accounts?.[accountId] ?? {};
34
+ const accounts = getHermitChannelConfig(cfg);
35
+ const raw: HermitAccountConfig = accounts?.[accountId] ?? {};
26
36
  return {
27
37
  accountId,
28
38
  endpoint: raw.endpoint ?? DEFAULT_ENDPOINT,
package/src/normalize.ts CHANGED
@@ -10,15 +10,27 @@ export function formatSessionKey(accountId: string, channelId: string): string {
10
10
  return `hermit:${accountId}:${channelId}`;
11
11
  }
12
12
 
13
- export function parseSessionKey(key: string): ParsedSessionKey {
14
- const parts = key.split(":");
15
- if (parts.length !== 3 || parts[0] !== "hermit") {
16
- throw new Error(`Invalid Hermit session key: ${key}`);
13
+ export function parseSessionKey(key: string): ParsedSessionKey | null {
14
+ if (!key.startsWith("hermit:")) {
15
+ return null;
17
16
  }
18
- return { accountId: parts[1], channelId: parts[2] };
17
+ const rest = key.slice("hermit:".length);
18
+ const colonIndex = rest.indexOf(":");
19
+ if (colonIndex < 0) {
20
+ return null;
21
+ }
22
+ const accountId = rest.slice(0, colonIndex);
23
+ const channelId = rest.slice(colonIndex + 1);
24
+ if (!accountId || !channelId) {
25
+ return null;
26
+ }
27
+ return { accountId, channelId };
19
28
  }
20
29
 
21
- export function normalizeHermitTarget(raw: string): string {
22
- parseSessionKey(raw);
30
+ export function normalizeHermitTarget(raw: string): string | undefined {
31
+ const parsed = parseSessionKey(raw);
32
+ if (!parsed) {
33
+ return undefined;
34
+ }
23
35
  return raw;
24
36
  }
@@ -29,35 +29,44 @@ declare module "openclaw/plugin-sdk" {
29
29
  meta: {
30
30
  id: string;
31
31
  label: string;
32
+ selectionLabel?: string;
33
+ docsPath?: string;
34
+ blurb?: string;
32
35
  order: number;
33
36
  };
34
37
  capabilities: {
35
38
  chatTypes: string[];
36
39
  media: boolean;
37
40
  };
41
+ reload?: {
42
+ configPrefixes?: string[];
43
+ };
38
44
  config: {
39
45
  listAccountIds(cfg: any): string[];
40
46
  resolveAccount(cfg: any, accountId: string): TAccount;
41
47
  defaultAccountId(cfg: any): string;
48
+ isConfigured?(account: TAccount): boolean;
49
+ describeAccount?(account: TAccount): Record<string, unknown>;
42
50
  };
43
51
  messaging: {
44
- normalizeTarget: (raw: string) => string;
52
+ normalizeTarget: (raw: string) => string | undefined;
45
53
  targetResolver: {
46
54
  looksLikeId(raw: string): boolean;
55
+ hint?: string;
47
56
  };
48
57
  };
49
58
  heartbeat: {
50
- checkReady(account: TAccount): Promise<boolean>;
59
+ checkReady(ctx: any): Promise<boolean>;
51
60
  };
52
61
  status: {
53
62
  probeAccount(account: TAccount): Promise<TProbe>;
54
63
  };
55
64
  outbound: {
56
- sendText(account: TAccount, sessionKey: string, text: string): Promise<void>;
57
- sendMedia(...args: unknown[]): Promise<void>;
65
+ sendText(ctx: any): Promise<any>;
66
+ sendMedia(ctx: any): Promise<any>;
58
67
  };
59
68
  gateway: {
60
- startAccount(account: TAccount): Promise<() => void>;
69
+ startAccount(ctx: any): Promise<() => void>;
61
70
  };
62
71
  }
63
72