@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,183 @@
|
|
|
1
|
+
import { DEFAULT_ACTIVE_POLL_MS, DEFAULT_IDLE_POLL_MS, DEFAULT_RETRY_POLL_MS, jitterPollDelay, } from "../claimPolling.js";
|
|
2
|
+
import { runExclusiveInstall } from "../installMutex.js";
|
|
3
|
+
import { addInstallBreadcrumb, captureInstallFailure } from "../telemetry.js";
|
|
4
|
+
import { executeInstallJob } from "./executor.js";
|
|
5
|
+
/**
|
|
6
|
+
* Poll the durable install queue without blocking the existing heartbeat or realtime loops.
|
|
7
|
+
*/
|
|
8
|
+
export class InstallClaimLoop {
|
|
9
|
+
options;
|
|
10
|
+
timer = null;
|
|
11
|
+
wakeTimer = null;
|
|
12
|
+
wakePending = false;
|
|
13
|
+
claiming = false;
|
|
14
|
+
running = false;
|
|
15
|
+
activeRun = null;
|
|
16
|
+
lastError = null;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
start() {
|
|
21
|
+
if (this.running) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.running = true;
|
|
25
|
+
void this.tick(true);
|
|
26
|
+
}
|
|
27
|
+
async stop() {
|
|
28
|
+
this.running = false;
|
|
29
|
+
if (this.timer) {
|
|
30
|
+
clearTimeout(this.timer);
|
|
31
|
+
this.timer = null;
|
|
32
|
+
}
|
|
33
|
+
if (this.wakeTimer) {
|
|
34
|
+
clearTimeout(this.wakeTimer);
|
|
35
|
+
this.wakeTimer = null;
|
|
36
|
+
}
|
|
37
|
+
this.wakePending = false;
|
|
38
|
+
if (this.activeRun) {
|
|
39
|
+
await this.activeRun;
|
|
40
|
+
}
|
|
41
|
+
this.options.onActiveInstallChange?.(null);
|
|
42
|
+
}
|
|
43
|
+
getLastError() {
|
|
44
|
+
return this.lastError;
|
|
45
|
+
}
|
|
46
|
+
wake() {
|
|
47
|
+
if (!this.running) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.wakePending = true;
|
|
51
|
+
if (this.timer) {
|
|
52
|
+
clearTimeout(this.timer);
|
|
53
|
+
this.timer = null;
|
|
54
|
+
}
|
|
55
|
+
if (this.wakeTimer) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.wakeTimer = setTimeout(() => {
|
|
59
|
+
this.wakeTimer = null;
|
|
60
|
+
if (!this.running || !this.wakePending) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.wakePending = false;
|
|
64
|
+
void this.tick(false);
|
|
65
|
+
}, 25);
|
|
66
|
+
}
|
|
67
|
+
schedule(delayMs) {
|
|
68
|
+
if (!this.running) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const nextDelayMs = jitterPollDelay(delayMs);
|
|
72
|
+
this.timer = setTimeout(() => {
|
|
73
|
+
this.timer = null;
|
|
74
|
+
void this.tick(false);
|
|
75
|
+
}, nextDelayMs);
|
|
76
|
+
}
|
|
77
|
+
async tick(initial) {
|
|
78
|
+
if (!this.running) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (this.claiming) {
|
|
82
|
+
this.wakePending = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (this.activeRun) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.claiming = true;
|
|
89
|
+
try {
|
|
90
|
+
const result = await this.options.api.claimInstallJob({
|
|
91
|
+
pluginNodeId: this.options.pluginNodeId,
|
|
92
|
+
userId: this.options.userId,
|
|
93
|
+
});
|
|
94
|
+
this.claiming = false;
|
|
95
|
+
if (!result.job) {
|
|
96
|
+
this.lastError = null;
|
|
97
|
+
this.schedule(this.options.idlePollMs ?? DEFAULT_IDLE_POLL_MS);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.activeRun = this.processJob(result.job).finally(() => {
|
|
101
|
+
this.activeRun = null;
|
|
102
|
+
this.options.onActiveInstallChange?.(null);
|
|
103
|
+
});
|
|
104
|
+
await this.activeRun;
|
|
105
|
+
this.lastError = null;
|
|
106
|
+
this.schedule(DEFAULT_ACTIVE_POLL_MS);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
this.lastError = error;
|
|
110
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
111
|
+
this.options.logger?.warn(`ClawHive install claim loop failed initial=${initial}: ${reason}`);
|
|
112
|
+
this.schedule(this.options.retryPollMs ?? DEFAULT_RETRY_POLL_MS);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (this.claiming) {
|
|
116
|
+
this.claiming = false;
|
|
117
|
+
}
|
|
118
|
+
if (this.running && this.wakePending && !this.activeRun) {
|
|
119
|
+
this.wake();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async processJob(job) {
|
|
124
|
+
const pushStage = (stage) => {
|
|
125
|
+
this.options.onActiveInstallChange?.({
|
|
126
|
+
jobId: job.jobId,
|
|
127
|
+
listingId: job.listingId,
|
|
128
|
+
listingName: job.listingName,
|
|
129
|
+
releaseId: job.releaseId,
|
|
130
|
+
version: job.version,
|
|
131
|
+
targetNodeId: job.targetNodeId,
|
|
132
|
+
targetNodeName: job.targetNodeName,
|
|
133
|
+
stage,
|
|
134
|
+
updatedAt: new Date().toISOString(),
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
pushStage("claimed");
|
|
138
|
+
try {
|
|
139
|
+
await runExclusiveInstall(() => executeInstallJob(job, {
|
|
140
|
+
api: this.options.api,
|
|
141
|
+
pluginApi: this.options.pluginApi,
|
|
142
|
+
pluginNodeId: this.options.pluginNodeId,
|
|
143
|
+
userId: this.options.userId,
|
|
144
|
+
pluginVersion: this.options.pluginVersion,
|
|
145
|
+
openClawVersion: this.options.openClawVersion,
|
|
146
|
+
logger: this.options.logger ?? console,
|
|
147
|
+
onStageChange: pushStage,
|
|
148
|
+
onAgentsSynced: this.options.onAgentsSynced,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
153
|
+
this.options.logger?.warn(`ClawHive install executor crashed for job ${job.jobId}: ${reason}`);
|
|
154
|
+
pushStage("failed");
|
|
155
|
+
addInstallBreadcrumb({
|
|
156
|
+
stage: "executor-crashed",
|
|
157
|
+
job,
|
|
158
|
+
reasonKind: "install_apply_failed",
|
|
159
|
+
data: { error: reason },
|
|
160
|
+
});
|
|
161
|
+
captureInstallFailure({
|
|
162
|
+
error,
|
|
163
|
+
reasonKind: "install_apply_failed",
|
|
164
|
+
job,
|
|
165
|
+
});
|
|
166
|
+
await this.options.api.updateInstallJob({
|
|
167
|
+
pluginNodeId: this.options.pluginNodeId,
|
|
168
|
+
jobId: job.jobId,
|
|
169
|
+
userId: this.options.userId,
|
|
170
|
+
status: "failed",
|
|
171
|
+
reasonKind: "install_apply_failed",
|
|
172
|
+
errorMessage: reason,
|
|
173
|
+
details: {
|
|
174
|
+
crashDuring: "claim-loop",
|
|
175
|
+
listingId: job.listingId,
|
|
176
|
+
releaseId: job.releaseId,
|
|
177
|
+
targetNodeId: job.targetNodeId,
|
|
178
|
+
version: job.version,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ClawHiveCloudApi } from "../client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when download fails after all retries.
|
|
4
|
+
*/
|
|
5
|
+
export declare class DownloadError extends Error {
|
|
6
|
+
readonly releaseId: string;
|
|
7
|
+
readonly attempts: number;
|
|
8
|
+
constructor(message: string, releaseId: string, attempts: number);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Error thrown when signed URL has expired (403 from Storage).
|
|
12
|
+
*/
|
|
13
|
+
export declare class SignedUrlExpiredError extends Error {
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
type DownloadOptions = {
|
|
17
|
+
releaseId: string;
|
|
18
|
+
destinationPath: string;
|
|
19
|
+
accessToken: string;
|
|
20
|
+
maxRetries?: number;
|
|
21
|
+
onProgress?: (bytesDownloaded: number) => void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Download release package from Storage with retry and signed URL expiry handling.
|
|
25
|
+
*
|
|
26
|
+
* Retry strategy:
|
|
27
|
+
* - Max 3 attempts by default
|
|
28
|
+
* - Exponential backoff with jitter (1s → 2s → 4s, capped at 10s)
|
|
29
|
+
* - Automatic signed URL refresh on 403 (expiry)
|
|
30
|
+
*
|
|
31
|
+
* @param api - ClawHiveCloudApi instance
|
|
32
|
+
* @param options - Download options
|
|
33
|
+
* @throws {DownloadError} If download fails after all retries
|
|
34
|
+
*/
|
|
35
|
+
export declare function downloadReleasePackage(api: ClawHiveCloudApi, options: DownloadOptions): Promise<void>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when download fails after all retries.
|
|
4
|
+
*/
|
|
5
|
+
export class DownloadError extends Error {
|
|
6
|
+
releaseId;
|
|
7
|
+
attempts;
|
|
8
|
+
constructor(message, releaseId, attempts) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.releaseId = releaseId;
|
|
11
|
+
this.attempts = attempts;
|
|
12
|
+
this.name = "DownloadError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when signed URL has expired (403 from Storage).
|
|
17
|
+
*/
|
|
18
|
+
export class SignedUrlExpiredError extends Error {
|
|
19
|
+
constructor() {
|
|
20
|
+
super("Signed URL has expired");
|
|
21
|
+
this.name = "SignedUrlExpiredError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
25
|
+
maxRetries: 3,
|
|
26
|
+
initialDelayMs: 1000,
|
|
27
|
+
multiplier: 2,
|
|
28
|
+
maxDelayMs: 10000,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Calculate exponential backoff delay with jitter.
|
|
32
|
+
*/
|
|
33
|
+
function calculateBackoffDelay(attempt, config) {
|
|
34
|
+
const exponentialDelay = Math.min(config.initialDelayMs * Math.pow(config.multiplier, attempt - 1), config.maxDelayMs);
|
|
35
|
+
// Add jitter: ±20% of the delay
|
|
36
|
+
const jitter = exponentialDelay * 0.2 * (Math.random() * 2 - 1);
|
|
37
|
+
return Math.floor(exponentialDelay + jitter);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Sleep for specified milliseconds.
|
|
41
|
+
*/
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Download file from signed URL with progress tracking.
|
|
47
|
+
*/
|
|
48
|
+
async function downloadFromSignedUrl(signedUrl, destinationPath, onProgress) {
|
|
49
|
+
const response = await fetch(signedUrl);
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
if (response.status === 403) {
|
|
52
|
+
throw new SignedUrlExpiredError();
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Download failed with status ${response.status}: ${response.statusText}`);
|
|
55
|
+
}
|
|
56
|
+
if (!response.body) {
|
|
57
|
+
throw new Error("Response body is null");
|
|
58
|
+
}
|
|
59
|
+
const writeStream = createWriteStream(destinationPath);
|
|
60
|
+
const reader = response.body.getReader();
|
|
61
|
+
try {
|
|
62
|
+
let bytesDownloaded = 0;
|
|
63
|
+
while (true) {
|
|
64
|
+
const { done, value } = await reader.read();
|
|
65
|
+
if (done) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
writeStream.write(value);
|
|
69
|
+
bytesDownloaded += value.length;
|
|
70
|
+
if (onProgress) {
|
|
71
|
+
onProgress(bytesDownloaded);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
writeStream.end();
|
|
75
|
+
// Wait for write stream to finish
|
|
76
|
+
await new Promise((resolve, reject) => {
|
|
77
|
+
writeStream.on("finish", resolve);
|
|
78
|
+
writeStream.on("error", reject);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
writeStream.destroy();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Download release package from Storage with retry and signed URL expiry handling.
|
|
88
|
+
*
|
|
89
|
+
* Retry strategy:
|
|
90
|
+
* - Max 3 attempts by default
|
|
91
|
+
* - Exponential backoff with jitter (1s → 2s → 4s, capped at 10s)
|
|
92
|
+
* - Automatic signed URL refresh on 403 (expiry)
|
|
93
|
+
*
|
|
94
|
+
* @param api - ClawHiveCloudApi instance
|
|
95
|
+
* @param options - Download options
|
|
96
|
+
* @throws {DownloadError} If download fails after all retries
|
|
97
|
+
*/
|
|
98
|
+
export async function downloadReleasePackage(api, options) {
|
|
99
|
+
const config = {
|
|
100
|
+
...DEFAULT_RETRY_CONFIG,
|
|
101
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
|
|
102
|
+
};
|
|
103
|
+
let lastError = null;
|
|
104
|
+
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
|
|
105
|
+
try {
|
|
106
|
+
// Get fresh signed URL (handles expiry on retry)
|
|
107
|
+
const { signed_url, fingerprint } = await api.getSignedDownloadUrl(options.releaseId, options.accessToken);
|
|
108
|
+
// Download from signed URL
|
|
109
|
+
await downloadFromSignedUrl(signed_url, options.destinationPath, options.onProgress);
|
|
110
|
+
// Success - return fingerprint for verification
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
115
|
+
// If signed URL expired, retry immediately (no backoff)
|
|
116
|
+
if (error instanceof SignedUrlExpiredError) {
|
|
117
|
+
if (attempt < config.maxRetries) {
|
|
118
|
+
continue; // Retry immediately with fresh signed URL
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// For other errors, apply exponential backoff
|
|
122
|
+
if (attempt < config.maxRetries) {
|
|
123
|
+
const delayMs = calculateBackoffDelay(attempt, config);
|
|
124
|
+
await sleep(delayMs);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// All retries exhausted
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// All retries failed
|
|
132
|
+
throw new DownloadError(`Failed to download release package after ${config.maxRetries} attempts: ${lastError?.message ?? "unknown error"}`, options.releaseId, config.maxRetries);
|
|
133
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { type ClawHiveCloudApi, type SyncedAgent } from "../client.js";
|
|
3
|
+
import type { ActiveInstallStage, MarketInstallClaimedJob } from "./types.js";
|
|
4
|
+
type InstallExecutorLogger = {
|
|
5
|
+
info: (message: string) => void;
|
|
6
|
+
warn: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export type ExecuteInstallJobOptions = {
|
|
9
|
+
api: ClawHiveCloudApi;
|
|
10
|
+
pluginApi: OpenClawPluginApi;
|
|
11
|
+
pluginNodeId: string;
|
|
12
|
+
userId: string;
|
|
13
|
+
pluginVersion: string;
|
|
14
|
+
openClawVersion: string | null;
|
|
15
|
+
logger: InstallExecutorLogger;
|
|
16
|
+
onStageChange?: (stage: ActiveInstallStage) => void;
|
|
17
|
+
onAgentsSynced?: (items: SyncedAgent[]) => void;
|
|
18
|
+
requireClaimedArtifact?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Compare dotted versions without pulling in a full semver dependency.
|
|
22
|
+
*/
|
|
23
|
+
export declare function compareLooseVersions(current: string, minimum: string): number;
|
|
24
|
+
/**
|
|
25
|
+
* Execute one claimed install job and always durably report the final result.
|
|
26
|
+
*/
|
|
27
|
+
export declare function executeInstallJob(job: MarketInstallClaimedJob, options: ExecuteInstallJobOptions): Promise<void>;
|
|
28
|
+
export {};
|