@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.
- package/README.md +217 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +324 -0
- package/dist/runtime-api.d.ts +1 -0
- package/dist/runtime-api.js +1 -0
- package/dist/setup-entry.d.ts +4 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/agency-install/claimLoop.d.ts +41 -0
- package/dist/src/agency-install/claimLoop.js +188 -0
- package/dist/src/agent-panel/claimLoop.d.ts +32 -0
- package/dist/src/agent-panel/claimLoop.js +118 -0
- package/dist/src/agent-panel/executor.d.ts +8 -0
- package/dist/src/agent-panel/executor.js +368 -0
- package/dist/src/agent-panel/plan.d.ts +20 -0
- package/dist/src/agent-panel/plan.js +111 -0
- package/dist/src/agent-panel/prompts.d.ts +38 -0
- package/dist/src/agent-panel/prompts.js +87 -0
- package/dist/src/agent-panel/types.d.ts +45 -0
- package/dist/src/agent-panel/types.js +1 -0
- package/dist/src/agents.d.ts +44 -0
- package/dist/src/agents.js +195 -0
- package/dist/src/authorization.d.ts +34 -0
- package/dist/src/authorization.js +183 -0
- package/dist/src/channel.d.ts +5 -0
- package/dist/src/channel.js +93 -0
- package/dist/src/claimPolling.d.ts +9 -0
- package/dist/src/claimPolling.js +13 -0
- package/dist/src/client.d.ts +386 -0
- package/dist/src/client.js +595 -0
- package/dist/src/config.d.ts +28 -0
- package/dist/src/config.js +94 -0
- package/dist/src/defaults.d.ts +40 -0
- package/dist/src/defaults.js +52 -0
- package/dist/src/deviceCode.d.ts +16 -0
- package/dist/src/deviceCode.js +65 -0
- package/dist/src/heartbeat.d.ts +25 -0
- package/dist/src/heartbeat.js +65 -0
- package/dist/src/imageAttachments.d.ts +14 -0
- package/dist/src/imageAttachments.js +81 -0
- package/dist/src/inbound.d.ts +10 -0
- package/dist/src/inbound.js +140 -0
- package/dist/src/installMutex.d.ts +4 -0
- package/dist/src/installMutex.js +18 -0
- package/dist/src/market-install/claimLoop.d.ts +41 -0
- package/dist/src/market-install/claimLoop.js +183 -0
- package/dist/src/market-install/downloader.d.ts +36 -0
- package/dist/src/market-install/downloader.js +133 -0
- package/dist/src/market-install/executor.d.ts +28 -0
- package/dist/src/market-install/executor.js +352 -0
- package/dist/src/market-install/types.d.ts +62 -0
- package/dist/src/market-install/types.js +1 -0
- package/dist/src/market-install/verifier.d.ts +32 -0
- package/dist/src/market-install/verifier.js +60 -0
- package/dist/src/market-publish/packager.d.ts +34 -0
- package/dist/src/market-publish/packager.js +168 -0
- package/dist/src/market-publish/publishFlow.d.ts +70 -0
- package/dist/src/market-publish/publishFlow.js +107 -0
- package/dist/src/market-publish/uploader.d.ts +73 -0
- package/dist/src/market-publish/uploader.js +132 -0
- package/dist/src/openclawVersion.d.ts +4 -0
- package/dist/src/openclawVersion.js +13 -0
- package/dist/src/outbound.d.ts +13 -0
- package/dist/src/outbound.js +41 -0
- package/dist/src/pairing.d.ts +32 -0
- package/dist/src/pairing.js +64 -0
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +26 -0
- package/dist/src/telemetry.d.ts +34 -0
- package/dist/src/telemetry.js +89 -0
- package/dist/src/transport/realtime.d.ts +49 -0
- package/dist/src/transport/realtime.js +273 -0
- package/dist/src/transport.d.ts +38 -0
- package/dist/src/transport.js +15 -0
- package/dist/src/wake.d.ts +31 -0
- package/dist/src/wake.js +101 -0
- package/openclaw.config.example.yaml +10 -0
- package/openclaw.plugin.json +122 -0
- package/package.json +84 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { ClawHiveCloudApi, SyncAgentInput, SyncAgentsResult, SyncedAgent } from "./client.js";
|
|
3
|
+
type GatewayAgent = {
|
|
4
|
+
id?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
identity?: {
|
|
7
|
+
name?: string;
|
|
8
|
+
avatar?: string;
|
|
9
|
+
avatarUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
workspace?: string;
|
|
12
|
+
};
|
|
13
|
+
type GatewayAgentsResult = {
|
|
14
|
+
agents?: GatewayAgent[];
|
|
15
|
+
};
|
|
16
|
+
type ReadLocalAgentsDeps = {
|
|
17
|
+
listGatewayAgents?: (cfg: OpenClawConfig) => GatewayAgentsResult;
|
|
18
|
+
listExistingAgentIdsFromDisk?: () => string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function readLocalAgents(cfg: OpenClawConfig, deps?: ReadLocalAgentsDeps): SyncAgentInput[];
|
|
21
|
+
export declare function pushLocalAgents(cloudApi: ClawHiveCloudApi, cfg: OpenClawConfig): Promise<SyncAgentsResult>;
|
|
22
|
+
export type AgentSyncLoopLogger = {
|
|
23
|
+
info: (msg: string) => void;
|
|
24
|
+
warn: (msg: string) => void;
|
|
25
|
+
};
|
|
26
|
+
export type AgentSyncLoopOptions = {
|
|
27
|
+
cloudApi: ClawHiveCloudApi;
|
|
28
|
+
loadConfig: () => OpenClawConfig;
|
|
29
|
+
intervalSeconds: number;
|
|
30
|
+
logger?: AgentSyncLoopLogger;
|
|
31
|
+
onAgentsSynced?: (items: SyncedAgent[]) => void;
|
|
32
|
+
};
|
|
33
|
+
export declare class AgentSyncLoop {
|
|
34
|
+
private readonly options;
|
|
35
|
+
private timer;
|
|
36
|
+
private running;
|
|
37
|
+
private lastSignature;
|
|
38
|
+
constructor(options: AgentSyncLoopOptions);
|
|
39
|
+
start(): void;
|
|
40
|
+
stop(): void;
|
|
41
|
+
private schedule;
|
|
42
|
+
private tick;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function normalizeAgentId(id) {
|
|
5
|
+
return (id ?? "").trim().toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
function resolveUserPath(input) {
|
|
8
|
+
if (input === "~")
|
|
9
|
+
return os.homedir();
|
|
10
|
+
if (input.startsWith("~/"))
|
|
11
|
+
return path.join(os.homedir(), input.slice(2));
|
|
12
|
+
return input;
|
|
13
|
+
}
|
|
14
|
+
function resolveStateDir() {
|
|
15
|
+
return process.env.OPENCLAW_STATE_DIR?.trim()
|
|
16
|
+
? resolveUserPath(process.env.OPENCLAW_STATE_DIR.trim())
|
|
17
|
+
: path.join(os.homedir(), ".openclaw");
|
|
18
|
+
}
|
|
19
|
+
function resolveDefaultAgentId(cfg) {
|
|
20
|
+
const section = cfg.agents ?? {};
|
|
21
|
+
const list = Array.isArray(section.list) ? section.list : [];
|
|
22
|
+
if (list.length === 0)
|
|
23
|
+
return "main";
|
|
24
|
+
const defaultEntry = list.find((entry) => entry?.default);
|
|
25
|
+
return normalizeAgentId((defaultEntry ?? list[0])?.id) || "main";
|
|
26
|
+
}
|
|
27
|
+
function resolveAgentWorkspaceDir(cfg, id) {
|
|
28
|
+
const section = cfg.agents ?? {};
|
|
29
|
+
const list = Array.isArray(section.list) ? section.list : [];
|
|
30
|
+
const configured = list.find((entry) => normalizeAgentId(entry.id) === id);
|
|
31
|
+
if (configured?.workspace?.trim()) {
|
|
32
|
+
return resolveUserPath(configured.workspace.trim());
|
|
33
|
+
}
|
|
34
|
+
const defaultWorkspace = section.defaults?.workspace?.trim();
|
|
35
|
+
if (defaultWorkspace) {
|
|
36
|
+
const root = resolveUserPath(defaultWorkspace);
|
|
37
|
+
return id === resolveDefaultAgentId(cfg) ? root : path.join(root, id);
|
|
38
|
+
}
|
|
39
|
+
return id === resolveDefaultAgentId(cfg)
|
|
40
|
+
? path.join(resolveStateDir(), "workspace")
|
|
41
|
+
: path.join(resolveStateDir(), `workspace-${id}`);
|
|
42
|
+
}
|
|
43
|
+
function resolveAgentDir(cfg, id) {
|
|
44
|
+
const section = cfg.agents ?? {};
|
|
45
|
+
const list = Array.isArray(section.list) ? section.list : [];
|
|
46
|
+
const configured = list.find((entry) => normalizeAgentId(entry.id) === id);
|
|
47
|
+
if (configured?.agentDir?.trim()) {
|
|
48
|
+
return resolveUserPath(configured.agentDir.trim());
|
|
49
|
+
}
|
|
50
|
+
return path.join(resolveStateDir(), "agents", id, "agent");
|
|
51
|
+
}
|
|
52
|
+
function listExistingAgentIdsFromDisk() {
|
|
53
|
+
const agentsDir = path.join(resolveStateDir(), "agents");
|
|
54
|
+
try {
|
|
55
|
+
return fs
|
|
56
|
+
.readdirSync(agentsDir, { withFileTypes: true })
|
|
57
|
+
.filter((entry) => entry.isDirectory())
|
|
58
|
+
.map((entry) => normalizeAgentId(entry.name))
|
|
59
|
+
.filter((id) => id.length > 0);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function listGatewayAgents(cfg) {
|
|
66
|
+
const section = cfg.agents ?? {};
|
|
67
|
+
const configured = Array.isArray(section.list) ? section.list : [];
|
|
68
|
+
const explicitIds = new Set(configured.map((entry) => normalizeAgentId(entry.id)).filter((id) => id.length > 0));
|
|
69
|
+
const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
|
|
70
|
+
explicitIds.add(defaultId);
|
|
71
|
+
for (const id of listExistingAgentIdsFromDisk()) {
|
|
72
|
+
explicitIds.add(id);
|
|
73
|
+
}
|
|
74
|
+
const ids = Array.from(explicitIds).sort((a, b) => a.localeCompare(b));
|
|
75
|
+
const orderedIds = ids.includes(defaultId)
|
|
76
|
+
? [defaultId, ...ids.filter((id) => id !== defaultId)]
|
|
77
|
+
: ids;
|
|
78
|
+
return {
|
|
79
|
+
agents: orderedIds.map((id) => {
|
|
80
|
+
const entry = configured.find((item) => normalizeAgentId(item.id) === id);
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
name: entry?.name,
|
|
84
|
+
identity: entry?.identity,
|
|
85
|
+
workspace: resolveAgentWorkspaceDir(cfg, id),
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function readLocalAgents(cfg, deps = {}) {
|
|
91
|
+
const section = cfg.agents ?? {};
|
|
92
|
+
const list = Array.isArray(section.list) ? section.list : [];
|
|
93
|
+
const byId = new Map();
|
|
94
|
+
const configuredById = new Map(list
|
|
95
|
+
.filter((entry) => typeof entry?.id === "string" && entry.id.trim().length > 0)
|
|
96
|
+
.map((entry) => [normalizeAgentId(entry.id), entry]));
|
|
97
|
+
for (const agent of (deps.listGatewayAgents ?? listGatewayAgents)(cfg).agents ?? []) {
|
|
98
|
+
const id = normalizeAgentId(agent.id);
|
|
99
|
+
if (!id)
|
|
100
|
+
continue;
|
|
101
|
+
const configured = configuredById.get(id);
|
|
102
|
+
byId.set(id, {
|
|
103
|
+
openclawAgentId: id,
|
|
104
|
+
name: (agent.name?.trim() ||
|
|
105
|
+
agent.identity?.name?.trim() ||
|
|
106
|
+
configured?.name?.trim() ||
|
|
107
|
+
id),
|
|
108
|
+
description: configured?.description ?? null,
|
|
109
|
+
avatarUrl: agent.identity?.avatarUrl ?? agent.identity?.avatar ?? null,
|
|
110
|
+
config: {
|
|
111
|
+
agency: configured?.agency,
|
|
112
|
+
workspace: agent.workspace ?? configured?.workspace,
|
|
113
|
+
agentDir: configured?.agentDir ?? resolveAgentDir(cfg, id),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
for (const entry of list) {
|
|
118
|
+
const id = normalizeAgentId(entry.id);
|
|
119
|
+
if (!id || byId.has(id))
|
|
120
|
+
continue;
|
|
121
|
+
byId.set(id, {
|
|
122
|
+
openclawAgentId: id,
|
|
123
|
+
name: (entry.name?.trim() || id),
|
|
124
|
+
description: entry.description ?? null,
|
|
125
|
+
config: {
|
|
126
|
+
agency: entry.agency,
|
|
127
|
+
workspace: entry.workspace,
|
|
128
|
+
agentDir: entry.agentDir,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return Array.from(byId.values());
|
|
133
|
+
}
|
|
134
|
+
export async function pushLocalAgents(cloudApi, cfg) {
|
|
135
|
+
const agents = readLocalAgents(cfg);
|
|
136
|
+
if (agents.length === 0) {
|
|
137
|
+
return { items: [], skipped: 0 };
|
|
138
|
+
}
|
|
139
|
+
return await cloudApi.syncAgents(agents);
|
|
140
|
+
}
|
|
141
|
+
export class AgentSyncLoop {
|
|
142
|
+
options;
|
|
143
|
+
timer = null;
|
|
144
|
+
running = false;
|
|
145
|
+
lastSignature = null;
|
|
146
|
+
constructor(options) {
|
|
147
|
+
this.options = options;
|
|
148
|
+
}
|
|
149
|
+
start() {
|
|
150
|
+
if (this.running)
|
|
151
|
+
return;
|
|
152
|
+
this.running = true;
|
|
153
|
+
void this.tick(true);
|
|
154
|
+
}
|
|
155
|
+
stop() {
|
|
156
|
+
this.running = false;
|
|
157
|
+
if (this.timer) {
|
|
158
|
+
clearTimeout(this.timer);
|
|
159
|
+
this.timer = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
schedule() {
|
|
163
|
+
if (!this.running)
|
|
164
|
+
return;
|
|
165
|
+
this.timer = setTimeout(() => {
|
|
166
|
+
this.timer = null;
|
|
167
|
+
void this.tick(false);
|
|
168
|
+
}, this.options.intervalSeconds * 1000);
|
|
169
|
+
}
|
|
170
|
+
async tick(initial) {
|
|
171
|
+
if (!this.running)
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
const agents = readLocalAgents(this.options.loadConfig());
|
|
175
|
+
const signature = JSON.stringify(agents);
|
|
176
|
+
if (signature !== this.lastSignature) {
|
|
177
|
+
this.lastSignature = signature;
|
|
178
|
+
if (agents.length > 0) {
|
|
179
|
+
const result = await this.options.cloudApi.syncAgents(agents);
|
|
180
|
+
this.options.onAgentsSynced?.(result.items);
|
|
181
|
+
if (!initial || result.items.length > 0 || result.skipped > 0) {
|
|
182
|
+
this.options.logger?.info(`ClawHive synced ${result.items.length} agent(s) to cloud (skipped ${result.skipped}).`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
189
|
+
this.options.logger?.warn(`ClawHive: failed to refresh local agents: ${reason}`);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
this.schedule();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { ClawHiveCloudApi } from "./client.js";
|
|
3
|
+
import { resolveResolvedAccount } from "./config.js";
|
|
4
|
+
import { awaitDeviceCodeApproval, printDeviceCodePrompt } from "./deviceCode.js";
|
|
5
|
+
/**
|
|
6
|
+
* Build the cloud API client from the latest OpenClaw config state.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createClawHiveCloudApi(api: OpenClawPluginApi): {
|
|
9
|
+
account: ReturnType<typeof resolveResolvedAccount>;
|
|
10
|
+
anonKey: string;
|
|
11
|
+
cloudApi: ClawHiveCloudApi;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Print the next-step hint shown when the plugin is installed but not yet authorized.
|
|
15
|
+
*/
|
|
16
|
+
export declare function printAuthorizationCommandHint(logger: OpenClawPluginApi["logger"]): void;
|
|
17
|
+
type AuthorizationDeps = {
|
|
18
|
+
createCloudApi?: typeof createClawHiveCloudApi;
|
|
19
|
+
printDeviceCodePrompt?: typeof printDeviceCodePrompt;
|
|
20
|
+
awaitDeviceCodeApproval?: typeof awaitDeviceCodeApproval;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Print a fresh mobile pairing QR and short code on demand.
|
|
24
|
+
*/
|
|
25
|
+
export declare function printManualPairingCode(api: OpenClawPluginApi, ttlSeconds?: number): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Run the explicit device-authorization command for the ClawHive plugin.
|
|
28
|
+
*/
|
|
29
|
+
export declare function authorizeClawHivePlugin(api: OpenClawPluginApi, deps?: AuthorizationDeps): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Register the top-level `openclaw clawhive` CLI group for manual plugin authorization.
|
|
32
|
+
*/
|
|
33
|
+
export declare function registerClawHiveCli(api: OpenClawPluginApi): void;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { ClawHiveCloudApi, ClawHiveApiError, DeviceCodeDeniedError, DeviceCodeExpiredError, } from "./client.js";
|
|
2
|
+
import { resolveResolvedAccount, writeRegistrationIdentity } from "./config.js";
|
|
3
|
+
import { awaitDeviceCodeApproval, printDeviceCodePrompt, } from "./deviceCode.js";
|
|
4
|
+
import { getPairedDeviceCount, printPairingQr } from "./pairing.js";
|
|
5
|
+
/**
|
|
6
|
+
* Build the cloud API client from the latest OpenClaw config state.
|
|
7
|
+
*/
|
|
8
|
+
export function createClawHiveCloudApi(api) {
|
|
9
|
+
const cfg = api.runtime.config.loadConfig();
|
|
10
|
+
const account = resolveResolvedAccount(cfg);
|
|
11
|
+
const anonKey = account.anonKey;
|
|
12
|
+
if (!anonKey) {
|
|
13
|
+
throw new Error("ClawHive plugin requires a Supabase anon key. Set `channels.clawhive.anonKey` in OpenClaw config or export `CLAWHIVE_SUPABASE_ANON_KEY` in the current terminal.");
|
|
14
|
+
}
|
|
15
|
+
const cloudApi = new ClawHiveCloudApi({
|
|
16
|
+
projectUrl: account.projectUrl,
|
|
17
|
+
pluginToken: account.pluginToken ?? null,
|
|
18
|
+
userId: account.userId ?? null,
|
|
19
|
+
anonKey,
|
|
20
|
+
pluginNodeId: account.pluginNodeId,
|
|
21
|
+
nodeName: account.nodeName,
|
|
22
|
+
pluginVersion: account.pluginVersion,
|
|
23
|
+
});
|
|
24
|
+
return { account, anonKey, cloudApi };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Print the next-step hint shown when the plugin is installed but not yet authorized.
|
|
28
|
+
*/
|
|
29
|
+
export function printAuthorizationCommandHint(logger) {
|
|
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.");
|
|
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
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Persist any plugin-register identity drift back into the user's OpenClaw config.
|
|
36
|
+
*/
|
|
37
|
+
async function syncRegistrationIdentity(api, input) {
|
|
38
|
+
const patch = {};
|
|
39
|
+
if (!input.account.userId || input.account.userId !== input.registration.user_id) {
|
|
40
|
+
patch.userId = input.registration.user_id;
|
|
41
|
+
}
|
|
42
|
+
if (input.account.pluginNodeId !== input.registration.plugin_node.id) {
|
|
43
|
+
patch.pluginNodeId = input.registration.plugin_node.id;
|
|
44
|
+
}
|
|
45
|
+
const latestCfg = api.runtime.config.loadConfig();
|
|
46
|
+
const latestChannels = latestCfg.channels ?? {};
|
|
47
|
+
const latestChannel = latestChannels.clawhive ?? {};
|
|
48
|
+
if (!latestChannel.anonKey && input.account.anonKey) {
|
|
49
|
+
patch.anonKey = input.account.anonKey;
|
|
50
|
+
}
|
|
51
|
+
if (!patch.userId && !patch.pluginNodeId && !patch.anonKey) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await api.runtime.config.writeConfigFile(writeRegistrationIdentity(latestCfg, patch));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Print a fresh mobile pairing QR and short code on demand.
|
|
58
|
+
*/
|
|
59
|
+
export async function printManualPairingCode(api, ttlSeconds = 300) {
|
|
60
|
+
const { account, cloudApi } = createClawHiveCloudApi(api);
|
|
61
|
+
if (!cloudApi.pluginToken) {
|
|
62
|
+
api.logger.warn("ClawHive is not authorized yet, so a pairing code cannot be created.");
|
|
63
|
+
printAuthorizationCommandHint(api.logger);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
api.logger.info(`ClawHive: generating a mobile pairing code against ${account.projectUrl}.`);
|
|
67
|
+
const registration = await cloudApi.registerPluginNode();
|
|
68
|
+
await syncRegistrationIdentity(api, { account, registration });
|
|
69
|
+
let pairedDeviceCount = 0;
|
|
70
|
+
try {
|
|
71
|
+
pairedDeviceCount = await getPairedDeviceCount({
|
|
72
|
+
registration,
|
|
73
|
+
projectUrl: account.projectUrl,
|
|
74
|
+
anonKey: account.anonKey,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
79
|
+
api.logger.warn(`ClawHive: could not determine existing paired devices before printing the QR (${reason}).`);
|
|
80
|
+
}
|
|
81
|
+
await printPairingQr({
|
|
82
|
+
api: cloudApi,
|
|
83
|
+
pluginNodeId: registration.plugin_node.id,
|
|
84
|
+
ttlSeconds,
|
|
85
|
+
banner: pairedDeviceCount > 0
|
|
86
|
+
? `ClawHive: ${pairedDeviceCount} phone(s) already paired. Scan this QR with the ClawHive mobile app to pair an additional phone or recover after reinstall (${ttlSeconds} second TTL).`
|
|
87
|
+
: `ClawHive: no phones paired yet. Scan this QR with the ClawHive mobile app to pair (${ttlSeconds} second TTL).`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Run the explicit device-authorization command for the ClawHive plugin.
|
|
92
|
+
*/
|
|
93
|
+
export async function authorizeClawHivePlugin(api, deps = {}) {
|
|
94
|
+
const { account, cloudApi } = (deps.createCloudApi ?? createClawHiveCloudApi)(api);
|
|
95
|
+
if (cloudApi.pluginToken) {
|
|
96
|
+
try {
|
|
97
|
+
await cloudApi.registerPluginNode();
|
|
98
|
+
api.logger.info("ClawHive is already authorized. Restart the gateway if you are waiting for it to connect.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!isInvalidStoredPluginTokenError(error)) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
api.logger.warn("ClawHive stored plugin token is no longer valid; starting authorization again.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
api.logger.info(`ClawHive: starting manual device authorization against ${account.projectUrl}.`);
|
|
109
|
+
try {
|
|
110
|
+
const start = await cloudApi.startDeviceCode({
|
|
111
|
+
nodeName: account.nodeName,
|
|
112
|
+
pluginVersion: account.pluginVersion,
|
|
113
|
+
description: `OpenClaw plugin device authorization (${account.nodeName})`,
|
|
114
|
+
});
|
|
115
|
+
await (deps.printDeviceCodePrompt ?? printDeviceCodePrompt)(start, api.logger);
|
|
116
|
+
const approval = await (deps.awaitDeviceCodeApproval ?? awaitDeviceCodeApproval)({
|
|
117
|
+
api: cloudApi,
|
|
118
|
+
start,
|
|
119
|
+
logger: api.logger,
|
|
120
|
+
});
|
|
121
|
+
const latestCfg = api.runtime.config.loadConfig();
|
|
122
|
+
await api.runtime.config.writeConfigFile(writeRegistrationIdentity(latestCfg, {
|
|
123
|
+
anonKey: account.anonKey,
|
|
124
|
+
pluginToken: approval.plugin_token,
|
|
125
|
+
userId: approval.user_id,
|
|
126
|
+
}));
|
|
127
|
+
api.logger.info("ClawHive authorization completed. Restart the gateway to finish plugin registration.");
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error instanceof DeviceCodeDeniedError) {
|
|
131
|
+
api.logger.warn("ClawHive: the user denied the plugin authorization request.");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (error instanceof DeviceCodeExpiredError) {
|
|
135
|
+
api.logger.warn("ClawHive: device code expired before it was approved. Run `openclaw clawhive authorize` again.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function isInvalidStoredPluginTokenError(error) {
|
|
142
|
+
if (!(error instanceof ClawHiveApiError)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return (error.status === 401 ||
|
|
146
|
+
error.status === 403 ||
|
|
147
|
+
error.errorCode === "Invalid plugin token" ||
|
|
148
|
+
error.errorCode === "Plugin token has been revoked" ||
|
|
149
|
+
error.errorCode === "Missing plugin token");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Register the top-level `openclaw clawhive` CLI group for manual plugin authorization.
|
|
153
|
+
*/
|
|
154
|
+
export function registerClawHiveCli(api) {
|
|
155
|
+
api.registerCli(({ program }) => {
|
|
156
|
+
const clawhive = program
|
|
157
|
+
.command("clawhive")
|
|
158
|
+
.description("Manage ClawHive plugin authorization and pairing");
|
|
159
|
+
clawhive
|
|
160
|
+
.command("authorize")
|
|
161
|
+
.description("Display the approval URL and user code, then wait for plugin authorization")
|
|
162
|
+
.action(async () => {
|
|
163
|
+
await authorizeClawHivePlugin(api);
|
|
164
|
+
});
|
|
165
|
+
clawhive
|
|
166
|
+
.command("pair")
|
|
167
|
+
.description("Print a fresh mobile pairing QR code and short code")
|
|
168
|
+
.option("--ttl-seconds <seconds>", "Pairing code TTL in seconds", "300")
|
|
169
|
+
.action(async (options) => {
|
|
170
|
+
const ttlSeconds = Number.parseInt(options.ttlSeconds ?? "300", 10);
|
|
171
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
|
|
172
|
+
throw new Error("`--ttl-seconds` must be a positive integer.");
|
|
173
|
+
}
|
|
174
|
+
await printManualPairingCode(api, ttlSeconds);
|
|
175
|
+
});
|
|
176
|
+
}, {
|
|
177
|
+
descriptors: [{
|
|
178
|
+
name: "clawhive",
|
|
179
|
+
description: "Manage ClawHive plugin authorization and pairing",
|
|
180
|
+
hasSubcommands: true,
|
|
181
|
+
}],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ClawHiveResolvedAccount } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Keep install/setup permissive so runtime bootstrap can mint the first plugin token.
|
|
4
|
+
*/
|
|
5
|
+
export declare const clawhiveChannelPlugin: import("openclaw/plugin-sdk/channel-core").ChannelPlugin<ClawHiveResolvedAccount, unknown, unknown>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import { CLAWHIVE_CHANNEL_ID, readRawChannelSection, resolveResolvedAccount, writeChannelSection, } from "./config.js";
|
|
4
|
+
import { normalizeSupabaseMode } from "./defaults.js";
|
|
5
|
+
import { rejectMedia, sendAgentText } from "./outbound.js";
|
|
6
|
+
import { clawhiveRuntimeStore } from "./runtime.js";
|
|
7
|
+
/**
|
|
8
|
+
* Keep install/setup permissive so runtime bootstrap can mint the first plugin token.
|
|
9
|
+
*/
|
|
10
|
+
export const clawhiveChannelPlugin = createChatChannelPlugin({
|
|
11
|
+
base: {
|
|
12
|
+
id: CLAWHIVE_CHANNEL_ID,
|
|
13
|
+
meta: {
|
|
14
|
+
id: CLAWHIVE_CHANNEL_ID,
|
|
15
|
+
label: "ClawHive",
|
|
16
|
+
selectionLabel: "ClawHive",
|
|
17
|
+
docsPath: "/channels",
|
|
18
|
+
blurb: "ClawHive cloud pairing and messaging channel",
|
|
19
|
+
exposure: {
|
|
20
|
+
configured: true,
|
|
21
|
+
setup: true,
|
|
22
|
+
docs: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
capabilities: {
|
|
26
|
+
chatTypes: ["direct"],
|
|
27
|
+
reply: true,
|
|
28
|
+
media: false,
|
|
29
|
+
},
|
|
30
|
+
config: {
|
|
31
|
+
listAccountIds: (cfg) => {
|
|
32
|
+
const section = readRawChannelSection(cfg);
|
|
33
|
+
return section.projectUrl ? [DEFAULT_ACCOUNT_ID] : [];
|
|
34
|
+
},
|
|
35
|
+
resolveAccount: resolveResolvedAccount,
|
|
36
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
37
|
+
isEnabled: () => true,
|
|
38
|
+
isConfigured: (account) => Boolean(account.projectUrl),
|
|
39
|
+
unconfiguredReason: () => "ClawHive needs a Supabase target. Use CLAWHIVE_SUPABASE_MODE=cloud for the baked-in managed project, or set CLAWHIVE_SUPABASE_MODE=local with CLAWHIVE_PROJECT_URL and CLAWHIVE_SUPABASE_ANON_KEY.",
|
|
40
|
+
},
|
|
41
|
+
setup: {
|
|
42
|
+
applyAccountConfig: ({ cfg, input }) => writeChannelSection(cfg, input),
|
|
43
|
+
validateInput: ({ input }) => {
|
|
44
|
+
if (normalizeSupabaseMode(process.env.CLAWHIVE_SUPABASE_MODE) !== "local") {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (!input.url && !process.env.CLAWHIVE_PROJECT_URL) {
|
|
48
|
+
return "ClawHive local mode requires CLAWHIVE_PROJECT_URL or an explicit setup URL.";
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
security: {
|
|
55
|
+
dm: {
|
|
56
|
+
channelKey: CLAWHIVE_CHANNEL_ID,
|
|
57
|
+
resolvePolicy: (account) => account.dmPolicy ?? null,
|
|
58
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
59
|
+
defaultPolicy: "allowlist",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
pairing: {
|
|
63
|
+
text: {
|
|
64
|
+
idLabel: "ClawHive mobile device",
|
|
65
|
+
message: "Scan this QR or enter the short code in the ClawHive mobile app to pair:",
|
|
66
|
+
notify: async ({ id, message }) => {
|
|
67
|
+
const runtime = clawhiveRuntimeStore.tryGetRuntime();
|
|
68
|
+
runtime?.pluginApi.logger.info(`ClawHive pairing notification target=${id} message=${message}`);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
threading: {
|
|
73
|
+
topLevelReplyToMode: "reply",
|
|
74
|
+
},
|
|
75
|
+
outbound: {
|
|
76
|
+
base: {
|
|
77
|
+
deliveryMode: "direct",
|
|
78
|
+
},
|
|
79
|
+
attachedResults: {
|
|
80
|
+
channel: CLAWHIVE_CHANNEL_ID,
|
|
81
|
+
sendText: async (ctx) => {
|
|
82
|
+
const result = await sendAgentText({
|
|
83
|
+
cfg: ctx.cfg,
|
|
84
|
+
to: ctx.to,
|
|
85
|
+
text: ctx.text,
|
|
86
|
+
accountId: ctx.accountId,
|
|
87
|
+
});
|
|
88
|
+
return result;
|
|
89
|
+
},
|
|
90
|
+
sendMedia: async () => rejectMedia(),
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const DEFAULT_IDLE_POLL_MS = 180000;
|
|
2
|
+
export declare const DEFAULT_RETRY_POLL_MS = 30000;
|
|
3
|
+
export declare const DEFAULT_ACTIVE_POLL_MS = 1500;
|
|
4
|
+
export declare const DEFAULT_POLL_JITTER_RATIO = 0.2;
|
|
5
|
+
/**
|
|
6
|
+
* Add bounded jitter so multiple background claim loops do not keep hitting
|
|
7
|
+
* Edge Functions in the same narrow CPU window.
|
|
8
|
+
*/
|
|
9
|
+
export declare function jitterPollDelay(delayMs: number, random?: () => number): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const DEFAULT_IDLE_POLL_MS = 180_000;
|
|
2
|
+
export const DEFAULT_RETRY_POLL_MS = 30_000;
|
|
3
|
+
export const DEFAULT_ACTIVE_POLL_MS = 1_500;
|
|
4
|
+
export const DEFAULT_POLL_JITTER_RATIO = 0.2;
|
|
5
|
+
/**
|
|
6
|
+
* Add bounded jitter so multiple background claim loops do not keep hitting
|
|
7
|
+
* Edge Functions in the same narrow CPU window.
|
|
8
|
+
*/
|
|
9
|
+
export function jitterPollDelay(delayMs, random = Math.random) {
|
|
10
|
+
const jitterRange = Math.max(0, delayMs * DEFAULT_POLL_JITTER_RATIO);
|
|
11
|
+
const offset = (random() * 2 - 1) * jitterRange;
|
|
12
|
+
return Math.max(250, Math.round(delayMs + offset));
|
|
13
|
+
}
|