@clawhive/openclaw-plugin 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,7 @@ After plugin install, run:
33
33
  openclaw clawhive authorize
34
34
  ```
35
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.
36
+ The command prints an app authorization QR plus fallback `user_code`, waits for approval in the signed-in ClawHive app, and persists the resulting `pluginToken` and `userId` into the OpenClaw config. Then restart the gateway.
37
37
 
38
38
  On the next `openclaw gateway run`:
39
39
 
@@ -44,6 +44,25 @@ On the next `openclaw gateway run`:
44
44
 
45
45
  No shared dev secret exists; each install owns its own credential and can be revoked independently.
46
46
 
47
+ ### Move this runtime to another account
48
+
49
+ To remove this OpenClaw installation from its current ClawHive account, run:
50
+
51
+ ```bash
52
+ openclaw clawhive unlink --yes
53
+ ```
54
+
55
+ This revokes the current plugin token and removes its cloud node from the old
56
+ account. Stop or restart the gateway, then run `openclaw clawhive authorize`
57
+ and approve the QR in the new account's signed-in App. The new account gets a
58
+ new node; the old account's agents, conversations, and history are not moved.
59
+
60
+ The phone device is shared by OpenClaw and Hermes. If OpenClaw is the last
61
+ runtime using it, unlink releases the phone so the original or a different
62
+ account can authorize next. If Hermes still uses the phone, OpenClaw can be
63
+ authorized again only by the same account; unlink Hermes as well before moving
64
+ the shared phone to another account.
65
+
47
66
  ## Configuration
48
67
 
49
68
  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.
@@ -126,7 +145,9 @@ The current OpenClaw SDK surface does not yet expose a stable local marketplace
126
145
  ## Manual Commands
127
146
 
128
147
  - `openclaw clawhive authorize`
129
- Prints the browser approval URL and device user code, then waits for authorization.
148
+ Prints the app authorization QR and fallback device user code, then waits for authorization.
149
+ - `openclaw clawhive unlink`
150
+ Shows the account-transfer impact without changing anything. Add `--yes` to revoke this runtime authorization, remove its node from the current account, and clear the local identity.
130
151
  - `openclaw clawhive pair`
131
152
  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
153
  - `openclaw clawhive pair --ttl-seconds 600`
@@ -19,6 +19,9 @@ type AuthorizationDeps = {
19
19
  printDeviceCodePrompt?: typeof printDeviceCodePrompt;
20
20
  awaitDeviceCodeApproval?: typeof awaitDeviceCodeApproval;
21
21
  };
22
+ type UnlinkDeps = {
23
+ createCloudApi?: typeof createClawHiveCloudApi;
24
+ };
22
25
  /**
23
26
  * Print a fresh mobile pairing QR and short code on demand.
24
27
  */
@@ -27,6 +30,13 @@ export declare function printManualPairingCode(api: OpenClawPluginApi, ttlSecond
27
30
  * Run the explicit device-authorization command for the ClawHive plugin.
28
31
  */
29
32
  export declare function authorizeClawHivePlugin(api: OpenClawPluginApi, deps?: AuthorizationDeps): Promise<void>;
33
+ /**
34
+ * Revoke this runtime's current account authorization before it is approved
35
+ * from a different signed-in mobile App account.
36
+ */
37
+ export declare function unlinkClawHivePlugin(api: OpenClawPluginApi, options: {
38
+ yes?: boolean;
39
+ }, deps?: UnlinkDeps): Promise<void>;
30
40
  /**
31
41
  * Register the top-level `openclaw clawhive` CLI group for manual plugin authorization.
32
42
  */
@@ -1,5 +1,5 @@
1
1
  import { ClawHiveCloudApi, ClawHiveApiError, DeviceCodeDeniedError, DeviceCodeExpiredError, } from "./client.js";
2
- import { resolveResolvedAccount, writeRegistrationIdentity } from "./config.js";
2
+ import { clearAuthorizationIdentity, resolveResolvedAccount, writeRegistrationIdentity, } from "./config.js";
3
3
  import { awaitDeviceCodeApproval, printDeviceCodePrompt, } from "./deviceCode.js";
4
4
  import { getPairedDeviceCount, printPairingQr } from "./pairing.js";
5
5
  /**
@@ -28,7 +28,7 @@ export function createClawHiveCloudApi(api) {
28
28
  */
29
29
  export function printAuthorizationCommandHint(logger) {
30
30
  logger.warn("ClawHive plugin is installed but not authorized yet.");
31
- logger.info("Run `openclaw clawhive authorize` in your terminal to display the approval URL and user code.");
31
+ logger.info("Run `openclaw clawhive authorize` in your terminal to display an app authorization QR and fallback code.");
32
32
  logger.info("If the command reports a missing anon key, set `channels.clawhive.anonKey` in config or export `CLAWHIVE_SUPABASE_ANON_KEY` first.");
33
33
  }
34
34
  /**
@@ -94,7 +94,8 @@ export async function authorizeClawHivePlugin(api, deps = {}) {
94
94
  const { account, cloudApi } = (deps.createCloudApi ?? createClawHiveCloudApi)(api);
95
95
  if (cloudApi.pluginToken) {
96
96
  try {
97
- await cloudApi.registerPluginNode();
97
+ const registration = await cloudApi.registerPluginNode();
98
+ await syncRegistrationIdentity(api, { account, registration });
98
99
  api.logger.info("ClawHive is already authorized. Restart the gateway if you are waiting for it to connect.");
99
100
  return;
100
101
  }
@@ -118,13 +119,19 @@ export async function authorizeClawHivePlugin(api, deps = {}) {
118
119
  start,
119
120
  logger: api.logger,
120
121
  });
122
+ cloudApi.setAuthorizationIdentity({
123
+ pluginToken: approval.plugin_token,
124
+ userId: approval.user_id,
125
+ });
126
+ const registration = await cloudApi.registerPluginNode();
121
127
  const latestCfg = api.runtime.config.loadConfig();
122
128
  await api.runtime.config.writeConfigFile(writeRegistrationIdentity(latestCfg, {
123
129
  anonKey: account.anonKey,
124
130
  pluginToken: approval.plugin_token,
125
- userId: approval.user_id,
131
+ userId: registration.user_id,
132
+ pluginNodeId: registration.plugin_node.id,
126
133
  }));
127
- api.logger.info("ClawHive authorization completed. Restart the gateway to finish plugin registration.");
134
+ api.logger.info("ClawHive authorization completed and plugin node registered. Restart the gateway if you are waiting for it to connect.");
128
135
  }
129
136
  catch (error) {
130
137
  if (error instanceof DeviceCodeDeniedError) {
@@ -138,6 +145,54 @@ export async function authorizeClawHivePlugin(api, deps = {}) {
138
145
  throw error;
139
146
  }
140
147
  }
148
+ /**
149
+ * Revoke this runtime's current account authorization before it is approved
150
+ * from a different signed-in mobile App account.
151
+ */
152
+ export async function unlinkClawHivePlugin(api, options, deps = {}) {
153
+ if (!options.yes) {
154
+ api.logger.warn("This removes the current OpenClaw runtime from its ClawHive account. No changes were made.");
155
+ api.logger.info("Run openclaw clawhive unlink --yes to confirm.");
156
+ return;
157
+ }
158
+ const { account, cloudApi } = (deps.createCloudApi ?? createClawHiveCloudApi)(api);
159
+ const clearLocalIdentity = async () => {
160
+ const latestCfg = api.runtime.config.loadConfig();
161
+ await api.runtime.config.writeConfigFile(clearAuthorizationIdentity(latestCfg));
162
+ };
163
+ if (!account.pluginToken || !account.pluginNodeId) {
164
+ await clearLocalIdentity();
165
+ api.logger.info("ClawHive local authorization was already incomplete and has been cleared. Run openclaw clawhive authorize to connect an account.");
166
+ return;
167
+ }
168
+ try {
169
+ const result = await cloudApi.unlinkPluginNode();
170
+ await clearLocalIdentity();
171
+ api.logger.info(result.node_removed
172
+ ? "ClawHive runtime authorization revoked and node removed from the current account."
173
+ : "ClawHive runtime authorization was already removed from the current account.");
174
+ if (result.device_released) {
175
+ api.logger.info("ClawHive shared phone device was released. This runtime can now be authorized by the original or a different ClawHive account.");
176
+ }
177
+ else if (result.device_retained_reason === "other_runtime_linked") {
178
+ const runtimes = result.remaining_runtime_types
179
+ .map((runtime) => runtime === "hermes" ? "Hermes" : "OpenClaw")
180
+ .join(" and ");
181
+ api.logger.warn(`${runtimes || "Another runtime"} still uses this phone. Reauthorize OpenClaw with the same account, or unlink every remaining runtime before moving the phone to another account.`);
182
+ }
183
+ }
184
+ catch (error) {
185
+ if (!isInvalidStoredPluginTokenError(error)) {
186
+ throw error;
187
+ }
188
+ await clearLocalIdentity();
189
+ api.logger.info("ClawHive stored authorization was already invalid and has been cleared locally.");
190
+ }
191
+ api.logger.info("Stop or restart the OpenClaw gateway, then run openclaw clawhive authorize and approve with the new account in the ClawHive app.");
192
+ if (process.env.CLAWHIVE_PLUGIN_TOKEN) {
193
+ api.logger.warn("CLAWHIVE_PLUGIN_TOKEN is set in this shell. Run unset CLAWHIVE_PLUGIN_TOKEN to avoid using the old credential.");
194
+ }
195
+ }
141
196
  function isInvalidStoredPluginTokenError(error) {
142
197
  if (!(error instanceof ClawHiveApiError)) {
143
198
  return false;
@@ -158,10 +213,17 @@ export function registerClawHiveCli(api) {
158
213
  .description("Manage ClawHive plugin authorization and pairing");
159
214
  clawhive
160
215
  .command("authorize")
161
- .description("Display the approval URL and user code, then wait for plugin authorization")
216
+ .description("Display an app authorization QR and fallback code, then wait for plugin authorization")
162
217
  .action(async () => {
163
218
  await authorizeClawHivePlugin(api);
164
219
  });
220
+ clawhive
221
+ .command("unlink")
222
+ .description("Revoke this OpenClaw runtime authorization and remove it from the current ClawHive account")
223
+ .option("--yes", "Confirm unlinking this runtime")
224
+ .action(async (options) => {
225
+ await unlinkClawHivePlugin(api, options);
226
+ });
165
227
  clawhive
166
228
  .command("pair")
167
229
  .description("Print a fresh mobile pairing QR code and short code")
@@ -47,6 +47,7 @@ export type PluginNode = {
47
47
  node_name?: string | null;
48
48
  plugin_version?: string | null;
49
49
  plugin_status: "online" | "offline" | "degraded";
50
+ runtime_type?: "openclaw" | "hermes";
50
51
  last_heartbeat_at?: string | null;
51
52
  };
52
53
  export type PluginRegisterResult = {
@@ -57,6 +58,13 @@ export type PluginRegisterResult = {
57
58
  heartbeat_interval: number;
58
59
  user_session: UserSession;
59
60
  };
61
+ export type PluginUnlinkResult = {
62
+ unlinked: true;
63
+ node_removed: boolean;
64
+ device_released: boolean;
65
+ device_retained_reason: "other_runtime_linked" | "device_not_found" | null;
66
+ remaining_runtime_types: Array<"openclaw" | "hermes">;
67
+ };
60
68
  export type PairingCodeResult = {
61
69
  pair_token: string;
62
70
  expires_at: string;
@@ -77,11 +85,13 @@ export type SyncAgentInput = {
77
85
  export type SyncedAgent = {
78
86
  id: string;
79
87
  user_id: string;
88
+ plugin_node_id?: string | null;
89
+ runtime_type: "openclaw" | "hermes";
90
+ runtime_agent_id: string;
80
91
  openclaw_agent_id: string;
81
92
  name: string;
82
93
  description: string | null;
83
94
  avatar_url: string | null;
84
- source_type: string;
85
95
  config: Record<string, unknown>;
86
96
  created_at: string;
87
97
  };
@@ -179,6 +189,10 @@ export declare class ClawHiveCloudApi {
179
189
  get baseUrl(): string;
180
190
  get userId(): string | null;
181
191
  get pluginToken(): string | null;
192
+ setAuthorizationIdentity(identity: {
193
+ pluginToken: string;
194
+ userId: string;
195
+ }): void;
182
196
  private requireUserId;
183
197
  private requirePluginToken;
184
198
  startDeviceCode(opts?: {
@@ -188,6 +202,7 @@ export declare class ClawHiveCloudApi {
188
202
  }): Promise<DeviceCodeStartResult>;
189
203
  exchangeDeviceCode(deviceCode: string): Promise<DeviceCodeExchangeResult>;
190
204
  registerPluginNode(): Promise<PluginRegisterResult>;
205
+ unlinkPluginNode(): Promise<PluginUnlinkResult>;
191
206
  createPairingCode(pluginNodeId: string, ttlSeconds?: number): Promise<PairingCodeResult>;
192
207
  heartbeat(params: {
193
208
  pluginNodeId: string;
@@ -68,6 +68,10 @@ export class ClawHiveCloudApi {
68
68
  get pluginToken() {
69
69
  return this.config.pluginToken ?? null;
70
70
  }
71
+ setAuthorizationIdentity(identity) {
72
+ this.config.pluginToken = identity.pluginToken;
73
+ this.config.userId = identity.userId;
74
+ }
71
75
  requireUserId() {
72
76
  if (!this.config.userId) {
73
77
  throw new Error("ClawHiveCloudApi: userId is not set. Call registerPluginNode() before making user-scoped calls.");
@@ -86,6 +90,7 @@ export class ClawHiveCloudApi {
86
90
  method: "POST",
87
91
  auth: "none",
88
92
  body: {
93
+ runtime_type: "openclaw",
89
94
  node_name: opts?.nodeName ?? this.config.nodeName ?? "OpenClaw Desktop",
90
95
  plugin_version: opts?.pluginVersion ?? this.config.pluginVersion ?? "0.2.0",
91
96
  description: opts?.description,
@@ -128,6 +133,7 @@ export class ClawHiveCloudApi {
128
133
  plugin_node_id: this.config.pluginNodeId ?? null,
129
134
  node_name: this.config.nodeName ?? "OpenClaw Desktop",
130
135
  plugin_version: this.config.pluginVersion ?? "0.2.0",
136
+ runtime_type: "openclaw",
131
137
  };
132
138
  if (this.config.userId) {
133
139
  body.user_id = this.config.userId;
@@ -139,8 +145,23 @@ export class ClawHiveCloudApi {
139
145
  body,
140
146
  });
141
147
  this.config.userId = result.user_id;
148
+ this.config.pluginNodeId = result.plugin_node.id;
142
149
  return result;
143
150
  }
151
+ async unlinkPluginNode() {
152
+ if (!this.config.pluginNodeId) {
153
+ throw new Error("ClawHive plugin node id is missing");
154
+ }
155
+ return this.request({
156
+ path: "plugin-unlink",
157
+ method: "POST",
158
+ auth: "plugin",
159
+ body: {
160
+ plugin_node_id: this.config.pluginNodeId,
161
+ runtime_type: "openclaw",
162
+ },
163
+ });
164
+ }
144
165
  async createPairingCode(pluginNodeId, ttlSeconds) {
145
166
  return this.request({
146
167
  path: "pairing-code-create",
@@ -172,6 +193,7 @@ export class ClawHiveCloudApi {
172
193
  auth: "plugin",
173
194
  body: {
174
195
  plugin_node_id: params.pluginNodeId,
196
+ runtime_type: "openclaw",
175
197
  session_id: params.sessionId,
176
198
  agent_id: params.agentId,
177
199
  user_id: this.requireUserId(),
@@ -190,7 +212,10 @@ export class ClawHiveCloudApi {
190
212
  auth: "plugin",
191
213
  body: {
192
214
  user_id: this.requireUserId(),
215
+ plugin_node_id: this.config.pluginNodeId,
216
+ runtime_type: "openclaw",
193
217
  agents: agents.map((agent) => ({
218
+ runtime_agent_id: agent.openclawAgentId,
194
219
  openclaw_agent_id: agent.openclawAgentId,
195
220
  name: agent.name,
196
221
  description: agent.description ?? null,
@@ -26,3 +26,8 @@ export declare function writeRegistrationIdentity(cfg: OpenClawConfig, patch: {
26
26
  pluginToken?: string;
27
27
  anonKey?: string;
28
28
  }): OpenClawConfig;
29
+ /**
30
+ * Remove only the locally persisted identity for this ClawHive runtime.
31
+ * Connection defaults and publishing preferences survive account transfer.
32
+ */
33
+ export declare function clearAuthorizationIdentity(cfg: OpenClawConfig): OpenClawConfig;
@@ -79,6 +79,21 @@ export function writeRegistrationIdentity(cfg, patch) {
79
79
  next.anonKey = patch.anonKey;
80
80
  return patchChannelSection(cfg, next);
81
81
  }
82
+ /**
83
+ * Remove only the locally persisted identity for this ClawHive runtime.
84
+ * Connection defaults and publishing preferences survive account transfer.
85
+ */
86
+ export function clearAuthorizationIdentity(cfg) {
87
+ const { pluginToken: _pluginToken, userId: _userId, pluginNodeId: _pluginNodeId, ...remainingSection } = readRawChannelSection(cfg);
88
+ const channels = cfg.channels ?? {};
89
+ return {
90
+ ...cfg,
91
+ channels: {
92
+ ...channels,
93
+ [CLAWHIVE_CHANNEL_ID]: remainingSection,
94
+ },
95
+ };
96
+ }
82
97
  function patchChannelSection(cfg, patch) {
83
98
  const nextChannels = {
84
99
  ...(cfg.channels ?? {}),
@@ -7,6 +7,7 @@ export type DeviceCodeAwaitResult = {
7
7
  user_id: string;
8
8
  plugin_token: string;
9
9
  };
10
+ export declare function normalizeDeviceCodeUrl(url: string): string;
10
11
  export declare function printDeviceCodePrompt(start: DeviceCodeStartResult, logger: DeviceCodeLogger): Promise<void>;
11
12
  export declare function awaitDeviceCodeApproval(params: {
12
13
  api: ClawHiveCloudApi;
@@ -5,18 +5,24 @@ async function renderQr(text) {
5
5
  qrcodeTerminal.generate(text, { small: true }, (qr) => resolve(qr));
6
6
  });
7
7
  }
8
+ export function normalizeDeviceCodeUrl(url) {
9
+ const parsed = new URL(url);
10
+ if (parsed.hostname === "127.0.0.1") {
11
+ parsed.hostname = "localhost";
12
+ }
13
+ return parsed.toString();
14
+ }
8
15
  export async function printDeviceCodePrompt(start, logger) {
9
- const qr = await renderQr(start.verification_uri_complete);
16
+ const qr = await renderQr(`clawhive-auth:v1:${start.device_code}`);
10
17
  const expiresInMin = Math.floor(start.expires_in / 60);
11
18
  const lines = [
12
19
  "",
13
20
  "════════════════════════════════════════════════════════════",
14
21
  " ClawHive: authorize this OpenClaw install.",
15
22
  "",
16
- ` Visit: ${start.verification_uri}`,
17
23
  ` Enter the code: ${start.user_code}`,
18
24
  "",
19
- " Or scan this QR to jump straight to the approval page:",
25
+ " Scan this QR with the signed-in ClawHive mobile app:",
20
26
  qr,
21
27
  ` (Expires in ${expiresInMin}m; polling for approval…)`,
22
28
  "════════════════════════════════════════════════════════════",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawhive/openclaw-plugin",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "ClawHive channel plugin for OpenClaw",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://github.com/optimalAIs/clawhive#readme",