@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,168 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { create as createTar } from "tar";
|
|
6
|
+
export class AgentManifestNotFoundError extends Error {
|
|
7
|
+
constructor(agentDir) {
|
|
8
|
+
super(`Agent manifest (openclaw.agent.json) not found in ${agentDir}`);
|
|
9
|
+
this.name = "AgentManifestNotFoundError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class PackageTooLargeError extends Error {
|
|
13
|
+
constructor(sizeBytes, limitBytes) {
|
|
14
|
+
super(`Package size ${sizeBytes} bytes exceeds limit of ${limitBytes} bytes (${(limitBytes / 1024 / 1024).toFixed(1)} MiB)`);
|
|
15
|
+
this.name = "PackageTooLargeError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class PackagingError extends Error {
|
|
19
|
+
constructor(message, cause) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "PackagingError";
|
|
22
|
+
if (cause instanceof Error) {
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const MAX_PACKAGE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MiB
|
|
28
|
+
/**
|
|
29
|
+
* Determines if a file should be included in the agent package.
|
|
30
|
+
* Uses a strict whitelist approach per D-02: only manifest and definition files.
|
|
31
|
+
*/
|
|
32
|
+
function shouldInclude(path) {
|
|
33
|
+
const normalized = path.replace(/\\/g, "/");
|
|
34
|
+
// Always include manifest
|
|
35
|
+
if (normalized === "openclaw.agent.json") {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// Include common definition directories
|
|
39
|
+
if (normalized.startsWith("prompts/") ||
|
|
40
|
+
normalized.startsWith("tools/") ||
|
|
41
|
+
normalized.startsWith("knowledge/") ||
|
|
42
|
+
normalized.startsWith("definitions/")) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// Include common definition file extensions
|
|
46
|
+
const allowedExtensions = [
|
|
47
|
+
".md",
|
|
48
|
+
".txt",
|
|
49
|
+
".json",
|
|
50
|
+
".yaml",
|
|
51
|
+
".yml",
|
|
52
|
+
".py",
|
|
53
|
+
".js",
|
|
54
|
+
".ts",
|
|
55
|
+
];
|
|
56
|
+
if (allowedExtensions.some((ext) => normalized.endsWith(ext))) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// Exclude everything else
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Determines if a file should be excluded from the agent package.
|
|
64
|
+
* Blacklist for common unwanted files and directories.
|
|
65
|
+
*/
|
|
66
|
+
function shouldExclude(path) {
|
|
67
|
+
const normalized = path.replace(/\\/g, "/");
|
|
68
|
+
// Exclude build/runtime artifacts
|
|
69
|
+
if (normalized.includes("node_modules/") ||
|
|
70
|
+
normalized.includes(".git/") ||
|
|
71
|
+
normalized.includes("__pycache__/") ||
|
|
72
|
+
normalized.includes("dist/") ||
|
|
73
|
+
normalized.includes("build/")) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// Exclude specific files
|
|
77
|
+
if (normalized.endsWith(".log") ||
|
|
78
|
+
normalized.endsWith(".pyc") ||
|
|
79
|
+
normalized === ".DS_Store" ||
|
|
80
|
+
normalized === "package.json" ||
|
|
81
|
+
normalized === "package-lock.json" ||
|
|
82
|
+
normalized === "tsconfig.json") {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// Apply whitelist - if not included, exclude
|
|
86
|
+
return !shouldInclude(normalized);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Packages an agent directory into a tar.gz archive.
|
|
90
|
+
*
|
|
91
|
+
* Per D-01, D-02, D-03, D-04:
|
|
92
|
+
* - Format: tar.gz (standard Unix archive with gzip compression)
|
|
93
|
+
* - Contents: manifest (openclaw.agent.json) + definition files only
|
|
94
|
+
* - Manifest: embedded at tar.gz root
|
|
95
|
+
* - Fingerprint: SHA-256 over the tar.gz byte stream
|
|
96
|
+
*
|
|
97
|
+
* @param options - Packaging options
|
|
98
|
+
* @returns Package result with buffer, size, and fingerprint
|
|
99
|
+
* @throws {AgentManifestNotFoundError} if openclaw.agent.json is missing
|
|
100
|
+
* @throws {PackageTooLargeError} if package exceeds 10 MiB limit
|
|
101
|
+
* @throws {PackagingError} if tar operation fails
|
|
102
|
+
*/
|
|
103
|
+
export async function packageAgentDirectory(options) {
|
|
104
|
+
const { agentDir, manifestPath = "openclaw.agent.json" } = options;
|
|
105
|
+
// Verify manifest exists
|
|
106
|
+
const manifestFullPath = join(agentDir, manifestPath);
|
|
107
|
+
if (!existsSync(manifestFullPath)) {
|
|
108
|
+
throw new AgentManifestNotFoundError(agentDir);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
// Collect all files to include
|
|
112
|
+
const filesToInclude = [];
|
|
113
|
+
await collectFiles(agentDir, "", filesToInclude);
|
|
114
|
+
// Create tar.gz in memory
|
|
115
|
+
const chunks = [];
|
|
116
|
+
const tarStream = createTar({
|
|
117
|
+
gzip: true,
|
|
118
|
+
cwd: agentDir,
|
|
119
|
+
filter: (path) => !shouldExclude(path),
|
|
120
|
+
}, filesToInclude);
|
|
121
|
+
for await (const chunk of tarStream) {
|
|
122
|
+
chunks.push(Buffer.from(chunk));
|
|
123
|
+
}
|
|
124
|
+
const buffer = Buffer.concat(chunks);
|
|
125
|
+
const sizeBytes = buffer.length;
|
|
126
|
+
// Check size limit (client-side fast fail)
|
|
127
|
+
if (sizeBytes > MAX_PACKAGE_SIZE_BYTES) {
|
|
128
|
+
throw new PackageTooLargeError(sizeBytes, MAX_PACKAGE_SIZE_BYTES);
|
|
129
|
+
}
|
|
130
|
+
// Compute SHA-256 fingerprint
|
|
131
|
+
const hash = createHash("sha256");
|
|
132
|
+
hash.update(buffer);
|
|
133
|
+
const fingerprint = hash.digest("hex");
|
|
134
|
+
return {
|
|
135
|
+
buffer,
|
|
136
|
+
sizeBytes,
|
|
137
|
+
fingerprint,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error instanceof AgentManifestNotFoundError ||
|
|
142
|
+
error instanceof PackageTooLargeError) {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
throw new PackagingError(`Failed to package agent directory: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Recursively collects files from a directory that should be included in the package.
|
|
150
|
+
*/
|
|
151
|
+
async function collectFiles(baseDir, relativeDir, result) {
|
|
152
|
+
const fullDir = join(baseDir, relativeDir);
|
|
153
|
+
const entries = await readdir(fullDir, { withFileTypes: true });
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
const relativePath = relativeDir
|
|
156
|
+
? join(relativeDir, entry.name)
|
|
157
|
+
: entry.name;
|
|
158
|
+
if (shouldExclude(relativePath)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
await collectFiles(baseDir, relativePath, result);
|
|
163
|
+
}
|
|
164
|
+
else if (entry.isFile()) {
|
|
165
|
+
result.push(relativePath);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ClawHiveCloudApi } from "../client.js";
|
|
2
|
+
import type { ClawHiveResolvedAccount } from "../config.js";
|
|
3
|
+
import { packageAgentDirectory } from "./packager.js";
|
|
4
|
+
export type AgentManifest = {
|
|
5
|
+
name?: string;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
author?: {
|
|
11
|
+
name?: string;
|
|
12
|
+
};
|
|
13
|
+
compatibility?: {
|
|
14
|
+
minOpenClawVersion?: string;
|
|
15
|
+
minPluginVersion?: string;
|
|
16
|
+
};
|
|
17
|
+
metadata?: {
|
|
18
|
+
category?: string;
|
|
19
|
+
tags?: string[];
|
|
20
|
+
license?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type PublishAgentInfo = {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
manifest: AgentManifest;
|
|
27
|
+
};
|
|
28
|
+
export type PublishDefaults = {
|
|
29
|
+
name: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
summary: string | undefined;
|
|
32
|
+
authorDisplayName: string | undefined;
|
|
33
|
+
category: string | undefined;
|
|
34
|
+
license: string | undefined;
|
|
35
|
+
tags: string[];
|
|
36
|
+
version: string;
|
|
37
|
+
sourceRef: string;
|
|
38
|
+
minOpenClawVersion: string | undefined;
|
|
39
|
+
minPluginVersion: string | undefined;
|
|
40
|
+
};
|
|
41
|
+
export type PublishOverrides = Partial<PublishDefaults> & {
|
|
42
|
+
listingId?: string;
|
|
43
|
+
};
|
|
44
|
+
export type PublishFlowResult = {
|
|
45
|
+
listingId: string;
|
|
46
|
+
releaseId: string | null;
|
|
47
|
+
version: string;
|
|
48
|
+
storagePath: string;
|
|
49
|
+
fingerprint: string;
|
|
50
|
+
listing: Record<string, unknown>;
|
|
51
|
+
release: Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
export declare function slugifyForListing(value: string): string;
|
|
54
|
+
export declare function inferPublishDefaults(input: {
|
|
55
|
+
folderName: string;
|
|
56
|
+
manifest: AgentManifest;
|
|
57
|
+
sourceRef: string;
|
|
58
|
+
account?: Pick<ClawHiveResolvedAccount, "minOpenClawVersion" | "minPluginVersion">;
|
|
59
|
+
}): PublishDefaults;
|
|
60
|
+
export declare function runPublishFlow(input: {
|
|
61
|
+
agent: PublishAgentInfo;
|
|
62
|
+
account: ClawHiveResolvedAccount;
|
|
63
|
+
sourceRef: string;
|
|
64
|
+
overrides: PublishOverrides;
|
|
65
|
+
onProgress?: (uploaded: number, total: number) => void;
|
|
66
|
+
deps?: {
|
|
67
|
+
createClient?: (account: ClawHiveResolvedAccount) => Pick<ClawHiveCloudApi, "registerPluginNode" | "createMarketListing" | "uploadReleasePackage" | "createMarketRelease">;
|
|
68
|
+
packageAgentDirectory?: typeof packageAgentDirectory;
|
|
69
|
+
};
|
|
70
|
+
}): Promise<PublishFlowResult>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { ClawHiveCloudApi, } from "../client.js";
|
|
3
|
+
import { packageAgentDirectory } from "./packager.js";
|
|
4
|
+
export function slugifyForListing(value) {
|
|
5
|
+
const slug = value
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
9
|
+
.replace(/\s+/g, "-")
|
|
10
|
+
.replace(/-+/g, "-")
|
|
11
|
+
.replace(/^-|-$/g, "");
|
|
12
|
+
return slug || "agent";
|
|
13
|
+
}
|
|
14
|
+
function normalizeVersion(value) {
|
|
15
|
+
const raw = value?.trim() || "1.0.0";
|
|
16
|
+
return raw.startsWith("v") ? raw : `v${raw}`;
|
|
17
|
+
}
|
|
18
|
+
export function inferPublishDefaults(input) {
|
|
19
|
+
const manifest = input.manifest;
|
|
20
|
+
const name = manifest.displayName ?? manifest.name ?? input.folderName;
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
slug: slugifyForListing(manifest.name ?? name),
|
|
24
|
+
summary: manifest.summary ?? manifest.description,
|
|
25
|
+
authorDisplayName: manifest.author?.name,
|
|
26
|
+
category: manifest.metadata?.category,
|
|
27
|
+
license: manifest.metadata?.license,
|
|
28
|
+
tags: Array.isArray(manifest.metadata?.tags) ? manifest.metadata.tags : [],
|
|
29
|
+
version: normalizeVersion(manifest.version),
|
|
30
|
+
sourceRef: input.sourceRef,
|
|
31
|
+
minOpenClawVersion: manifest.compatibility?.minOpenClawVersion ?? input.account?.minOpenClawVersion ?? undefined,
|
|
32
|
+
minPluginVersion: manifest.compatibility?.minPluginVersion ?? input.account?.minPluginVersion ?? undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function runPublishFlow(input) {
|
|
36
|
+
if (!input.account.pluginToken) {
|
|
37
|
+
throw new Error("ClawHive is not authorized. Run `openclaw clawhive authorize` first.");
|
|
38
|
+
}
|
|
39
|
+
const defaults = inferPublishDefaults({
|
|
40
|
+
folderName: input.agent.name || basename(input.agent.path),
|
|
41
|
+
manifest: input.agent.manifest,
|
|
42
|
+
sourceRef: input.sourceRef,
|
|
43
|
+
account: input.account,
|
|
44
|
+
});
|
|
45
|
+
const publish = { ...defaults, ...input.overrides };
|
|
46
|
+
const createClient = input.deps?.createClient ??
|
|
47
|
+
((account) => new ClawHiveCloudApi({
|
|
48
|
+
projectUrl: account.projectUrl,
|
|
49
|
+
anonKey: account.anonKey,
|
|
50
|
+
pluginToken: account.pluginToken,
|
|
51
|
+
userId: account.userId,
|
|
52
|
+
pluginNodeId: account.pluginNodeId,
|
|
53
|
+
nodeName: account.nodeName,
|
|
54
|
+
pluginVersion: account.pluginVersion,
|
|
55
|
+
}));
|
|
56
|
+
const client = createClient(input.account);
|
|
57
|
+
const registration = await client.registerPluginNode();
|
|
58
|
+
const accessToken = registration.user_session.access_token;
|
|
59
|
+
const listingInput = {
|
|
60
|
+
accessToken,
|
|
61
|
+
listingId: input.overrides.listingId,
|
|
62
|
+
name: publish.name,
|
|
63
|
+
slug: publish.slug,
|
|
64
|
+
authorDisplayName: publish.authorDisplayName,
|
|
65
|
+
summary: publish.summary,
|
|
66
|
+
category: publish.category,
|
|
67
|
+
license: publish.license,
|
|
68
|
+
tags: publish.tags,
|
|
69
|
+
status: "draft",
|
|
70
|
+
};
|
|
71
|
+
const listingResponse = await client.createMarketListing(listingInput);
|
|
72
|
+
const listing = listingResponse.listing;
|
|
73
|
+
const listingId = String(listing.id);
|
|
74
|
+
const packageFn = input.deps?.packageAgentDirectory ?? packageAgentDirectory;
|
|
75
|
+
const packageResult = await packageFn({ agentDir: input.agent.path });
|
|
76
|
+
const upload = await client.uploadReleasePackage({
|
|
77
|
+
listingId,
|
|
78
|
+
version: publish.version,
|
|
79
|
+
buffer: packageResult.buffer,
|
|
80
|
+
fingerprint: packageResult.fingerprint,
|
|
81
|
+
accessToken,
|
|
82
|
+
onProgress: input.onProgress,
|
|
83
|
+
});
|
|
84
|
+
const releaseInput = {
|
|
85
|
+
accessToken,
|
|
86
|
+
listingId,
|
|
87
|
+
version: publish.version,
|
|
88
|
+
storagePath: upload.storage_path,
|
|
89
|
+
fingerprint: upload.fingerprint,
|
|
90
|
+
sourceType: "manifest",
|
|
91
|
+
sourceRef: publish.sourceRef,
|
|
92
|
+
publish: true,
|
|
93
|
+
minPluginVersion: publish.minPluginVersion,
|
|
94
|
+
minOpenClawVersion: publish.minOpenClawVersion,
|
|
95
|
+
};
|
|
96
|
+
const releaseResponse = await client.createMarketRelease(releaseInput);
|
|
97
|
+
const release = releaseResponse.release;
|
|
98
|
+
return {
|
|
99
|
+
listingId,
|
|
100
|
+
releaseId: release.id ? String(release.id) : null,
|
|
101
|
+
version: publish.version,
|
|
102
|
+
storagePath: upload.storage_path,
|
|
103
|
+
fingerprint: upload.fingerprint,
|
|
104
|
+
listing,
|
|
105
|
+
release,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ClawHiveCloudApi } from "../client.js";
|
|
2
|
+
export type UploadResult = {
|
|
3
|
+
storagePath: string;
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
sizeBytes: number;
|
|
6
|
+
};
|
|
7
|
+
export type UploadOptions = {
|
|
8
|
+
listingId: string;
|
|
9
|
+
version: string;
|
|
10
|
+
buffer: Buffer;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
accessToken: string;
|
|
13
|
+
onProgress?: (bytesUploaded: number, totalBytes: number) => void;
|
|
14
|
+
};
|
|
15
|
+
export declare class UploadFailedError extends Error {
|
|
16
|
+
readonly attempts: number;
|
|
17
|
+
constructor(message: string, attempts: number, cause?: unknown);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Uploads a release package to Supabase Storage with retry logic.
|
|
21
|
+
*
|
|
22
|
+
* Per D-08, D-09, D-10, D-11:
|
|
23
|
+
* - Two-step publish flow: upload first, then create release record
|
|
24
|
+
* - Storage path: releases/{listing_id}/{version}.tar.gz
|
|
25
|
+
* - Size limit: 10 MiB (checked by edge function)
|
|
26
|
+
* - Retry: exponential backoff with jitter, max 3 attempts
|
|
27
|
+
* - Idempotency: uploading same content to same path is safe
|
|
28
|
+
*
|
|
29
|
+
* @param client - ClawHiveCloudApi instance
|
|
30
|
+
* @param options - Upload options
|
|
31
|
+
* @returns Upload result with storage path and fingerprint
|
|
32
|
+
* @throws {UploadFailedError} if all retry attempts fail
|
|
33
|
+
*/
|
|
34
|
+
export declare function uploadReleasePackage(client: ClawHiveCloudApi, options: UploadOptions): Promise<UploadResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Two-step publish flow helper that packages, uploads, and returns data for release creation.
|
|
37
|
+
*
|
|
38
|
+
* This is a convenience wrapper that combines packaging and upload.
|
|
39
|
+
* The caller must still call market-releases-create with the returned data.
|
|
40
|
+
*
|
|
41
|
+
* Example usage:
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import { packageAndUpload } from "./market-publish/uploader.js";
|
|
44
|
+
* import { packageAgentDirectory } from "./market-publish/packager.js";
|
|
45
|
+
*
|
|
46
|
+
* // Step 1: Package and upload
|
|
47
|
+
* const uploadResult = await packageAndUpload(client, {
|
|
48
|
+
* agentDir: "/path/to/agent",
|
|
49
|
+
* listingId: "listing-uuid",
|
|
50
|
+
* version: "v1.0.0",
|
|
51
|
+
* accessToken: "user-jwt",
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // Step 2: Create release record (caller's responsibility)
|
|
55
|
+
* await client.createMarketRelease({
|
|
56
|
+
* listingId: uploadResult.listingId,
|
|
57
|
+
* version: uploadResult.version,
|
|
58
|
+
* storagePath: uploadResult.storagePath,
|
|
59
|
+
* fingerprint: uploadResult.fingerprint,
|
|
60
|
+
* // ... other release metadata
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function packageAndUpload(client: ClawHiveCloudApi, options: {
|
|
65
|
+
agentDir: string;
|
|
66
|
+
listingId: string;
|
|
67
|
+
version: string;
|
|
68
|
+
accessToken: string;
|
|
69
|
+
onProgress?: (bytesUploaded: number, totalBytes: number) => void;
|
|
70
|
+
}): Promise<UploadResult & {
|
|
71
|
+
listingId: string;
|
|
72
|
+
version: string;
|
|
73
|
+
}>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export class UploadFailedError extends Error {
|
|
2
|
+
attempts;
|
|
3
|
+
constructor(message, attempts, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.attempts = attempts;
|
|
6
|
+
this.name = "UploadFailedError";
|
|
7
|
+
if (cause instanceof Error) {
|
|
8
|
+
this.cause = cause;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const MAX_RETRY_ATTEMPTS = 3;
|
|
13
|
+
const INITIAL_BACKOFF_MS = 1000;
|
|
14
|
+
const BACKOFF_MULTIPLIER = 2;
|
|
15
|
+
const JITTER_MAX_MS = 500;
|
|
16
|
+
/**
|
|
17
|
+
* Uploads a release package to Supabase Storage with retry logic.
|
|
18
|
+
*
|
|
19
|
+
* Per D-08, D-09, D-10, D-11:
|
|
20
|
+
* - Two-step publish flow: upload first, then create release record
|
|
21
|
+
* - Storage path: releases/{listing_id}/{version}.tar.gz
|
|
22
|
+
* - Size limit: 10 MiB (checked by edge function)
|
|
23
|
+
* - Retry: exponential backoff with jitter, max 3 attempts
|
|
24
|
+
* - Idempotency: uploading same content to same path is safe
|
|
25
|
+
*
|
|
26
|
+
* @param client - ClawHiveCloudApi instance
|
|
27
|
+
* @param options - Upload options
|
|
28
|
+
* @returns Upload result with storage path and fingerprint
|
|
29
|
+
* @throws {UploadFailedError} if all retry attempts fail
|
|
30
|
+
*/
|
|
31
|
+
export async function uploadReleasePackage(client, options) {
|
|
32
|
+
const { listingId, version, buffer, fingerprint, accessToken, onProgress } = options;
|
|
33
|
+
let lastError;
|
|
34
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
|
35
|
+
try {
|
|
36
|
+
// Report progress at start of attempt
|
|
37
|
+
if (onProgress) {
|
|
38
|
+
onProgress(0, buffer.length);
|
|
39
|
+
}
|
|
40
|
+
const result = await client.uploadReleasePackage({
|
|
41
|
+
listingId,
|
|
42
|
+
version,
|
|
43
|
+
buffer,
|
|
44
|
+
fingerprint,
|
|
45
|
+
accessToken,
|
|
46
|
+
});
|
|
47
|
+
// Report progress at completion
|
|
48
|
+
if (onProgress) {
|
|
49
|
+
onProgress(buffer.length, buffer.length);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
storagePath: result.storage_path,
|
|
53
|
+
fingerprint: result.fingerprint,
|
|
54
|
+
sizeBytes: buffer.length,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
lastError = error;
|
|
59
|
+
// Don't retry on client errors (4xx) - these are permanent failures
|
|
60
|
+
if (error &&
|
|
61
|
+
typeof error === "object" &&
|
|
62
|
+
"status" in error &&
|
|
63
|
+
typeof error.status === "number" &&
|
|
64
|
+
error.status >= 400 &&
|
|
65
|
+
error.status < 500) {
|
|
66
|
+
throw new UploadFailedError(`Upload failed with client error (${error.status}): ${error instanceof Error ? error.message : String(error)}`, attempt, error);
|
|
67
|
+
}
|
|
68
|
+
// If this was the last attempt, throw
|
|
69
|
+
if (attempt === MAX_RETRY_ATTEMPTS) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
// Calculate backoff with exponential increase and jitter
|
|
73
|
+
const backoffMs = INITIAL_BACKOFF_MS * Math.pow(BACKOFF_MULTIPLIER, attempt - 1);
|
|
74
|
+
const jitterMs = Math.random() * JITTER_MAX_MS;
|
|
75
|
+
const delayMs = backoffMs + jitterMs;
|
|
76
|
+
// Wait before retry
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// All attempts failed
|
|
81
|
+
throw new UploadFailedError(`Upload failed after ${MAX_RETRY_ATTEMPTS} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`, MAX_RETRY_ATTEMPTS, lastError);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Two-step publish flow helper that packages, uploads, and returns data for release creation.
|
|
85
|
+
*
|
|
86
|
+
* This is a convenience wrapper that combines packaging and upload.
|
|
87
|
+
* The caller must still call market-releases-create with the returned data.
|
|
88
|
+
*
|
|
89
|
+
* Example usage:
|
|
90
|
+
* ```typescript
|
|
91
|
+
* import { packageAndUpload } from "./market-publish/uploader.js";
|
|
92
|
+
* import { packageAgentDirectory } from "./market-publish/packager.js";
|
|
93
|
+
*
|
|
94
|
+
* // Step 1: Package and upload
|
|
95
|
+
* const uploadResult = await packageAndUpload(client, {
|
|
96
|
+
* agentDir: "/path/to/agent",
|
|
97
|
+
* listingId: "listing-uuid",
|
|
98
|
+
* version: "v1.0.0",
|
|
99
|
+
* accessToken: "user-jwt",
|
|
100
|
+
* });
|
|
101
|
+
*
|
|
102
|
+
* // Step 2: Create release record (caller's responsibility)
|
|
103
|
+
* await client.createMarketRelease({
|
|
104
|
+
* listingId: uploadResult.listingId,
|
|
105
|
+
* version: uploadResult.version,
|
|
106
|
+
* storagePath: uploadResult.storagePath,
|
|
107
|
+
* fingerprint: uploadResult.fingerprint,
|
|
108
|
+
* // ... other release metadata
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export async function packageAndUpload(client, options) {
|
|
113
|
+
const { packageAgentDirectory } = await import("./packager.js");
|
|
114
|
+
// Package the agent directory
|
|
115
|
+
const packageResult = await packageAgentDirectory({
|
|
116
|
+
agentDir: options.agentDir,
|
|
117
|
+
});
|
|
118
|
+
// Upload to Storage
|
|
119
|
+
const uploadResult = await uploadReleasePackage(client, {
|
|
120
|
+
listingId: options.listingId,
|
|
121
|
+
version: options.version,
|
|
122
|
+
buffer: packageResult.buffer,
|
|
123
|
+
fingerprint: packageResult.fingerprint,
|
|
124
|
+
accessToken: options.accessToken,
|
|
125
|
+
onProgress: options.onProgress,
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
...uploadResult,
|
|
129
|
+
listingId: options.listingId,
|
|
130
|
+
version: options.version,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-harness";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the current OpenClaw host version from runtime exports or env overrides.
|
|
4
|
+
*/
|
|
5
|
+
export function resolveOpenClawVersion() {
|
|
6
|
+
const fromEnv = process.env.OPENCLAW_VERSION?.trim();
|
|
7
|
+
if (fromEnv) {
|
|
8
|
+
return fromEnv;
|
|
9
|
+
}
|
|
10
|
+
return typeof OPENCLAW_VERSION === "string" && OPENCLAW_VERSION.trim()
|
|
11
|
+
? OPENCLAW_VERSION
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type SendTextContext = {
|
|
2
|
+
cfg: unknown;
|
|
3
|
+
to: string;
|
|
4
|
+
text: string;
|
|
5
|
+
accountId?: string | null;
|
|
6
|
+
};
|
|
7
|
+
type SendResult = {
|
|
8
|
+
messageId: string;
|
|
9
|
+
meta?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
export declare function sendAgentText(ctx: SendTextContext): Promise<SendResult>;
|
|
12
|
+
export declare function rejectMedia(): Promise<never>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { clawhiveRuntimeStore, lookupSession, } from "./runtime.js";
|
|
2
|
+
import { CLAWHIVE_CHANNEL_ID } from "./config.js";
|
|
3
|
+
function stripChannelPrefix(raw) {
|
|
4
|
+
if (raw.startsWith(`${CLAWHIVE_CHANNEL_ID}:`)) {
|
|
5
|
+
return raw.slice(CLAWHIVE_CHANNEL_ID.length + 1);
|
|
6
|
+
}
|
|
7
|
+
return raw;
|
|
8
|
+
}
|
|
9
|
+
function resolveBinding(runtime, to) {
|
|
10
|
+
const normalized = stripChannelPrefix(to);
|
|
11
|
+
return (lookupSession(runtime, to) ??
|
|
12
|
+
lookupSession(runtime, normalized) ??
|
|
13
|
+
lookupSession(runtime, `clawhive:default:${normalized}`));
|
|
14
|
+
}
|
|
15
|
+
export async function sendAgentText(ctx) {
|
|
16
|
+
const runtime = clawhiveRuntimeStore.getRuntime();
|
|
17
|
+
const binding = resolveBinding(runtime, ctx.to);
|
|
18
|
+
if (!binding) {
|
|
19
|
+
throw new Error(`ClawHive outbound: no session binding for target "${ctx.to}"`);
|
|
20
|
+
}
|
|
21
|
+
const content = ctx.text?.trim();
|
|
22
|
+
if (!content) {
|
|
23
|
+
throw new Error("ClawHive outbound: refusing to send empty text");
|
|
24
|
+
}
|
|
25
|
+
const { message } = await runtime.api.writeAgentMessage({
|
|
26
|
+
pluginNodeId: runtime.pluginNodeId,
|
|
27
|
+
sessionId: binding.sessionId,
|
|
28
|
+
agentId: binding.cloudAgentId,
|
|
29
|
+
content,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
messageId: message.id,
|
|
33
|
+
meta: {
|
|
34
|
+
sequence: message.sequence,
|
|
35
|
+
createdAt: message.created_at,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function rejectMedia() {
|
|
40
|
+
throw new Error("ClawHive plugin does not yet support outbound media attachments (MVP scope).");
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ClawHiveCloudApi, PluginRegisterResult } from "./client.js";
|
|
2
|
+
export type PairingPrintResult = {
|
|
3
|
+
pairToken: string;
|
|
4
|
+
expiresAt: string;
|
|
5
|
+
payloadUrl: string;
|
|
6
|
+
qrText: string;
|
|
7
|
+
};
|
|
8
|
+
export type PairingPrintOptions = {
|
|
9
|
+
api: ClawHiveCloudApi;
|
|
10
|
+
pluginNodeId: string;
|
|
11
|
+
ttlSeconds?: number;
|
|
12
|
+
ascii?: (text: string) => void;
|
|
13
|
+
/** Optional banner printed above the QR. */
|
|
14
|
+
banner?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function generatePairingPayload(api: ClawHiveCloudApi, pluginNodeId: string, ttlSeconds?: number): Promise<{
|
|
17
|
+
pairToken: string;
|
|
18
|
+
expiresAt: string;
|
|
19
|
+
payloadUrl: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function renderQrText(payloadUrl: string): Promise<string>;
|
|
22
|
+
export declare function printPairingQr(options: PairingPrintOptions): Promise<PairingPrintResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Return the number of active paired devices for the current user.
|
|
25
|
+
* Uses the user-scoped session minted by plugin-register so the query
|
|
26
|
+
* passes RLS without bringing service-role credentials into the plugin.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getPairedDeviceCount(params: {
|
|
29
|
+
registration: PluginRegisterResult;
|
|
30
|
+
projectUrl: string;
|
|
31
|
+
anonKey: string;
|
|
32
|
+
}): Promise<number>;
|