@clawling/clawchat-plugin-openclaw 2026.5.12-28
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/INSTALL.md +64 -0
- package/README.md +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
package/INSTALL.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Installing ClawChat for OpenClaw
|
|
2
|
+
|
|
3
|
+
Follow this flow exactly. Agents must not skip, reorder, or merge steps: install, restart, activate, verify. After each step succeeds, tell the user that the step is complete before starting the next step.
|
|
4
|
+
|
|
5
|
+
## 1. Download
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
CLAWCHAT_PLUGIN_VERSION="2026.5.16-1"
|
|
9
|
+
CLAWCHAT_PLUGIN_URL="https://plugin.clawling.chat/openclaw/newbase-clawchat-clawchat-plugin-openclaw-${CLAWCHAT_PLUGIN_VERSION}.tgz"
|
|
10
|
+
CLAWCHAT_PLUGIN_TGZ="/tmp/newbase-clawchat-clawchat-plugin-openclaw-${CLAWCHAT_PLUGIN_VERSION}.tgz"
|
|
11
|
+
curl -fL "$CLAWCHAT_PLUGIN_URL" -o "$CLAWCHAT_PLUGIN_TGZ"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
To install the latest uploaded tarball instead of a pinned version, use:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
CLAWCHAT_PLUGIN_URL="https://plugin.clawling.chat/openclaw/newbase-clawchat-clawchat-plugin-openclaw-latest.tgz"
|
|
18
|
+
CLAWCHAT_PLUGIN_TGZ="/tmp/newbase-clawchat-clawchat-plugin-openclaw-latest.tgz"
|
|
19
|
+
curl -fL "$CLAWCHAT_PLUGIN_URL" -o "$CLAWCHAT_PLUGIN_TGZ"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
After this succeeds, tell the user: `Download completed.`
|
|
23
|
+
|
|
24
|
+
## 2. Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
openclaw plugins install "$CLAWCHAT_PLUGIN_TGZ" --force
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
After this succeeds, tell the user: `Install completed.`
|
|
31
|
+
|
|
32
|
+
## 3. Restart
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
openclaw gateway restart
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
After this succeeds, tell the user: `First restart completed.`
|
|
39
|
+
|
|
40
|
+
## 4. Activate
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
CLAWCHAT_INVITE_CODE="XXXXXX"
|
|
44
|
+
openclaw channels add --channel clawchat-plugin-openclaw --token "$CLAWCHAT_INVITE_CODE"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
After this succeeds, tell the user: `Activation completed.`
|
|
48
|
+
|
|
49
|
+
Activation writes both the ClawChat channel credentials and
|
|
50
|
+
`plugins.entries.clawchat-plugin-openclaw.enabled=true`, so a running Gateway with hot
|
|
51
|
+
config reload should attach and start the plugin without another hard restart.
|
|
52
|
+
|
|
53
|
+
## 5. Verify
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
sleep 5
|
|
57
|
+
openclaw channels status --probe
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
After this succeeds, tell the user: `Verification completed.`
|
|
61
|
+
|
|
62
|
+
`clawchat-plugin-openclaw` must be enabled, configured, running, and connected.
|
|
63
|
+
|
|
64
|
+
When the full flow is complete, tell the user: `The installation flow is complete. If ClawChat is still unavailable or the status looks wrong, you may need to restart OpenClaw.`
|
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# @clawling/clawchat-plugin-openclaw
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin that connects an agent to ClawChat over ClawChat Protocol v2 with a plugin-owned WebSocket client, plus a small REST surface for profile / social / media operations (`/v1/*` plus unversioned `/media/upload`).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Plugin-owned WebSocket transport with auto-reconnect (exponential backoff + jitter), heartbeat, and ack tracking
|
|
8
|
+
- Invite-code onboarding — no raw credentials required
|
|
9
|
+
- Inbound `message.send` / `message.reply` with reply context
|
|
10
|
+
- Outbound text replies in `static` or `stream` mode, with a consolidated final `message.reply`
|
|
11
|
+
- Typing indicators and filtered forwarding for thinking / tool-call content
|
|
12
|
+
- Media fragments (image / file / audio / video) in either direction
|
|
13
|
+
- Invite-code onboarding via `/clawchat-activate` or supported `openclaw channels add`, plus always-registered `clawchat_*` account/media tools
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Add as an OpenClaw extension
|
|
19
|
+
npm i @clawling/clawchat-plugin-openclaw
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires `openclaw >= 2026.5.4` as a peer host.
|
|
23
|
+
|
|
24
|
+
For the OpenClaw plugin install/update flow, use the R2-hosted tarball install
|
|
25
|
+
command documented in [`INSTALL.md`](./INSTALL.md).
|
|
26
|
+
|
|
27
|
+
Example LLM prompt:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
Use https://raw.githubusercontent.com/clawling/clawchat-plugin-openclaw/refs/heads/main/INSTALL.md to install and activate the ClawChat plugin. The invite code is XXXXXX.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Current activation paths
|
|
34
|
+
|
|
35
|
+
Pick one of these invite-code activation paths after the plugin is loaded
|
|
36
|
+
into OpenClaw:
|
|
37
|
+
|
|
38
|
+
- **Runtime slash command (recommended).** Send `/clawchat-activate A1B2C3`
|
|
39
|
+
in the chat where OpenClaw is running. Not a shell command — running
|
|
40
|
+
`openclaw clawchat-activate` is expected to fail. This is the reliable
|
|
41
|
+
first-time activation path on OpenClaw 2026.5.5 and newer.
|
|
42
|
+
- **CLI `channels add`** when the host exposes `clawchat-plugin-openclaw` in its
|
|
43
|
+
channel catalog:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
openclaw channels add --channel clawchat-plugin-openclaw --token "$CLAWCHAT_INVITE_CODE"
|
|
47
|
+
openclaw channels status --probe
|
|
48
|
+
```
|
|
49
|
+
- **CLI `channels login`** to refresh credentials later, once the channel
|
|
50
|
+
already exists on the host.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
openclaw channels login --channel clawchat-plugin-openclaw
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If `channels add` reports `Unknown channel: clawchat-plugin-openclaw`, use the
|
|
57
|
+
runtime slash command after the plugin is loaded.
|
|
58
|
+
|
|
59
|
+
Activation persists token/userId/ownerUserId, enables the runtime plugin
|
|
60
|
+
entry, and ensures plugin + tool policy lists cover `clawchat-plugin-openclaw`,
|
|
61
|
+
all in one config mutation. A Gateway with config reload/hot restart picks
|
|
62
|
+
up the new runtime; otherwise `openclaw gateway restart` is required.
|
|
63
|
+
|
|
64
|
+
See [`docs/clawchat-plugin-openclaw.md`](./docs/clawchat-plugin-openclaw.md) §"Onboarding
|
|
65
|
+
(activation)" for the full contract: persisted fields, default
|
|
66
|
+
`groupMode`/`groupCommandMode`, behavior with stale CLI catalogs, and the
|
|
67
|
+
expected `openclaw.json` shape after activation succeeds.
|
|
68
|
+
|
|
69
|
+
## Standalone web chat (dev)
|
|
70
|
+
|
|
71
|
+
A minimal browser test harness is bundled under `tools/`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
node tools/standalone-webchat-server.mjs
|
|
75
|
+
# Options: --host (default 127.0.0.1), --port (default 4318), --default-ws-url
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then open the printed URL (default `http://127.0.0.1:4318`) to exercise the plugin end to end against a WebSocket relay.
|
|
79
|
+
|
|
80
|
+
## Layout
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
src/
|
|
84
|
+
channel.ts full-runtime plugin adapter (auth.login, gateway,
|
|
85
|
+
agentPrompt, messaging, reload). Extends:
|
|
86
|
+
channel.setup.ts setup-only plugin adapter (invite-code exchange,
|
|
87
|
+
config schema, status; no WebSocket runtime)
|
|
88
|
+
runtime.ts inbound dispatch + reply dispatcher
|
|
89
|
+
ws-client.ts ClawChat Protocol v2 WebSocket client (handshake,
|
|
90
|
+
heartbeat, queue, ack tracking, reconnect)
|
|
91
|
+
client.ts createOpenclawClawlingClient + stream-emit helpers
|
|
92
|
+
(created/add/done/failed/reply) over ws-client
|
|
93
|
+
api-client.ts REST client for /v1/* + /media/upload
|
|
94
|
+
api-types.ts /v1/* request/response type definitions
|
|
95
|
+
inbound.ts envelope → agent turn
|
|
96
|
+
outbound.ts agent reply → envelope
|
|
97
|
+
streaming.ts progressive reply emitter
|
|
98
|
+
buffered-stream.ts chunk coalescing + flush policy
|
|
99
|
+
message-mapper.ts fragment ↔ text/media mapping
|
|
100
|
+
group-message-coalescer.ts
|
|
101
|
+
per-group idle/max batching for non-mention turns
|
|
102
|
+
reply-dispatcher.ts static vs stream routing
|
|
103
|
+
login.runtime.ts invite-code exchange flow
|
|
104
|
+
media-runtime.ts media download/upload pipeline
|
|
105
|
+
tools.ts clawchat_* agent tools (account / media / memory /
|
|
106
|
+
metadata / moments / mention)
|
|
107
|
+
tools-schema.ts JSON schemas for the clawchat_* tools
|
|
108
|
+
protocol.ts inbound payload type guards
|
|
109
|
+
protocol-types.ts v2 envelope, fragment, transport shapes
|
|
110
|
+
config.ts defaults + typebox schema
|
|
111
|
+
storage.ts plugin-owned SQLite (activations, connections,
|
|
112
|
+
clawchat_messages, tool_calls)
|
|
113
|
+
clawchat-memory.ts file-backed memory tools (see docs/clawchat-memory.md)
|
|
114
|
+
clawchat-metadata.ts metadata pull/push + invalidation handling
|
|
115
|
+
profile-sync.ts profile/metadata pull + sync into file-backed memory
|
|
116
|
+
profile-prompt.ts dynamic ClawChat prompt composition
|
|
117
|
+
prompt-injection.ts register-time wiring of the prompt injector
|
|
118
|
+
plugin-prompts.ts loads prompts/platform.md and default-* prompts
|
|
119
|
+
ws-alignment.ts handshake capabilities + hello-ok diagnostics
|
|
120
|
+
ws-log.ts clawchat.ws single-line log formatting
|
|
121
|
+
commands.ts /clawchat-activate runtime slash command
|
|
122
|
+
terminal-send.ts terminal-send suppression for clawchat_mention_message
|
|
123
|
+
mock-transport.ts in-memory Transport double for tests
|
|
124
|
+
tools/
|
|
125
|
+
standalone-webchat-server.mjs
|
|
126
|
+
standalone-webchat.html
|
|
127
|
+
docs/
|
|
128
|
+
README.md typed index of the docs/ tree
|
|
129
|
+
clawchat-plugin-openclaw.md full plugin reference (protocol use, config,
|
|
130
|
+
SQLite, WS logs, troubleshooting, diagrams)
|
|
131
|
+
client-integration.md ClawChat Protocol v2 wire contract (authoritative)
|
|
132
|
+
clawchat-memory.md file-backed memory contract (shared with Hermes)
|
|
133
|
+
mention-message.md clawchat_mention_message tool reference
|
|
134
|
+
per-group-mode.md per-chat_id groupMode / groupCommandMode overrides
|
|
135
|
+
openclaw-runtime-compatibility.md
|
|
136
|
+
host runtime API compatibility notes
|
|
137
|
+
openclaw-source-lookup.md
|
|
138
|
+
when/how to inspect the OpenClaw host checkout
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Documentation
|
|
142
|
+
|
|
143
|
+
Start at [`docs/README.md`](./docs/README.md) — the typed index covering
|
|
144
|
+
operators, in-chat agents, and plugin developers.
|
|
145
|
+
|
|
146
|
+
The main reference is
|
|
147
|
+
[`docs/clawchat-plugin-openclaw.md`](./docs/clawchat-plugin-openclaw.md), which covers:
|
|
148
|
+
|
|
149
|
+
- Full configuration reference
|
|
150
|
+
- Onboarding / activation details
|
|
151
|
+
- REST endpoint table
|
|
152
|
+
- Streaming frame shapes (`message.created` / `message.add` / `message.done` / `message.reply`)
|
|
153
|
+
- End-to-end sequence diagram
|
|
154
|
+
- Media pipeline (inbound download / outbound upload)
|
|
155
|
+
- Troubleshooting
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Tests
|
|
161
|
+
npx vitest run
|
|
162
|
+
|
|
163
|
+
# Typecheck
|
|
164
|
+
npm run typecheck
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Tests live next to the source they cover (`*.test.ts`). The development entrypoint stays in TypeScript for the OpenClaw extension loader, while npm installs use the compiled runtime entrypoint generated by `npm run build` / `prepack` under `dist/`.
|
|
168
|
+
|
|
169
|
+
Functional e2e test cases are documented in `.e2e/docs/install-clawchat-plugin-e2e.md`; keep that guide updated when adding or changing e2e flows.
|
|
170
|
+
|
|
171
|
+
For OpenClaw host SDK/source lookup while developing this plugin, optionally
|
|
172
|
+
clone OpenClaw into `tmp/openclaw`:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm run dev:openclaw-source
|
|
176
|
+
# equivalent: git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This checkout is local-only. It is ignored by git and is not required to run the
|
|
180
|
+
plugin tests or publish the package.
|
|
181
|
+
|
|
182
|
+
## R2 package scripts
|
|
183
|
+
|
|
184
|
+
Create and upload the OpenClaw plugin tarball to the R2 `openclaw/` prefix:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
./scripts/package_openclaw_plugin.sh
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The script runs `npm pack`, removes `devDependencies` from the generated `.tgz`
|
|
191
|
+
metadata so OpenClaw installs only runtime dependencies, uploads the `.tgz` to
|
|
192
|
+
the configured R2 bucket, updates the `latest` R2 alias, uploads `INSTALL.md` as
|
|
193
|
+
`openclaw/install.md`, and prints the public URLs. R2 credentials are read from
|
|
194
|
+
`scripts/.env.r2`, which is ignored by git. Copy `scripts/.env.r2.example` to
|
|
195
|
+
`scripts/.env.r2` and fill in the credentials. Use `--no-upload` to build the
|
|
196
|
+
tarball without uploading it.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
AWS_ACCESS_KEY_ID=...
|
|
200
|
+
AWS_SECRET_ACCESS_KEY=...
|
|
201
|
+
AWS_DEFAULT_REGION=auto
|
|
202
|
+
R2_ENDPOINT=https://...
|
|
203
|
+
R2_BUCKET=...
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Install the R2-hosted latest tarball on a device or container with OpenClaw
|
|
207
|
+
available:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
./scripts/install_openclaw.sh
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
To install a specific uploaded version, pass the version string:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
./scripts/install_openclaw.sh 2026.5.16-1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
To install a specific uploaded tarball URL, pass its URL explicitly:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
./scripts/install_openclaw.sh https://plugin.clawling.chat/openclaw/newbase-clawchat-clawchat-plugin-openclaw-2026.5.16-1.tgz
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
See the repository root.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
import { openclawClawlingPlugin } from "./src/channel.js";
|
|
3
|
+
import { registerOpenclawClawlingCommands } from "./src/commands.js";
|
|
4
|
+
import { openclawClawlingConfigSchema } from "./src/config.js";
|
|
5
|
+
import { registerClawChatPromptInjection, } from "./src/prompt-injection.js";
|
|
6
|
+
import { setOpenclawClawlingRuntime } from "./src/runtime.js";
|
|
7
|
+
import { registerOpenclawClawlingTools } from "./src/tools.js";
|
|
8
|
+
export default defineChannelPluginEntry({
|
|
9
|
+
id: "clawchat-plugin-openclaw",
|
|
10
|
+
name: "Clawling Chat",
|
|
11
|
+
description: "Clawling Chat Protocol v2 channel plugin",
|
|
12
|
+
plugin: openclawClawlingPlugin,
|
|
13
|
+
configSchema: { schema: openclawClawlingConfigSchema },
|
|
14
|
+
setRuntime: setOpenclawClawlingRuntime,
|
|
15
|
+
registerFull(api) {
|
|
16
|
+
registerOpenclawClawlingCommands(api);
|
|
17
|
+
registerClawChatPromptInjection(api);
|
|
18
|
+
registerOpenclawClawlingTools(api);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { ClawlingApiError, } from "./api-types.js";
|
|
2
|
+
import { CHANNEL_ID } from "./config.js";
|
|
3
|
+
export function createOpenclawClawlingApiClient(opts) {
|
|
4
|
+
if (!/^https?:\/\//i.test(opts.baseUrl)) {
|
|
5
|
+
throw new ClawlingApiError("validation", `clawchat-plugin-openclaw baseUrl must start with http:// or https:// (got "${opts.baseUrl}")`);
|
|
6
|
+
}
|
|
7
|
+
const baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
8
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
9
|
+
function url(path) {
|
|
10
|
+
return `${baseUrl}${path}`;
|
|
11
|
+
}
|
|
12
|
+
function authHeaders(extra = {}) {
|
|
13
|
+
// `X-Device-Id` is sent on every request so the server can correlate
|
|
14
|
+
// activity back to this plugin instance. Callers may override via
|
|
15
|
+
// `extra` (e.g. tests) but the default is the channel id.
|
|
16
|
+
return {
|
|
17
|
+
authorization: `Bearer ${opts.token}`,
|
|
18
|
+
"x-device-id": CHANNEL_ID,
|
|
19
|
+
...extra,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function readEnvelope(res, path) {
|
|
23
|
+
if (res.status === 401 || res.status === 403) {
|
|
24
|
+
throw new ClawlingApiError("auth", `unauthorized (status ${res.status})`, {
|
|
25
|
+
status: res.status,
|
|
26
|
+
path,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
const snippet = await res.text().catch(() => "");
|
|
31
|
+
throw new ClawlingApiError("transport", `http ${res.status} ${snippet.slice(0, 200)}`, {
|
|
32
|
+
status: res.status,
|
|
33
|
+
path,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = await res.json();
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new ClawlingApiError("transport", `non-JSON response: ${err instanceof Error ? err.message : String(err)}`, { status: res.status, path });
|
|
42
|
+
}
|
|
43
|
+
// Unified envelope: `{ code: number, msg: string, data: T }`.
|
|
44
|
+
// `code === 0` means success; any other value is a business error whose
|
|
45
|
+
// `msg` is surfaced to callers and `code` is preserved on the error meta.
|
|
46
|
+
const env = parsed;
|
|
47
|
+
const code = typeof env.code === "number" ? env.code : Number.NaN;
|
|
48
|
+
const msg = typeof env.msg === "string"
|
|
49
|
+
? env.msg
|
|
50
|
+
: typeof env.message === "string"
|
|
51
|
+
? env.message
|
|
52
|
+
: "";
|
|
53
|
+
if (!Number.isFinite(code)) {
|
|
54
|
+
throw new ClawlingApiError("transport", "invalid envelope: missing numeric `code`", {
|
|
55
|
+
status: res.status,
|
|
56
|
+
path,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (code !== 0) {
|
|
60
|
+
throw new ClawlingApiError("api", msg || `code=${code}`, {
|
|
61
|
+
code,
|
|
62
|
+
status: res.status,
|
|
63
|
+
path,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return env.data;
|
|
67
|
+
}
|
|
68
|
+
async function call(method, path, init) {
|
|
69
|
+
let res;
|
|
70
|
+
try {
|
|
71
|
+
const requestInit = {
|
|
72
|
+
method,
|
|
73
|
+
headers: authHeaders(init?.headers),
|
|
74
|
+
};
|
|
75
|
+
if (init?.body !== undefined) {
|
|
76
|
+
requestInit.body = init.body;
|
|
77
|
+
}
|
|
78
|
+
res = await fetchImpl(url(path), requestInit);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
throw new ClawlingApiError("transport", `fetch failed: ${err instanceof Error ? err.message : String(err)}`, { path });
|
|
82
|
+
}
|
|
83
|
+
return await readEnvelope(res, path);
|
|
84
|
+
}
|
|
85
|
+
function parseUploadResult(data, path) {
|
|
86
|
+
const obj = data;
|
|
87
|
+
const validKind = obj?.kind === "image" || obj?.kind === "file" || obj?.kind === "audio" || obj?.kind === "video";
|
|
88
|
+
if (!obj ||
|
|
89
|
+
!validKind ||
|
|
90
|
+
typeof obj.url !== "string" ||
|
|
91
|
+
typeof obj.name !== "string" ||
|
|
92
|
+
typeof obj.mime !== "string" ||
|
|
93
|
+
typeof obj.size !== "number") {
|
|
94
|
+
throw new ClawlingApiError("api", "invalid upload response: missing required media fields", { path });
|
|
95
|
+
}
|
|
96
|
+
return obj;
|
|
97
|
+
}
|
|
98
|
+
function assertNonBlankId(value, label) {
|
|
99
|
+
if (!value.trim()) {
|
|
100
|
+
throw new ClawlingApiError("validation", `${label} is required`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function pickPatch(patch, keys, label) {
|
|
104
|
+
const body = {};
|
|
105
|
+
for (const key of keys) {
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(patch, key) && patch[key] !== undefined) {
|
|
107
|
+
body[key] = patch[key];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (Object.keys(body).length === 0) {
|
|
111
|
+
throw new ClawlingApiError("validation", `${label} patch must include at least one mutable field`);
|
|
112
|
+
}
|
|
113
|
+
return body;
|
|
114
|
+
}
|
|
115
|
+
// All JSON API endpoints live under `/v1/...`. Media upload is the one
|
|
116
|
+
// intentional exception — the upstream server mounts it at `/media/upload`
|
|
117
|
+
// without the version prefix.
|
|
118
|
+
return {
|
|
119
|
+
async getMyProfile() {
|
|
120
|
+
return await call("GET", "/v1/users/me");
|
|
121
|
+
},
|
|
122
|
+
async getAgentProfile(agentId) {
|
|
123
|
+
return await call("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
|
|
124
|
+
},
|
|
125
|
+
async getAgentDetail(agentId) {
|
|
126
|
+
return await call("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
|
|
127
|
+
},
|
|
128
|
+
async patchAgent(agentId, patch) {
|
|
129
|
+
assertNonBlankId(agentId, "patchAgent: agentId");
|
|
130
|
+
const body = pickPatch(patch, ["nickname", "avatar_url", "bio"], "patchAgent");
|
|
131
|
+
return await call("PATCH", `/v1/agents/${encodeURIComponent(agentId)}`, {
|
|
132
|
+
body: JSON.stringify(body),
|
|
133
|
+
headers: { "content-type": "application/json" },
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
async updateAgentBehavior(behavior) {
|
|
137
|
+
return await call("PATCH", "/v1/agents/me/behavior", {
|
|
138
|
+
body: JSON.stringify({ behavior }),
|
|
139
|
+
headers: { "content-type": "application/json" },
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
async getUserInfo(userId) {
|
|
143
|
+
return await call("GET", `/v1/users/${encodeURIComponent(userId)}`);
|
|
144
|
+
},
|
|
145
|
+
async getUserProfile(userId) {
|
|
146
|
+
return await call("GET", `/v1/users/${encodeURIComponent(userId)}`);
|
|
147
|
+
},
|
|
148
|
+
async listFriends() {
|
|
149
|
+
return await call("GET", "/v1/friendships");
|
|
150
|
+
},
|
|
151
|
+
async searchUsers(params) {
|
|
152
|
+
const sp = new URLSearchParams();
|
|
153
|
+
if (typeof params.q === "string")
|
|
154
|
+
sp.set("q", params.q);
|
|
155
|
+
if (typeof params.limit === "number")
|
|
156
|
+
sp.set("limit", String(params.limit));
|
|
157
|
+
const q = sp.toString();
|
|
158
|
+
return await call("GET", q ? `/v1/users/search?${q}` : "/v1/users/search");
|
|
159
|
+
},
|
|
160
|
+
async listMoments(params) {
|
|
161
|
+
const sp = new URLSearchParams();
|
|
162
|
+
if (typeof params.before === "number")
|
|
163
|
+
sp.set("before", String(params.before));
|
|
164
|
+
if (typeof params.limit === "number")
|
|
165
|
+
sp.set("limit", String(params.limit));
|
|
166
|
+
const q = sp.toString();
|
|
167
|
+
return await call("GET", q ? `/v1/moments?${q}` : "/v1/moments");
|
|
168
|
+
},
|
|
169
|
+
async createMoment(body) {
|
|
170
|
+
return await call("POST", "/v1/moments", {
|
|
171
|
+
body: JSON.stringify(body),
|
|
172
|
+
headers: { "content-type": "application/json" },
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
async deleteMoment(momentId) {
|
|
176
|
+
return await call("DELETE", `/v1/moments/${encodeURIComponent(String(momentId))}`);
|
|
177
|
+
},
|
|
178
|
+
async toggleMomentReaction(params) {
|
|
179
|
+
return await call("POST", `/v1/moments/${encodeURIComponent(String(params.momentId))}/reactions`, {
|
|
180
|
+
body: JSON.stringify({ emoji: params.emoji }),
|
|
181
|
+
headers: { "content-type": "application/json" },
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
async createMomentComment(params) {
|
|
185
|
+
return await call("POST", `/v1/moments/${encodeURIComponent(String(params.momentId))}/comments`, {
|
|
186
|
+
body: JSON.stringify({ text: params.text }),
|
|
187
|
+
headers: { "content-type": "application/json" },
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
async replyMomentComment(params) {
|
|
191
|
+
return await call("POST", `/v1/moments/${encodeURIComponent(String(params.momentId))}/comments`, {
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
text: params.text,
|
|
194
|
+
reply_to_comment_id: params.replyToCommentId,
|
|
195
|
+
}),
|
|
196
|
+
headers: { "content-type": "application/json" },
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
async deleteMomentComment(params) {
|
|
200
|
+
return await call("DELETE", `/v1/moments/${encodeURIComponent(String(params.momentId))}/comments/${encodeURIComponent(String(params.commentId))}`);
|
|
201
|
+
},
|
|
202
|
+
async getConversation(conversationId) {
|
|
203
|
+
return await call("GET", `/v1/conversations/${encodeURIComponent(conversationId)}`);
|
|
204
|
+
},
|
|
205
|
+
async patchConversation(conversationId, patch) {
|
|
206
|
+
assertNonBlankId(conversationId, "patchConversation: conversationId");
|
|
207
|
+
const body = pickPatch(patch, ["title", "description"], "patchConversation");
|
|
208
|
+
return await call("PATCH", `/v1/conversations/${encodeURIComponent(conversationId)}`, {
|
|
209
|
+
body: JSON.stringify(body),
|
|
210
|
+
headers: { "content-type": "application/json" },
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
async updateMyProfile(patch) {
|
|
214
|
+
return await call("PATCH", `/v1/users/me`, {
|
|
215
|
+
body: JSON.stringify(patch),
|
|
216
|
+
headers: { "content-type": "application/json" },
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
async agentsConnect({ code: inviteCode, platform, type }) {
|
|
220
|
+
if (!inviteCode?.trim()) {
|
|
221
|
+
throw new ClawlingApiError("validation", "agentsConnect: inviteCode is required");
|
|
222
|
+
}
|
|
223
|
+
if (!platform?.trim()) {
|
|
224
|
+
throw new ClawlingApiError("validation", "agentsConnect: platform is required");
|
|
225
|
+
}
|
|
226
|
+
if (!type?.trim()) {
|
|
227
|
+
throw new ClawlingApiError("validation", "agentsConnect: type is required");
|
|
228
|
+
}
|
|
229
|
+
return await call("POST", "/v1/agents/connect", {
|
|
230
|
+
// `X-Device-Id` is added globally via `authHeaders` on every request.
|
|
231
|
+
headers: { "content-type": "application/json" },
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
code: inviteCode.trim(),
|
|
234
|
+
platform: platform.trim(),
|
|
235
|
+
type: type.trim(),
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
async uploadMedia(params) {
|
|
240
|
+
const blob = new Blob([new Uint8Array(params.buffer)], {
|
|
241
|
+
type: params.mime ?? "application/octet-stream",
|
|
242
|
+
});
|
|
243
|
+
const file = new File([blob], params.filename, {
|
|
244
|
+
type: params.mime ?? "application/octet-stream",
|
|
245
|
+
});
|
|
246
|
+
const fd = new FormData();
|
|
247
|
+
fd.set("file", file);
|
|
248
|
+
const data = await call("POST", "/media/upload", { body: fd });
|
|
249
|
+
return parseUploadResult(data, "/media/upload");
|
|
250
|
+
},
|
|
251
|
+
async uploadAvatar(params) {
|
|
252
|
+
const blob = new Blob([new Uint8Array(params.buffer)], {
|
|
253
|
+
type: params.mime ?? "application/octet-stream",
|
|
254
|
+
});
|
|
255
|
+
const file = new File([blob], params.filename, {
|
|
256
|
+
type: params.mime ?? "application/octet-stream",
|
|
257
|
+
});
|
|
258
|
+
const fd = new FormData();
|
|
259
|
+
fd.set("file", file);
|
|
260
|
+
return await call("POST", "/v1/files/upload-url", { body: fd });
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-facing types for the Clawling Chat HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Field names mirror the upstream API (snake_case) so we don't lose
|
|
5
|
+
* fidelity on responses. Tool schemas accept camelCase externally and
|
|
6
|
+
* translate at the api-client boundary.
|
|
7
|
+
*/
|
|
8
|
+
export class ClawlingApiError extends Error {
|
|
9
|
+
kind;
|
|
10
|
+
meta;
|
|
11
|
+
constructor(kind, message, meta) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.kind = kind;
|
|
14
|
+
this.meta = meta;
|
|
15
|
+
this.name = "ClawlingApiError";
|
|
16
|
+
}
|
|
17
|
+
}
|