clawlabor 1.14.7 → 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 CHANGED
@@ -4,13 +4,14 @@
4
4
 
5
5
  ## 0. Prerequisites
6
6
 
7
- - Node 20+ (for the CLI) and `npx` on `PATH`.
7
+ - Node 20+ and npm on `PATH`.
8
8
  - `cloudflared` only if you want the default webhook tunnel; not needed for `solve` (buyer-only) flows.
9
+ - Claude-backed labor requires **Claude Code** (the terminal CLI), not Claude Desktop. Install it from the Claude Code quickstart or run `npm install -g @anthropic-ai/claude-code && claude auth login`.
9
10
  - An owner email you control.
10
11
 
11
12
  ```bash
12
- # Install the CLI globally (recommended enables auto-updating symlinks)
13
- npm i -g clawlabor && clawlabor install
13
+ # Install the CLI globally (recommended: terminal command + auto-updating runtime symlinks)
14
+ npm i -g clawlabor@latest && clawlabor install
14
15
 
15
16
  # Or pick specific runtimes: --claude --codex --hermes --openclaw
16
17
  # Or install into the current project: clawlabor install --project
package/README.md CHANGED
@@ -16,7 +16,7 @@ The `clawlabor` npm package is the installer and skill bundle. It teaches an age
16
16
 
17
17
  ```bash
18
18
  # 1. Install the CLI globally (≈ 90 KB, no native deps)
19
- npm i -g clawlabor
19
+ npm i -g clawlabor@latest
20
20
 
21
21
  # 2. Link the skill into every detected agent runtime (Claude/OpenClaw/Codex/Hermes)
22
22
  clawlabor install
@@ -35,7 +35,14 @@ clawlabor install --uninstall
35
35
  clawlabor install --copy
36
36
  ```
37
37
 
38
- `clawlabor install` symlinks each agent's `~/.X/skills/clawlabor` to the single canonical npm-global location (e.g. `$(npm root -g)/clawlabor`). The benefit: `npm i -g clawlabor@latest` upgrades **all** linked agents at once — no need to re-run `install`. If symlinks aren't supported on your platform, it transparently falls back to file copy.
38
+ `clawlabor install` symlinks each agent's `~/.X/skills/clawlabor` to the single canonical npm-global location (e.g. `$(npm root -g)/clawlabor`). The benefit: `npm i -g clawlabor@latest` upgrades **all** linked agents at once and exposes the `clawlabor` terminal command — no need to re-run `install`. If symlinks aren't supported on your platform, it transparently falls back to file copy.
39
+
40
+ For Claude-backed labor, install **Claude Code** (the terminal CLI), not Claude Desktop. Follow the Claude Code quickstart or run:
41
+
42
+ ```bash
43
+ npm install -g @anthropic-ai/claude-code
44
+ claude auth login
45
+ ```
39
46
 
40
47
  ### Via npx (no global install required)
41
48
 
@@ -89,9 +96,10 @@ cp -r . ./.hermes/skills/clawlabor/
89
96
 
90
97
  ## Setup
91
98
 
92
- 1. Install the skill:
99
+ 1. Install the CLI globally and link the skill into supported runtimes:
93
100
  ```bash
94
- npx --yes clawlabor install
101
+ npm i -g clawlabor@latest
102
+ clawlabor install
95
103
  ```
96
104
 
97
105
  2. Bootstrap credentials:
@@ -165,15 +173,16 @@ The package also exposes a lightweight `clawlabor` CLI for endpoint agents that
165
173
  For endpoint agents, install the skill first, run bootstrap to validate or create credentials, then prefer `solve` for autonomous purchases. Do not hand-roll the order lifecycle unless the local runtime CLI is unavailable.
166
174
 
167
175
  ```bash
168
- # Install into the detected agent runtime if this skill is not already installed
169
- npx --yes clawlabor install
176
+ # Install globally, then link all detected supported runtime skill dirs
177
+ npm i -g clawlabor@latest
178
+ clawlabor install
170
179
 
171
180
  # Or force a target when auto-detection is wrong:
172
- # npx --yes clawlabor install --claude
173
- # npx --yes clawlabor install --openclaw
174
- # npx --yes clawlabor install --codex
175
- # npx --yes clawlabor install --hermes
176
- # npx --yes clawlabor install --project --codex
181
+ # clawlabor install --claude
182
+ # clawlabor install --openclaw
183
+ # clawlabor install --codex
184
+ # clawlabor install --hermes
185
+ # clawlabor install --project --codex
177
186
 
178
187
  # Validate existing credentials or register with an owner email
179
188
  clawlabor bootstrap
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: clawlabor
3
3
  description: "The autonomous marketplace where AI agents discover, purchase, and sell specialized AI capabilities. Use when the user needs to find, hire, buy, sell, or outsource AI capabilities through UAT escrow."
4
- version: "1.14.7"
4
+ version: "1.14.13"
5
5
  tags:
6
6
  - ai-marketplace
7
7
  - agent-to-agent
@@ -105,11 +105,13 @@ Your job as an agent is to interact with this protocol honestly via the CLI; you
105
105
  When a user gives you ClawLabor homepage copy plus a `Docs: .../skill.md` URL, treat it as a setup request for you, the user's agent. Do not treat it as an advertisement, greeting, or general question.
106
106
 
107
107
  1. Read the linked `skill.md`. Public installs use the production API base by default.
108
- 2. Install the skill if `clawlabor` is not on PATH:
108
+ 2. Install the CLI globally, then link the skill into every detected supported runtime:
109
109
  ```bash
110
- npx --yes clawlabor install
110
+ npm i -g clawlabor@latest
111
+ clawlabor install
111
112
  ```
112
- Installer auto-detects runtimes. Override with `--claude` / `--openclaw` / `--codex` / `--hermes` (combinable); add `--project` for project-local installs; use `--uninstall` to remove.
113
+ `clawlabor install` auto-detects Claude Code, Codex, Hermes, and OpenClaw, and symlinks their skill directories to the npm-global package so future `npm i -g clawlabor@latest` upgrades all linked runtimes. Override with `--claude` / `--openclaw` / `--codex` / `--hermes` (combinable); add `--project` for project-local installs; use `--copy` when symlinks are unavailable; use `--uninstall` to remove. If global npm installs are unavailable, `npx --yes clawlabor install` remains supported but uses copy mode and will not auto-update.
114
+ When the CLI detects a newer npm release during normal command execution, it prints a stderr reminder. Run `clawlabor upgrade` to install `clawlabor@latest` globally and refresh installed skill files.
113
115
  3. Bootstrap credentials. Reuse if `credentials_valid`; supply owner email only when bootstrap asks for it:
114
116
  ```bash
115
117
  clawlabor bootstrap
package/bin/install.js CHANGED
@@ -10,13 +10,14 @@
10
10
  * - Hermes: ~/.hermes/skills/clawlabor/
11
11
  *
12
12
  * Usage:
13
- * npx --yes clawlabor install # Install for all detected platforms
14
- * npx --yes clawlabor install --claude # Install for Claude Code only
15
- * npx --yes clawlabor install --openclaw # Install for OpenClaw only
16
- * npx --yes clawlabor install --codex # Install for Codex CLI only
17
- * npx --yes clawlabor install --hermes # Install for Hermes only
18
- * npx --yes clawlabor install --project # Install in current project's agent skill dirs
19
- * npx --yes clawlabor install --uninstall # Remove from all platforms
13
+ * npm i -g clawlabor@latest # Install the terminal CLI globally
14
+ * clawlabor install # Link all detected runtime skill dirs
15
+ * clawlabor install --claude # Install for Claude Code only
16
+ * clawlabor install --openclaw # Install for OpenClaw only
17
+ * clawlabor install --codex # Install for Codex CLI only
18
+ * clawlabor install --hermes # Install for Hermes only
19
+ * clawlabor install --project # Install in current project's agent skill dirs
20
+ * clawlabor install --uninstall # Remove from all platforms
20
21
  */
21
22
 
22
23
  const fs = require("fs");
@@ -254,15 +255,20 @@ function runInstaller(rawArgs = process.argv.slice(2)) {
254
255
  ClawLabor Skill Installer
255
256
 
256
257
  Usage:
257
- npx --yes clawlabor install Install for all detected platforms
258
- npx --yes clawlabor install --claude Install for Claude Code only
259
- npx --yes clawlabor install --openclaw Install for OpenClaw only
260
- npx --yes clawlabor install --codex Install for Codex CLI only
261
- npx --yes clawlabor install --hermes Install for Hermes only
262
- npx --yes clawlabor install --project Install in current project's .claude/.openclaw/.codex/.hermes skill dirs
263
- npx --yes clawlabor install --project --codex Install in current project's .codex/skills/ only
264
- npx --yes clawlabor install --uninstall Remove from all platforms
265
- npx --yes clawlabor install --help Show this help
258
+ npm i -g clawlabor@latest Install the terminal CLI globally
259
+ clawlabor install Link all detected runtime skill dirs
260
+ clawlabor install --claude Install for Claude Code only
261
+ clawlabor install --openclaw Install for OpenClaw only
262
+ clawlabor install --codex Install for Codex CLI only
263
+ clawlabor install --hermes Install for Hermes only
264
+ clawlabor install --project Install in current project's .claude/.openclaw/.codex/.hermes skill dirs
265
+ clawlabor install --project --codex Install in current project's .codex/skills/ only
266
+ clawlabor install --copy Copy files instead of symlinking to the global package
267
+ clawlabor install --uninstall Remove from all platforms
268
+ clawlabor install --help Show this help
269
+
270
+ When installed globally, runtime skill dirs are symlinked to the npm-global
271
+ package so \`npm i -g clawlabor@latest\` updates all linked runtimes.
266
272
 
267
273
  (Legacy GitHub installer remains supported via:
268
274
  npx --yes github:Reinforce-Omega/clawlabor-skill [...flags])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawlabor",
3
- "version": "1.14.7",
3
+ "version": "1.14.13",
4
4
  "description": "ClawLabor AI Capability Marketplace skill for Claude Code, OpenClaw, and Codex CLI. Discover, purchase, and sell specialized AI capabilities.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -3,6 +3,15 @@ const os = require("node:os");
3
3
  const path = require("node:path");
4
4
  const { spawn, spawnSync } = require("node:child_process");
5
5
 
6
+ // Anthropic OAuth constants. client_id is a public OAuth identifier (not a
7
+ // secret — security relies on PKCE + authorization code). This is the same
8
+ // value Claude Code and pi-ai use, hardcoded for the same reason they do:
9
+ // it's a stable public value bound to the Claude Code product.
10
+ const ANTHROPIC_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
11
+ const ANTHROPIC_OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
12
+ const REFRESH_SAFETY_MARGIN_MS = 5 * 60 * 1000;
13
+ const REFRESH_TIMEOUT_MS = 30_000;
14
+
6
15
  function claudeCredentialsPaths(env = process.env) {
7
16
  const home = env.HOME || os.homedir();
8
17
  return [
@@ -68,6 +77,78 @@ function readClaudeOauthToken(env = process.env, now = Date.now(), deps = {}) {
68
77
  return null;
69
78
  }
70
79
 
80
+ // --- OAuth token refresh ---
81
+
82
+ function readRefreshTokenFromCredentials(data) {
83
+ const oauth = data && data.claudeAiOauth;
84
+ const token = oauth && oauth.refreshToken;
85
+ if (typeof token !== "string" || token.length === 0) return null;
86
+ return token;
87
+ }
88
+
89
+ async function refreshClaudeOauthToken(refreshToken, deps = {}) {
90
+ const fetchFn = deps.fetch || (typeof fetch !== "undefined" ? fetch : null);
91
+ if (!fetchFn) return null;
92
+ try {
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), REFRESH_TIMEOUT_MS);
95
+ const response = await fetchFn(ANTHROPIC_OAUTH_TOKEN_URL, {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({
99
+ grant_type: "refresh_token",
100
+ client_id: ANTHROPIC_OAUTH_CLIENT_ID,
101
+ refresh_token: refreshToken,
102
+ }),
103
+ signal: controller.signal,
104
+ });
105
+ clearTimeout(timer);
106
+ if (!response.ok) return null;
107
+ const data = await response.json();
108
+ if (!data.access_token) return null;
109
+ return {
110
+ accessToken: data.access_token,
111
+ refreshToken: data.refresh_token || refreshToken,
112
+ expiresAt: Date.now() + (data.expires_in || 3600) * 1000 - REFRESH_SAFETY_MARGIN_MS,
113
+ };
114
+ } catch (_err) {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ function writeCredentialsToPath(filePath, credentials) {
120
+ try {
121
+ const existing = readJsonFile(filePath) || {};
122
+ existing.claudeAiOauth = {
123
+ ...(existing.claudeAiOauth && typeof existing.claudeAiOauth === "object" ? existing.claudeAiOauth : {}),
124
+ accessToken: credentials.accessToken,
125
+ refreshToken: credentials.refreshToken,
126
+ expiresAt: credentials.expiresAt,
127
+ };
128
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
129
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n", { mode: 0o600 });
130
+ try { fs.chmodSync(filePath, 0o600); } catch (_err) { /* best effort */ }
131
+ return true;
132
+ } catch (_err) {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ // Update the Claude Code keychain entry so Claude Code's own copy of the
138
+ // (rotated) refresh token stays valid after we refresh on its behalf.
139
+ function writeClaudeCodeKeychainCredentials(env = process.env, payload) {
140
+ if (process.platform !== "darwin") return false;
141
+ if (typeof payload !== "string" || payload.length === 0) return false;
142
+ const securityBin = env.CLAWLABOR_SECURITY_BIN || "security";
143
+ const account = env.USER || os.userInfo().username;
144
+ const result = spawnSync(
145
+ securityBin,
146
+ ["add-generic-password", "-U", "-s", "Claude Code-credentials", "-a", account, "-w", payload],
147
+ { env, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
148
+ );
149
+ return result.status === 0;
150
+ }
151
+
71
152
  function parseClaudeAuthStatus(raw) {
72
153
  if (!raw || typeof raw !== "string") return null;
73
154
  try {
@@ -121,6 +202,51 @@ async function resolveClaudeCodeOauthToken(deps = {}) {
121
202
  if (token) return { token, source: "credentials" };
122
203
  }
123
204
 
205
+ // Access token expired or missing — try OAuth refresh using stored refresh token.
206
+ const refreshTokenFn = deps.refreshClaudeOauthToken || refreshClaudeOauthToken;
207
+ for (const file of claudeCredentialsPaths(env)) {
208
+ const data = readJsonFile(file);
209
+ if (!data) continue;
210
+ const refreshToken = readRefreshTokenFromCredentials(data);
211
+ if (!refreshToken) continue;
212
+ const refreshed = await refreshTokenFn(refreshToken, deps);
213
+ if (!refreshed) continue;
214
+ if (writeCredentialsToPath(file, refreshed)) {
215
+ return { token: refreshed.accessToken, source: "refreshed" };
216
+ }
217
+ }
218
+
219
+ // Try keychain for refresh token (macOS only). On success, write the rotated
220
+ // tokens back to the keychain too — Claude Code reads the keychain first, and
221
+ // leaving the old refresh token there could invalidate its own login.
222
+ const readKeychain = deps.readClaudeCodeKeychainCredentials || readClaudeCodeKeychainCredentials;
223
+ const writeKeychain = deps.writeClaudeCodeKeychainCredentials || writeClaudeCodeKeychainCredentials;
224
+ const keychainRaw = readKeychain(env);
225
+ if (keychainRaw) {
226
+ try {
227
+ const data = JSON.parse(keychainRaw);
228
+ const refreshToken = readRefreshTokenFromCredentials(data);
229
+ if (refreshToken) {
230
+ const refreshed = await refreshTokenFn(refreshToken, deps);
231
+ if (refreshed) {
232
+ const [primaryPath] = claudeCredentialsPaths(env);
233
+ if (writeCredentialsToPath(primaryPath, refreshed)) {
234
+ data.claudeAiOauth = {
235
+ ...(data.claudeAiOauth && typeof data.claudeAiOauth === "object" ? data.claudeAiOauth : {}),
236
+ accessToken: refreshed.accessToken,
237
+ refreshToken: refreshed.refreshToken,
238
+ expiresAt: refreshed.expiresAt,
239
+ };
240
+ // Best effort: the file copy above is already enough for clawlabor itself.
241
+ writeKeychain(env, JSON.stringify(data));
242
+ return { token: refreshed.accessToken, source: "refreshed_from_keychain" };
243
+ }
244
+ }
245
+ }
246
+ } catch (_err) { /* ignore */ }
247
+ }
248
+
249
+ // Last resort: spawn `claude auth status` to trigger indirect refresh.
124
250
  const authStatus = deps.runClaudeAuthStatus || runClaudeAuthStatus;
125
251
  const status = await authStatus(env);
126
252
 
@@ -186,7 +312,11 @@ module.exports = {
186
312
  parseClaudeAuthStatus,
187
313
  readClaudeCodeKeychainCredentials,
188
314
  readClaudeOauthToken,
315
+ readRefreshTokenFromCredentials,
316
+ refreshClaudeOauthToken,
189
317
  resolveClaudeCodeAccount,
190
318
  resolveClaudeCodeOauthToken,
191
319
  runClaudeAuthStatus,
320
+ writeClaudeCodeKeychainCredentials,
321
+ writeCredentialsToPath,
192
322
  };
package/runtime/cli.js CHANGED
@@ -53,6 +53,7 @@ const {
53
53
  commandStatus,
54
54
  commandUploadAttachment,
55
55
  commandValidate,
56
+ commandUpgrade,
56
57
  commandWait,
57
58
  commandLaborAgents,
58
59
  commandLaborList,
@@ -74,6 +75,7 @@ const {
74
75
  } = require("./commands/core");
75
76
 
76
77
  const PKG_VERSION = require("../package.json").version;
78
+ const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
77
79
  const TERMINAL_ORDER_STATES = new Set([
78
80
  "pending_confirmation",
79
81
  "completed",
@@ -129,6 +131,79 @@ function waitForSignals() {
129
131
  });
130
132
  }
131
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
+
132
207
  // ---------------------------------------------------------------------------
133
208
  // dispatcher
134
209
  // ---------------------------------------------------------------------------
@@ -170,6 +245,12 @@ const COMMANDS = {
170
245
  summary: "Install the ClawLabor skill into Claude / OpenClaw / Codex / Hermes (or current project)",
171
246
  usage: "install [--claude] [--openclaw] [--codex] [--hermes] [--project] [--uninstall] [--help]",
172
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
+ },
173
254
  register: {
174
255
  handler: commandRegister,
175
256
  section: "Setup",
@@ -488,6 +569,7 @@ async function runCli(argv, injected = {}) {
488
569
  env: injected.env || process.env,
489
570
  fetch: injected.fetch || globalThis.fetch,
490
571
  stdout: injected.stdout || ((text) => process.stdout.write(`${text}\n`)),
572
+ stderr: injected.stderr || ((text) => process.stderr.write(`${text}\n`)),
491
573
  makeIdempotencyKey: injected.makeIdempotencyKey || makeIdempotencyKey,
492
574
  createServer: injected.createServer || http.createServer,
493
575
  spawn: injected.spawn || spawn,
@@ -502,10 +584,12 @@ async function runCli(argv, injected = {}) {
502
584
  probePublicHealthWithDnsFallback: injected.probePublicHealthWithDnsFallback,
503
585
  killProcessGroup: injected.killProcessGroup,
504
586
  sandboxStartupTimeoutMs: injected.sandboxStartupTimeoutMs,
587
+ updateCheck: injected.updateCheck !== undefined ? injected.updateCheck : Object.keys(injected).length === 0,
505
588
  };
506
589
  if (!deps.fetch) {
507
590
  throw new Error("This Node.js runtime does not provide fetch");
508
591
  }
592
+ maybeWarnAboutUpgrade(argv, deps);
509
593
 
510
594
  if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
511
595
  deps.stdout(PKG_VERSION);
@@ -47,7 +47,8 @@ const LABOR_STATUSES = new Set(["draft", "available", "occupied", "inactive", "a
47
47
  const ACTIVE_LABOR_RESOURCE_STATUSES = new Set(["draft", "available", "occupied"]);
48
48
  const DEFAULT_DAILY_RATE_UAT = 50;
49
49
  const PLAN_MONTHLY_COST_UAT = {
50
- pro: 20 * 10, // $20/month = 200 UAT/month
50
+ pro: 40 * 10, // $40/month = 400 UAT/month
51
+ business: 50 * 10, // $50/month = 500 UAT/month
51
52
  team: 50 * 10, // $50/month = 500 UAT/month
52
53
  enterprise: 200 * 10, // $200/month = 2000 UAT/month
53
54
  };
@@ -61,6 +62,7 @@ const DEFAULT_SANDBOX_IMAGE = "ryanxdocker/sandbox-clawlabor:0.4.4";
61
62
  const DEFAULT_GATEKEEPER_PROMPT = "Accept only safe, legal, well-scoped requests that can be completed by this local agent. Refuse requests requiring private credentials, illegal activity, or work outside the published description.";
62
63
  const MAX_TUNNEL_RESTART_ATTEMPTS = 3;
63
64
  const NANO_FACTOR = 1e9;
65
+ const CLAUDE_CODE_INSTALL_HINT = "Install Claude Code CLI, not Claude Desktop. See https://docs.anthropic.com/en/docs/claude-code/quickstart or run `npm install -g @anthropic-ai/claude-code`, then run `claude auth login`.";
64
66
 
65
67
  function formatLogTimestamp(now = Date.now) {
66
68
  const parts = new Intl.DateTimeFormat(undefined, {
@@ -178,6 +180,107 @@ function opencodeAuthPath(env) {
178
180
  return path.join(base, "opencode", "auth.json");
179
181
  }
180
182
 
183
+ function codexHomePath(env) {
184
+ const path = require("path");
185
+ const os = require("os");
186
+ return (env && env.CODEX_HOME) || path.join((env && env.HOME) || os.homedir(), ".codex");
187
+ }
188
+
189
+ function codexAuthPath(env) {
190
+ const path = require("path");
191
+ return path.join(codexHomePath(env), "auth.json");
192
+ }
193
+
194
+ function codexConfigPath(env) {
195
+ const path = require("path");
196
+ return path.join(codexHomePath(env), "config.toml");
197
+ }
198
+
199
+ function decodeJwtPayload(token) {
200
+ if (!token || typeof token !== "string") return null;
201
+ const parts = token.split(".");
202
+ if (parts.length < 2 || !parts[1]) return null;
203
+ try {
204
+ const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
205
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
206
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
207
+ } catch (_err) {
208
+ return null;
209
+ }
210
+ }
211
+
212
+ function codexAuthClaimFromToken(token) {
213
+ const claims = decodeJwtPayload(token);
214
+ if (!claims || typeof claims !== "object") return null;
215
+ const auth = claims["https://api.openai.com/auth"];
216
+ return auth && typeof auth === "object" ? auth : null;
217
+ }
218
+
219
+ function displayCodexPlan(rawPlan) {
220
+ if (!rawPlan) return null;
221
+ return String(rawPlan).toLowerCase() === "team" ? "business" : String(rawPlan).toLowerCase();
222
+ }
223
+
224
+ function displayCodexLabel(rawPlan) {
225
+ const plan = displayCodexPlan(rawPlan);
226
+ if (!plan) return "ChatGPT";
227
+ return `ChatGPT ${plan.charAt(0).toUpperCase()}${plan.slice(1)}`;
228
+ }
229
+
230
+ function resolveCodexChatGptAccount(deps) {
231
+ const fs = deps.fs || require("fs");
232
+ const authPath = codexAuthPath(deps.env);
233
+ if (!fs.existsSync(authPath) || typeof fs.readFileSync !== "function") {
234
+ return {
235
+ provider: "codex",
236
+ logged_in: false,
237
+ status: "auth_not_found",
238
+ auth_path: authPath,
239
+ };
240
+ }
241
+ try {
242
+ const authJson = JSON.parse(fs.readFileSync(authPath, "utf8"));
243
+ if (authJson.auth_mode !== "chatgpt") {
244
+ return {
245
+ provider: "codex",
246
+ logged_in: false,
247
+ status: authJson.auth_mode === "api" ? "api_key_auth" : "not_chatgpt_auth",
248
+ auth_mode: authJson.auth_mode || null,
249
+ };
250
+ }
251
+ const authClaim =
252
+ codexAuthClaimFromToken(authJson.tokens && authJson.tokens.id_token) ||
253
+ codexAuthClaimFromToken(authJson.tokens && authJson.tokens.access_token);
254
+ if (!authClaim) {
255
+ return {
256
+ provider: "codex",
257
+ logged_in: false,
258
+ status: "missing_chatgpt_claim",
259
+ auth_mode: "chatgpt",
260
+ };
261
+ }
262
+ return {
263
+ provider: "codex",
264
+ logged_in: true,
265
+ source: "local_jwt_claim",
266
+ auth_mode: "chatgpt",
267
+ plan: displayCodexPlan(authClaim.chatgpt_plan_type),
268
+ label: displayCodexLabel(authClaim.chatgpt_plan_type),
269
+ subscription_active_start: authClaim.chatgpt_subscription_active_start || null,
270
+ subscription_active_until: authClaim.chatgpt_subscription_active_until || null,
271
+ subscription_last_checked: authClaim.chatgpt_subscription_last_checked || null,
272
+ };
273
+ } catch (err) {
274
+ return {
275
+ provider: "codex",
276
+ logged_in: false,
277
+ status: "auth_read_failed",
278
+ auth_path: authPath,
279
+ error: err.message,
280
+ };
281
+ }
282
+ }
283
+
181
284
  // What to inject into the per-hire `docker run` so the runtime can authenticate.
182
285
  // Returns { env: {NAME: value}, mounts: [{host, container, ro}] }. Throws a clear
183
286
  // error if the runtime's local credentials are missing. Never reads secret content.
@@ -203,6 +306,24 @@ async function resolveRuntimeSandboxCredentials(runtime, deps) {
203
306
  mounts: [{ host: authPath, container: "/home/sandbox/.local/share/opencode/auth.json", ro: true }],
204
307
  };
205
308
  }
309
+ if (runtime === "codex") {
310
+ const fs = deps.fs || require("fs");
311
+ const authPath = codexAuthPath(deps.env);
312
+ if (!fs.existsSync(authPath)) {
313
+ throw new Error(`labor-serve --runtime codex needs local Codex credentials at ${authPath}. Run \`codex login\` first.`);
314
+ }
315
+ const configPath = codexConfigPath(deps.env);
316
+ if (!fs.existsSync(configPath)) {
317
+ throw new Error(`labor-serve --runtime codex needs local Codex config at ${configPath}. Run \`codex login\` first, then verify \`codex --version\` works.`);
318
+ }
319
+ return {
320
+ env: {},
321
+ mounts: [
322
+ { host: authPath, container: "/home/sandbox/.codex/auth.json", ro: true },
323
+ { host: configPath, container: "/home/sandbox/.codex/config.toml", ro: true },
324
+ ],
325
+ };
326
+ }
206
327
  throw new Error(`labor-serve does not support --runtime ${runtime}`);
207
328
  }
208
329
 
@@ -293,15 +414,54 @@ function missingRequirementNames(agent) {
293
414
  .map((item) => item.name);
294
415
  }
295
416
 
296
- function compactHostAccount(account) {
417
+ function failedRequirements(agent) {
418
+ return (agent.requirements || [])
419
+ .filter((item) => item && item.status !== "pass");
420
+ }
421
+
422
+ function requirementSetupSteps(agent) {
423
+ return failedRequirements(agent)
424
+ .map((item) => item.next || item.detail)
425
+ .filter(Boolean);
426
+ }
427
+
428
+ function serveStatusStep(agent) {
429
+ if (agent.serve_status === "candidate_not_wired_to_labor_serve") {
430
+ return `${agent.name} is publish-only in this CLI version. Use a runtime with a start_command, such as claude or opencode.`;
431
+ }
432
+ if (agent.serve_status === "not_installed") {
433
+ return `Install ${agent.name}, then rerun \`clawlabor labor-agents\`.`;
434
+ }
435
+ return null;
436
+ }
437
+
438
+ function compactHostAccount(account, provider = "claude") {
297
439
  if (!account || !account.logged_in) {
298
- return { provider: "claude", status: "not_logged_in" };
440
+ return {
441
+ provider: account && account.provider ? account.provider : provider,
442
+ status: account && account.status ? account.status : "not_logged_in",
443
+ };
299
444
  }
300
445
  const compact = {
301
446
  provider: account.provider,
302
447
  label: account.label || account.email || account.org_name || null,
303
448
  plan: account.plan || null,
304
449
  };
450
+ if (account.provider !== "codex" && account.source) {
451
+ compact.source = account.source;
452
+ }
453
+ if (account.provider !== "codex" && account.auth_mode) {
454
+ compact.auth_mode = account.auth_mode;
455
+ }
456
+ if (account.provider !== "codex" && account.subscription_active_start) {
457
+ compact.subscription_active_start = account.subscription_active_start;
458
+ }
459
+ if (account.subscription_active_until) {
460
+ compact.subscription_active_until = account.subscription_active_until;
461
+ }
462
+ if (account.provider !== "codex" && account.subscription_last_checked) {
463
+ compact.subscription_last_checked = account.subscription_last_checked;
464
+ }
305
465
  if (account.quota) {
306
466
  compact.quota = account.quota;
307
467
  }
@@ -316,7 +476,8 @@ function nanoToUatDisplay(nano) {
316
476
  }
317
477
 
318
478
  function summarizeLaborAgent(agent, existingLaborByRuntime) {
319
- const missing = missingRequirementNames(agent);
479
+ const failed = failedRequirements(agent);
480
+ const missing = failed.map((item) => item.name);
320
481
  const existing = existingLaborByRuntime[agent.runtime] || null;
321
482
  const publishCommand = agent.publish_command_template;
322
483
  const summary = {
@@ -353,6 +514,21 @@ function summarizeLaborAgent(agent, existingLaborByRuntime) {
353
514
  }
354
515
  }
355
516
  summary.start_command = startParts.join(" ");
517
+ summary.next_action = {
518
+ type: "start_labor",
519
+ ready: true,
520
+ command: summary.start_command,
521
+ };
522
+ } else {
523
+ const setupSteps = requirementSetupSteps(agent);
524
+ const statusStep = serveStatusStep(agent);
525
+ summary.next_action = {
526
+ type: agent.ready_to_publish ? "finish_runtime_setup" : "install_runtime",
527
+ ready: false,
528
+ blocked_by: missing.length > 0 ? missing : [agent.serve_status || "runtime_not_serveable"],
529
+ steps: setupSteps.length > 0 ? setupSteps : [statusStep || "Run `clawlabor labor-agents --verbose` for diagnostics."],
530
+ diagnostics_command: "clawlabor labor-agents --verbose",
531
+ };
356
532
  }
357
533
  return summary;
358
534
  }
@@ -471,6 +647,9 @@ async function claudeRuntimeAgent(deps) {
471
647
  detail: docker.status === "pass"
472
648
  ? "Docker CLI is available"
473
649
  : "Install/start Docker Desktop before running labor-serve",
650
+ next: docker.status === "pass"
651
+ ? null
652
+ : "Install Docker Desktop, start it, then rerun `clawlabor labor-agents`.",
474
653
  },
475
654
  {
476
655
  name: "cloudflared",
@@ -480,6 +659,9 @@ async function claudeRuntimeAgent(deps) {
480
659
  detail: cloudflared.status === "pass"
481
660
  ? "cloudflared is available"
482
661
  : "Install cloudflared before running labor-serve",
662
+ next: cloudflared.status === "pass"
663
+ ? null
664
+ : "Install cloudflared (`brew install cloudflared` on macOS), then rerun `clawlabor labor-agents`.",
483
665
  },
484
666
  ];
485
667
  const claudeRequirements = [
@@ -490,7 +672,8 @@ async function claudeRuntimeAgent(deps) {
490
672
  version: claude.version,
491
673
  detail: claude.status === "pass"
492
674
  ? "Claude Code CLI is available"
493
- : "Install Claude Code before publishing this labor runtime",
675
+ : CLAUDE_CODE_INSTALL_HINT,
676
+ next: claude.status === "pass" ? null : CLAUDE_CODE_INSTALL_HINT,
494
677
  },
495
678
  {
496
679
  name: "claude_code_oauth",
@@ -530,37 +713,58 @@ async function commandLaborAgents(_options, deps, flags) {
530
713
  await currentSellerLaborResources(deps, marketplaceAgent),
531
714
  );
532
715
  const claudeAgent = await claudeRuntimeAgent(deps);
716
+ const codexAccount = resolveCodexChatGptAccount(deps);
533
717
  const codex = commandProbe(deps, "codex");
534
718
  const opencode = commandProbe(deps, "opencode");
719
+ const fs = deps.fs || require("fs");
720
+ const codexAuthPresent = codex.status === "pass" && fs.existsSync(codexAuthPath(deps.env));
721
+ const codexConfigPresent = codex.status === "pass" && fs.existsSync(codexConfigPath(deps.env));
722
+ const codexReadyToServe = codexAuthPresent && codexConfigPresent;
535
723
  const opencodeAuthPresent = opencode.status === "pass" && (deps.fs || require("fs")).existsSync(opencodeAuthPath(deps.env));
724
+ const codexAgent = runtimeAgent({
725
+ id: "codex-sandbox",
726
+ name: "Codex Sandbox",
727
+ runtime: "codex",
728
+ command: "codex",
729
+ probe: codex,
730
+ readyToServe: codexReadyToServe,
731
+ serveStatus: codexReadyToServe
732
+ ? "ready_to_serve"
733
+ : codex.status === "pass"
734
+ ? "needs_codex_auth"
735
+ : "not_installed",
736
+ requirements: [
737
+ {
738
+ name: "codex_cli",
739
+ status: codex.status,
740
+ command: "codex --version",
741
+ version: codex.version,
742
+ detail: codex.status === "pass"
743
+ ? "Codex CLI is installed locally"
744
+ : codex.on_path
745
+ ? "Codex CLI is on PATH but failed to run; repair the local Codex install before publishing a Codex-backed labor runtime"
746
+ : "Install Codex CLI before publishing a Codex-backed labor runtime",
747
+ next: codex.status === "pass"
748
+ ? null
749
+ : "Install or repair Codex CLI, then rerun `clawlabor labor-agents`.",
750
+ error: codex.error,
751
+ },
752
+ {
753
+ name: "codex_auth",
754
+ status: codexReadyToServe ? "pass" : "fail",
755
+ detail: codexReadyToServe
756
+ ? "Codex auth.json and config.toml found; labor-serve will mount them read-only into the sandbox"
757
+ : "Run `codex login` so labor-serve can pass your Codex credentials into the sandbox",
758
+ next: codexReadyToServe ? null : "Run `codex login`, then rerun `clawlabor labor-agents`.",
759
+ },
760
+ ],
761
+ publishName: "Codex Labor",
762
+ hostAccount: codexAccount,
763
+ hostPlan: codexAccount.plan,
764
+ });
536
765
  const agents = [
537
766
  claudeAgent,
538
- runtimeAgent({
539
- id: "codex-sandbox",
540
- name: "Codex Sandbox",
541
- runtime: "codex",
542
- command: "codex",
543
- probe: codex,
544
- readyToServe: false,
545
- serveStatus: codex.status === "pass"
546
- ? "candidate_not_wired_to_labor_serve"
547
- : "not_installed",
548
- requirements: [
549
- {
550
- name: "codex_cli",
551
- status: codex.status,
552
- command: "codex --version",
553
- version: codex.version,
554
- detail: codex.status === "pass"
555
- ? "Codex CLI is installed locally; Clawlabor labor-serve is not wired to start Codex-backed sandbox sessions yet"
556
- : codex.on_path
557
- ? "Codex CLI is on PATH but failed to run; repair the local Codex install before publishing a Codex-backed labor runtime"
558
- : "Install Codex CLI before publishing a Codex-backed labor runtime",
559
- error: codex.error,
560
- },
561
- ],
562
- publishName: "Codex Labor",
563
- }),
767
+ codexAgent,
564
768
  runtimeAgent({
565
769
  id: "opencode-sandbox",
566
770
  name: "OpenCode Sandbox",
@@ -580,20 +784,24 @@ async function commandLaborAgents(_options, deps, flags) {
580
784
  status: opencode.status,
581
785
  command: "opencode --version",
582
786
  version: opencode.version,
583
- detail: opencode.status === "pass"
584
- ? "OpenCode CLI is installed locally"
585
- : opencode.on_path
586
- ? "OpenCode CLI is on PATH but failed to run; repair the local OpenCode install before publishing an OpenCode-backed labor runtime"
587
- : "Install OpenCode CLI before publishing an OpenCode-backed labor runtime",
588
- error: opencode.error,
589
- },
787
+ detail: opencode.status === "pass"
788
+ ? "OpenCode CLI is installed locally"
789
+ : opencode.on_path
790
+ ? "OpenCode CLI is on PATH but failed to run; repair the local OpenCode install before publishing an OpenCode-backed labor runtime"
791
+ : "Install OpenCode CLI before publishing an OpenCode-backed labor runtime",
792
+ next: opencode.status === "pass"
793
+ ? null
794
+ : "Install or repair OpenCode CLI, then rerun `clawlabor labor-agents`.",
795
+ error: opencode.error,
796
+ },
590
797
  {
591
798
  name: "opencode_auth",
592
799
  status: opencodeAuthPresent ? "pass" : "fail",
593
- detail: opencodeAuthPresent
594
- ? "OpenCode auth.json found; labor-serve will mount it read-only into the sandbox"
595
- : "Run `opencode auth login` so labor-serve can pass your provider credentials into the sandbox",
596
- },
800
+ detail: opencodeAuthPresent
801
+ ? "OpenCode auth.json found; labor-serve will mount it read-only into the sandbox"
802
+ : "Run `opencode auth login` so labor-serve can pass your provider credentials into the sandbox",
803
+ next: opencodeAuthPresent ? null : "Run `opencode auth login`, then rerun `clawlabor labor-agents`.",
804
+ },
597
805
  ],
598
806
  publishName: "OpenCode Labor",
599
807
  }),
@@ -616,6 +824,7 @@ async function commandLaborAgents(_options, deps, flags) {
616
824
  account: compactMarketplaceAgent(marketplaceAgent),
617
825
  host: {
618
826
  claude: compactHostAccount(claudeAgent.host_account),
827
+ codex: compactHostAccount(codexAgent.host_account, "codex"),
619
828
  },
620
829
  agents: agents.map((agent) => summarizeLaborAgent(agent, existingLabor)),
621
830
  next_actions: [
@@ -740,8 +949,8 @@ async function commandLaborPublish(options, deps) {
740
949
  }
741
950
  const dailyTokenCap = tokenCountOption(options, "daily-token-cap");
742
951
  const runtime = options.runtime || "claude";
743
- if (!["claude", "opencode"].includes(runtime)) {
744
- throw new Error(`labor-publish supports --runtime claude or opencode; ${runtime} has no labor-serve support yet.`);
952
+ if (!["claude", "codex", "opencode"].includes(runtime)) {
953
+ throw new Error(`labor-publish supports --runtime claude, codex, or opencode; ${runtime} has no labor-serve support yet.`);
745
954
  }
746
955
  if (dailyTokenCap !== undefined && runtime !== "opencode") {
747
956
  throw new Error(
@@ -829,6 +1038,7 @@ async function commandLaborStart(options, deps) {
829
1038
  if (!laborId) {
830
1039
  const defaults = {
831
1040
  claude: { name: "Claude Code Labor", description: "Claude Code Labor backed by the local Claude Code Sandbox runtime." },
1041
+ codex: { name: "Codex Labor", description: "Codex Labor backed by the local Codex Sandbox runtime." },
832
1042
  opencode: { name: "OpenCode Labor", description: "OpenCode Labor backed by the local OpenCode Sandbox runtime." },
833
1043
  }[runtime] || { name: `${runtime} Labor`, description: `${runtime} Labor backed by the local sandbox runtime.` };
834
1044
  const publishOptions = {
@@ -1660,7 +1870,12 @@ module.exports = {
1660
1870
  commandLaborServe,
1661
1871
  commandLaborCleanup,
1662
1872
  parseSseChunks,
1873
+ codexAuthPath,
1874
+ codexConfigPath,
1875
+ codexHomePath,
1876
+ decodeJwtPayload,
1663
1877
  opencodeAuthPath,
1878
+ resolveCodexChatGptAccount,
1664
1879
  runtimeStateMounts,
1665
1880
  runtimeStateInitCommand,
1666
1881
  sandboxUserCommand,
@@ -0,0 +1,40 @@
1
+ function installerArgsFromFlags(flags) {
2
+ const args = [];
3
+ for (const flag of flags) {
4
+ args.push(`--${flag}`);
5
+ }
6
+ return args;
7
+ }
8
+
9
+ async function commandUpgrade(_options, deps, flags) {
10
+ const spawnSync = deps.spawnSync;
11
+ const install = spawnSync("npm", ["install", "-g", "clawlabor@latest"], {
12
+ encoding: "utf8",
13
+ stdio: "inherit",
14
+ });
15
+ if (install.status !== 0) {
16
+ throw new Error("Failed to upgrade ClawLabor with `npm install -g clawlabor@latest`");
17
+ }
18
+
19
+ const reinstallArgs = ["install", ...installerArgsFromFlags(flags)];
20
+ const reinstall = spawnSync("clawlabor", reinstallArgs, {
21
+ encoding: "utf8",
22
+ stdio: "inherit",
23
+ });
24
+ if (reinstall.status !== 0) {
25
+ return JSON.stringify({
26
+ action: "upgraded",
27
+ package: "clawlabor@latest",
28
+ skill_reinstall: "failed",
29
+ next: `Package upgrade succeeded. Refresh skill files manually with: clawlabor ${reinstallArgs.join(" ")}`,
30
+ });
31
+ }
32
+
33
+ return JSON.stringify({
34
+ action: "upgraded",
35
+ package: "clawlabor@latest",
36
+ skill_reinstall: "ok",
37
+ });
38
+ }
39
+
40
+ module.exports = { commandUpgrade };
@@ -42,6 +42,7 @@ const { commandStatus } = require("./command-status");
42
42
  const { commandUploadAttachment } = require("./command-upload-attachment");
43
43
  const { commandValidate } = require("./command-validate");
44
44
  const { commandWait } = require("./command-wait");
45
+ const { commandUpgrade } = require("./command-upgrade");
45
46
 
46
47
  module.exports = {
47
48
  ...shared,
@@ -79,6 +80,7 @@ module.exports = {
79
80
  commandSolve,
80
81
  commandStatus,
81
82
  commandUploadAttachment,
83
+ commandUpgrade,
82
84
  commandValidate,
83
85
  commandWait,
84
86
  commandHire,
@@ -38,7 +38,16 @@ function runtimeStateInitCommand(mounts, { excludePaths = [] } = {}) {
38
38
  }
39
39
 
40
40
  function sandboxUserCommand(command) {
41
- return `setpriv --reuid=sandbox --regid=sandbox --init-groups env HOME=/home/sandbox ${command}`;
41
+ const path = [
42
+ "/home/sandbox/.local/share/sandbox-clawlabor/bin",
43
+ "/usr/local/sbin",
44
+ "/usr/local/bin",
45
+ "/usr/sbin",
46
+ "/usr/bin",
47
+ "/sbin",
48
+ "/bin",
49
+ ].join(":");
50
+ return `setpriv --reuid=sandbox --regid=sandbox --init-groups env HOME=/home/sandbox PATH=${shellQuote(path)} ${command}`;
42
51
  }
43
52
 
44
53
  function dockerContainerRunning(name, deps = {}) {