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 +4 -3
- package/README.md +20 -11
- package/SKILL.md +6 -4
- package/bin/install.js +22 -16
- package/package.json +1 -1
- package/runtime/claude_auth.js +130 -0
- package/runtime/cli.js +84 -0
- package/runtime/commands/command-labor.js +259 -44
- package/runtime/commands/command-upgrade.js +40 -0
- package/runtime/commands/core.js +2 -0
- package/runtime/commands/labor-sandbox.js +10 -1
package/QUICKSTART.md
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
## 0. Prerequisites
|
|
6
6
|
|
|
7
|
-
- Node 20+
|
|
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
|
|
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
|
-
|
|
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
|
|
169
|
-
|
|
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
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
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.
|
|
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
|
|
108
|
+
2. Install the CLI globally, then link the skill into every detected supported runtime:
|
|
109
109
|
```bash
|
|
110
|
-
|
|
110
|
+
npm i -g clawlabor@latest
|
|
111
|
+
clawlabor install
|
|
111
112
|
```
|
|
112
|
-
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
package/runtime/claude_auth.js
CHANGED
|
@@ -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:
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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 };
|
package/runtime/commands/core.js
CHANGED
|
@@ -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
|
-
|
|
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 = {}) {
|