@clawling/clawchat-plugin-openclaw 2026.10.21-1 → 2026.10.27-1
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 +124 -22
- package/dist/index.js +5 -2
- package/dist/src/api-client.js +46 -28
- package/dist/src/cli.js +74 -0
- package/dist/src/commands.js +96 -30
- package/dist/src/config.js +8 -4
- package/dist/src/framework-error-copy.js +1 -1
- package/dist/src/gate-outcome.js +4 -9
- package/dist/src/host-tools-guard.js +31 -7
- package/dist/src/orchestration.js +29 -0
- package/dist/src/outbound.js +315 -190
- package/dist/src/owner-sibling-notify.js +9 -4
- package/dist/src/reply-dispatcher.js +48 -16
- package/dist/src/runtime.js +92 -25
- package/dist/src/skill-update.js +9 -10
- package/dist/src/storage.js +48 -22
- package/dist/src/tool-capabilities.js +20 -1
- package/dist/src/tool-grants.js +9 -8
- package/dist/src/tools-schema.js +18 -9
- package/dist/src/tools.js +125 -36
- package/dist/src/turn-grants.js +61 -0
- package/dist/src/widget-guide.js +137 -0
- package/dist/src/widget-send.js +172 -0
- package/dist/src/widget-target.js +90 -0
- package/dist/src/ws-client.js +1 -1
- package/index.ts +9 -2
- package/openclaw.plugin.json +11 -2
- package/package.json +3 -3
- package/skills/clawchat-core/SKILL.md +144 -7
- package/skills/clawchat-liveware/SKILL.md +37 -25
- package/skills/clawchat-liveware-sample/SKILL.md +18 -7
- package/skills/manifest.json +18 -18
- package/src/api-client.ts +65 -43
- package/src/cli.ts +77 -0
- package/src/commands.ts +106 -26
- package/src/config.ts +10 -5
- package/src/framework-error-copy.ts +1 -1
- package/src/gate-outcome.ts +6 -1
- package/src/group-message-coalescer.ts +2 -2
- package/src/host-tools-guard.ts +33 -7
- package/src/orchestration.ts +35 -0
- package/src/outbound.ts +343 -215
- package/src/owner-sibling-notify.ts +9 -3
- package/src/protocol-types.ts +6 -0
- package/src/reply-dispatcher.ts +45 -17
- package/src/runtime.ts +93 -26
- package/src/skill-update.ts +9 -10
- package/src/storage.ts +75 -38
- package/src/tool-capabilities.ts +19 -1
- package/src/tool-grants.ts +10 -9
- package/src/tools-schema.ts +19 -9
- package/src/tools.ts +115 -23
- package/src/turn-grants.ts +58 -0
- package/src/widget-guide.ts +137 -0
- package/src/widget-send.ts +169 -0
- package/src/widget-target.ts +91 -0
- package/src/ws-client.ts +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ OpenClaw channel plugin that connects an agent to ClawChat over ClawChat Protoco
|
|
|
6
6
|
|
|
7
7
|
- Plugin-owned WebSocket transport with auto-reconnect (exponential backoff + jitter), heartbeat, and ack tracking
|
|
8
8
|
- Inbound `message.send` / `message.reply` with reply context
|
|
9
|
-
- Outbound text replies as complete `message.send` / `message.reply` frames
|
|
9
|
+
- Outbound text replies as complete `message.send` / `message.reply` frames by default; experimental `streamReplies: true` streams direct-chat replies (see [Reply Streaming](./docs/clawchat-plugin-openclaw.md#reply-streaming-experimental))
|
|
10
10
|
- Typing indicators and filtered forwarding for thinking / tool-call content
|
|
11
11
|
- Media fragments (image / file / audio / video) in either direction
|
|
12
12
|
- Invite-code onboarding (no raw credentials) via `/clawchat-activate` or supported `openclaw channels add`, plus always-registered `clawchat_*` account/media tools
|
|
@@ -34,7 +34,7 @@ into OpenClaw:
|
|
|
34
34
|
- **Runtime slash command (recommended).** Send `/clawchat-activate K7RM4TQP`
|
|
35
35
|
in the chat where OpenClaw is running. Not a shell command — running
|
|
36
36
|
`openclaw clawchat-activate` is expected to fail. This is the reliable
|
|
37
|
-
first-time activation path on OpenClaw 2026.
|
|
37
|
+
first-time activation path on supported hosts (OpenClaw 2026.8.1 and newer).
|
|
38
38
|
- **CLI `channels add`** when the host exposes `clawchat-plugin-openclaw` in its
|
|
39
39
|
channel catalog:
|
|
40
40
|
|
|
@@ -57,10 +57,9 @@ entry, and ensures plugin + tool policy lists cover `clawchat-plugin-openclaw`,
|
|
|
57
57
|
all in one config mutation. A Gateway with config reload/hot restart picks
|
|
58
58
|
up the new runtime; otherwise `openclaw gateway restart` is required.
|
|
59
59
|
|
|
60
|
-
See [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
expected `openclaw.json` shape after activation succeeds.
|
|
60
|
+
See [activation](./docs/activation.md) for persisted fields, stale-catalog
|
|
61
|
+
recovery and bootstrap; [runtime registration](./docs/runtime-and-connection.md#correct-setupruntime-implementation-pattern)
|
|
62
|
+
covers the config mutation and host reload boundary.
|
|
64
63
|
|
|
65
64
|
## Standalone web chat (dev)
|
|
66
65
|
|
|
@@ -95,7 +94,7 @@ src/
|
|
|
95
94
|
per-group idle/max batching for non-mention turns
|
|
96
95
|
group-settings.ts per-group agent settings cache (GET /v1/agents/me/
|
|
97
96
|
group-settings)
|
|
98
|
-
reply-dispatcher.ts outbound reply routing (
|
|
97
|
+
reply-dispatcher.ts outbound reply routing (optional direct-chat streaming)
|
|
99
98
|
no-reply.ts clawchat:no-reply / :silent suppression token guard
|
|
100
99
|
login.runtime.ts invite-code exchange flow
|
|
101
100
|
refresh-manager.ts ClawChat token refresh + auto-logout orchestration
|
|
@@ -130,9 +129,17 @@ tools/
|
|
|
130
129
|
standalone-webchat.html
|
|
131
130
|
docs/
|
|
132
131
|
README.md typed index of the docs/ tree
|
|
133
|
-
clawchat-plugin-openclaw.md
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
clawchat-plugin-openclaw.md overview, config, replies, media, troubleshooting
|
|
133
|
+
runtime-and-connection.md SQLite, installation recovery, host entrypoints, WS logs
|
|
134
|
+
skill-updates.md bundled skills, consent, updates and tombstones
|
|
135
|
+
liveware-sample.md sample auto-boot, relaunch and process lifecycle
|
|
136
|
+
tools.md registered tools and argument/server constraints
|
|
137
|
+
tool-grants.md non-owner access, approval and local-media boundaries
|
|
138
|
+
client-integration.md Protocol v2 plugin delta and upstream pointers
|
|
139
|
+
activation.md activation entry points, persistence, and recovery
|
|
140
|
+
token-refresh.md token refresh and auto-logout; per-plugin differences
|
|
141
|
+
http-endpoints.md HTTP request inventory mapped to client methods
|
|
142
|
+
cc-memory.md legacy redirect to clawchat-memory.md
|
|
136
143
|
clawchat-memory.md file-backed memory contract (shared with Hermes)
|
|
137
144
|
mention-message.md clawchat_mention_message tool reference
|
|
138
145
|
per-group-mode.md per-chat_id groupMode / groupCommandMode overrides
|
|
@@ -147,22 +154,26 @@ docs/
|
|
|
147
154
|
Start at [`docs/README.md`](./docs/README.md) — the typed index covering
|
|
148
155
|
operators, in-chat agents, and plugin developers.
|
|
149
156
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
-
|
|
154
|
-
|
|
155
|
-
-
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
|
|
159
|
-
-
|
|
157
|
+
[`docs/clawchat-plugin-openclaw.md`](./docs/clawchat-plugin-openclaw.md) is the
|
|
158
|
+
configuration, replies/media and troubleshooting overview. Detailed contracts:
|
|
159
|
+
|
|
160
|
+
- [Runtime and connection](./docs/runtime-and-connection.md): SQLite, installation
|
|
161
|
+
conflicts, reclaim, host registration, WebSocket delivery and log templates.
|
|
162
|
+
- [Skill updates](./docs/skill-updates.md): six bundled skills, consent, integrity
|
|
163
|
+
checks and managed-skill cleanup.
|
|
164
|
+
- [Liveware Sample](./docs/liveware-sample.md): bootstrap/relaunch, isolated CLI
|
|
165
|
+
homes, persistence, restart budgets and recovery.
|
|
166
|
+
- [Tools](./docs/tools.md) and [tool grants](./docs/tool-grants.md): registered
|
|
167
|
+
operations and non-owner access boundaries.
|
|
168
|
+
- [Activation](./docs/activation.md), [memory and prompts](./docs/clawchat-memory.md),
|
|
169
|
+
[HTTP inventory](./docs/http-endpoints.md) and [Protocol v2](./docs/client-integration.md)
|
|
170
|
+
cover their respective interfaces; the server implementation wins for wire formats.
|
|
160
171
|
|
|
161
172
|
## Development
|
|
162
173
|
|
|
163
174
|
```bash
|
|
164
175
|
# Tests
|
|
165
|
-
|
|
176
|
+
npm test -- --run
|
|
166
177
|
|
|
167
178
|
# Typecheck
|
|
168
179
|
npm run typecheck
|
|
@@ -170,7 +181,7 @@ npm run typecheck
|
|
|
170
181
|
|
|
171
182
|
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/`.
|
|
172
183
|
|
|
173
|
-
Functional E2E test cases are documented in the
|
|
184
|
+
Functional E2E test cases are documented in the aggregator repo's `e2e/`
|
|
174
185
|
docs; keep that guide updated when adding or changing E2E flows.
|
|
175
186
|
|
|
176
187
|
For OpenClaw host SDK/source lookup while developing this plugin, optionally
|
|
@@ -184,6 +195,97 @@ npm run dev:openclaw-source
|
|
|
184
195
|
This checkout is local-only. It is ignored by git and is not required to run the
|
|
185
196
|
plugin tests or publish the package.
|
|
186
197
|
|
|
198
|
+
## Branches and releasing
|
|
199
|
+
|
|
200
|
+
Everything reaches `main` through a GitHub PR; nothing is pushed to `main`
|
|
201
|
+
directly.
|
|
202
|
+
|
|
203
|
+
- Day-to-day, non-urgent changes are merged into the `pre-release` branch, not
|
|
204
|
+
`main`, also through a GitHub PR (`--base pre-release`) that is
|
|
205
|
+
squash-merged. `main` only moves when a release is cut, so it always equals
|
|
206
|
+
the newest published version.
|
|
207
|
+
- An urgent fix (security, widespread connection failure) may target `main`
|
|
208
|
+
directly with a squash-merged PR that includes the version bump; afterwards
|
|
209
|
+
merge `main` back into `pre-release`.
|
|
210
|
+
|
|
211
|
+
A release runs in this fixed order — never publish first and merge later, and
|
|
212
|
+
never tag a commit that is not on `main`:
|
|
213
|
+
|
|
214
|
+
1. **Bump the version on `pre-release`.** Set `package.json` `version` to the
|
|
215
|
+
new `YYYY.M.D-N` (no zero padding, e.g. `2026.10.7-1`; `-N` counts releases
|
|
216
|
+
on the same day) in a commit titled `chore(release): openclaw <old> -> <new>`,
|
|
217
|
+
and run `npm test -- --run` and `npm run typecheck`.
|
|
218
|
+
2. **Merge `pre-release` into `main` through a PR, with a merge commit:**
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
(
|
|
222
|
+
set -e
|
|
223
|
+
gh pr create --base main --head pre-release --title "release: <version>"
|
|
224
|
+
gh pr merge <n> --merge # merge commit, not squash
|
|
225
|
+
test "$(gh pr view <n> --json state --jq .state)" = MERGED
|
|
226
|
+
git fetch origin
|
|
227
|
+
git push origin origin/main:pre-release # fast-forward pre-release to main (not a force push)
|
|
228
|
+
)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
3. **Tag the merge commit on `main` and push the tag.** The tag name equals
|
|
232
|
+
the `package.json` version, without a `v`; `<n>` is the release PR from
|
|
233
|
+
step 2, whose merge commit is tagged (not whatever `main` points to now):
|
|
234
|
+
|
|
235
|
+
Use the PR's merge commit because the floating `origin/main` may already
|
|
236
|
+
contain a later merge.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
(
|
|
240
|
+
set -e
|
|
241
|
+
git fetch origin --tags
|
|
242
|
+
release_commit=$(gh pr view <n> --json mergeCommit --jq .mergeCommit.oid)
|
|
243
|
+
git merge-base --is-ancestor "$release_commit" origin/main
|
|
244
|
+
git tag -a <version> "$release_commit" -m "<version>: <one-line summary>"
|
|
245
|
+
git push origin <version>
|
|
246
|
+
)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
4. **Publish to npm from a clean checkout of the tag:**
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
(
|
|
253
|
+
set -e
|
|
254
|
+
git worktree add --detach .worktrees/release-<version> <version>
|
|
255
|
+
cd .worktrees/release-<version>
|
|
256
|
+
test -z "$(git status --porcelain)" # must be clean
|
|
257
|
+
npm install # no lockfile is committed (package-lock.json is gitignored)
|
|
258
|
+
npm test -- --run
|
|
259
|
+
npm run release # npm publish --tag latest; guard, typecheck, then prepack builds dist/
|
|
260
|
+
)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The subshell stops at the first failing command, so a failed install or
|
|
264
|
+
test never reaches `npm run release`.
|
|
265
|
+
|
|
266
|
+
Every `npm publish` runs the release guard through `prepublishOnly`, before
|
|
267
|
+
typecheck and before `prepack` changes the manifest. The guard requires a
|
|
268
|
+
local annotated tag named exactly after `package.json`'s version, HEAD at
|
|
269
|
+
that tag's commit, and a clean working tree (including non-ignored untracked
|
|
270
|
+
files). It checks that `origin` has the same tag object, fetches `origin/main`,
|
|
271
|
+
and requires the tag commit to be an ancestor of that refreshed branch.
|
|
272
|
+
Any Git error blocks publishing; there is no npm dist-tag exemption.
|
|
273
|
+
Run `npm publish --dry-run` from the tag checkout to exercise the guard.
|
|
274
|
+
Never skip the guard; fix the checkout instead.
|
|
275
|
+
|
|
276
|
+
5. **Record the release.** Use the commit SHA tagged in step 3 as `<sha>`:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
gh release create <version> --verify-tag --title <version> --notes "commit: <sha>
|
|
280
|
+
npm: @clawling/clawchat-plugin-openclaw@<version>"
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Add later results with `gh release edit <version> --notes "<updated notes>"`,
|
|
284
|
+
keeping the commit and npm version in the notes; never move the tag.
|
|
285
|
+
|
|
286
|
+
Tag messages, PR titles, PR bodies and release notes are public like commit
|
|
287
|
+
messages.
|
|
288
|
+
|
|
187
289
|
## License
|
|
188
290
|
|
|
189
291
|
MIT — see [LICENSE](./LICENSE).
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
2
2
|
import { openclawClawlingPlugin } from "./src/channel.js";
|
|
3
|
-
import { registerOpenclawClawlingCommands } from "./src/commands.js";
|
|
3
|
+
import { registerInstallationCli, registerInstallationGatewayMethods, registerOpenclawClawlingCommands, } from "./src/commands.js";
|
|
4
4
|
import { openclawClawlingConfigSchema } from "./src/config.js";
|
|
5
5
|
import { registerHostToolsGuard } from "./src/host-tools-guard.js";
|
|
6
6
|
import { registerClawChatPromptInjection, } from "./src/prompt-injection.js";
|
|
@@ -29,7 +29,7 @@ const channelEntry = defineChannelPluginEntry({
|
|
|
29
29
|
},
|
|
30
30
|
});
|
|
31
31
|
/**
|
|
32
|
-
* Chat slash commands
|
|
32
|
+
* Chat slash commands and installation gateway methods are registered
|
|
33
33
|
* in EVERY registration mode except `cli-metadata`, not inside `registerFull`.
|
|
34
34
|
*
|
|
35
35
|
* The host loads the plugin more than once per process — a `full` load and,
|
|
@@ -59,8 +59,11 @@ export default {
|
|
|
59
59
|
...channelEntry,
|
|
60
60
|
register: (api) => {
|
|
61
61
|
channelEntry.register(api);
|
|
62
|
+
// Every mode, including cli-metadata: CLI discovery loads that mode.
|
|
63
|
+
registerInstallationCli(api);
|
|
62
64
|
if (api.registrationMode !== "cli-metadata") {
|
|
63
65
|
registerOpenclawClawlingCommands(api);
|
|
66
|
+
registerInstallationGatewayMethods(api);
|
|
64
67
|
if (!FULL_REGISTRATION_MODES.has(String(api.registrationMode)))
|
|
65
68
|
registerHostToolsGuard(api);
|
|
66
69
|
}
|
package/dist/src/api-client.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { orchestrationOriginHeaders } from "./orchestration.js";
|
|
2
|
+
export { orchestrationOriginHeaders } from "./orchestration.js";
|
|
1
3
|
import { ClawlingApiError, } from "./api-types.js";
|
|
2
4
|
import { CHANNEL_ID } from "./config.js";
|
|
3
5
|
const ORCH = "/v1/agents/me/orchestration";
|
|
@@ -80,7 +82,8 @@ export const REFRESH_REQUEST_TIMEOUT_MS = 20_000;
|
|
|
80
82
|
* Unauthenticated: the refresh token in the body IS the credential, so we send
|
|
81
83
|
* NO `Authorization` header. `X-Device-Id` MUST equal the connect-time device
|
|
82
84
|
* id (the backend rejects on `sess.DeviceID != X-Device-Id`). The endpoint is
|
|
83
|
-
*
|
|
85
|
+
* HTTP 200 by contract — branch on the envelope `code`; numeric `10003` also
|
|
86
|
+
* proves invalid refresh credentials on non-200 responses. This is
|
|
84
87
|
* a standalone function (not a method on the token-bearing client) precisely
|
|
85
88
|
* because no bearer token participates.
|
|
86
89
|
*
|
|
@@ -137,8 +140,22 @@ async function authRefreshAttempt(baseUrl, fetchImpl, params, signal) {
|
|
|
137
140
|
};
|
|
138
141
|
}
|
|
139
142
|
if (res.status !== 200) {
|
|
140
|
-
//
|
|
143
|
+
// Explicit invalid-refresh credentials are PERMANENT regardless of HTTP
|
|
144
|
+
// status, matching Hermes; retries must not hide the backend's rejection.
|
|
141
145
|
const text = await res.text().catch(() => "");
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(text);
|
|
148
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) &&
|
|
149
|
+
typeof parsed.code === "number" && parsed.code === CODE_INVALID_REFRESH) {
|
|
150
|
+
const message = (typeof parsed.msg === "string" && parsed.msg) ||
|
|
151
|
+
(typeof parsed.message === "string" && parsed.message) ||
|
|
152
|
+
`refresh http ${res.status} code=10003`;
|
|
153
|
+
return { kind: "permanent", code: CODE_INVALID_REFRESH, message };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Unparseable bodies do not prove invalid credentials; keep them transient.
|
|
158
|
+
}
|
|
142
159
|
return {
|
|
143
160
|
kind: "transient",
|
|
144
161
|
status: res.status,
|
|
@@ -316,8 +333,9 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
316
333
|
* build a raw client the same way. Proactive rotation is still picked up here for
|
|
317
334
|
* free, since the token is read fresh from the account at call time.
|
|
318
335
|
*/
|
|
319
|
-
async function orchCall(method, path, init) {
|
|
320
|
-
const
|
|
336
|
+
async function orchCall(method, path, origin, init) {
|
|
337
|
+
const headers = orchestrationOriginHeaders(origin);
|
|
338
|
+
const res = await doFetch(method, path, { ...init, headers: { ...init?.headers, ...headers } });
|
|
321
339
|
if (res.status === 401 || res.status === 403) {
|
|
322
340
|
throw new ClawlingApiError("auth", `unauthorized (status ${res.status})`, {
|
|
323
341
|
status: res.status,
|
|
@@ -453,34 +471,34 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
453
471
|
assertNonBlankId(friendUserId, "removeFriend: friendUserId");
|
|
454
472
|
return await call("DELETE", `/v1/friendships/${encodeURIComponent(friendUserId)}`);
|
|
455
473
|
},
|
|
456
|
-
async orchListAgents() {
|
|
457
|
-
return await orchCall("GET", `${ORCH}/agents
|
|
474
|
+
async orchListAgents(origin) {
|
|
475
|
+
return await orchCall("GET", `${ORCH}/agents`, origin);
|
|
458
476
|
},
|
|
459
|
-
async orchGetAgent(agentId) {
|
|
460
|
-
return await orchCall("GET", `${ORCH}/agents/${encodeURIComponent(agentId)}
|
|
477
|
+
async orchGetAgent(agentId, origin) {
|
|
478
|
+
return await orchCall("GET", `${ORCH}/agents/${encodeURIComponent(agentId)}`, origin);
|
|
461
479
|
},
|
|
462
|
-
async orchSetAgentBehavior(agentId, behavior) {
|
|
463
|
-
return await orchCall("PATCH", `${ORCH}/agents/${encodeURIComponent(agentId)}`, orchJson({ behavior }));
|
|
480
|
+
async orchSetAgentBehavior(agentId, behavior, origin) {
|
|
481
|
+
return await orchCall("PATCH", `${ORCH}/agents/${encodeURIComponent(agentId)}`, origin, orchJson({ behavior }));
|
|
464
482
|
},
|
|
465
|
-
async orchListGroups() {
|
|
466
|
-
return await orchCall("GET", `${ORCH}/groups
|
|
483
|
+
async orchListGroups(origin) {
|
|
484
|
+
return await orchCall("GET", `${ORCH}/groups`, origin);
|
|
467
485
|
},
|
|
468
|
-
async orchGetGroup(cid) {
|
|
469
|
-
return await orchCall("GET", `${ORCH}/groups/${encodeURIComponent(cid)}
|
|
486
|
+
async orchGetGroup(cid, origin) {
|
|
487
|
+
return await orchCall("GET", `${ORCH}/groups/${encodeURIComponent(cid)}`, origin);
|
|
470
488
|
},
|
|
471
|
-
async orchSetGroupPrompt(cid, description) {
|
|
472
|
-
return await orchCall("PATCH", `${ORCH}/groups/${encodeURIComponent(cid)}`, orchJson({ description }));
|
|
489
|
+
async orchSetGroupPrompt(cid, description, origin) {
|
|
490
|
+
return await orchCall("PATCH", `${ORCH}/groups/${encodeURIComponent(cid)}`, origin, orchJson({ description }));
|
|
473
491
|
},
|
|
474
|
-
async orchCreateGroup(title, agentIds) {
|
|
475
|
-
return await orchCall("POST", `${ORCH}/groups`, orchJson({ title, agent_ids: agentIds }));
|
|
492
|
+
async orchCreateGroup(title, agentIds, origin) {
|
|
493
|
+
return await orchCall("POST", `${ORCH}/groups`, origin, orchJson({ title, agent_ids: agentIds }));
|
|
476
494
|
},
|
|
477
|
-
async orchAddGroupMember(cid, agentId) {
|
|
478
|
-
return await orchCall("POST", `${ORCH}/groups/${encodeURIComponent(cid)}/members`, orchJson({ agent_id: agentId }));
|
|
495
|
+
async orchAddGroupMember(cid, agentId, origin) {
|
|
496
|
+
return await orchCall("POST", `${ORCH}/groups/${encodeURIComponent(cid)}/members`, origin, orchJson({ agent_id: agentId }));
|
|
479
497
|
},
|
|
480
|
-
async orchRemoveGroupMember(cid, agentId) {
|
|
481
|
-
return await orchCall("DELETE", `${ORCH}/groups/${encodeURIComponent(cid)}/members/${encodeURIComponent(agentId)}
|
|
498
|
+
async orchRemoveGroupMember(cid, agentId, origin) {
|
|
499
|
+
return await orchCall("DELETE", `${ORCH}/groups/${encodeURIComponent(cid)}/members/${encodeURIComponent(agentId)}`, origin);
|
|
482
500
|
},
|
|
483
|
-
async orchSetGroupAgentSettings(cid, agentId, patch) {
|
|
501
|
+
async orchSetGroupAgentSettings(cid, agentId, patch, origin) {
|
|
484
502
|
// `!== undefined` rather than truthiness: `muted: false` is a real value
|
|
485
503
|
// the backend must see; an absent key means "leave unchanged".
|
|
486
504
|
const body = {};
|
|
@@ -490,13 +508,13 @@ export function createOpenclawClawlingApiClient(opts) {
|
|
|
490
508
|
body.reply_mode = patch.replyMode;
|
|
491
509
|
if (patch.batchDelaySeconds !== undefined)
|
|
492
510
|
body.batch_delay_seconds = patch.batchDelaySeconds;
|
|
493
|
-
return await orchCall("PATCH", `${ORCH}/groups/${encodeURIComponent(cid)}/agents/${encodeURIComponent(agentId)}`, orchJson(body));
|
|
511
|
+
return await orchCall("PATCH", `${ORCH}/groups/${encodeURIComponent(cid)}/agents/${encodeURIComponent(agentId)}`, origin, orchJson(body));
|
|
494
512
|
},
|
|
495
|
-
async orchCreateConnectCode() {
|
|
496
|
-
return await orchCall("POST", `${ORCH}/connect-codes
|
|
513
|
+
async orchCreateConnectCode(origin) {
|
|
514
|
+
return await orchCall("POST", `${ORCH}/connect-codes`, origin);
|
|
497
515
|
},
|
|
498
|
-
async orchGetConnectCode(code) {
|
|
499
|
-
return await orchCall("GET", `${ORCH}/connect-codes/${encodeURIComponent(code)}
|
|
516
|
+
async orchGetConnectCode(code, origin) {
|
|
517
|
+
return await orchCall("GET", `${ORCH}/connect-codes/${encodeURIComponent(code)}`, origin);
|
|
500
518
|
},
|
|
501
519
|
async registerApp(params) {
|
|
502
520
|
assertNonBlankId(params.appId, "registerApp: appId");
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as gatewayRuntime from "openclaw/plugin-sdk/gateway-runtime";
|
|
2
|
+
/** Older SDKs have no public URL redactor. In that case omit error text entirely. */
|
|
3
|
+
function safeRpcError(error, opts) {
|
|
4
|
+
const name = error instanceof Error && /^(?:Error|[A-Za-z][A-Za-z0-9]*Error)$/u.test(error.name)
|
|
5
|
+
? error.name : "Error";
|
|
6
|
+
const redact = "redactSensitiveUrlLikeString" in gatewayRuntime ? gatewayRuntime.redactSensitiveUrlLikeString : undefined;
|
|
7
|
+
if (typeof redact === "function" && error instanceof Error) {
|
|
8
|
+
try {
|
|
9
|
+
let message = error.message;
|
|
10
|
+
// Never echo explicitly supplied gateway credentials, even outside URLs.
|
|
11
|
+
for (const secret of [opts.token, opts.password]) {
|
|
12
|
+
if (secret)
|
|
13
|
+
message = message.replaceAll(secret, "[redacted]");
|
|
14
|
+
}
|
|
15
|
+
return redact(message).replace(/[\r\n]+/gu, " ").slice(0, 240);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return name;
|
|
22
|
+
}
|
|
23
|
+
export function registerClawChatCli(program, api) {
|
|
24
|
+
const root = program.command("clawchat").description("ClawChat installation conflict tools");
|
|
25
|
+
for (const action of ["reclaim", "status"]) {
|
|
26
|
+
const sub = root.command(action)
|
|
27
|
+
.description(action === "reclaim" ? "Reclaim this ClawChat installation" : "Show ClawChat installation status")
|
|
28
|
+
.option("--account <id>", "ClawChat account id (default: default)")
|
|
29
|
+
.option("--offline", "Operate on local storage; use only while the gateway is stopped (cannot clear a running gateway's memory stop)")
|
|
30
|
+
.option("--json", "Print the result as JSON");
|
|
31
|
+
gatewayRuntime.addGatewayClientOptions(sub);
|
|
32
|
+
sub.action(async (opts) => {
|
|
33
|
+
await runCliAction(action, opts, api);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function runCliAction(action, opts, api) {
|
|
38
|
+
// The registrar builds only Commander metadata; storage/runtime work starts
|
|
39
|
+
// after a leaf command is invoked, and only offline calls open local storage.
|
|
40
|
+
const { resolveInstallationAccountId, runInstallationCommand } = await import("./commands.js");
|
|
41
|
+
let accountId;
|
|
42
|
+
try {
|
|
43
|
+
accountId = resolveInstallationAccountId(opts.account);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.error("--account requires a valid non-empty account id");
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
if (opts.offline) {
|
|
53
|
+
result = await runInstallationCommand(action, accountId, { stateDir: api.runtime.state?.resolveStateDir?.() });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const response = await gatewayRuntime.callGatewayFromCli(`clawchat.${action}`, opts, { accountId });
|
|
57
|
+
if (typeof response.ok !== "boolean" || typeof response.text !== "string") {
|
|
58
|
+
throw new Error("Invalid gateway response");
|
|
59
|
+
}
|
|
60
|
+
result = { ok: response.ok, text: response.text };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(`ClawChat ${action} failed: ${safeRpcError(error, opts)}`);
|
|
65
|
+
if (!opts.offline) {
|
|
66
|
+
console.error(`If the OpenClaw gateway is not running, use \`openclaw clawchat ${action} --offline\` to operate on local storage directly.`);
|
|
67
|
+
}
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
console.log(opts.json ? JSON.stringify(result) : result.text);
|
|
72
|
+
if (!result.ok)
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
package/dist/src/commands.js
CHANGED
|
@@ -73,6 +73,100 @@ function extractActivationIntent(value) {
|
|
|
73
73
|
function errorMessage(err) {
|
|
74
74
|
return err instanceof Error ? err.message : String(err);
|
|
75
75
|
}
|
|
76
|
+
const INSTALLATION_COMMAND_FAILED = "ClawChat installation conflict storage unavailable; command failed.";
|
|
77
|
+
/**
|
|
78
|
+
* Reject malformed explicit IDs rather than accidentally targeting default.
|
|
79
|
+
* Validity follows the host's account-id normalizer: an ID is accepted only if
|
|
80
|
+
* it is already canonical (up to case), so IDs the host would rewrite or map
|
|
81
|
+
* to `default` are rejected.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveInstallationAccountId(value) {
|
|
84
|
+
if (value === undefined)
|
|
85
|
+
return normalizeOpenclawClawlingAccountId(undefined);
|
|
86
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
87
|
+
const normalized = normalizeOpenclawClawlingAccountId(trimmed);
|
|
88
|
+
if (!trimmed || normalized !== trimmed.toLowerCase()) {
|
|
89
|
+
throw new Error("accountId must be a valid non-empty account id");
|
|
90
|
+
}
|
|
91
|
+
return normalized;
|
|
92
|
+
}
|
|
93
|
+
/** One implementation for slash commands, gateway RPC and offline CLI. */
|
|
94
|
+
export async function runInstallationCommand(action, accountId, deps = {}) {
|
|
95
|
+
try {
|
|
96
|
+
const store = getClawChatStore(deps.stateDir ? { dbPath: clawChatDbPathForStateDir(deps.stateDir) } : {});
|
|
97
|
+
const lookup = store.lookupSuperseded(accountId);
|
|
98
|
+
if (lookup.status === "unavailable")
|
|
99
|
+
return { ok: false, text: "ClawChat installation conflict storage unavailable; try again after storage recovers (a gateway restart also drops a stop that could not be saved)." };
|
|
100
|
+
if (action === "reclaim") {
|
|
101
|
+
if (lookup.status === "absent" && !hasReclaimWaiter(accountId) && !recalledSuperseded(accountId))
|
|
102
|
+
return { ok: true, text: "This ClawChat installation is not superseded; nothing to reclaim." };
|
|
103
|
+
if (!store.clearSuperseded(accountId))
|
|
104
|
+
return { ok: false, text: "Unable to clear ClawChat installation conflict; reclaim failed." };
|
|
105
|
+
rememberSuperseded(accountId, null);
|
|
106
|
+
return { ok: true, text: signalReclaim(accountId)
|
|
107
|
+
? "ClawChat reclaim requested; reconnecting this installation."
|
|
108
|
+
: "ClawChat conflict cleared; restart the OpenClaw gateway to reconnect this installation." };
|
|
109
|
+
}
|
|
110
|
+
const fields = installationStatus(lookup.status === "row" ? lookup.row : recalledSuperseded(accountId), resolveInstallId(store));
|
|
111
|
+
const connection = fields.superseded ? "superseded" : deps.connectionState
|
|
112
|
+
? await deps.connectionState(accountId) ?? "disconnected"
|
|
113
|
+
: "unknown (gateway not reachable; offline snapshot)";
|
|
114
|
+
return { ok: true, text: [
|
|
115
|
+
`ClawChat account: ${accountId}`, `install_id: ${fields.installId ?? "unavailable"}`,
|
|
116
|
+
`connection: ${connection}`, `superseded: ${fields.superseded}`,
|
|
117
|
+
`superseded_at: ${fields.supersededAt ? new Date(fields.supersededAt).toISOString() : "-"}`,
|
|
118
|
+
`by_client_version: ${fields.byClientVersion ?? "unknown"}`,
|
|
119
|
+
`owner_notified: ${Boolean(fields.ownerNotifiedAt)}`,
|
|
120
|
+
].join("\n") };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { ok: false, text: INSTALLATION_COMMAND_FAILED };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function runLiveInstallationCommand(api, action, accountId) {
|
|
127
|
+
try {
|
|
128
|
+
return await runInstallationCommand(action, accountId, {
|
|
129
|
+
stateDir: api.runtime.state?.resolveStateDir?.(),
|
|
130
|
+
connectionState: async (id) => {
|
|
131
|
+
const { getOpenclawClawlingClient } = await import("./runtime.js");
|
|
132
|
+
return getOpenclawClawlingClient(id)?.state;
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { ok: false, text: INSTALLATION_COMMAND_FAILED };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function registerInstallationGatewayMethods(api) {
|
|
141
|
+
for (const action of ["reclaim", "status"]) {
|
|
142
|
+
api.registerGatewayMethod(`clawchat.${action}`, async ({ params, respond }) => {
|
|
143
|
+
let accountId;
|
|
144
|
+
try {
|
|
145
|
+
accountId = resolveInstallationAccountId(params.accountId);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
respond(false, undefined, { code: "INVALID_REQUEST", message: "accountId must be a valid non-empty account id" });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
respond(true, await runLiveInstallationCommand(api, action, accountId));
|
|
152
|
+
}, { scope: action === "reclaim" ? "operator.admin" : "operator.read" });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Register the `openclaw clawchat reclaim|status` CLI root. The implementation
|
|
157
|
+
* is imported lazily inside the registrar so registration never opens storage
|
|
158
|
+
* or starts a client. Keep this dynamic import in src/: tsc leaves the `.ts`
|
|
159
|
+
* extension of a dynamic import in the root index.ts unrewritten in dist.
|
|
160
|
+
*/
|
|
161
|
+
export function registerInstallationCli(api) {
|
|
162
|
+
api.registerCli(async ({ program }) => {
|
|
163
|
+
const { registerClawChatCli } = await import("./cli.js");
|
|
164
|
+
registerClawChatCli(program, api);
|
|
165
|
+
}, {
|
|
166
|
+
commands: ["clawchat"],
|
|
167
|
+
descriptors: [{ name: "clawchat", description: "ClawChat installation conflict tools", hasSubcommands: true }],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
76
170
|
function readRecord(value) {
|
|
77
171
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
78
172
|
? value
|
|
@@ -212,36 +306,8 @@ export function registerOpenclawClawlingCommands(api) {
|
|
|
212
306
|
if (flag.error)
|
|
213
307
|
return { text: `${flag.error}. Usage: /clawchat-${action} [--account <id>]` };
|
|
214
308
|
const accountId = flag.accountId ?? normalizeOpenclawClawlingAccountId(ctx.accountId);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const store = getClawChatStore(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {});
|
|
218
|
-
const lookup = store.lookupSuperseded(accountId);
|
|
219
|
-
if (lookup.status === "unavailable")
|
|
220
|
-
return { text: "ClawChat installation conflict storage unavailable; try again after storage recovers (a gateway restart also drops a stop that could not be saved)." };
|
|
221
|
-
if (action === "reclaim") {
|
|
222
|
-
if (lookup.status === "absent" && !hasReclaimWaiter(accountId) && !recalledSuperseded(accountId))
|
|
223
|
-
return { text: "This ClawChat installation is not superseded; nothing to reclaim." };
|
|
224
|
-
if (!store.clearSuperseded(accountId))
|
|
225
|
-
return { text: "Unable to clear ClawChat installation conflict; reclaim failed." };
|
|
226
|
-
rememberSuperseded(accountId, null);
|
|
227
|
-
return { text: signalReclaim(accountId)
|
|
228
|
-
? "ClawChat reclaim requested; reconnecting this installation."
|
|
229
|
-
: "ClawChat conflict cleared; restart the OpenClaw gateway to reconnect this installation." };
|
|
230
|
-
}
|
|
231
|
-
const fields = installationStatus(lookup.status === "row" ? lookup.row : recalledSuperseded(accountId), resolveInstallId(store));
|
|
232
|
-
const { getOpenclawClawlingClient } = await import("./runtime.js");
|
|
233
|
-
const connection = fields.superseded ? "superseded" : getOpenclawClawlingClient(accountId)?.state ?? "disconnected";
|
|
234
|
-
return { text: [
|
|
235
|
-
`ClawChat account: ${accountId}`, `install_id: ${fields.installId ?? "unavailable"}`,
|
|
236
|
-
`connection: ${connection}`, `superseded: ${fields.superseded}`,
|
|
237
|
-
`superseded_at: ${fields.supersededAt ? new Date(fields.supersededAt).toISOString() : "-"}`,
|
|
238
|
-
`by_client_version: ${fields.byClientVersion ?? "unknown"}`,
|
|
239
|
-
`owner_notified: ${Boolean(fields.ownerNotifiedAt)}`,
|
|
240
|
-
].join("\n") };
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
return { text: "ClawChat installation conflict storage unavailable; command failed." };
|
|
244
|
-
}
|
|
309
|
+
const result = await runLiveInstallationCommand(api, action, accountId);
|
|
310
|
+
return { text: result.text };
|
|
245
311
|
},
|
|
246
312
|
});
|
|
247
313
|
}
|
package/dist/src/config.js
CHANGED
|
@@ -35,10 +35,10 @@ export const DEFAULT_HEARTBEAT = {
|
|
|
35
35
|
timeout: 10_000,
|
|
36
36
|
};
|
|
37
37
|
export const DEFAULT_ACK = {
|
|
38
|
-
//
|
|
38
|
+
// Allow a slow server 15s before each same-frame timeout retry.
|
|
39
39
|
timeout: 15_000,
|
|
40
|
-
|
|
41
|
-
//
|
|
40
|
+
timeoutRetries: 2,
|
|
41
|
+
// Core ws-client policy; aligned sends use timeoutRetries.
|
|
42
42
|
autoResendOnTimeout: false,
|
|
43
43
|
};
|
|
44
44
|
export const openclawClawlingAccountConfigSchema = {
|
|
@@ -116,6 +116,7 @@ export const openclawClawlingAccountConfigSchema = {
|
|
|
116
116
|
additionalProperties: false,
|
|
117
117
|
properties: {
|
|
118
118
|
timeout: { type: "integer", minimum: 100 },
|
|
119
|
+
timeoutRetries: { type: "integer", minimum: 0, maximum: 5 },
|
|
119
120
|
autoResendOnTimeout: { type: "boolean" },
|
|
120
121
|
},
|
|
121
122
|
},
|
|
@@ -396,7 +397,7 @@ export function effectiveOutputVisibility(account, chatId, chatType) {
|
|
|
396
397
|
* `SESSION_INT_KEYS`). An out-of-range value is clamped to the nearest bound,
|
|
397
398
|
* a non-number falls back to the factory value. Every key here has a reader;
|
|
398
399
|
* the shared keys this plugin has no use for are listed in
|
|
399
|
-
* docs/clawchat-plugin-openclaw.md with the reason.
|
|
400
|
+
* docs/clawchat-plugin-openclaw.md#session-and-note-keys with the reason.
|
|
400
401
|
* key -> [factory, minimum, maximum]
|
|
401
402
|
*/
|
|
402
403
|
export const SESSION_INT_KEYS = {
|
|
@@ -439,6 +440,9 @@ function readAck(raw) {
|
|
|
439
440
|
const s = raw && typeof raw === "object" ? raw : {};
|
|
440
441
|
return {
|
|
441
442
|
timeout: typeof s.timeout === "number" ? s.timeout : DEFAULT_ACK.timeout,
|
|
443
|
+
timeoutRetries: typeof s.timeoutRetries === "number" && Number.isFinite(s.timeoutRetries)
|
|
444
|
+
? Math.max(0, Math.min(5, Math.trunc(s.timeoutRetries)))
|
|
445
|
+
: DEFAULT_ACK.timeoutRetries,
|
|
442
446
|
autoResendOnTimeout: typeof s.autoResendOnTimeout === "boolean"
|
|
443
447
|
? s.autoResendOnTimeout
|
|
444
448
|
: DEFAULT_ACK.autoResendOnTimeout,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The host hands the channel its own English failure text (rate limit, expired
|
|
6
6
|
* provider login, context overflow, billing, timeouts — see
|
|
7
|
-
* `docs/clawchat-plugin-openclaw.md
|
|
7
|
+
* `docs/clawchat-plugin-openclaw.md#framework-error-replies`). Posting that
|
|
8
8
|
* raw leaves a non-English owner with a message they cannot act on. Instead we
|
|
9
9
|
* lead with one sentence in the owner's language saying what happened and what
|
|
10
10
|
* to do, then keep the original text at the end so it can still be reported.
|
package/dist/src/gate-outcome.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
* Maps backend owner-approval gate codes to structured tool outcomes.
|
|
3
|
-
*
|
|
4
|
-
* Backend gate codes (confirmed live contract):
|
|
5
|
-
* 21001 — pending owner approval (payload: { request_id, operation, expires_at })
|
|
6
|
-
* 21003 — policy forbidden (payload: { operation })
|
|
7
|
-
*
|
|
8
|
-
* Returns null for any other code so callers can fall through to normal handling.
|
|
9
|
-
*/
|
|
1
|
+
import { ownerOnlyMessage } from "./orchestration.js";
|
|
10
2
|
export function mapGateOutcome(code, data) {
|
|
3
|
+
if (code === 21007)
|
|
4
|
+
return { status: "owner_turn_only",
|
|
5
|
+
operation: typeof data.operation === "string" ? data.operation : "", instruction: ownerOnlyMessage(false) };
|
|
11
6
|
if (code === 21001) {
|
|
12
7
|
return {
|
|
13
8
|
status: "pending_owner_approval",
|