@spikelabs/lobster-shell-plugin 0.2.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/README.md +80 -0
- package/dist/chunk-RNTMVHEF.js +202 -0
- package/dist/index.d.ts +191 -0
- package/dist/index.js +905 -0
- package/dist/oauth-flow-O6AKA5CZ.js +6 -0
- package/openclaw.plugin.json +22 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @spikelabs/lobster-shell-plugin
|
|
2
|
+
|
|
3
|
+
Connect your OpenClaw agent to [Lobster Shell](https://www.lobstershell.ai) — give your bot a face, voice, and video presence.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @spikelabs/lobster-shell-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
OpenClaw discovers the plugin automatically via its `openclaw.plugin.json` manifest.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
1. Start your OpenClaw gateway
|
|
16
|
+
2. Visit **http://localhost:18789/lobster/setup** in your browser
|
|
17
|
+
3. Click **Connect to Lobster Shell**
|
|
18
|
+
4. Sign in (or create an account) on lobstershell.ai
|
|
19
|
+
5. You'll be redirected back — the setup page should show "Connected!"
|
|
20
|
+
|
|
21
|
+
No API keys needed. The plugin uses OAuth 2.1 with PKCE to securely link your gateway to your Lobster Shell account.
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
Once connected, the plugin:
|
|
26
|
+
|
|
27
|
+
- **Relays messages** between your OpenClaw agent and the Lobster Shell avatar renderer via an Ably real-time bridge
|
|
28
|
+
- **Publishes events** (`message_received`, `agent_thinking`, `agent_response`) so the avatar reacts in real time
|
|
29
|
+
- **Supports Shell Live** — real-time voice/video conversations through LiveKit
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
The plugin works with zero configuration. Optional settings:
|
|
34
|
+
|
|
35
|
+
| Variable | Default | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `OPENCLAW_GATEWAY_TOKEN` | `dev-token-1234` | Gateway WebSocket auth token (set this if you changed the default) |
|
|
38
|
+
|
|
39
|
+
You can also override the cloud URL via the plugin config in OpenClaw if you're running a custom Lobster Shell instance:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"pluginConfig": {
|
|
44
|
+
"lobster-shell": {
|
|
45
|
+
"cloudUrl": "https://your-custom-instance.com"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Diagnostics
|
|
52
|
+
|
|
53
|
+
Use the `/lobster-status` command in your OpenClaw chat to check connection status:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
/lobster-status
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This shows plugin version, gateway port, cloud bridge status, and channel info.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pnpm install
|
|
65
|
+
pnpm build # Build once
|
|
66
|
+
pnpm dev # Watch mode
|
|
67
|
+
pnpm lint # Type-check
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To test with a local OpenClaw Docker container, copy the built plugin:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pnpm build
|
|
74
|
+
cp dist/* .openclaw-dev/config/extensions/lobster-shell/dist/
|
|
75
|
+
docker compose restart
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// src/oauth-flow.ts
|
|
2
|
+
import { randomBytes, createHash } from "crypto";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
function generateCodeVerifier() {
|
|
6
|
+
return randomBytes(32).toString("base64url");
|
|
7
|
+
}
|
|
8
|
+
function generateCodeChallenge(verifier) {
|
|
9
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
10
|
+
}
|
|
11
|
+
var OAuthFlow = class {
|
|
12
|
+
stateDir;
|
|
13
|
+
cloudUrl;
|
|
14
|
+
logger;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.stateDir = opts.stateDir;
|
|
17
|
+
this.cloudUrl = opts.cloudUrl;
|
|
18
|
+
this.logger = opts.logger;
|
|
19
|
+
}
|
|
20
|
+
// -------------------------------------------------------------------------
|
|
21
|
+
// Persistence
|
|
22
|
+
// -------------------------------------------------------------------------
|
|
23
|
+
async loadAuth() {
|
|
24
|
+
try {
|
|
25
|
+
const raw = await readFile(join(this.stateDir, "auth.json"), "utf-8");
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async saveAuth(state) {
|
|
32
|
+
await mkdir(this.stateDir, { recursive: true });
|
|
33
|
+
await writeFile(join(this.stateDir, "auth.json"), JSON.stringify(state, null, 2));
|
|
34
|
+
}
|
|
35
|
+
async loadPending() {
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(join(this.stateDir, "pending-auth.json"), "utf-8");
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async savePending(pending) {
|
|
44
|
+
await mkdir(this.stateDir, { recursive: true });
|
|
45
|
+
await writeFile(join(this.stateDir, "pending-auth.json"), JSON.stringify(pending, null, 2));
|
|
46
|
+
}
|
|
47
|
+
// -------------------------------------------------------------------------
|
|
48
|
+
// Step 1: Discover OAuth metadata
|
|
49
|
+
// -------------------------------------------------------------------------
|
|
50
|
+
async discover() {
|
|
51
|
+
const url = `${this.cloudUrl}/.well-known/oauth-authorization-server`;
|
|
52
|
+
this.logger.info(`[lobster:oauth] Discovering OAuth metadata from ${url}`);
|
|
53
|
+
const res = await fetch(url);
|
|
54
|
+
if (!res.ok) throw new Error(`OAuth discovery failed: ${res.status}`);
|
|
55
|
+
return await res.json();
|
|
56
|
+
}
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
// Step 2: Dynamic client registration
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
async registerClient(registrationEndpoint, redirectUri) {
|
|
61
|
+
this.logger.info("[lobster:oauth] Registering OAuth client dynamically");
|
|
62
|
+
const res = await fetch(registrationEndpoint, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
client_name: "Lobster Shell Plugin",
|
|
67
|
+
redirect_uris: [redirectUri],
|
|
68
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
69
|
+
response_types: ["code"],
|
|
70
|
+
token_endpoint_auth_method: "none"
|
|
71
|
+
// public client (PKCE)
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const body = await res.text();
|
|
76
|
+
throw new Error(`Client registration failed: ${res.status} ${body}`);
|
|
77
|
+
}
|
|
78
|
+
return await res.json();
|
|
79
|
+
}
|
|
80
|
+
// -------------------------------------------------------------------------
|
|
81
|
+
// Initiate flow (returns URL to redirect browser to)
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
async initiateFlow(redirectUri) {
|
|
84
|
+
const metadata = await this.discover();
|
|
85
|
+
if (!metadata.registration_endpoint) {
|
|
86
|
+
throw new Error("OAuth server does not support dynamic client registration");
|
|
87
|
+
}
|
|
88
|
+
const { client_id } = await this.registerClient(metadata.registration_endpoint, redirectUri);
|
|
89
|
+
const codeVerifier = generateCodeVerifier();
|
|
90
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
91
|
+
const state = randomBytes(16).toString("hex");
|
|
92
|
+
await this.savePending({
|
|
93
|
+
state,
|
|
94
|
+
codeVerifier,
|
|
95
|
+
clientId: client_id,
|
|
96
|
+
tokenEndpoint: metadata.token_endpoint,
|
|
97
|
+
redirectUri
|
|
98
|
+
});
|
|
99
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
100
|
+
authUrl.searchParams.set("response_type", "code");
|
|
101
|
+
authUrl.searchParams.set("client_id", client_id);
|
|
102
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
103
|
+
authUrl.searchParams.set("state", state);
|
|
104
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
105
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
106
|
+
authUrl.searchParams.set("scope", "profile");
|
|
107
|
+
this.logger.info("[lobster:oauth] Authorization URL generated");
|
|
108
|
+
return authUrl.toString();
|
|
109
|
+
}
|
|
110
|
+
// -------------------------------------------------------------------------
|
|
111
|
+
// Handle callback (exchange code for tokens, register gateway)
|
|
112
|
+
// -------------------------------------------------------------------------
|
|
113
|
+
async handleCallback(query, gatewayInfo) {
|
|
114
|
+
const { code, state } = query;
|
|
115
|
+
if (!code || !state) throw new Error("Missing code or state parameter");
|
|
116
|
+
const pending = await this.loadPending();
|
|
117
|
+
if (!pending || pending.state !== state) {
|
|
118
|
+
throw new Error("Invalid state parameter \u2014 possible CSRF");
|
|
119
|
+
}
|
|
120
|
+
this.logger.info("[lobster:oauth] Exchanging authorization code for tokens");
|
|
121
|
+
const tokenRes = await fetch(pending.tokenEndpoint, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
124
|
+
body: new URLSearchParams({
|
|
125
|
+
grant_type: "authorization_code",
|
|
126
|
+
code,
|
|
127
|
+
redirect_uri: pending.redirectUri,
|
|
128
|
+
client_id: pending.clientId,
|
|
129
|
+
code_verifier: pending.codeVerifier
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
if (!tokenRes.ok) {
|
|
133
|
+
const body = await tokenRes.text();
|
|
134
|
+
throw new Error(`Token exchange failed: ${tokenRes.status} ${body}`);
|
|
135
|
+
}
|
|
136
|
+
const tokens = await tokenRes.json();
|
|
137
|
+
this.logger.info("[lobster:oauth] Registering gateway with Spike cloud");
|
|
138
|
+
const registerRes = await fetch(`${this.cloudUrl}/api/gateways/register`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
Authorization: `Bearer ${tokens.access_token}`
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({ gatewayInfo })
|
|
145
|
+
});
|
|
146
|
+
if (!registerRes.ok) {
|
|
147
|
+
const body = await registerRes.text();
|
|
148
|
+
throw new Error(`Gateway registration failed: ${registerRes.status} ${body}`);
|
|
149
|
+
}
|
|
150
|
+
const registration = await registerRes.json();
|
|
151
|
+
const authState = {
|
|
152
|
+
clientId: pending.clientId,
|
|
153
|
+
accessToken: tokens.access_token,
|
|
154
|
+
refreshToken: tokens.refresh_token,
|
|
155
|
+
expiresAt: Date.now() + (tokens.expires_in ?? 3600) * 1e3,
|
|
156
|
+
gatewayPublicId: registration.gatewayPublicId,
|
|
157
|
+
channelName: registration.channelName,
|
|
158
|
+
tenantToken: registration.tenantToken,
|
|
159
|
+
tenantTokenExpiresAt: registration.tenantTokenExpiresAt
|
|
160
|
+
};
|
|
161
|
+
await this.saveAuth(authState);
|
|
162
|
+
this.logger.info(
|
|
163
|
+
`[lobster:oauth] OAuth flow completed, gateway registered: ${registration.gatewayPublicId}`
|
|
164
|
+
);
|
|
165
|
+
return authState;
|
|
166
|
+
}
|
|
167
|
+
// -------------------------------------------------------------------------
|
|
168
|
+
// Refresh token
|
|
169
|
+
// -------------------------------------------------------------------------
|
|
170
|
+
async refreshAccessToken() {
|
|
171
|
+
const auth = await this.loadAuth();
|
|
172
|
+
if (!auth?.refreshToken) return null;
|
|
173
|
+
const metadata = await this.discover();
|
|
174
|
+
this.logger.info("[lobster:oauth] Refreshing access token");
|
|
175
|
+
const res = await fetch(metadata.token_endpoint, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
178
|
+
body: new URLSearchParams({
|
|
179
|
+
grant_type: "refresh_token",
|
|
180
|
+
refresh_token: auth.refreshToken,
|
|
181
|
+
client_id: auth.clientId
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
this.logger.warn(`[lobster:oauth] Token refresh failed: ${res.status}`);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const tokens = await res.json();
|
|
189
|
+
const updated = {
|
|
190
|
+
...auth,
|
|
191
|
+
accessToken: tokens.access_token,
|
|
192
|
+
refreshToken: tokens.refresh_token ?? auth.refreshToken,
|
|
193
|
+
expiresAt: Date.now() + (tokens.expires_in ?? 3600) * 1e3
|
|
194
|
+
};
|
|
195
|
+
await this.saveAuth(updated);
|
|
196
|
+
return updated;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
OAuthFlow
|
|
202
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal type declarations for the OpenClaw Plugin SDK.
|
|
5
|
+
* Aligned with openclaw/src/plugins/types.ts — only what we actually use.
|
|
6
|
+
* This avoids a dependency on the full openclaw package.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type PluginLogger = {
|
|
10
|
+
debug?: (message: string) => void;
|
|
11
|
+
info: (message: string) => void;
|
|
12
|
+
warn: (message: string) => void;
|
|
13
|
+
error: (message: string) => void;
|
|
14
|
+
};
|
|
15
|
+
type ChannelId = string;
|
|
16
|
+
type ChannelMeta = {
|
|
17
|
+
id: ChannelId;
|
|
18
|
+
label: string;
|
|
19
|
+
selectionLabel: string;
|
|
20
|
+
docsPath: string;
|
|
21
|
+
blurb: string;
|
|
22
|
+
order?: number;
|
|
23
|
+
aliases?: string[];
|
|
24
|
+
detailLabel?: string;
|
|
25
|
+
systemImage?: string;
|
|
26
|
+
};
|
|
27
|
+
type ChannelCapabilities = {
|
|
28
|
+
chatTypes: Array<"direct" | "group" | "thread">;
|
|
29
|
+
media?: boolean;
|
|
30
|
+
polls?: boolean;
|
|
31
|
+
reactions?: boolean;
|
|
32
|
+
edit?: boolean;
|
|
33
|
+
threads?: boolean;
|
|
34
|
+
};
|
|
35
|
+
type ChannelConfigAdapter<ResolvedAccount = unknown> = {
|
|
36
|
+
listAccountIds: (cfg: unknown) => string[];
|
|
37
|
+
resolveAccount: (cfg: unknown, accountId?: string | null) => ResolvedAccount;
|
|
38
|
+
};
|
|
39
|
+
type ChannelOutboundContext = {
|
|
40
|
+
cfg: unknown;
|
|
41
|
+
to: string;
|
|
42
|
+
text: string;
|
|
43
|
+
mediaUrl?: string;
|
|
44
|
+
mediaLocalRoots?: readonly string[];
|
|
45
|
+
replyToId?: string | null;
|
|
46
|
+
threadId?: string | number | null;
|
|
47
|
+
accountId?: string | null;
|
|
48
|
+
silent?: boolean;
|
|
49
|
+
};
|
|
50
|
+
type OutboundDeliveryResult = {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
messageId?: string;
|
|
53
|
+
error?: string;
|
|
54
|
+
};
|
|
55
|
+
type ChannelOutboundAdapter = {
|
|
56
|
+
deliveryMode: "direct" | "gateway" | "hybrid";
|
|
57
|
+
sendText?: (ctx: ChannelOutboundContext) => Promise<OutboundDeliveryResult>;
|
|
58
|
+
sendMedia?: (ctx: ChannelOutboundContext) => Promise<OutboundDeliveryResult>;
|
|
59
|
+
};
|
|
60
|
+
type ChannelPlugin = {
|
|
61
|
+
id: ChannelId;
|
|
62
|
+
meta: ChannelMeta;
|
|
63
|
+
capabilities: ChannelCapabilities;
|
|
64
|
+
config: ChannelConfigAdapter;
|
|
65
|
+
outbound?: ChannelOutboundAdapter;
|
|
66
|
+
};
|
|
67
|
+
type PluginServiceContext = {
|
|
68
|
+
config: unknown;
|
|
69
|
+
workspaceDir?: string;
|
|
70
|
+
stateDir: string;
|
|
71
|
+
logger: PluginLogger;
|
|
72
|
+
};
|
|
73
|
+
type PluginService = {
|
|
74
|
+
id: string;
|
|
75
|
+
start: (ctx: PluginServiceContext) => void | Promise<void>;
|
|
76
|
+
stop?: (ctx: PluginServiceContext) => void | Promise<void>;
|
|
77
|
+
};
|
|
78
|
+
type PluginCommandContext = {
|
|
79
|
+
senderId?: string;
|
|
80
|
+
channel: string;
|
|
81
|
+
channelId?: string;
|
|
82
|
+
isAuthorizedSender: boolean;
|
|
83
|
+
args?: string;
|
|
84
|
+
commandBody: string;
|
|
85
|
+
config: unknown;
|
|
86
|
+
from?: string;
|
|
87
|
+
to?: string;
|
|
88
|
+
accountId?: string;
|
|
89
|
+
};
|
|
90
|
+
type PluginCommandResult = {
|
|
91
|
+
text?: string;
|
|
92
|
+
mediaUrl?: string;
|
|
93
|
+
};
|
|
94
|
+
type PluginCommandDefinition = {
|
|
95
|
+
name: string;
|
|
96
|
+
description: string;
|
|
97
|
+
acceptsArgs?: boolean;
|
|
98
|
+
requireAuth?: boolean;
|
|
99
|
+
handler: (ctx: PluginCommandContext) => PluginCommandResult | Promise<PluginCommandResult>;
|
|
100
|
+
};
|
|
101
|
+
type PluginHookMessageContext = {
|
|
102
|
+
channelId: string;
|
|
103
|
+
accountId?: string;
|
|
104
|
+
conversationId?: string;
|
|
105
|
+
};
|
|
106
|
+
type PluginHookMessageSentEvent = {
|
|
107
|
+
to: string;
|
|
108
|
+
content: string;
|
|
109
|
+
success: boolean;
|
|
110
|
+
error?: string;
|
|
111
|
+
};
|
|
112
|
+
type PluginHookMessageReceivedEvent = {
|
|
113
|
+
from: string;
|
|
114
|
+
content: string;
|
|
115
|
+
timestamp?: number;
|
|
116
|
+
metadata?: Record<string, unknown>;
|
|
117
|
+
};
|
|
118
|
+
type PluginHookGatewayStartEvent = {
|
|
119
|
+
port: number;
|
|
120
|
+
};
|
|
121
|
+
type PluginHookGatewayContext = {
|
|
122
|
+
port?: number;
|
|
123
|
+
};
|
|
124
|
+
type PluginHookHandlerMap = {
|
|
125
|
+
message_sent: (event: PluginHookMessageSentEvent, ctx: PluginHookMessageContext) => Promise<void> | void;
|
|
126
|
+
message_received: (event: PluginHookMessageReceivedEvent, ctx: PluginHookMessageContext) => Promise<void> | void;
|
|
127
|
+
gateway_start: (event: PluginHookGatewayStartEvent, ctx: PluginHookGatewayContext) => Promise<void> | void;
|
|
128
|
+
gateway_stop: (event: {
|
|
129
|
+
reason?: string;
|
|
130
|
+
}, ctx: PluginHookGatewayContext) => Promise<void> | void;
|
|
131
|
+
};
|
|
132
|
+
type PluginHookName = keyof PluginHookHandlerMap;
|
|
133
|
+
/**
|
|
134
|
+
* HTTP route handler receives raw Node.js IncomingMessage and ServerResponse.
|
|
135
|
+
* Return `true` (or void) if handled, `false` to pass to next route.
|
|
136
|
+
*/
|
|
137
|
+
type OpenClawPluginHttpRouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean | void> | boolean | void;
|
|
138
|
+
type OpenClawPluginHttpRouteAuth = "gateway" | "plugin";
|
|
139
|
+
type OpenClawPluginHttpRouteMatch = "exact" | "prefix";
|
|
140
|
+
type OpenClawPluginHttpRouteParams = {
|
|
141
|
+
path: string;
|
|
142
|
+
handler: OpenClawPluginHttpRouteHandler;
|
|
143
|
+
/** "gateway" = gateway auth enforced; "plugin" = plugin handles its own auth */
|
|
144
|
+
auth: OpenClawPluginHttpRouteAuth;
|
|
145
|
+
match?: OpenClawPluginHttpRouteMatch;
|
|
146
|
+
replaceExisting?: boolean;
|
|
147
|
+
};
|
|
148
|
+
type OpenClawPluginApi = {
|
|
149
|
+
id: string;
|
|
150
|
+
name: string;
|
|
151
|
+
version?: string;
|
|
152
|
+
description?: string;
|
|
153
|
+
source: string;
|
|
154
|
+
config: unknown;
|
|
155
|
+
pluginConfig?: Record<string, unknown>;
|
|
156
|
+
logger: PluginLogger;
|
|
157
|
+
registerChannel: (plugin: ChannelPlugin) => void;
|
|
158
|
+
registerService: (service: PluginService) => void;
|
|
159
|
+
registerCommand: (command: PluginCommandDefinition) => void;
|
|
160
|
+
registerHttpRoute: (params: OpenClawPluginHttpRouteParams) => void;
|
|
161
|
+
registerHook: (events: string | string[], handler: (...args: unknown[]) => unknown, opts?: {
|
|
162
|
+
name?: string;
|
|
163
|
+
description?: string;
|
|
164
|
+
}) => void;
|
|
165
|
+
on: <K extends PluginHookName>(hookName: K, handler: PluginHookHandlerMap[K], opts?: {
|
|
166
|
+
priority?: number;
|
|
167
|
+
}) => void;
|
|
168
|
+
};
|
|
169
|
+
type OpenClawPluginDefinition = {
|
|
170
|
+
id?: string;
|
|
171
|
+
name?: string;
|
|
172
|
+
description?: string;
|
|
173
|
+
version?: string;
|
|
174
|
+
register?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
175
|
+
};
|
|
176
|
+
type OpenClawPluginModule = OpenClawPluginDefinition | ((api: OpenClawPluginApi) => void | Promise<void>);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @spikelabs/lobster-shell-plugin — Connect your OpenClaw agent to Lobster Shell cloud.
|
|
180
|
+
*
|
|
181
|
+
* This plugin registers:
|
|
182
|
+
* - A "lobster-shell" channel so the agent can send responses to the avatar renderer
|
|
183
|
+
* - Hooks on message_sent / message_received to relay events to the cloud bridge
|
|
184
|
+
* - A background service that manages the Ably bridge to Lobster Shell
|
|
185
|
+
* - HTTP routes for OAuth setup flow
|
|
186
|
+
* - A /lobster-status command for quick diagnostics
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
declare const plugin: OpenClawPluginModule;
|
|
190
|
+
|
|
191
|
+
export { plugin as default };
|