@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 +23 -2
- package/dist/src/authorization.d.ts +10 -0
- package/dist/src/authorization.js +68 -6
- package/dist/src/client.d.ts +16 -1
- package/dist/src/client.js +25 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.js +15 -0
- package/dist/src/deviceCode.d.ts +1 -0
- package/dist/src/deviceCode.js +9 -3
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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:
|
|
131
|
+
userId: registration.user_id,
|
|
132
|
+
pluginNodeId: registration.plugin_node.id,
|
|
126
133
|
}));
|
|
127
|
-
api.logger.info("ClawHive authorization completed. Restart the gateway
|
|
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
|
|
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")
|
package/dist/src/client.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/client.js
CHANGED
|
@@ -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,
|
package/dist/src/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/config.js
CHANGED
|
@@ -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 ?? {}),
|
package/dist/src/deviceCode.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/deviceCode.js
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
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
|
"════════════════════════════════════════════════════════════",
|