clawjobs 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # ClawJobs
2
+
3
+ ClawJobs is an OpenClaw plugin for peer-powered task collaboration.
4
+
5
+ The assignee contributes reasoning on their own machine, while every real command still executes on the task owner's machine.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ openclaw plugins install clawjobs
11
+ openclaw config set plugins.allow '["clawjobs"]' --strict-json
12
+ openclaw config set plugins.entries.clawjobs.enabled true
13
+ ```
14
+
15
+ Then write `plugins.entries.clawjobs.config`.
16
+
17
+ ## Minimal config
18
+
19
+ ```json
20
+ {
21
+ "hubUrl": "https://your-hub.example.com",
22
+ "hubToken": "your-shared-token",
23
+ "nickname": "Your Nickname",
24
+ "workspaceDir": "/your/workspace"
25
+ }
26
+ ```
27
+
28
+ ## Task page
29
+
30
+ ```text
31
+ http://127.0.0.1:18789/plugins/clawjobs
32
+ ```
33
+
34
+ ## Requirements
35
+
36
+ - every participating machine installs the plugin
37
+ - one central hub is reachable by all peers
38
+ - the assignee machine has a usable OpenClaw model configuration
39
+
40
+ ## License
41
+
42
+ MIT
package/README_CN.md ADDED
@@ -0,0 +1,42 @@
1
+ # ClawJobs
2
+
3
+ `ClawJobs` 是一个 `OpenClaw` 插件,用来做多台小龙虾之间的任务协作。
4
+
5
+ 接单人负责推理,真实命令始终只在任务发起人的本机执行。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ openclaw plugins install clawjobs
11
+ openclaw config set plugins.allow '["clawjobs"]' --strict-json
12
+ openclaw config set plugins.entries.clawjobs.enabled true
13
+ ```
14
+
15
+ 然后写入 `plugins.entries.clawjobs.config`。
16
+
17
+ ## 最小配置
18
+
19
+ ```json
20
+ {
21
+ "hubUrl": "https://你的-hub-地址",
22
+ "hubToken": "共享口令",
23
+ "nickname": "你的昵称",
24
+ "workspaceDir": "/你的工作目录"
25
+ }
26
+ ```
27
+
28
+ ## 任务页
29
+
30
+ ```text
31
+ http://127.0.0.1:18789/plugins/clawjobs
32
+ ```
33
+
34
+ ## 依赖
35
+
36
+ - 每台参与机器都安装插件
37
+ - 所有机器都能访问同一个中心 hub
38
+ - 接单机器本地有可用的 OpenClaw 模型配置
39
+
40
+ ## License
41
+
42
+ MIT
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import register from "./src/plugin.js";
2
+
3
+ export default register;
4
+
@@ -0,0 +1,95 @@
1
+ {
2
+ "id": "clawjobs",
3
+ "name": "ClawJobs",
4
+ "description": "Peer-powered jobs for OpenClaw, with remote reasoning and owner-side execution.",
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "hubUrl": {
10
+ "type": "string"
11
+ },
12
+ "hubToken": {
13
+ "type": "string"
14
+ },
15
+ "nickname": {
16
+ "type": "string"
17
+ },
18
+ "peerId": {
19
+ "type": "string"
20
+ },
21
+ "workspaceDir": {
22
+ "type": "string"
23
+ },
24
+ "brain": {
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "properties": {
28
+ "provider": {
29
+ "type": "string"
30
+ },
31
+ "model": {
32
+ "type": "string"
33
+ },
34
+ "maxSteps": {
35
+ "type": "number"
36
+ },
37
+ "timeoutMs": {
38
+ "type": "number"
39
+ }
40
+ }
41
+ },
42
+ "execution": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "defaultCwd": {
47
+ "type": "string"
48
+ },
49
+ "maxCommandMs": {
50
+ "type": "number"
51
+ },
52
+ "maxOutputChars": {
53
+ "type": "number"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ },
59
+ "uiHints": {
60
+ "hubUrl": {
61
+ "label": "Hub URL"
62
+ },
63
+ "hubToken": {
64
+ "label": "Hub Token",
65
+ "sensitive": true
66
+ },
67
+ "nickname": {
68
+ "label": "Nickname"
69
+ },
70
+ "workspaceDir": {
71
+ "label": "Default Workspace"
72
+ },
73
+ "brain.provider": {
74
+ "label": "Brain Provider"
75
+ },
76
+ "brain.model": {
77
+ "label": "Brain Model"
78
+ },
79
+ "brain.maxSteps": {
80
+ "label": "Max Reasoning Steps"
81
+ },
82
+ "brain.timeoutMs": {
83
+ "label": "Reasoning Timeout (ms)"
84
+ },
85
+ "execution.defaultCwd": {
86
+ "label": "Default Execution CWD"
87
+ },
88
+ "execution.maxCommandMs": {
89
+ "label": "Command Timeout (ms)"
90
+ },
91
+ "execution.maxOutputChars": {
92
+ "label": "Max Output Characters"
93
+ }
94
+ }
95
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "clawjobs",
3
+ "version": "0.2.0",
4
+ "description": "Peer-powered jobs for OpenClaw: remote reasoning with execution kept on the task owner's machine.",
5
+ "private": false,
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "openclaw",
10
+ "plugin",
11
+ "clawjobs",
12
+ "peer-to-peer",
13
+ "tasks",
14
+ "local-execution"
15
+ ],
16
+ "openclaw": {
17
+ "extensions": [
18
+ "./index.ts"
19
+ ]
20
+ },
21
+ "files": [
22
+ "index.ts",
23
+ "openclaw.plugin.json",
24
+ "src",
25
+ "README.md",
26
+ "README_CN.md",
27
+ "LICENSE"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/gtoadio-cyber/openclaw-clawjobs.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/gtoadio-cyber/openclaw-clawjobs/issues"
38
+ },
39
+ "homepage": "https://github.com/gtoadio-cyber/openclaw-clawjobs#readme"
40
+ }
@@ -0,0 +1,290 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ type RunJsonTaskParams = {
7
+ api: {
8
+ config: Record<string, unknown>;
9
+ logger: {
10
+ warn: (message: string) => void;
11
+ };
12
+ };
13
+ workspaceDir: string;
14
+ provider?: string;
15
+ model?: string;
16
+ authProfileId?: string;
17
+ timeoutMs: number;
18
+ prompt: string;
19
+ input: unknown;
20
+ };
21
+
22
+ export type BrainDecision =
23
+ | {
24
+ action: "owner_exec";
25
+ note: string;
26
+ command: string;
27
+ cwd?: string | null;
28
+ timeoutMs?: number;
29
+ }
30
+ | {
31
+ action: "finish";
32
+ note: string;
33
+ resultText: string;
34
+ }
35
+ | {
36
+ action: "fail";
37
+ note: string;
38
+ errorText: string;
39
+ };
40
+
41
+ type RunEmbeddedPiAgentFn = (params: Record<string, unknown>) => Promise<{
42
+ payloads?: Array<{ text?: string; isError?: boolean }>;
43
+ }>;
44
+
45
+ function collectAssistantText(payloads: Array<{ text?: string; isError?: boolean }> | undefined) {
46
+ return (payloads || [])
47
+ .filter((item) => !item.isError && typeof item.text === "string")
48
+ .map((item) => item.text || "")
49
+ .join("\n")
50
+ .trim();
51
+ }
52
+
53
+ function stripCodeFence(value: string) {
54
+ const trimmed = value.trim();
55
+ const matched = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
56
+ return matched ? (matched[1] || "").trim() : trimmed;
57
+ }
58
+
59
+ function validateDecision(value: unknown): BrainDecision {
60
+ if (!value || typeof value !== "object") {
61
+ throw new Error("The model did not return an object.");
62
+ }
63
+ const action = typeof value.action === "string" ? value.action : "";
64
+ const note = typeof value.note === "string" ? value.note.trim() : "";
65
+ if (!note) {
66
+ throw new Error("The model response is missing note.");
67
+ }
68
+ if (action === "owner_exec") {
69
+ const command = typeof value.command === "string" ? value.command.trim() : "";
70
+ if (!command) {
71
+ throw new Error("owner_exec is missing command.");
72
+ }
73
+ return {
74
+ action,
75
+ note,
76
+ command,
77
+ cwd: typeof value.cwd === "string" ? value.cwd : null,
78
+ timeoutMs:
79
+ typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs)
80
+ ? value.timeoutMs
81
+ : undefined,
82
+ };
83
+ }
84
+ if (action === "finish") {
85
+ const resultText = typeof value.resultText === "string" ? value.resultText.trim() : "";
86
+ if (!resultText) {
87
+ throw new Error("finish is missing resultText.");
88
+ }
89
+ return {
90
+ action,
91
+ note,
92
+ resultText,
93
+ };
94
+ }
95
+ if (action === "fail") {
96
+ const errorText = typeof value.errorText === "string" ? value.errorText.trim() : "";
97
+ if (!errorText) {
98
+ throw new Error("fail is missing errorText.");
99
+ }
100
+ return {
101
+ action,
102
+ note,
103
+ errorText,
104
+ };
105
+ }
106
+ throw new Error(`Unknown action: ${action || "<empty>"}`);
107
+ }
108
+
109
+ async function pathExists(targetPath: string) {
110
+ try {
111
+ await fs.access(targetPath);
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ function addCandidate(candidatePaths: Set<string>, basePath: string) {
119
+ const normalizedBase = path.resolve(basePath);
120
+ let current = normalizedBase;
121
+
122
+ while (true) {
123
+ candidatePaths.add(path.join(current, "dist", "extensionAPI.js"));
124
+ const parent = path.dirname(current);
125
+ if (parent === current) {
126
+ break;
127
+ }
128
+ current = parent;
129
+ }
130
+ }
131
+
132
+ async function tryAddRealPathCandidates(candidatePaths: Set<string>, inputPath: string) {
133
+ if (!inputPath || typeof inputPath !== "string") {
134
+ return;
135
+ }
136
+ try {
137
+ const realPath = await fs.realpath(inputPath);
138
+ addCandidate(candidatePaths, path.dirname(realPath));
139
+ } catch {
140
+ addCandidate(candidatePaths, path.dirname(inputPath));
141
+ }
142
+ }
143
+
144
+ function resolveCliPathFromShell() {
145
+ const lookup = process.platform === "win32" ? "where" : "which";
146
+ const result = spawnSync(lookup, ["openclaw"], {
147
+ encoding: "utf8",
148
+ windowsHide: true,
149
+ });
150
+ if (result.status !== 0) {
151
+ return null;
152
+ }
153
+ const firstLine = String(result.stdout || "")
154
+ .split(/\r?\n/)
155
+ .map((line) => line.trim())
156
+ .find(Boolean);
157
+ return firstLine || null;
158
+ }
159
+
160
+ async function resolveExtensionApiPath(): Promise<string> {
161
+ const candidatePaths = new Set<string>();
162
+
163
+ for (const runtimePath of [process.argv[1], process.execPath]) {
164
+ if (!runtimePath || typeof runtimePath !== "string") {
165
+ continue;
166
+ }
167
+ await tryAddRealPathCandidates(candidatePaths, runtimePath);
168
+ }
169
+
170
+ const cliPath = resolveCliPathFromShell();
171
+ if (cliPath) {
172
+ await tryAddRealPathCandidates(candidatePaths, cliPath);
173
+ }
174
+
175
+ for (const candidatePath of candidatePaths) {
176
+ if (await pathExists(candidatePath)) {
177
+ return candidatePath;
178
+ }
179
+ }
180
+
181
+ throw new Error("Unable to locate OpenClaw extensionAPI.js.");
182
+ }
183
+
184
+ async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
185
+ const extensionApiPath = await resolveExtensionApiPath();
186
+ const mod = (await import(pathToFileURL(extensionApiPath).href)) as {
187
+ runEmbeddedPiAgent?: unknown;
188
+ };
189
+ if (typeof mod.runEmbeddedPiAgent !== "function") {
190
+ throw new Error("OpenClaw extensionAPI does not expose runEmbeddedPiAgent.");
191
+ }
192
+ return mod.runEmbeddedPiAgent as RunEmbeddedPiAgentFn;
193
+ }
194
+
195
+ export async function runJsonTask(params: RunJsonTaskParams): Promise<BrainDecision> {
196
+ const inputJson = JSON.stringify(params.input ?? null, null, 2);
197
+ const schemaText = JSON.stringify(
198
+ {
199
+ type: "object",
200
+ additionalProperties: false,
201
+ required: ["action", "note"],
202
+ properties: {
203
+ action: {
204
+ type: "string",
205
+ enum: ["owner_exec", "finish", "fail"],
206
+ },
207
+ note: {
208
+ type: "string",
209
+ },
210
+ command: {
211
+ type: "string",
212
+ },
213
+ cwd: {
214
+ type: ["string", "null"],
215
+ },
216
+ timeoutMs: {
217
+ type: "number",
218
+ },
219
+ resultText: {
220
+ type: "string",
221
+ },
222
+ errorText: {
223
+ type: "string",
224
+ },
225
+ },
226
+ },
227
+ null,
228
+ 2,
229
+ );
230
+
231
+ const prompt = [
232
+ "You are the assignee brain for a remote ClawJobs task.",
233
+ "You cannot execute commands yourself and you must not pretend that local execution already happened.",
234
+ "All real execution must be sent back to the task owner through owner_exec.",
235
+ "Return exactly one action at a time.",
236
+ "If more information is needed, return owner_exec.",
237
+ "If the answer is ready, return finish.",
238
+ "If the task cannot continue safely, return fail.",
239
+ "Return strict JSON only, with no markdown or extra commentary.",
240
+ "",
241
+ "TASK_PROMPT:",
242
+ params.prompt,
243
+ "",
244
+ "OUTPUT_SCHEMA_JSON:",
245
+ schemaText,
246
+ "",
247
+ "INPUT_JSON:",
248
+ inputJson,
249
+ ].join("\n");
250
+
251
+ const tmpDir = await fs.mkdtemp(path.join(process.env.TMPDIR || "/tmp", "clawjobs-"));
252
+ const sessionId = `clawjobs-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
253
+ const sessionFile = path.join(tmpDir, "session.json");
254
+ const runEmbeddedPiAgent = await loadRunEmbeddedPiAgent();
255
+
256
+ try {
257
+ for (let attempt = 0; attempt < 2; attempt += 1) {
258
+ const runResult = await runEmbeddedPiAgent({
259
+ sessionId: `${sessionId}-${attempt}`,
260
+ sessionFile,
261
+ workspaceDir: params.workspaceDir,
262
+ config: params.api.config,
263
+ prompt:
264
+ attempt === 0
265
+ ? prompt
266
+ : `${prompt}\n\nYour last response was not valid JSON. Return valid JSON only this time.`,
267
+ timeoutMs: params.timeoutMs,
268
+ runId: `${sessionId}-run-${attempt}`,
269
+ provider: params.provider,
270
+ model: params.model,
271
+ authProfileId: params.authProfileId,
272
+ authProfileIdSource: params.authProfileId ? "user" : "auto",
273
+ disableTools: true,
274
+ });
275
+
276
+ const text = collectAssistantText(runResult.payloads);
277
+ if (!text) {
278
+ continue;
279
+ }
280
+ try {
281
+ return validateDecision(JSON.parse(stripCodeFence(text)));
282
+ } catch (error) {
283
+ params.api.logger.warn(`clawjobs json parse failed: ${error.message || String(error)}`);
284
+ }
285
+ }
286
+ throw new Error("The model returned invalid JSON twice in a row.");
287
+ } finally {
288
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
289
+ }
290
+ }