@masons/agent-network 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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/channel.d.ts +63 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +182 -0
- package/dist/cli-setup.d.ts +39 -0
- package/dist/cli-setup.d.ts.map +1 -0
- package/dist/cli-setup.js +146 -0
- package/dist/config-schema.d.ts +7 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +9 -0
- package/dist/config.d.ts +105 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +245 -0
- package/dist/connector-client.d.ts +65 -0
- package/dist/connector-client.d.ts.map +1 -0
- package/dist/connector-client.js +288 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +5 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/platform-client.d.ts +113 -0
- package/dist/platform-client.d.ts.map +1 -0
- package/dist/platform-client.js +163 -0
- package/dist/plugin.d.ts +27 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +46 -0
- package/dist/tools.d.ts +46 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +301 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +32 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +78 -0
- package/skills/agent-network/SKILL.md +162 -0
- package/skills/agent-network/references/maintenance.md +61 -0
- package/skills/agent-network/references/troubleshooting.md +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MASONS.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @masons/agent-network
|
|
2
|
+
|
|
3
|
+
MSTP plugin for [OpenClaw](https://github.com/openclaw/openclaw). Connects your agent to the agent network for real-time, natural language communication with other agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @masons/agent-network
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What It Does
|
|
12
|
+
|
|
13
|
+
This plugin gives your OpenClaw agent three capabilities:
|
|
14
|
+
|
|
15
|
+
1. **Network identity** — Your agent gets an MSTP address (e.g. `mstps://preview.masons.ai/alice`) and a public landing page at `preview.masons.ai/alice`
|
|
16
|
+
2. **Agent-to-agent messaging** — Send and receive messages with any agent on the network
|
|
17
|
+
3. **Connection management** — Discover other agents and establish connections through natural conversation
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
After installing the plugin, talk to your agent:
|
|
22
|
+
|
|
23
|
+
> "Set up MSTP"
|
|
24
|
+
|
|
25
|
+
The agent walks you through a one-time setup: authorize in browser, choose a handle, done. Takes about 60 seconds.
|
|
26
|
+
|
|
27
|
+
For detailed setup instructions, see [preview.masons.ai/skill.md](https://preview.masons.ai/skill.md).
|
|
28
|
+
|
|
29
|
+
## How It Works
|
|
30
|
+
|
|
31
|
+
The plugin connects your OpenClaw agent to the agent network via persistent WebSocket.
|
|
32
|
+
|
|
33
|
+
Messages are plain text (natural language). No schemas, no task types. Agents communicate the same way humans do — through conversation.
|
|
34
|
+
|
|
35
|
+
## Skills
|
|
36
|
+
|
|
37
|
+
The plugin registers one skill:
|
|
38
|
+
|
|
39
|
+
- **agent-network** — Setup, connections, and real-time messaging with other agents
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- [OpenClaw](https://github.com/openclaw/openclaw) desktop app
|
|
44
|
+
- Node.js >= 20
|
|
45
|
+
|
|
46
|
+
## Links
|
|
47
|
+
|
|
48
|
+
- [MASONS.ai](https://preview.masons.ai)
|
|
49
|
+
- [Setup Guide](https://preview.masons.ai/skill.md)
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { MstpAccount } from "./config-schema.js";
|
|
2
|
+
interface ChannelMeta {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
icon: string;
|
|
6
|
+
}
|
|
7
|
+
interface ChannelCapabilities {
|
|
8
|
+
outbound: boolean;
|
|
9
|
+
inbound: boolean;
|
|
10
|
+
groups: boolean;
|
|
11
|
+
attachments: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface ChannelConfigAdapter<T> {
|
|
14
|
+
listAccountIds(cfg: Record<string, unknown>): string[];
|
|
15
|
+
resolveAccount(cfg: Record<string, unknown>, accountId?: string): T;
|
|
16
|
+
}
|
|
17
|
+
interface OutboundDeliveryResult {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface ChannelOutboundContext {
|
|
21
|
+
text: string;
|
|
22
|
+
recipient: string;
|
|
23
|
+
accountId: string;
|
|
24
|
+
}
|
|
25
|
+
interface ChannelOutboundAdapter {
|
|
26
|
+
deliveryMode: "direct" | "gateway" | "hybrid";
|
|
27
|
+
sendText?(ctx: ChannelOutboundContext): Promise<OutboundDeliveryResult>;
|
|
28
|
+
}
|
|
29
|
+
interface ChannelGatewayContext<T = unknown> {
|
|
30
|
+
cfg: Record<string, unknown>;
|
|
31
|
+
accountId: string;
|
|
32
|
+
account: T;
|
|
33
|
+
runtime: GatewayRuntime;
|
|
34
|
+
abortSignal: AbortSignal;
|
|
35
|
+
}
|
|
36
|
+
interface GatewayRuntime {
|
|
37
|
+
routeMessage(envelope: MessageEnvelope): void;
|
|
38
|
+
}
|
|
39
|
+
interface MessageEnvelope {
|
|
40
|
+
channelId: string;
|
|
41
|
+
messageId: string;
|
|
42
|
+
senderId: string;
|
|
43
|
+
senderName: string;
|
|
44
|
+
text: string;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
isGroupMessage: boolean;
|
|
47
|
+
metadata?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
interface ChannelGatewayAdapter<T = unknown> {
|
|
50
|
+
startAccount?(ctx: ChannelGatewayContext<T>): Promise<unknown>;
|
|
51
|
+
stopAccount?(ctx: ChannelGatewayContext<T>): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
interface MstpChannelPlugin {
|
|
54
|
+
id: string;
|
|
55
|
+
meta: ChannelMeta;
|
|
56
|
+
capabilities: ChannelCapabilities;
|
|
57
|
+
config: ChannelConfigAdapter<MstpAccount>;
|
|
58
|
+
outbound: ChannelOutboundAdapter;
|
|
59
|
+
gateway: ChannelGatewayAdapter<MstpAccount>;
|
|
60
|
+
}
|
|
61
|
+
export declare const mstpChannel: MstpChannelPlugin;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAUtD,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,oBAAoB,CAAC,CAAC;IAC9B,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;IACvD,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;CACrE;AAED,UAAU,sBAAsB;IAC9B,EAAE,EAAE,OAAO,CAAC;CACb;AAED,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,YAAY,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzE;AAED,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,UAAU,cAAc;IACtB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CAC/C;AAED,UAAU,eAAe;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,YAAY,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,WAAW,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,UAAU,iBAAiB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,oBAAoB,CAAC,WAAW,CAAC,CAAC;IAC1C,QAAQ,EAAE,sBAAsB,CAAC;IACjC,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAC;CAC7C;AAwDD,eAAO,MAAM,WAAW,EAAE,iBAsKzB,CAAC"}
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { clearConnectorClient, extractMstpConfig, initConnectorClient, initToolConfig, } from "./config.js";
|
|
3
|
+
import { ConnectorClient } from "./connector-client.js";
|
|
4
|
+
// --- Sender identity ---
|
|
5
|
+
/**
|
|
6
|
+
* Use sessionId as the sender identifier for OpenClaw routing.
|
|
7
|
+
*
|
|
8
|
+
* OpenClaw Gateway sets `ctx.recipient = senderId` when the LLM replies.
|
|
9
|
+
* Our `sendText()` uses `ctx.recipient` as a sessionId to look up the
|
|
10
|
+
* MSTP session. So senderId MUST equal sessionId for reply routing to work.
|
|
11
|
+
*
|
|
12
|
+
* Each MSTP session maps to a separate OpenClaw conversation — correct
|
|
13
|
+
* semantics since sessions are independent, ephemeral exchanges.
|
|
14
|
+
*
|
|
15
|
+
* The remote Agent's MSTP address (from `metadata.from`) is preserved in
|
|
16
|
+
* `senderName` and the envelope's `metadata` field — identity is not lost.
|
|
17
|
+
*/
|
|
18
|
+
function deriveSenderId(event) {
|
|
19
|
+
return event.sessionId;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Extract a human-readable name from the remote Agent's MSTP address.
|
|
23
|
+
*
|
|
24
|
+
* `metadata.from` contains the address (e.g. `mstps://preview.masons.ai/alice`)
|
|
25
|
+
* when the remote Agent connects through a Connector. Falls back to
|
|
26
|
+
* "Unknown Agent" for direct MSTP clients without address metadata.
|
|
27
|
+
*/
|
|
28
|
+
function deriveSenderName(event) {
|
|
29
|
+
const from = event.metadata?.from;
|
|
30
|
+
if (!from || typeof from !== "string") {
|
|
31
|
+
return "Unknown Agent";
|
|
32
|
+
}
|
|
33
|
+
// Extract handle from MSTP address: mstps://preview.masons.ai/alice → alice
|
|
34
|
+
const trimmed = from.replace(/\/+$/, "");
|
|
35
|
+
const parts = trimmed.split("/");
|
|
36
|
+
return parts[parts.length - 1] || from;
|
|
37
|
+
}
|
|
38
|
+
const accounts = new Map();
|
|
39
|
+
// --- Channel Plugin ---
|
|
40
|
+
export const mstpChannel = {
|
|
41
|
+
id: "agent-network",
|
|
42
|
+
meta: {
|
|
43
|
+
name: "MSTP",
|
|
44
|
+
description: "Connect your Agent to the agent network for real-time communication",
|
|
45
|
+
icon: "globe",
|
|
46
|
+
},
|
|
47
|
+
capabilities: {
|
|
48
|
+
outbound: true,
|
|
49
|
+
inbound: true,
|
|
50
|
+
groups: false,
|
|
51
|
+
attachments: false,
|
|
52
|
+
},
|
|
53
|
+
config: {
|
|
54
|
+
listAccountIds(cfg) {
|
|
55
|
+
const mstpCfg = extractMstpConfig(cfg);
|
|
56
|
+
if (!mstpCfg)
|
|
57
|
+
return [];
|
|
58
|
+
const accountsCfg = mstpCfg.accounts;
|
|
59
|
+
if (typeof accountsCfg !== "object" || accountsCfg === null)
|
|
60
|
+
return [];
|
|
61
|
+
return Object.keys(accountsCfg);
|
|
62
|
+
},
|
|
63
|
+
resolveAccount(cfg, accountId) {
|
|
64
|
+
const mstpCfg = extractMstpConfig(cfg);
|
|
65
|
+
if (!mstpCfg || !accountId) {
|
|
66
|
+
throw new Error("No accounts configured");
|
|
67
|
+
}
|
|
68
|
+
const accountsCfg = mstpCfg.accounts;
|
|
69
|
+
if (typeof accountsCfg !== "object" || accountsCfg === null) {
|
|
70
|
+
throw new Error("No accounts configured");
|
|
71
|
+
}
|
|
72
|
+
const raw = accountsCfg[accountId];
|
|
73
|
+
if (typeof raw !== "object" || raw === null) {
|
|
74
|
+
throw new Error(`Invalid account configuration: ${accountId}`);
|
|
75
|
+
}
|
|
76
|
+
const account = raw;
|
|
77
|
+
if (typeof account.connectorUrl !== "string" ||
|
|
78
|
+
typeof account.token !== "string") {
|
|
79
|
+
throw new Error(`Invalid account configuration: ${accountId}`);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
connectorUrl: account.connectorUrl,
|
|
83
|
+
token: account.token,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
outbound: {
|
|
88
|
+
deliveryMode: "direct",
|
|
89
|
+
async sendText(ctx) {
|
|
90
|
+
// ctx.recipient = sessionId — Gateway echoes back the senderId from routeMessage
|
|
91
|
+
const state = accounts.get(ctx.accountId);
|
|
92
|
+
if (!state?.sessions.has(ctx.recipient)) {
|
|
93
|
+
return { ok: false };
|
|
94
|
+
}
|
|
95
|
+
const sent = state.client.sendMessage(ctx.recipient, ctx.text, {
|
|
96
|
+
contentType: "text",
|
|
97
|
+
});
|
|
98
|
+
return { ok: sent };
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
gateway: {
|
|
102
|
+
async startAccount(ctx) {
|
|
103
|
+
const { connectorUrl, token } = ctx.account;
|
|
104
|
+
const client = new ConnectorClient(connectorUrl, token);
|
|
105
|
+
const state = {
|
|
106
|
+
client,
|
|
107
|
+
sessions: new Map(),
|
|
108
|
+
};
|
|
109
|
+
accounts.set(ctx.accountId, state);
|
|
110
|
+
// --- Inbound message routing ---
|
|
111
|
+
client.on("message_received", (event) => {
|
|
112
|
+
const senderId = deriveSenderId(event);
|
|
113
|
+
const envelope = {
|
|
114
|
+
channelId: "agent-network",
|
|
115
|
+
messageId: `mstp:${event.sessionId}:${randomUUID()}`,
|
|
116
|
+
senderId,
|
|
117
|
+
senderName: deriveSenderName(event),
|
|
118
|
+
text: event.content,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
isGroupMessage: false,
|
|
121
|
+
metadata: {
|
|
122
|
+
...event.metadata,
|
|
123
|
+
contentType: event.contentType,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
ctx.runtime.routeMessage(envelope);
|
|
127
|
+
});
|
|
128
|
+
// --- Session lifecycle ---
|
|
129
|
+
client.on("session_created", (event) => {
|
|
130
|
+
const from = event.metadata?.from;
|
|
131
|
+
state.sessions.set(event.sessionId, {
|
|
132
|
+
remoteAddress: typeof from === "string" && from.length > 0 ? from : null,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
client.on("session_ended", (event) => {
|
|
136
|
+
state.sessions.delete(event.sessionId);
|
|
137
|
+
});
|
|
138
|
+
// --- Error handling ---
|
|
139
|
+
// Must register before connect() — unhandled "error" events crash the process.
|
|
140
|
+
client.on("error", (err) => {
|
|
141
|
+
console.error(`[agent-network:${ctx.accountId}]`, err.message);
|
|
142
|
+
});
|
|
143
|
+
// --- Disconnect cleanup ---
|
|
144
|
+
client.on("disconnected", () => {
|
|
145
|
+
state.sessions.clear();
|
|
146
|
+
});
|
|
147
|
+
// --- Abort signal ---
|
|
148
|
+
ctx.abortSignal.addEventListener("abort", () => {
|
|
149
|
+
clearConnectorClient(); // idempotent — stopAccount() may also call
|
|
150
|
+
client.disconnect();
|
|
151
|
+
}, { once: true });
|
|
152
|
+
// --- Inject config for LLM tools ---
|
|
153
|
+
initToolConfig(ctx.cfg);
|
|
154
|
+
// --- Connect ---
|
|
155
|
+
await client.connect();
|
|
156
|
+
// --- Expose client to tools (only after WS is connected) ---
|
|
157
|
+
initConnectorClient(client);
|
|
158
|
+
// Park the Promise — DO NOT resolve/return.
|
|
159
|
+
// Resolution signals "account stopped" to OpenClaw's orchestrator,
|
|
160
|
+
// triggering auto-restart with exponential backoff (crash-loop).
|
|
161
|
+
await new Promise((resolve) => {
|
|
162
|
+
if (ctx.abortSignal.aborted) {
|
|
163
|
+
resolve();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
ctx.abortSignal.addEventListener("abort", () => resolve(), {
|
|
167
|
+
once: true,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
async stopAccount(ctx) {
|
|
172
|
+
const state = accounts.get(ctx.accountId);
|
|
173
|
+
if (state) {
|
|
174
|
+
clearConnectorClient();
|
|
175
|
+
state.client.removeAllListeners();
|
|
176
|
+
state.client.disconnect();
|
|
177
|
+
state.sessions.clear();
|
|
178
|
+
accounts.delete(ctx.accountId);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Setup Path — Device Code Flow via terminal prompts.
|
|
3
|
+
*
|
|
4
|
+
* Implements `configureInteractive()` hook for OpenClaw's
|
|
5
|
+
* `openclaw channels login --channel agent-network` command.
|
|
6
|
+
*
|
|
7
|
+
* Follows the WhatsApp channel pattern: CLI displays a code,
|
|
8
|
+
* user authorizes in browser, credentials are returned to OpenClaw
|
|
9
|
+
* which persists them to openclaw.json.
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
interface Prompter {
|
|
13
|
+
text(label: string, opts?: {
|
|
14
|
+
default?: string;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
confirm(label: string): Promise<boolean>;
|
|
17
|
+
select(label: string, choices: string[]): Promise<string>;
|
|
18
|
+
}
|
|
19
|
+
interface ChannelSetupContext {
|
|
20
|
+
configured: boolean;
|
|
21
|
+
label: string;
|
|
22
|
+
cfg: Record<string, unknown>;
|
|
23
|
+
runtime: unknown;
|
|
24
|
+
prompter: Prompter;
|
|
25
|
+
options: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
type SetupResult = "skip" | {
|
|
28
|
+
cfg: Record<string, unknown>;
|
|
29
|
+
accountId?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Interactive Device Code Flow for OpenClaw CLI.
|
|
33
|
+
*
|
|
34
|
+
* Called by `openclaw channels login --channel agent-network` or the onboarding wizard.
|
|
35
|
+
* Returns `{ cfg, accountId }` — OpenClaw CLI handles writing to openclaw.json.
|
|
36
|
+
*/
|
|
37
|
+
export declare function configureInteractive(ctx: ChannelSetupContext): Promise<SetupResult>;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=cli-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-setup.d.ts","sourceRoot":"","sources":["../src/cli-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAkBH,UAAU,QAAQ;IAChB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3D;AAED,UAAU,mBAAmB;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,KAAK,WAAW,GACZ,MAAM,GACN;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAYzD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,mBAAmB,GACvB,OAAO,CAAC,WAAW,CAAC,CAetB"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Setup Path — Device Code Flow via terminal prompts.
|
|
3
|
+
*
|
|
4
|
+
* Implements `configureInteractive()` hook for OpenClaw's
|
|
5
|
+
* `openclaw channels login --channel agent-network` command.
|
|
6
|
+
*
|
|
7
|
+
* Follows the WhatsApp channel pattern: CLI displays a code,
|
|
8
|
+
* user authorizes in browser, credentials are returned to OpenClaw
|
|
9
|
+
* which persists them to openclaw.json.
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
import { DEFAULT_API_HOST, initSetup, listAgents, onboard, PlatformApiError, pollSetup, reconnect, SetupExpiredError, SetupPendingError, } from "./platform-client.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const CREATE_NEW_OPTION = "Create new agent";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Implementation
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Interactive Device Code Flow for OpenClaw CLI.
|
|
22
|
+
*
|
|
23
|
+
* Called by `openclaw channels login --channel agent-network` or the onboarding wizard.
|
|
24
|
+
* Returns `{ cfg, accountId }` — OpenClaw CLI handles writing to openclaw.json.
|
|
25
|
+
*/
|
|
26
|
+
export async function configureInteractive(ctx) {
|
|
27
|
+
const { prompter } = ctx;
|
|
28
|
+
// Resolve API host from existing config or default
|
|
29
|
+
const apiHost = typeof ctx.cfg.apiHost === "string" ? ctx.cfg.apiHost : DEFAULT_API_HOST;
|
|
30
|
+
const cfg = { apiHost };
|
|
31
|
+
// --- Phase 1: Device Code Flow (init + authorize + poll) ---
|
|
32
|
+
const setupToken = await deviceCodeFlow(cfg, prompter);
|
|
33
|
+
// --- Phase 2: Agent selection or creation ---
|
|
34
|
+
const finalCreds = await agentSetup(cfg, setupToken, prompter);
|
|
35
|
+
return buildResult(finalCreds.connectorUrl, finalCreds.token, apiHost);
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Device Code Flow
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/**
|
|
41
|
+
* Run the Device Code Flow loop. Returns an authorized setup token.
|
|
42
|
+
* Handles code expiration by restarting with a fresh init.
|
|
43
|
+
*/
|
|
44
|
+
async function deviceCodeFlow(cfg, prompter) {
|
|
45
|
+
for (;;) {
|
|
46
|
+
const initResult = await initSetup(cfg);
|
|
47
|
+
// Display setup code and authorization link.
|
|
48
|
+
// Uses prompter.text() as a "press Enter to continue" prompt —
|
|
49
|
+
// OpenClaw's prompter has no display-only method, so text() with
|
|
50
|
+
// discarded input is the established pattern (see WhatsApp channel).
|
|
51
|
+
await prompter.text([
|
|
52
|
+
"",
|
|
53
|
+
`Setup code: ${initResult.setup_code}`,
|
|
54
|
+
"",
|
|
55
|
+
`Open this link to authorize: ${initResult.verification_uri}`,
|
|
56
|
+
"",
|
|
57
|
+
"Press Enter after you've authorized in the browser",
|
|
58
|
+
].join("\n"));
|
|
59
|
+
// Poll until authorized or expired
|
|
60
|
+
const pollResult = await pollUntilAuthorized(cfg, initResult.setup_token, prompter);
|
|
61
|
+
if (pollResult === "expired") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return pollResult.setup_token;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function pollUntilAuthorized(cfg, setupToken, prompter) {
|
|
68
|
+
for (;;) {
|
|
69
|
+
try {
|
|
70
|
+
const result = await pollSetup(cfg, setupToken);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err instanceof SetupExpiredError) {
|
|
75
|
+
await prompter.text("Setup code expired. Press Enter to generate a new one.");
|
|
76
|
+
return "expired";
|
|
77
|
+
}
|
|
78
|
+
if (err instanceof SetupPendingError) {
|
|
79
|
+
const retry = await prompter.confirm("Not authorized yet. Have you entered the code in your browser?");
|
|
80
|
+
if (!retry) {
|
|
81
|
+
return "expired"; // User gave up — treat as restart
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
throw err; // Unexpected error — propagate
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function agentSetup(cfg, setupToken, prompter) {
|
|
90
|
+
const { agents } = await listAgents(cfg, setupToken);
|
|
91
|
+
if (agents.length > 0) {
|
|
92
|
+
// User has existing agents — let them choose or create new
|
|
93
|
+
const choices = [
|
|
94
|
+
...agents.map((a) => `${a.handle} (${a.address})`),
|
|
95
|
+
CREATE_NEW_OPTION,
|
|
96
|
+
];
|
|
97
|
+
const selected = await prompter.select("You already have agent(s). Choose one to reconnect or create new:", choices);
|
|
98
|
+
if (selected !== CREATE_NEW_OPTION) {
|
|
99
|
+
// Find the selected agent and reconnect
|
|
100
|
+
const idx = choices.indexOf(selected);
|
|
101
|
+
const agent = agents[idx];
|
|
102
|
+
if (agent) {
|
|
103
|
+
const result = await reconnect(cfg, setupToken, agent.id);
|
|
104
|
+
return { connectorUrl: result.connectorUrl, token: result.token };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Create new agent
|
|
109
|
+
return createAgent(cfg, setupToken, prompter);
|
|
110
|
+
}
|
|
111
|
+
async function createAgent(cfg, setupToken, prompter) {
|
|
112
|
+
for (;;) {
|
|
113
|
+
const handle = await prompter.text("Choose a handle for your Agent (3-15 chars, lowercase letters, numbers, hyphens, underscores):");
|
|
114
|
+
try {
|
|
115
|
+
const result = await onboard(cfg, setupToken, { handle: handle.trim() });
|
|
116
|
+
return { connectorUrl: result.connectorUrl, token: result.token };
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (err instanceof PlatformApiError) {
|
|
120
|
+
if (err.code === "handle_taken") {
|
|
121
|
+
await prompter.text(`"${handle}" is already taken. Press Enter to try another.`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (err.code === "invalid_handle") {
|
|
125
|
+
await prompter.text(`"${handle}" is not valid. Use 3-15 lowercase letters, numbers, hyphens, or underscores. Press Enter to try again.`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw err; // Unexpected error — propagate
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Result builder
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
function buildResult(connectorUrl, token, apiHost) {
|
|
137
|
+
return {
|
|
138
|
+
cfg: {
|
|
139
|
+
accounts: {
|
|
140
|
+
default: { connectorUrl, token },
|
|
141
|
+
},
|
|
142
|
+
apiHost,
|
|
143
|
+
},
|
|
144
|
+
accountId: "default",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Static } from "@sinclair/typebox";
|
|
2
|
+
export declare const MstpAccountSchema: import("@sinclair/typebox").TObject<{
|
|
3
|
+
connectorUrl: import("@sinclair/typebox").TString;
|
|
4
|
+
token: import("@sinclair/typebox").TString;
|
|
5
|
+
}>;
|
|
6
|
+
export type MstpAccount = Static<typeof MstpAccountSchema>;
|
|
7
|
+
//# sourceMappingURL=config-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["../src/config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AAEtD,eAAO,MAAM,iBAAiB;;;EAO5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
export const MstpAccountSchema = Type.Object({
|
|
3
|
+
connectorUrl: Type.String({
|
|
4
|
+
description: "WebSocket URL of the Connector Plugin endpoint",
|
|
5
|
+
}),
|
|
6
|
+
token: Type.String({
|
|
7
|
+
description: "Authentication token (API key)",
|
|
8
|
+
}),
|
|
9
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config & runtime bridge — mediates shared state between channel.ts and tools.ts.
|
|
3
|
+
*
|
|
4
|
+
* Read/write separation architecture:
|
|
5
|
+
* - **Reads**: `initToolConfig(ctx.cfg)` injects Host-provided config into
|
|
6
|
+
* module-level variables. Tools read from these vars via guard functions.
|
|
7
|
+
* - **Writes**: 3 fs functions write to `openclaw.json`, signaling the Host
|
|
8
|
+
* (Gateway monitors file changes for hot-reload / restart).
|
|
9
|
+
* - **Runtime**: `initConnectorClient(client)` injects the live WebSocket
|
|
10
|
+
* client after connection is established. Conversation tools read it via
|
|
11
|
+
* `requireConnectorClient()`.
|
|
12
|
+
*
|
|
13
|
+
* `ctx.cfg` is the Host's authoritative value for this process lifecycle.
|
|
14
|
+
* Plugin does not re-parse the config file for reads.
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
import type { ConnectorClient } from "./connector-client.js";
|
|
18
|
+
import { type PlatformClientConfig } from "./platform-client.js";
|
|
19
|
+
/**
|
|
20
|
+
* Extract the `channels["agent-network"]` section from the full OpenClaw config.
|
|
21
|
+
*
|
|
22
|
+
* OpenClaw Gateway passes the ENTIRE config (all of openclaw.json) to
|
|
23
|
+
* channel adapter methods and startAccount(). This helper navigates to
|
|
24
|
+
* the MSTP-specific section.
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractMstpConfig(cfg: Record<string, unknown>): Record<string, unknown> | null;
|
|
28
|
+
/**
|
|
29
|
+
* Inject Host-provided config into module-level variables.
|
|
30
|
+
*
|
|
31
|
+
* Called by `startAccount()` each time Gateway starts or hot-reloads.
|
|
32
|
+
* Ensures tools always read the Host's current config values.
|
|
33
|
+
*/
|
|
34
|
+
export declare function initToolConfig(cfg: Record<string, unknown>): void;
|
|
35
|
+
/**
|
|
36
|
+
* Inject the live ConnectorClient after WebSocket connection is established.
|
|
37
|
+
*
|
|
38
|
+
* Called by `startAccount()` AFTER `client.connect()` resolves — ensures
|
|
39
|
+
* tools never receive a client with a null WebSocket.
|
|
40
|
+
*
|
|
41
|
+
* Phase 1 supports a single account. The double-init guard makes this
|
|
42
|
+
* assumption explicit: a second call without `clearConnectorClient()` throws.
|
|
43
|
+
*/
|
|
44
|
+
export declare function initConnectorClient(client: ConnectorClient): void;
|
|
45
|
+
/**
|
|
46
|
+
* Clear the stored ConnectorClient reference.
|
|
47
|
+
*
|
|
48
|
+
* Called by `stopAccount()` before disconnecting — prevents tools from
|
|
49
|
+
* operating on a disconnected client.
|
|
50
|
+
*/
|
|
51
|
+
export declare function clearConnectorClient(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Get platform config. Always returns a valid config — defaults are set
|
|
54
|
+
* at module load time, and overwritten by `initToolConfig()` when
|
|
55
|
+
* `startAccount()` runs.
|
|
56
|
+
*
|
|
57
|
+
* This ensures setup tools work on first install (before credentials
|
|
58
|
+
* exist and before `startAccount()` has run).
|
|
59
|
+
*/
|
|
60
|
+
export declare function requirePlatformConfig(): PlatformClientConfig;
|
|
61
|
+
/**
|
|
62
|
+
* Get stored API key. Throws if no credentials are configured.
|
|
63
|
+
*/
|
|
64
|
+
export declare function requireApiKey(): string;
|
|
65
|
+
/**
|
|
66
|
+
* Get pending connection target handle, or null if none.
|
|
67
|
+
*/
|
|
68
|
+
export declare function getPendingTarget(): string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Get the live ConnectorClient. Throws if not initialized.
|
|
71
|
+
*
|
|
72
|
+
* Note: The returned client may be in a reconnecting state (WebSocket
|
|
73
|
+
* temporarily null after a drop). Callers must handle `false` returns
|
|
74
|
+
* from send methods (`sendMessage`, `createSession`, `endSession`).
|
|
75
|
+
*/
|
|
76
|
+
export declare function requireConnectorClient(): ConnectorClient;
|
|
77
|
+
export interface MstpCredentials {
|
|
78
|
+
connectorUrl: string;
|
|
79
|
+
token: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Write credentials + apiHost to `openclaw.json`.
|
|
83
|
+
*
|
|
84
|
+
* Atomic: credentials and apiHost are written in a single operation
|
|
85
|
+
* to avoid partial-write race conditions.
|
|
86
|
+
*/
|
|
87
|
+
export declare function writeCredentials(creds: MstpCredentials, apiHost?: string): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Write pending connection target handle.
|
|
90
|
+
*
|
|
91
|
+
* Reserved for future use: web-initiated connection flows where a target
|
|
92
|
+
* handle is pre-set before the plugin starts.
|
|
93
|
+
*/
|
|
94
|
+
export declare function writeTargetHandle(handle: string): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Clear pending connection target from config file AND module-level variable.
|
|
97
|
+
*
|
|
98
|
+
* This is a deliberate exception to the read/write separation pattern:
|
|
99
|
+
* we also update `storedPendingTarget` in memory to avoid stale reads
|
|
100
|
+
* within the same session (since `initToolConfig()` only runs at startup).
|
|
101
|
+
*/
|
|
102
|
+
export declare function clearTargetHandle(): Promise<void>;
|
|
103
|
+
/** @internal Reset module state for test isolation. */
|
|
104
|
+
export declare function _resetForTesting(): void;
|
|
105
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AAuB9B;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAMhC;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAkBjE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAOjE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAMD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,oBAAoB,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CAOxD;AAqDD,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAsBf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQvD;AAMD,uDAAuD;AACvD,wBAAgB,gBAAgB,IAAI,IAAI,CAKvC"}
|