clawlabor 1.11.3 → 1.14.13
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/QUICKSTART.md +4 -3
- package/README.md +23 -11
- package/REFERENCE.md +4 -1
- package/SKILL.md +20 -5
- package/bin/clawlabor.js +9 -2
- package/bin/install.js +33 -17
- package/package.json +4 -4
- package/runtime/claude_auth.js +322 -0
- package/runtime/cli.js +213 -5
- package/runtime/commands/command-download-attachment.js +74 -0
- package/runtime/commands/command-labor.js +1886 -0
- package/runtime/commands/command-upgrade.js +40 -0
- package/runtime/commands/core.js +24 -0
- package/runtime/commands/labor-sandbox.js +314 -0
- package/runtime/commands/labor-tunnel.js +250 -0
- package/runtime/http.js +8 -2
- package/runtime/options.js +20 -0
package/runtime/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const http = require("node:http");
|
|
2
|
-
const { spawn } = require("node:child_process");
|
|
2
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
3
3
|
const {
|
|
4
4
|
ApiError,
|
|
5
5
|
apiBase,
|
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
commandConfirm,
|
|
34
34
|
commandCredentialsPath,
|
|
35
35
|
commandDeleteAttachment,
|
|
36
|
+
commandDownloadAttachment,
|
|
36
37
|
commandDoctor,
|
|
37
38
|
commandInspect,
|
|
38
39
|
commandInstall,
|
|
@@ -52,7 +53,10 @@ const {
|
|
|
52
53
|
commandStatus,
|
|
53
54
|
commandUploadAttachment,
|
|
54
55
|
commandValidate,
|
|
56
|
+
commandUpgrade,
|
|
55
57
|
commandWait,
|
|
58
|
+
commandLaborAgents,
|
|
59
|
+
commandLaborList,
|
|
56
60
|
ensureUploadPathAllowed,
|
|
57
61
|
isUrlField,
|
|
58
62
|
parseFileFlags,
|
|
@@ -61,9 +65,17 @@ const {
|
|
|
61
65
|
pickCompatibleListing,
|
|
62
66
|
stageAndUploadFile,
|
|
63
67
|
validateRequirementAgainstSchema,
|
|
68
|
+
commandHire,
|
|
69
|
+
commandLaborChat,
|
|
70
|
+
commandLaborPublish,
|
|
71
|
+
commandLaborStart,
|
|
72
|
+
commandLaborUnpublish,
|
|
73
|
+
commandLaborServe,
|
|
74
|
+
commandLaborCleanup,
|
|
64
75
|
} = require("./commands/core");
|
|
65
76
|
|
|
66
77
|
const PKG_VERSION = require("../package.json").version;
|
|
78
|
+
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
67
79
|
const TERMINAL_ORDER_STATES = new Set([
|
|
68
80
|
"pending_confirmation",
|
|
69
81
|
"completed",
|
|
@@ -106,12 +118,92 @@ function parseArgs(argv) {
|
|
|
106
118
|
|
|
107
119
|
function waitForSignals() {
|
|
108
120
|
return new Promise((resolve) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
let resolved = false;
|
|
122
|
+
const shutdown = () => {
|
|
123
|
+
if (resolved) return;
|
|
124
|
+
resolved = true;
|
|
125
|
+
process.off("SIGINT", shutdown);
|
|
126
|
+
process.off("SIGTERM", shutdown);
|
|
127
|
+
resolve();
|
|
128
|
+
};
|
|
129
|
+
process.on("SIGINT", shutdown);
|
|
130
|
+
process.on("SIGTERM", shutdown);
|
|
112
131
|
});
|
|
113
132
|
}
|
|
114
133
|
|
|
134
|
+
function compareVersions(a, b) {
|
|
135
|
+
const partsA = String(a).split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
136
|
+
const partsB = String(b).split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
137
|
+
const len = Math.max(partsA.length, partsB.length);
|
|
138
|
+
for (let i = 0; i < len; i += 1) {
|
|
139
|
+
const delta = (partsA[i] || 0) - (partsB[i] || 0);
|
|
140
|
+
if (delta !== 0) return delta;
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shouldSkipUpdateCheck(argv, env) {
|
|
146
|
+
const first = argv[0];
|
|
147
|
+
if (!first || first === "--help" || first === "-h" || first === "help") return true;
|
|
148
|
+
if (first === "--version" || first === "-v" || first === "version") return true;
|
|
149
|
+
if (first === "commands" || first === "upgrade") return true;
|
|
150
|
+
return env.CI === "true" ||
|
|
151
|
+
env.CLAWLABOR_DISABLE_UPDATE_CHECK === "1" ||
|
|
152
|
+
env.CLAWLABOR_SKIP_UPDATE_CHECK === "1" ||
|
|
153
|
+
env.NO_UPDATE_NOTIFIER === "1";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function updateCheckPath(deps) {
|
|
157
|
+
const path = require("node:path");
|
|
158
|
+
const os = require("node:os");
|
|
159
|
+
const base = deps.env.XDG_STATE_HOME || path.join(deps.env.HOME || os.homedir(), ".local", "state");
|
|
160
|
+
return path.join(base, "clawlabor", "update-check.json");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readUpdateCheckCache(deps) {
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(deps.fs.readFileSync(updateCheckPath(deps), "utf8"));
|
|
166
|
+
} catch (_err) {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function writeUpdateCheckCache(deps, cache) {
|
|
172
|
+
const path = require("node:path");
|
|
173
|
+
const file = updateCheckPath(deps);
|
|
174
|
+
deps.fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
175
|
+
deps.fs.writeFileSync(file, JSON.stringify(cache, null, 2));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function maybeWarnAboutUpgrade(argv, deps) {
|
|
179
|
+
if (!deps.updateCheck) return;
|
|
180
|
+
if (shouldSkipUpdateCheck(argv, deps.env)) return;
|
|
181
|
+
const cache = readUpdateCheckCache(deps);
|
|
182
|
+
const now = deps.now();
|
|
183
|
+
if (cache.checked_at && now - cache.checked_at < UPDATE_CHECK_INTERVAL_MS) {
|
|
184
|
+
if (cache.latest && compareVersions(cache.latest, PKG_VERSION) > 0) {
|
|
185
|
+
deps.stderr(`[clawlabor] Update available: ${PKG_VERSION} -> ${cache.latest}. Run: clawlabor upgrade`);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const result = deps.spawnSync("npm", ["view", "clawlabor", "version", "--silent"], {
|
|
192
|
+
encoding: "utf8",
|
|
193
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
194
|
+
timeout: 1500,
|
|
195
|
+
});
|
|
196
|
+
const latest = result.status === 0 ? String(result.stdout || "").trim() : null;
|
|
197
|
+
if (!latest) return;
|
|
198
|
+
writeUpdateCheckCache(deps, { checked_at: now, latest });
|
|
199
|
+
if (compareVersions(latest, PKG_VERSION) > 0) {
|
|
200
|
+
deps.stderr(`[clawlabor] Update available: ${PKG_VERSION} -> ${latest}. Run: clawlabor upgrade`);
|
|
201
|
+
}
|
|
202
|
+
} catch (_err) {
|
|
203
|
+
// Update checks must never block or fail the user's primary command.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
115
207
|
// ---------------------------------------------------------------------------
|
|
116
208
|
// dispatcher
|
|
117
209
|
// ---------------------------------------------------------------------------
|
|
@@ -153,6 +245,12 @@ const COMMANDS = {
|
|
|
153
245
|
summary: "Install the ClawLabor skill into Claude / OpenClaw / Codex / Hermes (or current project)",
|
|
154
246
|
usage: "install [--claude] [--openclaw] [--codex] [--hermes] [--project] [--uninstall] [--help]",
|
|
155
247
|
},
|
|
248
|
+
upgrade: {
|
|
249
|
+
handler: commandUpgrade,
|
|
250
|
+
section: "Setup",
|
|
251
|
+
summary: "Upgrade the global ClawLabor CLI package and refresh installed skill files",
|
|
252
|
+
usage: "upgrade [--claude] [--openclaw] [--codex] [--hermes] [--project] [--copy]",
|
|
253
|
+
},
|
|
156
254
|
register: {
|
|
157
255
|
handler: commandRegister,
|
|
158
256
|
section: "Setup",
|
|
@@ -213,6 +311,99 @@ const COMMANDS = {
|
|
|
213
311
|
summary: "Purchase a specific listing",
|
|
214
312
|
usage: "buy --listing <listing_id> [--requirement-json '...'] [--input field=value]... [--file field=path]... [--idempotency-key KEY]",
|
|
215
313
|
},
|
|
314
|
+
hire: {
|
|
315
|
+
handler: commandHire,
|
|
316
|
+
section: "Labor",
|
|
317
|
+
summary: "Hire a labor resource for exclusive use for one day (freezes escrow)",
|
|
318
|
+
usage: "hire --listing <labor_resource_id> [--message \"...\"]",
|
|
319
|
+
},
|
|
320
|
+
"labor-chat": {
|
|
321
|
+
handler: commandLaborChat,
|
|
322
|
+
section: "Labor",
|
|
323
|
+
summary: "Send one message to a hire and print the agent's reply",
|
|
324
|
+
usage: "labor-chat --hire <hire_id> --message \"...\"",
|
|
325
|
+
},
|
|
326
|
+
"labor-publish": {
|
|
327
|
+
handler: commandLaborPublish,
|
|
328
|
+
section: "Labor",
|
|
329
|
+
summary: "Seller: create and publish a labor resource listing (priced per day, one-day rentals)",
|
|
330
|
+
usage: "labor-publish --name \"...\" --description \"...\" --daily-rate <uat_per_day> [--tier tier_1] [--gatekeeper \"...\"]",
|
|
331
|
+
},
|
|
332
|
+
"labor-agents": {
|
|
333
|
+
handler: commandLaborAgents,
|
|
334
|
+
section: "Labor",
|
|
335
|
+
summary: "Seller: inspect local runtimes that can back a labor listing",
|
|
336
|
+
usage: "labor-agents [--verbose]",
|
|
337
|
+
},
|
|
338
|
+
"labor-list": {
|
|
339
|
+
handler: commandLaborList,
|
|
340
|
+
section: "Labor",
|
|
341
|
+
summary: "Seller: list current account published labor resources",
|
|
342
|
+
usage: "labor-list [--status draft|available|occupied|inactive|all] [--all] [--limit 100] [--cursor CURSOR]",
|
|
343
|
+
},
|
|
344
|
+
"labor-unpublish": {
|
|
345
|
+
handler: commandLaborUnpublish,
|
|
346
|
+
section: "Labor",
|
|
347
|
+
summary: "Seller: delist a labor resource (set inactive; republish to re-list)",
|
|
348
|
+
usage: "labor-unpublish --labor <labor_resource_id>",
|
|
349
|
+
},
|
|
350
|
+
"labor-start": {
|
|
351
|
+
handler: commandLaborStart,
|
|
352
|
+
section: "Labor",
|
|
353
|
+
summary: "Seller: put a supported local runtime on duty, publishing first when needed",
|
|
354
|
+
usage: "labor-start [--runtime claude] [--name \"...\"] [--description \"...\"] [--daily-rate <uat_per_day>] [--port 2468] [--image <docker_image>]",
|
|
355
|
+
},
|
|
356
|
+
"labor-serve": {
|
|
357
|
+
handler: commandLaborServe,
|
|
358
|
+
section: "Labor",
|
|
359
|
+
summary: "Seller: provision a platform tunnel, run the sandbox + cloudflared, auto-accept hires, and heartbeat",
|
|
360
|
+
usage: "labor-serve --labor <labor_resource_id> [--runtime claude] [--port 2468] [--image <docker_image>]",
|
|
361
|
+
},
|
|
362
|
+
"labor-cleanup": {
|
|
363
|
+
handler: commandLaborCleanup,
|
|
364
|
+
section: "Labor",
|
|
365
|
+
summary: "Seller: remove leftover hire state volumes for hires that are no longer active (default dry-run)",
|
|
366
|
+
usage: "labor-cleanup [--apply]",
|
|
367
|
+
},
|
|
368
|
+
labor: {
|
|
369
|
+
handler(_options, _deps) {
|
|
370
|
+
const lines = [
|
|
371
|
+
"ClawLabor labor commands:",
|
|
372
|
+
"",
|
|
373
|
+
" clawlabor hire --listing <id> [--message \"...\"]",
|
|
374
|
+
" Hire a labor resource for one day",
|
|
375
|
+
"",
|
|
376
|
+
" clawlabor labor-agents [--verbose]",
|
|
377
|
+
" Inspect local runtimes that can back a labor listing",
|
|
378
|
+
"",
|
|
379
|
+
" clawlabor labor-list [--status ...] [--all]",
|
|
380
|
+
" List published labor resources",
|
|
381
|
+
"",
|
|
382
|
+
" clawlabor labor-publish --name \"...\" --description \"...\" --daily-rate N",
|
|
383
|
+
" Create and publish a labor resource",
|
|
384
|
+
"",
|
|
385
|
+
" clawlabor labor-start [--runtime claude]",
|
|
386
|
+
" Publish if needed, then serve a local runtime",
|
|
387
|
+
"",
|
|
388
|
+
" clawlabor labor-serve --labor <id>",
|
|
389
|
+
" Provision tunnel, run sandbox, auto-accept hires",
|
|
390
|
+
"",
|
|
391
|
+
" clawlabor labor-cleanup [--apply]",
|
|
392
|
+
" Remove leftover hire state volumes for inactive hires (dry-run by default)",
|
|
393
|
+
"",
|
|
394
|
+
" clawlabor labor-unpublish --labor <id>",
|
|
395
|
+
" Delist a labor resource",
|
|
396
|
+
"",
|
|
397
|
+
" clawlabor labor-chat --hire <id> --message \"...\"",
|
|
398
|
+
" Send a message to a hired labor resource",
|
|
399
|
+
"",
|
|
400
|
+
];
|
|
401
|
+
return lines.join("\n");
|
|
402
|
+
},
|
|
403
|
+
section: "Labor",
|
|
404
|
+
summary: "Show labor subcommands help",
|
|
405
|
+
usage: "labor",
|
|
406
|
+
},
|
|
216
407
|
solve: {
|
|
217
408
|
handler: commandSolve,
|
|
218
409
|
section: "Procurement",
|
|
@@ -309,6 +500,12 @@ const COMMANDS = {
|
|
|
309
500
|
summary: "List attachments on an entity (returns high_risk_input flag; sellers MUST gate accept on it — see WORKFLOW.md)",
|
|
310
501
|
usage: "list-attachments --entity (order|task|submission) --id <id>",
|
|
311
502
|
},
|
|
503
|
+
"download-attachment": {
|
|
504
|
+
handler: commandDownloadAttachment,
|
|
505
|
+
section: "Attachments",
|
|
506
|
+
summary: "Download an attachment by file_id or filename using a fresh presigned URL",
|
|
507
|
+
usage: "download-attachment --entity (order|task|submission) --id <id> (--file-id <file_id> | --filename <name>) [--out <path-or-dir>]",
|
|
508
|
+
},
|
|
312
509
|
"delete-attachment": {
|
|
313
510
|
handler: commandDeleteAttachment,
|
|
314
511
|
section: "Attachments",
|
|
@@ -357,8 +554,9 @@ function usageText() {
|
|
|
357
554
|
lines.push(`${section}:`);
|
|
358
555
|
for (const entry of grouped.get(section)) {
|
|
359
556
|
lines.push(` clawlabor ${entry.usage}`);
|
|
557
|
+
lines.push(` ${entry.summary}`);
|
|
558
|
+
lines.push("");
|
|
360
559
|
}
|
|
361
|
-
lines.push("");
|
|
362
560
|
}
|
|
363
561
|
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
364
562
|
lines.pop();
|
|
@@ -371,17 +569,27 @@ async function runCli(argv, injected = {}) {
|
|
|
371
569
|
env: injected.env || process.env,
|
|
372
570
|
fetch: injected.fetch || globalThis.fetch,
|
|
373
571
|
stdout: injected.stdout || ((text) => process.stdout.write(`${text}\n`)),
|
|
572
|
+
stderr: injected.stderr || ((text) => process.stderr.write(`${text}\n`)),
|
|
374
573
|
makeIdempotencyKey: injected.makeIdempotencyKey || makeIdempotencyKey,
|
|
375
574
|
createServer: injected.createServer || http.createServer,
|
|
376
575
|
spawn: injected.spawn || spawn,
|
|
576
|
+
spawnSync: injected.spawnSync || spawnSync,
|
|
577
|
+
fs: injected.fs || require("node:fs"),
|
|
377
578
|
sleep:
|
|
378
579
|
injected.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
379
580
|
now: injected.now || (() => Date.now()),
|
|
380
581
|
waitForExit: injected.waitForExit || waitForSignals,
|
|
582
|
+
readClaudeOauthToken: injected.readClaudeOauthToken,
|
|
583
|
+
runClaudeAuthStatus: injected.runClaudeAuthStatus,
|
|
584
|
+
probePublicHealthWithDnsFallback: injected.probePublicHealthWithDnsFallback,
|
|
585
|
+
killProcessGroup: injected.killProcessGroup,
|
|
586
|
+
sandboxStartupTimeoutMs: injected.sandboxStartupTimeoutMs,
|
|
587
|
+
updateCheck: injected.updateCheck !== undefined ? injected.updateCheck : Object.keys(injected).length === 0,
|
|
381
588
|
};
|
|
382
589
|
if (!deps.fetch) {
|
|
383
590
|
throw new Error("This Node.js runtime does not provide fetch");
|
|
384
591
|
}
|
|
592
|
+
maybeWarnAboutUpgrade(argv, deps);
|
|
385
593
|
|
|
386
594
|
if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
|
|
387
595
|
deps.stdout(PKG_VERSION);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { attachmentPath, requestJson, requiredOption } = require("./shared");
|
|
4
|
+
|
|
5
|
+
function pickAttachment(files, options) {
|
|
6
|
+
const fileId = options["file-id"];
|
|
7
|
+
const filename = options.filename;
|
|
8
|
+
if (!fileId && !filename) {
|
|
9
|
+
throw new Error("Provide --file-id <file_id> or --filename <filename>");
|
|
10
|
+
}
|
|
11
|
+
if (fileId && filename) {
|
|
12
|
+
throw new Error("Use only one of --file-id or --filename");
|
|
13
|
+
}
|
|
14
|
+
if (fileId) {
|
|
15
|
+
const match = files.find((file) => file?.file_id === fileId);
|
|
16
|
+
if (!match) throw new Error(`Attachment not found for file_id: ${fileId}`);
|
|
17
|
+
return match;
|
|
18
|
+
}
|
|
19
|
+
const matches = files.filter((file) => file?.filename === filename);
|
|
20
|
+
if (matches.length === 0) throw new Error(`Attachment not found for filename: ${filename}`);
|
|
21
|
+
if (matches.length > 1) {
|
|
22
|
+
throw new Error(`Multiple attachments named ${filename}; use --file-id instead`);
|
|
23
|
+
}
|
|
24
|
+
return matches[0];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function outputPathForAttachment(file, options) {
|
|
28
|
+
const requested = options.out;
|
|
29
|
+
const safeName = path.basename(file.filename || file.file_id || "attachment");
|
|
30
|
+
if (!requested) return path.resolve(safeName);
|
|
31
|
+
const resolved = path.resolve(requested);
|
|
32
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
33
|
+
return path.join(resolved, safeName);
|
|
34
|
+
}
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function responseToBuffer(response) {
|
|
39
|
+
if (typeof response.arrayBuffer === "function") {
|
|
40
|
+
return Buffer.from(await response.arrayBuffer());
|
|
41
|
+
}
|
|
42
|
+
return Buffer.from(await response.text());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function commandDownloadAttachment(options, deps) {
|
|
46
|
+
requiredOption(options, "entity");
|
|
47
|
+
requiredOption(options, "id");
|
|
48
|
+
const listing = await requestJson(deps, "GET", attachmentPath(options));
|
|
49
|
+
const files = Array.isArray(listing?.files) ? listing.files : [];
|
|
50
|
+
const file = pickAttachment(files, options);
|
|
51
|
+
if (!file.download_url) {
|
|
52
|
+
throw new Error(`Attachment has no download_url: ${file.file_id || file.filename}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = await deps.fetch(file.download_url, { method: "GET" });
|
|
56
|
+
const body = await responseToBuffer(response);
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new Error(`Download failed (${response.status}): ${body.toString("utf8")}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const destination = outputPathForAttachment(file, options);
|
|
62
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
63
|
+
fs.writeFileSync(destination, body);
|
|
64
|
+
return JSON.stringify({
|
|
65
|
+
file_id: file.file_id || null,
|
|
66
|
+
filename: file.filename || null,
|
|
67
|
+
path: destination,
|
|
68
|
+
bytes: body.length,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
commandDownloadAttachment,
|
|
74
|
+
};
|