@clawhive/openclaw-plugin 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +217 -0
  2. package/dist/api.d.ts +6 -0
  3. package/dist/api.js +6 -0
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.js +324 -0
  6. package/dist/runtime-api.d.ts +1 -0
  7. package/dist/runtime-api.js +1 -0
  8. package/dist/setup-entry.d.ts +4 -0
  9. package/dist/setup-entry.js +3 -0
  10. package/dist/src/agency-install/claimLoop.d.ts +41 -0
  11. package/dist/src/agency-install/claimLoop.js +188 -0
  12. package/dist/src/agent-panel/claimLoop.d.ts +32 -0
  13. package/dist/src/agent-panel/claimLoop.js +118 -0
  14. package/dist/src/agent-panel/executor.d.ts +8 -0
  15. package/dist/src/agent-panel/executor.js +368 -0
  16. package/dist/src/agent-panel/plan.d.ts +20 -0
  17. package/dist/src/agent-panel/plan.js +111 -0
  18. package/dist/src/agent-panel/prompts.d.ts +38 -0
  19. package/dist/src/agent-panel/prompts.js +87 -0
  20. package/dist/src/agent-panel/types.d.ts +45 -0
  21. package/dist/src/agent-panel/types.js +1 -0
  22. package/dist/src/agents.d.ts +44 -0
  23. package/dist/src/agents.js +195 -0
  24. package/dist/src/authorization.d.ts +34 -0
  25. package/dist/src/authorization.js +183 -0
  26. package/dist/src/channel.d.ts +5 -0
  27. package/dist/src/channel.js +93 -0
  28. package/dist/src/claimPolling.d.ts +9 -0
  29. package/dist/src/claimPolling.js +13 -0
  30. package/dist/src/client.d.ts +386 -0
  31. package/dist/src/client.js +595 -0
  32. package/dist/src/config.d.ts +28 -0
  33. package/dist/src/config.js +94 -0
  34. package/dist/src/defaults.d.ts +40 -0
  35. package/dist/src/defaults.js +52 -0
  36. package/dist/src/deviceCode.d.ts +16 -0
  37. package/dist/src/deviceCode.js +65 -0
  38. package/dist/src/heartbeat.d.ts +25 -0
  39. package/dist/src/heartbeat.js +65 -0
  40. package/dist/src/imageAttachments.d.ts +14 -0
  41. package/dist/src/imageAttachments.js +81 -0
  42. package/dist/src/inbound.d.ts +10 -0
  43. package/dist/src/inbound.js +140 -0
  44. package/dist/src/installMutex.d.ts +4 -0
  45. package/dist/src/installMutex.js +18 -0
  46. package/dist/src/market-install/claimLoop.d.ts +41 -0
  47. package/dist/src/market-install/claimLoop.js +183 -0
  48. package/dist/src/market-install/downloader.d.ts +36 -0
  49. package/dist/src/market-install/downloader.js +133 -0
  50. package/dist/src/market-install/executor.d.ts +28 -0
  51. package/dist/src/market-install/executor.js +352 -0
  52. package/dist/src/market-install/types.d.ts +62 -0
  53. package/dist/src/market-install/types.js +1 -0
  54. package/dist/src/market-install/verifier.d.ts +32 -0
  55. package/dist/src/market-install/verifier.js +60 -0
  56. package/dist/src/market-publish/packager.d.ts +34 -0
  57. package/dist/src/market-publish/packager.js +168 -0
  58. package/dist/src/market-publish/publishFlow.d.ts +70 -0
  59. package/dist/src/market-publish/publishFlow.js +107 -0
  60. package/dist/src/market-publish/uploader.d.ts +73 -0
  61. package/dist/src/market-publish/uploader.js +132 -0
  62. package/dist/src/openclawVersion.d.ts +4 -0
  63. package/dist/src/openclawVersion.js +13 -0
  64. package/dist/src/outbound.d.ts +13 -0
  65. package/dist/src/outbound.js +41 -0
  66. package/dist/src/pairing.d.ts +32 -0
  67. package/dist/src/pairing.js +64 -0
  68. package/dist/src/runtime.d.ts +52 -0
  69. package/dist/src/runtime.js +26 -0
  70. package/dist/src/telemetry.d.ts +34 -0
  71. package/dist/src/telemetry.js +89 -0
  72. package/dist/src/transport/realtime.d.ts +49 -0
  73. package/dist/src/transport/realtime.js +273 -0
  74. package/dist/src/transport.d.ts +38 -0
  75. package/dist/src/transport.js +15 -0
  76. package/dist/src/wake.d.ts +31 -0
  77. package/dist/src/wake.js +101 -0
  78. package/openclaw.config.example.yaml +10 -0
  79. package/openclaw.plugin.json +122 -0
  80. package/package.json +84 -0
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # ClawHive OpenClaw Plugin
2
+
3
+ Desktop-side OpenClaw channel plugin that bridges a local OpenClaw node to the ClawHive cloud so the ClawHive mobile app can send messages to and receive replies from the user's local agents.
4
+
5
+ For end-user setup and daily usage in Chinese, see [INSTALLATION-GUIDE.zh-CN.md](./INSTALLATION-GUIDE.zh-CN.md).
6
+ For the latest stabilization summary, see [RELEASE-NOTES-2026-04-24.md](../RELEASE-NOTES-2026-04-24.md).
7
+
8
+ ## Architecture at a glance
9
+
10
+ ```
11
+ ClawHive mobile app ──► Supabase (Postgres + Realtime + Edge Functions) ──► this plugin ──► OpenClaw core ──► local agent
12
+ ◄── writes agent reply ◄──
13
+ ```
14
+
15
+ - Inbound: user messages are inserted by the mobile app into `public.messages`. This plugin subscribes to the `messages` Realtime publication and dispatches each `role='user'` row into the local agent via `api.runtime.subagent.run({ sessionKey, message, deliver: true })`.
16
+ - Outbound: OpenClaw core routes the agent reply back through the channel's `outbound.attachedResults.sendText`, which calls the `plugin-message-write` edge function to persist the reply with `role='agent'`.
17
+ - Heartbeat: a background loop calls `plugin-heartbeat` every `heartbeat_interval` seconds (default 30) to keep `plugin_nodes.plugin_status='online'`.
18
+ - Market install orchestration: a separate background loop polls `market-install-claim`, keeps the approved `releaseId`/`version`/`publishSourceRef`/`publishFingerprint` pinned, and reports `installing` plus durable `failed`/`cancelled` outcomes through `market-install-update`.
19
+
20
+ ## Zero-config install
21
+
22
+ The install flow is intentionally self-provisioning. `openclaw plugins install` must not require a pre-created `pluginToken` or manual edits to `~/.openclaw/openclaw.json`.
23
+
24
+ Install the published npm package:
25
+
26
+ ```bash
27
+ openclaw plugins install @clawhive/openclaw-plugin
28
+ ```
29
+
30
+ After plugin install, run:
31
+
32
+ ```bash
33
+ openclaw clawhive authorize
34
+ ```
35
+
36
+ The command prints the approval URL plus `user_code`, waits for approval, and persists the resulting `pluginToken` and `userId` into the OpenClaw config. Then restart the gateway.
37
+
38
+ On the next `openclaw gateway run`:
39
+
40
+ 1. Reads config — `pluginToken` is now present from the explicit authorization command.
41
+ 2. Calls `plugin-register` and receives `{plugin_node_id, realtime_topic, user_session}`.
42
+ 3. Persists any new `pluginNodeId` or updated `userId` into config.
43
+ 4. All subsequent calls (heartbeat, agent-reply writes, pair-code generation, agent sync) present the token via `x-plugin-token`, which the server validates against `plugin_tokens`.
44
+
45
+ No shared dev secret exists; each install owns its own credential and can be revoked independently.
46
+
47
+ ## Configuration
48
+
49
+ Minimum managed-cloud install: **nothing**. The plugin falls back to the baked `DEFAULT_PROJECT_URL` in `src/defaults.ts`, silently bootstraps, and writes everything else into config.
50
+
51
+ Self-hosted or local-dev setup overrides the URL:
52
+
53
+ ```bash
54
+ openclaw channels add --channel clawhive --url https://my-clawhive.example.com
55
+ ```
56
+
57
+ …or via env for one-shot dev:
58
+
59
+ ```bash
60
+ export CLAWHIVE_SUPABASE_MODE=local
61
+ export CLAWHIVE_PROJECT_URL=http://127.0.0.1:54321
62
+ export CLAWHIVE_SUPABASE_ANON_KEY=your-local-anon-or-publishable-key
63
+ ```
64
+
65
+ ### Runtime env vars
66
+
67
+ | Name | Required | Purpose |
68
+ |---|---|---|
69
+ | `CLAWHIVE_SUPABASE_MODE` | optional | `cloud` uses the baked-in managed Supabase project. `local` uses `CLAWHIVE_PROJECT_URL` and `CLAWHIVE_SUPABASE_ANON_KEY`. |
70
+ | `CLAWHIVE_PROJECT_URL` | optional | Local Supabase project base URL used when `CLAWHIVE_SUPABASE_MODE=local`. Do not include `/rest/v1`. |
71
+ | `CLAWHIVE_PLUGIN_TOKEN` | optional | Pre-seed the plugin token instead of letting the plugin bootstrap. Normally auto-populated after first run. |
72
+ | `CLAWHIVE_USER_ID` | optional | Pre-seed the owner user id. Normally auto-populated after bootstrap. |
73
+ | `CLAWHIVE_SUPABASE_ANON_KEY` | optional | Local Supabase anon/publishable key used when `CLAWHIVE_SUPABASE_MODE=local` and `channels.clawhive.anonKey` is not set in OpenClaw config. |
74
+
75
+ Build-time override (baked into `dist/`):
76
+
77
+ | Name | Purpose |
78
+ |---|---|
79
+ | `CLAWHIVE_DEFAULT_PROJECT_URL` | Replace the compiled-in default project URL. Set this before `npm run build` when producing a self-hosted plugin package. |
80
+
81
+ ## Runtime lifecycle
82
+
83
+ 1. `gateway_start` fires. Resolve account from config + env + baked default.
84
+ 2. If no `pluginToken`, print a hint telling the user to run `openclaw clawhive authorize`, then stay idle.
85
+ 3. Once authorized, call `plugin-register` → receive `plugin_node_id`, `realtime_topic`, and a user-session JWT`.
86
+ 4. Persist any config drift (`pluginNodeId`, updated `userId`).
87
+ 5. Push local agents (`cfg.agents.list`) to the cloud via `plugin-agents-sync`.
88
+ 6. Open the Supabase Realtime channel on `public.messages` filtered by `user_id`.
89
+ 7. Start the heartbeat loop.
90
+ 8. Start the market install claim loop. It runs independently from messaging and heartbeat so long-running install work cannot block inbound chat.
91
+ 9. Always print an ANSI QR (via `qrcode-terminal`) with a 5-minute TTL `pair_token` so first-time installs, additional phones, and reinstalled apps can pair without manual cleanup. The banner changes when phones are already paired.
92
+ 10. On each inbound `role='user'` row, call `api.runtime.subagent.run({ sessionKey, message, deliver: true })`. Reconnects fetch missed messages via `messages-sync`.
93
+ 11. On `gateway_stop`, tear down the install loop, transport, and heartbeat, then mark `plugin_status='offline'`.
94
+
95
+ ## Marketplace install runner
96
+
97
+ The plugin now claims durable install jobs from the market backend. The current execution contract is intentionally strict:
98
+
99
+ 1. Claim only jobs targeted at this `plugin_node_id`.
100
+ 2. Keep the approved `releaseId`, `version`, `publishSourceRef`, and `publishFingerprint` pinned for the whole execution attempt.
101
+ 3. Reject incompatible releases if `minPluginVersion` or `minOpenClawVersion` is not satisfied.
102
+ 4. Mark the durable job as `installing` once preflight passes.
103
+ 5. Always write a final durable `failed` or `cancelled` state with a machine-readable `reasonKind` and a plain-language `errorMessage` if local apply cannot complete.
104
+
105
+ ### Failure mapping
106
+
107
+ The install runner currently emits these durable failure kinds:
108
+
109
+ | `reasonKind` | Meaning |
110
+ |---|---|
111
+ | `claim_rejected` | The claimed job does not match the current node or is missing pinned release identity. |
112
+ | `artifact_fetch_failed` | The approved release is missing `publishSourceRef` or `publishFingerprint`, so the plugin cannot verify the exact artifact. |
113
+ | `compatibility_rejected` | `minPluginVersion` or `minOpenClawVersion` rejects the current desktop runtime. |
114
+ | `install_apply_failed` | The job passed preflight but the local host still has no stable marketplace install API to apply the artifact. |
115
+ | `cancelled` | Reserved for future explicit cancellation flows. |
116
+
117
+ ### Release metadata relevance
118
+
119
+ - `publishSourceRef`: stable source locator the plugin keeps pinned after the user confirms a release on mobile.
120
+ - `publishFingerprint`: immutable checksum/fingerprint the plugin expects before local apply.
121
+ - `minPluginVersion`: minimum ClawHive plugin runtime required by the release.
122
+ - `minOpenClawVersion`: minimum OpenClaw host version required by the release.
123
+
124
+ The current OpenClaw SDK surface does not yet expose a stable local marketplace install API, so the runner stops after durable preflight and reports a visible failure instead of silently dropping the job or guessing how to install it.
125
+
126
+ ## Manual Commands
127
+
128
+ - `openclaw clawhive authorize`
129
+ Prints the browser approval URL and device user code, then waits for authorization.
130
+ - `openclaw clawhive pair`
131
+ Prints a fresh mobile pairing QR plus short code on demand so you can re-pair a phone after reinstall, add another phone, or recover if you missed the startup banner.
132
+ - `openclaw clawhive pair --ttl-seconds 600`
133
+ Optional override for the pairing-code TTL when you need a longer window.
134
+
135
+ ## Manual smoke-testing
136
+
137
+ Everything below runs against a local `supabase start`:
138
+
139
+ ```bash
140
+ # 1. Start the stack
141
+ cd /path/to/openclaw-assistant
142
+ supabase start
143
+ source <(supabase status -o env)
144
+ export PROJECT_URL=http://127.0.0.1:54321
145
+
146
+ # 2. Mint a real plugin token (no shared dev secret)
147
+ BOOT=$(curl -s -X POST "$PROJECT_URL/functions/v1/plugin-bootstrap" \
148
+ -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY" \
149
+ -H "Content-Type: application/json" -d '{"description":"manual-test"}')
150
+ PLUGIN_TOKEN=$(echo "$BOOT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["plugin_token"])')
151
+
152
+ # 3. Exercise the plugin-authed endpoints with the real token
153
+ curl -s -X POST "$PROJECT_URL/functions/v1/plugin-register" \
154
+ -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY" \
155
+ -H "x-plugin-token: $PLUGIN_TOKEN" -H "Content-Type: application/json" \
156
+ -d '{"node_name":"smoke"}'
157
+ ```
158
+
159
+ To run the plugin itself against the local stack:
160
+
161
+ ```bash
162
+ cd openclaw-plugin && npm install && npm run build && cd ..
163
+ openclaw plugins install "$PWD/openclaw-plugin" --force
164
+
165
+ export CLAWHIVE_PROJECT_URL="$PROJECT_URL"
166
+ export CLAWHIVE_SUPABASE_ANON_KEY="$ANON_KEY"
167
+ export CLAWHIVE_SUPABASE_MODE=local
168
+ openclaw clawhive authorize
169
+ openclaw gateway run --dev --verbose
170
+ ```
171
+
172
+ The local dev flow above should not require hand-editing `~/.openclaw/openclaw.json`. The explicit authorization command persists the token, user id, and anon key before the gateway starts the runtime.
173
+
174
+ ## Marketplace install smoke flow
175
+
176
+ Use this flow when validating Phase 9 install orchestration end to end:
177
+
178
+ 1. Start the plugin and confirm the desktop node registers successfully.
179
+ 2. Publish a market release whose metadata includes `publishSourceRef`, `publishFingerprint`, `minPluginVersion`, and `minOpenClawVersion` as needed.
180
+ 3. In the mobile app, discover the listing, open the detail screen, confirm a compatible node, and create the durable install job.
181
+ 4. Watch the plugin claim the job and report `installing`, then either:
182
+ - a durable success once a stable host install API exists, or
183
+ - a durable failure with `reasonKind` and plain-language `errorMessage`.
184
+ 5. Confirm the app shows the latest install status and `My installs` history, and confirm operator evidence exists via install events / health summary.
185
+
186
+ ### Expert Panel Sessions
187
+
188
+ When ClawHive creates an expert panel session, the cloud stores the turn and contribution state. The plugin claims panel turns, runs local OpenClaw agents in two rounds, updates each contribution as it completes or times out, and writes only the moderator's final answer back to the normal message stream.
189
+
190
+ The first version assumes all selected agents are available on the same OpenClaw node. Multi-node distributed execution is not supported yet.
191
+
192
+ ## MVP non-goals
193
+
194
+ - Successful local apply of a marketplace artifact before OpenClaw exposes a stable native install API.
195
+ - Outbound media / file attachments — `sendMedia` throws `unsupported_media`.
196
+ - Plugin token rotation / expiry (tokens are currently long-lived; revoke manually via `update public.plugin_tokens set revoked_at = now() where id = '...'` until a rotation endpoint ships).
197
+
198
+ ## Development
199
+
200
+ ```
201
+ npm install
202
+ npm run check # tsc --noEmit
203
+ npm run build # emits dist/
204
+ ```
205
+
206
+ ## Publishing to npm
207
+
208
+ The package is published from this directory:
209
+
210
+ ```bash
211
+ npm login
212
+ npm publish --access public
213
+ ```
214
+
215
+ `prepublishOnly` runs TypeScript checking, builds `dist/`, and prints the exact npm tarball contents with `npm pack --dry-run` before publishing.
216
+
217
+ The required cloud deltas live under `supabase/` — migrations add `plugin_tokens` and enable Realtime on `messages`, and edge functions cover bootstrap, register, heartbeat, message-write, agents-sync, pair-code-create. Apply with `supabase db reset` in local dev.
package/dist/api.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { clawhiveChannelPlugin } from "./src/channel.js";
2
+ export { ClawHiveApiError, ClawHiveCloudApi, type AppendedMessage, type ClawHivePluginConfig, type PairingCodeResult, type PluginNode, type PluginRegisterResult, type SyncMessagesResult, type UserSession, } from "./src/client.js";
3
+ export { CLAWHIVE_CHANNEL_ID, readRawChannelSection, resolveResolvedAccount, writeChannelSection, writePluginNodeId, type ClawHiveResolvedAccount, } from "./src/config.js";
4
+ export { HeartbeatLoop, type HeartbeatStatus } from "./src/heartbeat.js";
5
+ export { inboundMessageFromRow, type InboundMessage, type TransportAdapter, type TransportHandlers, type TransportStatus, } from "./src/transport.js";
6
+ export { RealtimeTransport } from "./src/transport/realtime.js";
package/dist/api.js ADDED
@@ -0,0 +1,6 @@
1
+ export { clawhiveChannelPlugin } from "./src/channel.js";
2
+ export { ClawHiveApiError, ClawHiveCloudApi, } from "./src/client.js";
3
+ export { CLAWHIVE_CHANNEL_ID, readRawChannelSection, resolveResolvedAccount, writeChannelSection, writePluginNodeId, } from "./src/config.js";
4
+ export { HeartbeatLoop } from "./src/heartbeat.js";
5
+ export { inboundMessageFromRow, } from "./src/transport.js";
6
+ export { RealtimeTransport } from "./src/transport/realtime.js";
@@ -0,0 +1,4 @@
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import { clawhiveChannelPlugin } from "./src/channel.js";
3
+ declare const clawhiveEntry: ReturnType<typeof defineChannelPluginEntry<typeof clawhiveChannelPlugin>>;
4
+ export default clawhiveEntry;
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import { AgentSyncLoop, pushLocalAgents } from "./src/agents.js";
3
+ import { AgentPanelClaimLoop } from "./src/agent-panel/claimLoop.js";
4
+ import { AgencyInstallClaimLoop } from "./src/agency-install/claimLoop.js";
5
+ import { createClawHiveCloudApi, printAuthorizationCommandHint, registerClawHiveCli, } from "./src/authorization.js";
6
+ import { clawhiveChannelPlugin } from "./src/channel.js";
7
+ import { resolveResolvedAccount, writeRegistrationIdentity } from "./src/config.js";
8
+ import { HeartbeatLoop } from "./src/heartbeat.js";
9
+ import { handleInboundMessage } from "./src/inbound.js";
10
+ import { InstallClaimLoop } from "./src/market-install/claimLoop.js";
11
+ import { resolveOpenClawVersion } from "./src/openclawVersion.js";
12
+ import { getPairedDeviceCount, printPairingQr } from "./src/pairing.js";
13
+ import { clawhiveRuntimeStore, indexSyncedAgents, setActiveInstall, } from "./src/runtime.js";
14
+ import { addPluginBreadcrumb, capturePluginError, initPluginTelemetry, } from "./src/telemetry.js";
15
+ import { RealtimeTransport } from "./src/transport/realtime.js";
16
+ import { PluginWakeSubscription } from "./src/wake.js";
17
+ async function startRuntime(api) {
18
+ initPluginTelemetry();
19
+ const cfg = api.runtime.config.loadConfig();
20
+ const account = resolveResolvedAccount(cfg);
21
+ if (!account.projectUrl) {
22
+ api.logger.warn("ClawHive plugin has no projectUrl (config + env + baked default all empty); skipping runtime startup.");
23
+ return;
24
+ }
25
+ if (clawhiveRuntimeStore.tryGetRuntime()) {
26
+ api.logger.warn("ClawHive runtime already initialized; skipping re-entry.");
27
+ return;
28
+ }
29
+ const { anonKey, cloudApi } = createClawHiveCloudApi(api);
30
+ if (!cloudApi.pluginToken) {
31
+ api.logger.info(`ClawHive: no plugin token in config; skipping runtime startup until manual authorization is completed against ${account.projectUrl}.`);
32
+ printAuthorizationCommandHint(api.logger);
33
+ return;
34
+ }
35
+ const registration = await cloudApi.registerPluginNode();
36
+ const pluginNodeId = registration.plugin_node.id;
37
+ const resolvedUserId = registration.user_id;
38
+ const patch = {};
39
+ if (!account.userId || account.userId !== resolvedUserId) {
40
+ patch.userId = resolvedUserId;
41
+ }
42
+ if (account.pluginNodeId !== pluginNodeId) {
43
+ patch.pluginNodeId = pluginNodeId;
44
+ }
45
+ const latestCfg = api.runtime.config.loadConfig();
46
+ const latestChannels = latestCfg.channels ?? {};
47
+ const latestChannel = latestChannels.clawhive ?? {};
48
+ if (!latestChannel.anonKey && account.anonKey) {
49
+ patch.anonKey = account.anonKey;
50
+ }
51
+ if (patch.userId || patch.pluginNodeId || patch.pluginToken || patch.anonKey) {
52
+ await api.runtime.config.writeConfigFile(writeRegistrationIdentity(latestCfg, patch));
53
+ }
54
+ let syncResult = { items: [], skipped: 0 };
55
+ try {
56
+ syncResult = await pushLocalAgents(cloudApi, api.runtime.config.loadConfig());
57
+ if (syncResult.items.length > 0 || syncResult.skipped > 0) {
58
+ api.logger.info(`ClawHive synced ${syncResult.items.length} agent(s) to cloud (skipped ${syncResult.skipped}).`);
59
+ }
60
+ }
61
+ catch (error) {
62
+ const reason = error instanceof Error ? error.message : String(error);
63
+ api.logger.warn(`ClawHive: failed to sync local agents: ${reason}`);
64
+ capturePluginError(error, "sync-agents");
65
+ }
66
+ let wakeSubscription = null;
67
+ let rebindWakeSubscription = async () => { };
68
+ const transport = new RealtimeTransport({
69
+ api: cloudApi,
70
+ anonKey,
71
+ userSession: registration.user_session,
72
+ userId: resolvedUserId,
73
+ logger: api.logger,
74
+ onClientReady: () => rebindWakeSubscription(),
75
+ });
76
+ const heartbeat = new HeartbeatLoop({
77
+ api: cloudApi,
78
+ pluginNodeId,
79
+ intervalSeconds: registration.heartbeat_interval ?? 30,
80
+ logger: api.logger,
81
+ });
82
+ const agentSyncLoop = new AgentSyncLoop({
83
+ cloudApi,
84
+ loadConfig: () => api.runtime.config.loadConfig(),
85
+ intervalSeconds: Math.max(registration.heartbeat_interval ?? 30, 30),
86
+ logger: api.logger,
87
+ onAgentsSynced: (items) => {
88
+ const runtime = clawhiveRuntimeStore.tryGetRuntime();
89
+ if (!runtime) {
90
+ return;
91
+ }
92
+ indexSyncedAgents(runtime, items);
93
+ },
94
+ });
95
+ const installLoop = new InstallClaimLoop({
96
+ api: cloudApi,
97
+ pluginApi: api,
98
+ pluginNodeId,
99
+ userId: resolvedUserId,
100
+ pluginVersion: account.pluginVersion,
101
+ openClawVersion: resolveOpenClawVersion(),
102
+ logger: api.logger,
103
+ onActiveInstallChange: (state) => {
104
+ const runtime = clawhiveRuntimeStore.tryGetRuntime();
105
+ if (!runtime) {
106
+ return;
107
+ }
108
+ setActiveInstall(runtime, state);
109
+ },
110
+ onAgentsSynced: (items) => {
111
+ const runtime = clawhiveRuntimeStore.tryGetRuntime();
112
+ if (!runtime) {
113
+ return;
114
+ }
115
+ indexSyncedAgents(runtime, items);
116
+ },
117
+ });
118
+ const agencyInstallLoop = new AgencyInstallClaimLoop({
119
+ api: cloudApi,
120
+ pluginApi: api,
121
+ pluginNodeId,
122
+ userId: resolvedUserId,
123
+ pluginVersion: account.pluginVersion,
124
+ openClawVersion: resolveOpenClawVersion(),
125
+ logger: api.logger,
126
+ onActiveInstallChange: (state) => {
127
+ const runtime = clawhiveRuntimeStore.tryGetRuntime();
128
+ if (!runtime) {
129
+ return;
130
+ }
131
+ setActiveInstall(runtime, state);
132
+ },
133
+ onAgentsSynced: (items) => {
134
+ const runtime = clawhiveRuntimeStore.tryGetRuntime();
135
+ if (!runtime) {
136
+ return;
137
+ }
138
+ indexSyncedAgents(runtime, items);
139
+ },
140
+ });
141
+ const agentPanelLoop = new AgentPanelClaimLoop({
142
+ api: cloudApi,
143
+ pluginApi: api,
144
+ pluginNodeId,
145
+ userId: resolvedUserId,
146
+ logger: api.logger,
147
+ });
148
+ const createWakeSubscription = () => new PluginWakeSubscription({
149
+ client: transport.getClient(),
150
+ logger: api.logger,
151
+ pluginNodeId,
152
+ userId: resolvedUserId,
153
+ onWake: (event) => {
154
+ if (event.taskType === "agent_panel_turn") {
155
+ agentPanelLoop.wake();
156
+ return;
157
+ }
158
+ if (event.taskType === "market_install") {
159
+ installLoop.wake();
160
+ return;
161
+ }
162
+ if (event.taskType === "agency_install") {
163
+ agencyInstallLoop.wake();
164
+ }
165
+ },
166
+ });
167
+ let wakeRebind = Promise.resolve();
168
+ rebindWakeSubscription = async () => {
169
+ wakeRebind = wakeRebind.then(async () => {
170
+ try {
171
+ await wakeSubscription?.stop();
172
+ }
173
+ catch (error) {
174
+ const reason = error instanceof Error ? error.message : String(error);
175
+ api.logger.warn(`Failed to stop previous ClawHive wake subscription during rebind: ${reason}`);
176
+ }
177
+ wakeSubscription = null;
178
+ clawhiveRuntimeStore.getRuntime().wakeSubscription = null;
179
+ wakeSubscription = createWakeSubscription();
180
+ await wakeSubscription.start();
181
+ clawhiveRuntimeStore.getRuntime().wakeSubscription = wakeSubscription;
182
+ });
183
+ await wakeRebind;
184
+ };
185
+ try {
186
+ clawhiveRuntimeStore.setRuntime({
187
+ api: cloudApi,
188
+ transport,
189
+ heartbeat,
190
+ agentSyncLoop,
191
+ agentPanelLoop,
192
+ installLoop,
193
+ agencyInstallLoop,
194
+ wakeSubscription,
195
+ pluginNodeId,
196
+ userId: resolvedUserId,
197
+ pluginApi: api,
198
+ sessionIndex: new Map(),
199
+ agentIndex: new Map(),
200
+ activeInstall: null,
201
+ });
202
+ indexSyncedAgents(clawhiveRuntimeStore.getRuntime(), syncResult.items);
203
+ await transport.start({
204
+ onInboundMessage: (message) => handleInboundMessage(message),
205
+ onStatusChange: (status) => {
206
+ api.logger.info(`ClawHive transport status=${status.state}`);
207
+ addPluginBreadcrumb({
208
+ category: "realtime",
209
+ message: `transport.${status.state}`,
210
+ data: { userId: resolvedUserId },
211
+ });
212
+ },
213
+ });
214
+ heartbeat.start();
215
+ agentSyncLoop.start();
216
+ agentPanelLoop.start();
217
+ installLoop.start();
218
+ agencyInstallLoop.start();
219
+ api.logger.info(`ClawHive plugin runtime started plugin_node=${pluginNodeId} topic=${registration.realtime_topic}`);
220
+ try {
221
+ const pairedDeviceCount = await getPairedDeviceCount({
222
+ registration,
223
+ projectUrl: account.projectUrl,
224
+ anonKey,
225
+ });
226
+ await printPairingQr({
227
+ api: cloudApi,
228
+ pluginNodeId,
229
+ ttlSeconds: 300,
230
+ banner: pairedDeviceCount > 0
231
+ ? `ClawHive: ${pairedDeviceCount} phone(s) already paired. Scan this QR with the ClawHive mobile app to pair an additional phone or recover after reinstall (5 minute TTL).`
232
+ : "ClawHive: no phones paired yet. Scan this QR with the ClawHive mobile app to pair (5 minute TTL).",
233
+ });
234
+ }
235
+ catch (error) {
236
+ const reason = error instanceof Error ? error.message : String(error);
237
+ api.logger.warn(`ClawHive: could not determine pairing status — skipping startup QR (${reason}).`);
238
+ capturePluginError(error, "pairing-bootstrap");
239
+ }
240
+ }
241
+ catch (error) {
242
+ await stopRuntime(api);
243
+ throw error;
244
+ }
245
+ }
246
+ async function stopRuntime(api) {
247
+ const runtime = clawhiveRuntimeStore.tryGetRuntime();
248
+ if (!runtime)
249
+ return;
250
+ try {
251
+ runtime.agentSyncLoop.stop();
252
+ }
253
+ catch (error) {
254
+ const reason = error instanceof Error ? error.message : String(error);
255
+ api.logger.warn(`Failed to stop ClawHive agent sync loop cleanly: ${reason}`);
256
+ }
257
+ try {
258
+ await runtime.agentPanelLoop.stop();
259
+ }
260
+ catch (error) {
261
+ const reason = error instanceof Error ? error.message : String(error);
262
+ api.logger.warn(`Failed to stop ClawHive panel loop cleanly: ${reason}`);
263
+ }
264
+ try {
265
+ await runtime.installLoop.stop();
266
+ }
267
+ catch (error) {
268
+ const reason = error instanceof Error ? error.message : String(error);
269
+ api.logger.warn(`Failed to stop ClawHive install loop cleanly: ${reason}`);
270
+ }
271
+ try {
272
+ await runtime.agencyInstallLoop.stop();
273
+ }
274
+ catch (error) {
275
+ const reason = error instanceof Error ? error.message : String(error);
276
+ api.logger.warn(`Failed to stop ClawHive Agency install loop cleanly: ${reason}`);
277
+ }
278
+ try {
279
+ await runtime.wakeSubscription?.stop();
280
+ }
281
+ catch (error) {
282
+ const reason = error instanceof Error ? error.message : String(error);
283
+ api.logger.warn(`Failed to stop ClawHive wake subscription cleanly: ${reason}`);
284
+ }
285
+ try {
286
+ await runtime.transport.stop();
287
+ }
288
+ catch (error) {
289
+ const reason = error instanceof Error ? error.message : String(error);
290
+ api.logger.warn(`Failed to stop ClawHive transport cleanly: ${reason}`);
291
+ }
292
+ try {
293
+ await runtime.heartbeat.stop();
294
+ }
295
+ catch (error) {
296
+ const reason = error instanceof Error ? error.message : String(error);
297
+ api.logger.warn(`Failed to stop ClawHive heartbeat cleanly: ${reason}`);
298
+ }
299
+ clawhiveRuntimeStore.clearRuntime();
300
+ api.logger.info("ClawHive plugin runtime stopped");
301
+ }
302
+ const clawhiveEntry = defineChannelPluginEntry({
303
+ id: "clawhive",
304
+ name: "ClawHive",
305
+ description: "ClawHive channel plugin",
306
+ plugin: clawhiveChannelPlugin,
307
+ registerFull(api) {
308
+ registerClawHiveCli(api);
309
+ api.on("gateway_start", async () => {
310
+ try {
311
+ await startRuntime(api);
312
+ }
313
+ catch (error) {
314
+ const reason = error instanceof Error ? error.message : String(error);
315
+ api.logger.error(`ClawHive plugin failed to start: ${reason}`);
316
+ capturePluginError(error, "gateway-start");
317
+ }
318
+ });
319
+ api.on("gateway_stop", async () => {
320
+ await stopRuntime(api);
321
+ });
322
+ },
323
+ });
324
+ export default clawhiveEntry;
@@ -0,0 +1 @@
1
+ export { clawhiveRuntimeStore, buildClawHiveSessionKey, indexSession, lookupSession, type ClawHiveRuntime, type SessionBinding } from "./src/runtime.js";
@@ -0,0 +1 @@
1
+ export { clawhiveRuntimeStore, buildClawHiveSessionKey, indexSession, lookupSession } from "./src/runtime.js";
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ plugin: import("openclaw/plugin-sdk/channel-core").ChannelPlugin<import("./api.js").ClawHiveResolvedAccount, unknown, unknown>;
3
+ };
4
+ export default _default;
@@ -0,0 +1,3 @@
1
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import { clawhiveChannelPlugin } from "./src/channel.js";
3
+ export default defineSetupPluginEntry(clawhiveChannelPlugin);
@@ -0,0 +1,41 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import type { ClawHiveCloudApi, SyncedAgent } from "../client.js";
3
+ import type { ActiveInstallJobState } from "../market-install/types.js";
4
+ export type AgencyInstallClaimLoopLogger = {
5
+ info: (message: string) => void;
6
+ warn: (message: string) => void;
7
+ };
8
+ export type AgencyInstallClaimLoopOptions = {
9
+ api: ClawHiveCloudApi;
10
+ pluginApi: OpenClawPluginApi;
11
+ pluginNodeId: string;
12
+ userId: string;
13
+ pluginVersion: string;
14
+ openClawVersion: string | null;
15
+ logger?: AgencyInstallClaimLoopLogger;
16
+ onActiveInstallChange?: (state: ActiveInstallJobState | null) => void;
17
+ onAgentsSynced?: (items: SyncedAgent[]) => void;
18
+ idlePollMs?: number;
19
+ retryPollMs?: number;
20
+ };
21
+ /**
22
+ * Poll Agency install jobs and reuse the existing manifest executor with Agency endpoints.
23
+ */
24
+ export declare class AgencyInstallClaimLoop {
25
+ private readonly options;
26
+ private timer;
27
+ private wakeTimer;
28
+ private wakePending;
29
+ private claiming;
30
+ private running;
31
+ private activeRun;
32
+ private lastError;
33
+ constructor(options: AgencyInstallClaimLoopOptions);
34
+ start(): void;
35
+ stop(): Promise<void>;
36
+ getLastError(): unknown;
37
+ wake(): void;
38
+ private schedule;
39
+ private tick;
40
+ private processJob;
41
+ }