@clawhive/openclaw-plugin 0.2.4 → 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
  /**
@@ -145,6 +145,54 @@ export async function authorizeClawHivePlugin(api, deps = {}) {
145
145
  throw error;
146
146
  }
147
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
+ }
148
196
  function isInvalidStoredPluginTokenError(error) {
149
197
  if (!(error instanceof ClawHiveApiError)) {
150
198
  return false;
@@ -165,10 +213,17 @@ export function registerClawHiveCli(api) {
165
213
  .description("Manage ClawHive plugin authorization and pairing");
166
214
  clawhive
167
215
  .command("authorize")
168
- .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")
169
217
  .action(async () => {
170
218
  await authorizeClawHivePlugin(api);
171
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
+ });
172
227
  clawhive
173
228
  .command("pair")
174
229
  .description("Print a fresh mobile pairing QR code and short code")
@@ -58,6 +58,13 @@ export type PluginRegisterResult = {
58
58
  heartbeat_interval: number;
59
59
  user_session: UserSession;
60
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
+ };
61
68
  export type PairingCodeResult = {
62
69
  pair_token: string;
63
70
  expires_at: string;
@@ -195,6 +202,7 @@ export declare class ClawHiveCloudApi {
195
202
  }): Promise<DeviceCodeStartResult>;
196
203
  exchangeDeviceCode(deviceCode: string): Promise<DeviceCodeExchangeResult>;
197
204
  registerPluginNode(): Promise<PluginRegisterResult>;
205
+ unlinkPluginNode(): Promise<PluginUnlinkResult>;
198
206
  createPairingCode(pluginNodeId: string, ttlSeconds?: number): Promise<PairingCodeResult>;
199
207
  heartbeat(params: {
200
208
  pluginNodeId: string;
@@ -148,6 +148,20 @@ export class ClawHiveCloudApi {
148
148
  this.config.pluginNodeId = result.plugin_node.id;
149
149
  return result;
150
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
+ }
151
165
  async createPairingCode(pluginNodeId, ttlSeconds) {
152
166
  return this.request({
153
167
  path: "pairing-code-create",
@@ -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 ?? {}),
@@ -13,19 +13,16 @@ export function normalizeDeviceCodeUrl(url) {
13
13
  return parsed.toString();
14
14
  }
15
15
  export async function printDeviceCodePrompt(start, logger) {
16
- const verificationUri = normalizeDeviceCodeUrl(start.verification_uri);
17
- const verificationUriComplete = normalizeDeviceCodeUrl(start.verification_uri_complete);
18
- const qr = await renderQr(verificationUriComplete);
16
+ const qr = await renderQr(`clawhive-auth:v1:${start.device_code}`);
19
17
  const expiresInMin = Math.floor(start.expires_in / 60);
20
18
  const lines = [
21
19
  "",
22
20
  "════════════════════════════════════════════════════════════",
23
21
  " ClawHive: authorize this OpenClaw install.",
24
22
  "",
25
- ` Visit: ${verificationUri}`,
26
23
  ` Enter the code: ${start.user_code}`,
27
24
  "",
28
- " Or scan this QR to jump straight to the approval page:",
25
+ " Scan this QR with the signed-in ClawHive mobile app:",
29
26
  qr,
30
27
  ` (Expires in ${expiresInMin}m; polling for approval…)`,
31
28
  "════════════════════════════════════════════════════════════",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawhive/openclaw-plugin",
3
- "version": "0.2.4",
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",