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/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
- const shutdown = () => resolve();
110
- process.once("SIGINT", shutdown);
111
- process.once("SIGTERM", shutdown);
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
+ };