@helmio/openclaw-helmio-chat 0.1.0

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 ADDED
@@ -0,0 +1,36 @@
1
+ # @helmio/openclaw-helmio-chat
2
+
3
+ OpenClaw plugin that bridges Helmio's SignalR `ChatHub` to per-agent turns inside the Company VM. Phase 1 scope per ADR-0008.
4
+
5
+ ## What it does
6
+
7
+ - Connects to `${HELMIO_API_URL}/api/chat-hub` with the per-Company bridge token in env (`HELMIO_CHAT_BRIDGE_TOKEN`). The hub auto-joins the connection to the Company's `TeamDefault` conversation group.
8
+ - On every inbound `messageCreated` event with `participantTypeId === 1` (user-originated), dispatches a turn to the CEO agent (phase 1 routes CEO-only; multi-agent fan-out is phase 2).
9
+ - Registers a `helmio_chat_send({ body })` tool on each agent. When the agent's LLM calls it, the tool POSTs to `${HELMIO_API_URL}/api/chat/messages` with the calling agent's id, idempotency key (ULID), and the body. The backend persists + broadcasts; both browser tabs and this plugin see the new message.
10
+
11
+ ## Env vars
12
+
13
+ - `HELMIO_API_URL` — backend base URL, e.g. `https://10.0.0.2`. Required.
14
+ - `HELMIO_CHAT_BRIDGE_TOKEN` — per-Company bridge token, 32-byte URL-safe base64. Required.
15
+
16
+ Both are written into the VM `.env` by `CompanyProvisionService.RenderEnv` during provisioning.
17
+
18
+ ## Agent metadata contract
19
+
20
+ The plugin reads each agent's `metadata.isCeo` from the OpenClaw runtime config to identify the CEO. The agent's `id` field is the Helmio `Agent.Id` (stringified) per `CanonicalAgentList.Build`.
21
+
22
+ ## Install (cloud-init)
23
+
24
+ ```bash
25
+ docker exec openclaw openclaw plugins install npm:@helmio/openclaw-helmio-chat@<pinned-version>
26
+ ```
27
+
28
+ Retry up to 3 times with 10s backoff; second + third attempts pass `--force` (race against gateway startup writes — see `project_openclaw_external_plugin_pattern.md` memory).
29
+
30
+ ## Out of scope (phase 2)
31
+
32
+ - `@mention` parsing + multi-agent fan-out via `AgentRouteBinding[]`
33
+ - Token redaction (Notion/Linear paste → `(secret redacted)`)
34
+ - `helmio_chat_create_group` tool
35
+ - DM conversations and `conversationKindCode != 1`
36
+ - Threading (`ParentMessageId`)
@@ -0,0 +1,12 @@
1
+ import type { RuntimeAgentConfig } from "openclaw/plugin-sdk/plugin-entry";
2
+ export interface ResolvedAgent {
3
+ openclawAgentId: string;
4
+ helmioAgentId: number;
5
+ isCeo: boolean;
6
+ }
7
+ export interface ResolverResult {
8
+ agents: ResolvedAgent[];
9
+ ceo: ResolvedAgent | undefined;
10
+ }
11
+ export declare function resolveAgents(configs: RuntimeAgentConfig[]): ResolverResult;
12
+ export declare function parseHelmioAgentId(openclawAgentId: string): number | null;
@@ -0,0 +1,20 @@
1
+ export function resolveAgents(configs) {
2
+ const agents = [];
3
+ for (const cfg of configs) {
4
+ const helmioAgentId = Number.parseInt(cfg.id, 10);
5
+ if (!Number.isFinite(helmioAgentId) || helmioAgentId <= 0)
6
+ continue;
7
+ agents.push({
8
+ openclawAgentId: cfg.id,
9
+ helmioAgentId,
10
+ isCeo: cfg.metadata?.isCeo === true,
11
+ });
12
+ }
13
+ const ceo = agents.find((a) => a.isCeo);
14
+ return { agents, ceo };
15
+ }
16
+ export function parseHelmioAgentId(openclawAgentId) {
17
+ const n = Number.parseInt(openclawAgentId, 10);
18
+ return Number.isFinite(n) && n > 0 ? n : null;
19
+ }
20
+ //# sourceMappingURL=agent-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-resolver.js","sourceRoot":"","sources":["../src/agent-resolver.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,aAAa,CAAC,OAA6B;IACzD,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAClD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,aAAa,IAAI,CAAC;YAAE,SAAS;QACpE,MAAM,CAAC,IAAI,CAAC;YACV,eAAe,EAAE,GAAG,CAAC,EAAE;YACvB,aAAa;YACb,KAAK,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,KAAK,IAAI;SACpC,CAAC,CAAC;IACL,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,eAAuB;IACxD,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAC/C,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { HubConnectionState } from "@microsoft/signalr";
2
+ import type { ChatMessageDto } from "./events.js";
3
+ import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
4
+ export interface ChatConnectionOptions {
5
+ hubUrl: string;
6
+ bridgeToken: string;
7
+ logger: PluginLogger;
8
+ onMessage: (msg: ChatMessageDto) => void | Promise<void>;
9
+ }
10
+ export interface ChatConnection {
11
+ stop(): Promise<void>;
12
+ state(): HubConnectionState;
13
+ }
14
+ export declare function startChatConnection(opts: ChatConnectionOptions): ChatConnection;
@@ -0,0 +1,73 @@
1
+ // SignalR connection lifecycle to Helmio's ChatHub. Mirrors the probe plugin's
2
+ // runner pattern: build → wire lifecycle handlers → start → on unrecoverable
3
+ // close, restart with 5s backoff. The hub auto-joins this connection to the
4
+ // Company's TeamDefault group on connect (see ChatHub.OnConnectedAsync) — the
5
+ // plugin doesn't need to call JoinConversation.
6
+ import { HubConnectionBuilder, HubConnectionState, LogLevel, } from "@microsoft/signalr";
7
+ const RECONNECT_DELAYS_MS = [0, 2000, 5000, 10000];
8
+ const RESTART_BACKOFF_MS = 5000;
9
+ const HEARTBEAT_INTERVAL_MS = 60_000;
10
+ export function startChatConnection(opts) {
11
+ let conn;
12
+ let stopped = false;
13
+ const buildConnection = () => new HubConnectionBuilder()
14
+ .withUrl(opts.hubUrl, {
15
+ accessTokenFactory: () => opts.bridgeToken,
16
+ })
17
+ .withAutomaticReconnect({
18
+ nextRetryDelayInMilliseconds: (ctx) => RECONNECT_DELAYS_MS[ctx.previousRetryCount] ?? 15_000,
19
+ })
20
+ .configureLogging(LogLevel.Information)
21
+ .build();
22
+ const wire = (c) => {
23
+ c.on("messageCreated", (msg) => {
24
+ void Promise.resolve(opts.onMessage(msg)).catch((err) => opts.logger.error(`helmio-chat: onMessage threw for messageId=${msg?.id}: ${err.message}`));
25
+ });
26
+ c.onreconnecting((err) => {
27
+ opts.logger.warn(`helmio-chat: reconnecting (${err?.message ?? "no-error"})`);
28
+ });
29
+ c.onreconnected((connectionId) => {
30
+ opts.logger.info(`helmio-chat: reconnected (connectionId=${connectionId})`);
31
+ });
32
+ c.onclose((err) => {
33
+ opts.logger.warn(`helmio-chat: connection closed (${err?.message ?? "graceful"})`);
34
+ // Manual restart covers the unrecoverable-error case
35
+ // (auth rejected, etc.) where withAutomaticReconnect has given up.
36
+ if (!stopped)
37
+ setTimeout(() => void restart(), RESTART_BACKOFF_MS);
38
+ });
39
+ };
40
+ const restart = async () => {
41
+ if (stopped)
42
+ return;
43
+ conn = buildConnection();
44
+ wire(conn);
45
+ try {
46
+ await conn.start();
47
+ opts.logger.info(`helmio-chat: connected to ${opts.hubUrl}; auto-joined Team group on hub side`);
48
+ }
49
+ catch (err) {
50
+ opts.logger.error(`helmio-chat: connect failed: ${err.message}`);
51
+ if (!stopped)
52
+ setTimeout(() => void restart(), RESTART_BACKOFF_MS);
53
+ }
54
+ };
55
+ void restart();
56
+ const heartbeat = setInterval(() => {
57
+ opts.logger.debug?.(`helmio-chat: state=${conn?.state ?? HubConnectionState.Disconnected} connectionId=${conn?.connectionId ?? "none"}`);
58
+ }, HEARTBEAT_INTERVAL_MS);
59
+ return {
60
+ async stop() {
61
+ stopped = true;
62
+ clearInterval(heartbeat);
63
+ try {
64
+ await conn?.stop();
65
+ }
66
+ catch (err) {
67
+ opts.logger.warn(`helmio-chat: stop failed: ${err.message}`);
68
+ }
69
+ },
70
+ state: () => conn?.state ?? HubConnectionState.Disconnected,
71
+ };
72
+ }
73
+ //# sourceMappingURL=chat-connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-connection.js","sourceRoot":"","sources":["../src/chat-connection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { PluginApi, PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
2
+ export interface ChatSendToolOptions {
3
+ helmioApiUrl: string;
4
+ bridgeToken: string;
5
+ logger: PluginLogger;
6
+ fetchFn?: typeof fetch;
7
+ idFactory?: () => string;
8
+ }
9
+ export interface ChatSendResult {
10
+ ok: true;
11
+ messageId: number;
12
+ deduplicated?: true;
13
+ }
14
+ export declare function registerChatSendTool(api: PluginApi, opts: ChatSendToolOptions): void;
@@ -0,0 +1,99 @@
1
+ // Outbound chat-send tool. Registered once globally; the runtime passes the
2
+ // calling agent's id in the tool execute context, which we parse as the
3
+ // Helmio Agent.Id (CanonicalAgentList.Build sets openclaw agent id =
4
+ // Agent.Id.ToString()).
5
+ //
6
+ // Wire format matches ChatMessagesController.cs InboundMessageRequest:
7
+ // { clientMsgId, agentId, conversationKindCode: 1, body }
8
+ // 201 → fresh persist; 200 → idempotent duplicate (existing messageId
9
+ // returned, see ChatMessagesController.cs:164-171).
10
+ import { ulid } from "ulid";
11
+ import { ConversationKind } from "./events.js";
12
+ import { parseHelmioAgentId } from "./agent-resolver.js";
13
+ const MAX_RETRIES = 3;
14
+ const BASE_BACKOFF_MS = 250;
15
+ export function registerChatSendTool(api, opts) {
16
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
17
+ const newId = opts.idFactory ?? ulid;
18
+ const endpoint = `${opts.helmioApiUrl.replace(/\/+$/, "")}/api/chat/messages`;
19
+ api.registerTool({
20
+ name: "helmio_chat_send",
21
+ description: "Send a message to the Company's Team conversation. The message appears to all participants (the Company owner and every active agent). Use this to reply to user messages.",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {
25
+ body: {
26
+ type: "string",
27
+ description: "Message text. Plain or markdown.",
28
+ },
29
+ },
30
+ required: ["body"],
31
+ additionalProperties: false,
32
+ },
33
+ async execute(args, ctx) {
34
+ const helmioAgentId = parseHelmioAgentId(ctx.agentId);
35
+ if (helmioAgentId === null) {
36
+ throw new Error(`helmio_chat_send: cannot derive Helmio agent id from openclaw agentId=${ctx.agentId}`);
37
+ }
38
+ if (!args.body || args.body.trim().length === 0) {
39
+ throw new Error("helmio_chat_send: body must be non-empty");
40
+ }
41
+ const payload = {
42
+ clientMsgId: newId(),
43
+ agentId: helmioAgentId,
44
+ conversationKindCode: ConversationKind.TeamDefault,
45
+ body: args.body,
46
+ };
47
+ return await postWithRetry(fetchFn, endpoint, opts.bridgeToken, payload, opts.logger);
48
+ },
49
+ });
50
+ }
51
+ async function postWithRetry(fetchFn, endpoint, bridgeToken, payload, logger) {
52
+ let lastErr;
53
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
54
+ try {
55
+ const res = await fetchFn(endpoint, {
56
+ method: "POST",
57
+ headers: {
58
+ Authorization: `Bearer ${bridgeToken}`,
59
+ "Content-Type": "application/json",
60
+ },
61
+ body: JSON.stringify(payload),
62
+ });
63
+ if (res.status === 201) {
64
+ const body = (await res.json());
65
+ return { ok: true, messageId: body.messageId };
66
+ }
67
+ if (res.status === 200) {
68
+ // Idempotent replay path — same (conversationId, clientMsgId) already
69
+ // exists server-side. ChatMessagesController returns the prior id.
70
+ const body = (await res.json());
71
+ return { ok: true, messageId: body.messageId, deduplicated: true };
72
+ }
73
+ if (res.status >= 400 && res.status < 500) {
74
+ const text = await res.text();
75
+ throw new Error(`helmio_chat_send: ${res.status} ${text}`);
76
+ }
77
+ // 5xx — retry
78
+ const text = await res.text();
79
+ lastErr = new Error(`helmio_chat_send: ${res.status} ${text}`);
80
+ logger.warn(`helmio_chat_send: attempt ${attempt}/${MAX_RETRIES} failed (${res.status}); will retry`);
81
+ }
82
+ catch (err) {
83
+ if (err.message?.startsWith("helmio_chat_send: 4"))
84
+ throw err;
85
+ lastErr = err;
86
+ logger.warn(`helmio_chat_send: attempt ${attempt}/${MAX_RETRIES} threw: ${err.message}`);
87
+ }
88
+ if (attempt < MAX_RETRIES) {
89
+ await sleep(BASE_BACKOFF_MS * 2 ** (attempt - 1));
90
+ }
91
+ }
92
+ throw lastErr instanceof Error
93
+ ? lastErr
94
+ : new Error("helmio_chat_send: exhausted retries");
95
+ }
96
+ function sleep(ms) {
97
+ return new Promise((r) => setTimeout(r, ms));
98
+ }
99
+ //# sourceMappingURL=chat-send-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-send-tool.js","sourceRoot":"","sources":["../src/chat-send-tool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export interface ChatMessageDto {
2
+ id: number;
3
+ conversationId: number;
4
+ participantTypeId: number;
5
+ senderId: number;
6
+ body: string;
7
+ createdAt: string;
8
+ }
9
+ export declare const ParticipantType: {
10
+ readonly User: 1;
11
+ readonly Agent: 2;
12
+ };
13
+ export declare const ConversationKind: {
14
+ readonly TeamDefault: 1;
15
+ readonly Group: 2;
16
+ readonly DirectMessage: 3;
17
+ };
package/dist/events.js ADDED
@@ -0,0 +1,16 @@
1
+ // Wire-format DTO emitted by SignalRChatBroadcaster (backend) on every
2
+ // `messageCreated` event. Mirrors Helmio.WebAPI/Hubs/IChatBroadcaster.cs
3
+ // `ChatMessageDto` exactly — renaming a field on one side without the other
4
+ // breaks the bridge silently.
5
+ // Matches Helmio.Business.Enums.ParticipantTypeCodes.
6
+ export const ParticipantType = {
7
+ User: 1,
8
+ Agent: 2,
9
+ };
10
+ // Matches Helmio.Business.Enums.ConversationKindCodes.
11
+ export const ConversationKind = {
12
+ TeamDefault: 1,
13
+ Group: 2,
14
+ DirectMessage: 3,
15
+ };
16
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,yEAAyE;AACzE,4EAA4E;AAC5E,8BAA8B;AAW9B,sDAAsD;AACtD,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACA,CAAC;AAEX,uDAAuD;AACvD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,WAAW,EAAE,CAAC;IACd,KAAK,EAAE,CAAC;IACR,aAAa,EAAE,CAAC;CACR,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { type ChatMessageDto } from "./events.js";
2
+ import type { ResolvedAgent } from "./agent-resolver.js";
3
+ import type { PluginLogger, PluginRuntime } from "openclaw/plugin-sdk/plugin-entry";
4
+ export interface InboundRouterDeps {
5
+ runtime: PluginRuntime;
6
+ logger: PluginLogger;
7
+ ceo: ResolvedAgent | undefined;
8
+ }
9
+ export declare function routeInbound(msg: ChatMessageDto, deps: InboundRouterDeps): Promise<"dispatched" | "ignored-agent" | "ignored-no-ceo">;
@@ -0,0 +1,29 @@
1
+ // Routes inbound chat messages to agents. Phase 1: CEO-only — every
2
+ // user-typed message in the Team conversation dispatches a turn to the
3
+ // CEO agent. Agent-typed messages (participantTypeId === 2) are dropped
4
+ // to prevent self-loops (the CEO's own outbound reply is broadcast back
5
+ // to the plugin's connection).
6
+ //
7
+ // Phase 2 will replace this with @mention parsing + AgentRouteBinding[]
8
+ // fan-out per ADR-0008 probe C3 findings.
9
+ import { ParticipantType } from "./events.js";
10
+ export async function routeInbound(msg, deps) {
11
+ if (msg.participantTypeId !== ParticipantType.User) {
12
+ return "ignored-agent";
13
+ }
14
+ if (!deps.ceo) {
15
+ deps.logger.warn(`helmio-chat: dropped messageId=${msg.id} — no CEO agent found in runtime config`);
16
+ return "ignored-no-ceo";
17
+ }
18
+ await deps.runtime.channel.turn.run({
19
+ agentId: deps.ceo.openclawAgentId,
20
+ conversationId: String(msg.conversationId),
21
+ input: {
22
+ body: msg.body,
23
+ senderId: String(msg.senderId),
24
+ senderType: "user",
25
+ },
26
+ });
27
+ return "dispatched";
28
+ }
29
+ //# sourceMappingURL=inbound-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound-router.js","sourceRoot":"","sources":["../src/inbound-router.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,uEAAuE;AACvE,wEAAwE;AACxE,wEAAwE;AACxE,+BAA+B;AAC/B,EAAE;AACF,wEAAwE;AACxE,0CAA0C;AAE1C,OAAO,EAAuB,eAAe,EAAE,MAAM,aAAa,CAAC;AAUnE,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAmB,EACnB,IAAuB;IAEvB,IAAI,GAAG,CAAC,iBAAiB,KAAK,eAAe,CAAC,IAAI,EAAE,CAAC;QACnD,OAAO,eAAe,CAAC;IACzB,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,kCAAkC,GAAG,CAAC,EAAE,yCAAyC,CAClF,CAAC;QACF,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAClC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,eAAe;QACjC,cAAc,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC;QAC1C,KAAK,EAAE;YACL,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC9B,UAAU,EAAE,MAAM;SACnB;KACF,CAAC,CAAC;IACH,OAAO,YAAY,CAAC;AACtB,CAAC"}
@@ -0,0 +1,2 @@
1
+ declare const _default: import("openclaw/plugin-sdk/plugin-entry").PluginEntryDefinition;
2
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ // @helmio/openclaw-helmio-chat — ADR-0008 phase 1.
2
+ //
3
+ // Wires three pieces:
4
+ // 1. SignalR connection to ${HELMIO_API_URL}/api/chat-hub (bridge token in
5
+ // env). Hub auto-joins us to the Company's Team conversation.
6
+ // 2. Outbound chat-send tool registered globally; agents call it to reply,
7
+ // tool POSTs to /api/chat/messages.
8
+ // 3. Inbound router: user-typed messages dispatch a turn to the CEO agent.
9
+ // (Phase 2 will add @mention parsing + multi-agent fan-out.)
10
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
11
+ import { resolveAgents } from "./agent-resolver.js";
12
+ import { startChatConnection } from "./chat-connection.js";
13
+ import { registerChatSendTool } from "./chat-send-tool.js";
14
+ import { routeInbound } from "./inbound-router.js";
15
+ let connection;
16
+ export default definePluginEntry({
17
+ id: "helmio-chat",
18
+ displayName: "Helmio Chat",
19
+ version: "0.1.0",
20
+ register(api) {
21
+ const helmioApiUrl = process.env.HELMIO_API_URL;
22
+ const bridgeToken = process.env.HELMIO_CHAT_BRIDGE_TOKEN;
23
+ if (!helmioApiUrl || !bridgeToken) {
24
+ api.logger.error("helmio-chat: HELMIO_API_URL or HELMIO_CHAT_BRIDGE_TOKEN not set; plugin loaded but disabled.");
25
+ return;
26
+ }
27
+ // Plugin is installed at provision time BEFORE agents.create runs, so
28
+ // agents.list is empty at register(). Re-resolve on every inbound
29
+ // message instead of caching once.
30
+ const initial = resolveAgents(api.runtime.agents.list());
31
+ api.logger.info(`helmio-chat: register-time agents=${initial.agents.length} ceo=${initial.ceo?.openclawAgentId ?? "<none>"}`);
32
+ registerChatSendTool(api, {
33
+ helmioApiUrl,
34
+ bridgeToken,
35
+ logger: api.logger,
36
+ });
37
+ const hubUrl = `${helmioApiUrl.replace(/\/+$/, "")}/api/chat-hub`;
38
+ connection = startChatConnection({
39
+ hubUrl,
40
+ bridgeToken,
41
+ logger: api.logger,
42
+ onMessage: async (msg) => {
43
+ const { ceo } = resolveAgents(api.runtime.agents.list());
44
+ const verdict = await routeInbound(msg, {
45
+ runtime: api.runtime,
46
+ logger: api.logger,
47
+ ceo,
48
+ });
49
+ api.logger.info(`helmio-chat: messageId=${msg.id} participantType=${msg.participantTypeId} verdict=${verdict}`);
50
+ },
51
+ });
52
+ },
53
+ async dispose() {
54
+ await connection?.stop();
55
+ connection = undefined;
56
+ },
57
+ });
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,EAAE;AACF,sBAAsB;AACtB,6EAA6E;AAC7E,mEAAmE;AACnE,6EAA6E;AAC7E,yCAAyC;AACzC,6EAA6E;AAC7E,kEAAkE;AAElE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,IAAI,UAAsC,CAAC;AAE3C,eAAe,iBAAiB,CAAC;IAC/B,EAAE,EAAE,aAAa;IACjB,WAAW,EAAE,aAAa;IAC1B,OAAO,EAAE,OAAO;IAChB,QAAQ,CAAC,GAAG;QACV,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;QAChD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC;QACzD,IAAI,CAAC,YAAY,IAAI,CAAC,WAAW,EAAE,CAAC;YAClC,GAAG,CAAC,MAAM,CAAC,KAAK,CACd,8FAA8F,CAC/F,CAAC;YACF,OAAO;QACT,CAAC;QAED,sEAAsE;QACtE,kEAAkE;QAClE,mCAAmC;QACnC,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACzD,GAAG,CAAC,MAAM,CAAC,IAAI,CACb,qCAAqC,OAAO,CAAC,MAAM,CAAC,MAAM,QAAQ,OAAO,CAAC,GAAG,EAAE,eAAe,IAAI,QAAQ,EAAE,CAC7G,CAAC;QAEF,oBAAoB,CAAC,GAAG,EAAE;YACxB,YAAY;YACZ,WAAW;YACX,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,eAAe,CAAC;QAClE,UAAU,GAAG,mBAAmB,CAAC;YAC/B,MAAM;YACN,WAAW;YACX,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;gBACvB,MAAM,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;gBACzD,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE;oBACtC,OAAO,EAAE,GAAG,CAAC,OAAO;oBACpB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,GAAG;iBACJ,CAAC,CAAC;gBACH,GAAG,CAAC,MAAM,CAAC,IAAI,CACb,0BAA0B,GAAG,CAAC,EAAE,oBAAoB,GAAG,CAAC,iBAAiB,YAAY,OAAO,EAAE,CAC/F,CAAC;YACJ,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IACD,KAAK,CAAC,OAAO;QACX,MAAM,UAAU,EAAE,IAAI,EAAE,CAAC;QACzB,UAAU,GAAG,SAAS,CAAC;IACzB,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ {
2
+ "id": "helmio-chat",
3
+ "name": "Helmio Chat",
4
+ "description": "Bridges Helmio's SignalR ChatHub to per-agent turns inside the Company VM. Inbound user messages dispatch to agents via the runtime turn API; agent replies emit through the `helmio_chat_send` tool which POSTs to /api/chat/messages. ADR-0008.",
5
+ "activation": {
6
+ "onStartup": true
7
+ },
8
+ "configSchema": {
9
+ "type": "object",
10
+ "additionalProperties": false,
11
+ "properties": {}
12
+ }
13
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@helmio/openclaw-helmio-chat",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "OpenClaw plugin that bridges Helmio's SignalR ChatHub to per-agent turns inside the Company VM. Installed at cloud-init via `openclaw plugins install npm:@helmio/openclaw-helmio-chat@<pinned>`. ADR-0008.",
6
+ "type": "module",
7
+ "license": "UNLICENSED",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "openclaw.plugin.json"
12
+ ],
13
+ "main": "./dist/index.js",
14
+ "exports": {
15
+ ".": "./dist/index.js"
16
+ },
17
+ "openclaw": {
18
+ "extensions": [
19
+ "./dist/index.js"
20
+ ],
21
+ "compat": {
22
+ "minGatewayVersion": "2026.2.26"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "test": "node --import tsx --test src/__tests__/*.test.ts",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "dependencies": {
31
+ "@microsoft/signalr": "^8.0.7",
32
+ "ulid": "^2.3.0"
33
+ },
34
+ "devDependencies": {
35
+ "typescript": "^5.6.3",
36
+ "tsx": "^4.19.2",
37
+ "@types/node": "^20.12.7"
38
+ },
39
+ "engines": {
40
+ "node": ">=20.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }