@fulldivevr/workcord-cli 3.0.2 → 3.0.4
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/dist/commands/machine.js +53 -7
- package/dist/machine/adapters/codex.js +64 -6
- package/dist/machine/agents.js +38 -10
- package/dist/machine/policy.js +2 -2
- package/package.json +1 -1
package/dist/commands/machine.js
CHANGED
|
@@ -7,8 +7,8 @@ import { call, getBaseUrl } from "../client.js";
|
|
|
7
7
|
import { deleteMachineFile, getMachineLockPath, getMachinePath, getMachineThreadsPath, readMachineFile, readOrCreateInstallationId, saveMachineFile, } from "../config.js";
|
|
8
8
|
import { normalizeBaseUrl } from "../http.js";
|
|
9
9
|
import { runClaude } from "../machine/adapters/claude.js";
|
|
10
|
-
import { runCodex } from "../machine/adapters/codex.js";
|
|
11
|
-
import { ADAPTER_NAMES, detectAdapters } from "../machine/agents.js";
|
|
10
|
+
import { binaryVersion, probeCodex, runCodex } from "../machine/adapters/codex.js";
|
|
11
|
+
import { ADAPTER_NAMES, childEnv, codexCandidates, detectAdapters } from "../machine/agents.js";
|
|
12
12
|
import { handleTask, TaskContexts } from "../machine/handler.js";
|
|
13
13
|
import { pairComputer } from "../machine/pair.js";
|
|
14
14
|
import { nextRotationDelay, rotateMachineKey, ROTATE_EVERY_MS } from "../machine/rotate.js";
|
|
@@ -78,15 +78,18 @@ export function registerMachine(program) {
|
|
|
78
78
|
group
|
|
79
79
|
.command("start")
|
|
80
80
|
.description("Connect this computer and run your tasks on it until stopped (foreground). Tasks run with full access, without approvals.")
|
|
81
|
-
.option("--root <dir>", "directory a task may work in (repeatable
|
|
81
|
+
.option("--root <dir>", "directory a task may work in (repeatable, default: the current directory)", collect, [])
|
|
82
82
|
.option("--adapter <name>", "coding agent that runs tasks: auto, claude or codex", "auto")
|
|
83
83
|
.option("--home <dir>", "home directory the coding agent runs with (HOME, CODEX_HOME=<dir>/.codex, CLAUDE_CONFIG_DIR=<dir>/.claude)")
|
|
84
84
|
.action(async (opts) => {
|
|
85
85
|
const credential = resolveMachineCredential(process.env, await readMachineFile());
|
|
86
86
|
const roots = await resolveRoots(opts.root);
|
|
87
87
|
const home = await resolveHome(opts.home);
|
|
88
|
-
const
|
|
89
|
-
|
|
88
|
+
const detected = await detectAdapters();
|
|
89
|
+
pickAdapters(detected, opts.adapter, home);
|
|
90
|
+
// checkAdapters already narrowed the set to the chosen adapter.
|
|
91
|
+
const usable = await checkAdapters(detected, opts.adapter, home, undefined, process.env, await codexCandidates());
|
|
92
|
+
const adapters = pickAdapters(usable, "auto", home);
|
|
90
93
|
const baseUrl = credential.baseUrl || (await getBaseUrl());
|
|
91
94
|
const installationId = await readOrCreateInstallationId();
|
|
92
95
|
const release = await acquireMachineLock(getMachineLockPath());
|
|
@@ -105,7 +108,7 @@ export function registerMachine(program) {
|
|
|
105
108
|
...(credential.source === "file" ? { reloadKey: () => savedKeyFor(baseUrl) } : {}),
|
|
106
109
|
});
|
|
107
110
|
const rotation = credential.source === "file" ? scheduleRotation(client) : undefined;
|
|
108
|
-
process.stderr.write(`machine: full access, agents: ${Object.keys(adapters).join(", ") || "none"}, roots: ${roots.
|
|
111
|
+
process.stderr.write(`machine: full access, agents: ${Object.keys(adapters).join(", ") || "none"}, roots: ${roots.join(", ")}\n`);
|
|
109
112
|
process.once("SIGINT", () => client.stop());
|
|
110
113
|
process.once("SIGTERM", () => client.stop());
|
|
111
114
|
const outcome = await client.start().finally(() => {
|
|
@@ -119,7 +122,7 @@ export function registerMachine(program) {
|
|
|
119
122
|
group
|
|
120
123
|
.command("install-service")
|
|
121
124
|
.description("Keep `machine start` running in the background (launchd on macOS, systemd on Linux).")
|
|
122
|
-
.option("--root <dir>", "directory a task may work in (repeatable)", collect, [])
|
|
125
|
+
.option("--root <dir>", "directory a task may work in (repeatable, default: the current directory)", collect, [])
|
|
123
126
|
.option("--adapter <name>", "coding agent that runs tasks: auto, claude or codex", "auto")
|
|
124
127
|
.option("--home <dir>", "home directory the coding agent runs with (HOME, CODEX_HOME=<dir>/.codex, CLAUDE_CONFIG_DIR=<dir>/.claude)")
|
|
125
128
|
.action(async (opts) => {
|
|
@@ -180,6 +183,49 @@ function collect(value, previous) {
|
|
|
180
183
|
return [...previous, value];
|
|
181
184
|
}
|
|
182
185
|
const RUNNERS = { claude: runClaude, codex: runCodex };
|
|
186
|
+
/**
|
|
187
|
+
* Prints the binary each adapter uses. Codex runs from the first candidate that
|
|
188
|
+
* answers `app-server`; an installed Codex where none does fails the start, since
|
|
189
|
+
* a legacy Codex CLI cannot run tasks at all.
|
|
190
|
+
*/
|
|
191
|
+
export async function checkAdapters(installed, choice, home, log = (line) => process.stderr.write(line + "\n"), env = process.env, codexCandidates = installed.codex ? [installed.codex] : []) {
|
|
192
|
+
const usable = {};
|
|
193
|
+
for (const [name, binary] of Object.entries(installed)) {
|
|
194
|
+
if (!binary || (choice !== "auto" && choice !== name))
|
|
195
|
+
continue;
|
|
196
|
+
if (name === "codex") {
|
|
197
|
+
const failed = [];
|
|
198
|
+
for (const candidate of codexCandidates) {
|
|
199
|
+
const probe = await probeCodex(candidate, env, home);
|
|
200
|
+
if (probe.ok) {
|
|
201
|
+
usable.codex = candidate;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
failed.push({ ...probe, binary: candidate });
|
|
205
|
+
}
|
|
206
|
+
if (!usable.codex) {
|
|
207
|
+
for (const probe of failed) {
|
|
208
|
+
log(`ERROR: ${probe.message}`);
|
|
209
|
+
if (probe.stderr)
|
|
210
|
+
log(`machine: codex said: ${probe.stderr.replace(/\s+/g, " ").slice(-300)}`);
|
|
211
|
+
}
|
|
212
|
+
throw new Error("codex is installed but none of its copies can run tasks, see the ERROR lines above.");
|
|
213
|
+
}
|
|
214
|
+
for (const probe of failed) {
|
|
215
|
+
log(`machine: warning: skipped codex at ${probe.binary} (version ${probe.version}), it cannot run "codex app-server"`);
|
|
216
|
+
}
|
|
217
|
+
log(`machine: codex at ${usable.codex}`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (name === "claude" && (await binaryVersion(binary, childEnv(env, home))) === "unknown") {
|
|
221
|
+
log(`ERROR: claude at ${binary} does not answer "claude --version". Reinstall it: npm i -g @anthropic-ai/claude-code@latest`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
log(`machine: ${name} at ${binary}`);
|
|
225
|
+
usable[name] = binary;
|
|
226
|
+
}
|
|
227
|
+
return usable;
|
|
228
|
+
}
|
|
183
229
|
export function pickAdapters(installed, choice, home) {
|
|
184
230
|
const names = choice === "auto" ? ADAPTER_NAMES.filter((name) => installed[name]) : [choice];
|
|
185
231
|
const picked = {};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { childEnv } from "../agents.js";
|
|
4
4
|
const TASK_TIMEOUT_MS = 20 * 60_000;
|
|
5
5
|
const INTERRUPT_GRACE_MS = 5_000;
|
|
6
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
7
|
+
const VERSION_TIMEOUT_MS = 5_000;
|
|
8
|
+
const INITIALIZE_PARAMS = { clientInfo: { name: "workcord", title: "WorkCord machine", version: "1" }, capabilities: null };
|
|
6
9
|
const TOOL_ITEMS = new Set([
|
|
7
10
|
"commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall", "collabAgentToolCall",
|
|
8
11
|
"subAgentActivity", "webSearch", "imageView", "imageGeneration", "sleep",
|
|
@@ -39,15 +42,65 @@ export function codexItemEvent(method, item) {
|
|
|
39
42
|
...(typeof item.namespace === "string" ? { tool_namespace: item.namespace } : {}),
|
|
40
43
|
};
|
|
41
44
|
}
|
|
45
|
+
/** `<binary> --version`, or "unknown" when it does not answer. */
|
|
46
|
+
export function binaryVersion(binary, env) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
execFile(binary, ["--version"], { env, timeout: VERSION_TIMEOUT_MS }, (error, stdout) => {
|
|
49
|
+
const text = String(stdout || "").trim().split("\n")[0]?.trim();
|
|
50
|
+
resolve(error || !text ? "unknown" : text);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function codexUnusableMessage(binary, version) {
|
|
55
|
+
return `codex at ${binary} (version ${version}) cannot run "codex app-server". Update it: npm i -g @openai/codex@latest, or install the Codex desktop app (it bundles a current codex)`;
|
|
56
|
+
}
|
|
57
|
+
/** Starts `codex app-server` and accepts it once it answers `initialize`; a legacy Codex CLI never does. */
|
|
58
|
+
export async function probeCodex(binary, env, home, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
59
|
+
const spawnEnv = childEnv(env, home);
|
|
60
|
+
const stderr = await new Promise((resolve) => {
|
|
61
|
+
const child = spawn(binary, ["app-server"], { env: spawnEnv, stdio: ["pipe", "pipe", "pipe"] });
|
|
62
|
+
child.stdout.setEncoding("utf8");
|
|
63
|
+
child.stderr.setEncoding("utf8");
|
|
64
|
+
let tail = "";
|
|
65
|
+
child.stderr.on("data", (chunk) => {
|
|
66
|
+
tail = (tail + chunk).slice(-2_000);
|
|
67
|
+
});
|
|
68
|
+
const finish = (result) => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
child.stdin.destroy();
|
|
71
|
+
child.kill();
|
|
72
|
+
resolve(result);
|
|
73
|
+
};
|
|
74
|
+
const timer = setTimeout(() => finish(tail || `no answer to initialize within ${timeoutMs / 1000}s`), timeoutMs);
|
|
75
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
76
|
+
try {
|
|
77
|
+
const message = JSON.parse(line);
|
|
78
|
+
if (message?.id === 1 && message.result !== undefined)
|
|
79
|
+
finish(undefined);
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
});
|
|
83
|
+
child.on("error", (error) => finish(error.message));
|
|
84
|
+
child.on("close", (code) => finish(tail || `exited with ${code}`));
|
|
85
|
+
child.stdin.on("error", () => undefined);
|
|
86
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: INITIALIZE_PARAMS }) + "\n");
|
|
87
|
+
});
|
|
88
|
+
if (stderr === undefined)
|
|
89
|
+
return { ok: true };
|
|
90
|
+
const version = await binaryVersion(binary, spawnEnv);
|
|
91
|
+
return { ok: false, version, stderr: stderr.trim(), message: codexUnusableMessage(binary, version) };
|
|
92
|
+
}
|
|
42
93
|
/**
|
|
43
94
|
* One `codex app-server` per task, spoken to over stdio JSON-RPC as the Codex
|
|
44
95
|
* desktop and mobile clients do: initialize, thread/resume (or thread/start
|
|
45
96
|
* when there is no thread to resume), turn/start, and turn/interrupt for a cancel.
|
|
46
97
|
*/
|
|
47
98
|
export async function runCodex(input) {
|
|
48
|
-
const
|
|
99
|
+
const binary = input.binary ?? "codex";
|
|
100
|
+
const env = childEnv(input.env, input.home);
|
|
101
|
+
const child = spawn(binary, ["app-server"], {
|
|
49
102
|
cwd: input.cwd,
|
|
50
|
-
env
|
|
103
|
+
env,
|
|
51
104
|
stdio: ["pipe", "pipe", "pipe"],
|
|
52
105
|
});
|
|
53
106
|
child.stdout.setEncoding("utf8");
|
|
@@ -59,6 +112,7 @@ export async function runCodex(input) {
|
|
|
59
112
|
});
|
|
60
113
|
let nextId = 0;
|
|
61
114
|
const pending = new Map();
|
|
115
|
+
let initialized = false;
|
|
62
116
|
let threadId = "";
|
|
63
117
|
let turnId = "";
|
|
64
118
|
let finalText = "";
|
|
@@ -131,8 +185,11 @@ export async function runCodex(input) {
|
|
|
131
185
|
settle.reject(new Error(error.code === "ENOENT" ? "Codex is not installed on this machine (no `codex` on PATH)." : error.message));
|
|
132
186
|
});
|
|
133
187
|
// "close" fires after stdout and stderr have ended, so the stderr tail is complete here.
|
|
134
|
-
child.on("close", (code) => {
|
|
135
|
-
|
|
188
|
+
child.on("close", async (code) => {
|
|
189
|
+
let text = `Codex exited with ${code}: ${stderr.trim().slice(0, 500)}`;
|
|
190
|
+
if (!initialized)
|
|
191
|
+
text += `\n${codexUnusableMessage(binary, await binaryVersion(binary, env))}`;
|
|
192
|
+
const exited = new Error(text);
|
|
136
193
|
for (const waiter of pending.values())
|
|
137
194
|
waiter.reject(exited);
|
|
138
195
|
pending.clear();
|
|
@@ -148,7 +205,8 @@ export async function runCodex(input) {
|
|
|
148
205
|
};
|
|
149
206
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
150
207
|
try {
|
|
151
|
-
await request("initialize",
|
|
208
|
+
await request("initialize", INITIALIZE_PARAMS);
|
|
209
|
+
initialized = true;
|
|
152
210
|
write({ method: "initialized" });
|
|
153
211
|
const params = codexThreadParams(input.cwd);
|
|
154
212
|
const resumed = input.resume
|
package/dist/machine/agents.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { access, constants } from "node:fs/promises";
|
|
1
|
+
import { access, constants, realpath } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { delimiter, join } from "node:path";
|
|
3
4
|
export const ADAPTER_NAMES = ["claude", "codex"];
|
|
4
5
|
/**
|
|
@@ -12,19 +13,46 @@ export function childEnv(env = process.env, home) {
|
|
|
12
13
|
return inherited;
|
|
13
14
|
return { ...inherited, HOME: home, CODEX_HOME: join(home, ".codex"), CLAUDE_CONFIG_DIR: join(home, ".claude") };
|
|
14
15
|
}
|
|
16
|
+
/** Codex desktop bundles a current codex that is not on PATH. */
|
|
17
|
+
export function codexDesktopPaths(platform = process.platform, home = homedir()) {
|
|
18
|
+
if (platform !== "darwin")
|
|
19
|
+
return [];
|
|
20
|
+
return ["/Applications", join(home, "Applications")].flatMap((dir) => [
|
|
21
|
+
join(dir, "ChatGPT.app/Contents/Resources/codex-cli/bin/codex"),
|
|
22
|
+
join(dir, "Codex.app/Contents/Resources/codex"),
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
const isExecutable = (path) => access(path, constants.X_OK).then(() => true, () => false);
|
|
26
|
+
function onPath(name, env) {
|
|
27
|
+
return String(env.PATH || "").split(delimiter).filter(Boolean).map((dir) => join(dir, name));
|
|
28
|
+
}
|
|
29
|
+
/** Every installed codex, PATH order first, then the desktop bundles, one per real file. */
|
|
30
|
+
export async function codexCandidates(env = process.env, desktop = codexDesktopPaths()) {
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const found = [];
|
|
33
|
+
for (const candidate of [...onPath("codex", env), ...desktop]) {
|
|
34
|
+
if (!(await isExecutable(candidate)))
|
|
35
|
+
continue;
|
|
36
|
+
const real = await realpath(candidate).catch(() => candidate);
|
|
37
|
+
if (seen.has(real))
|
|
38
|
+
continue;
|
|
39
|
+
seen.add(real);
|
|
40
|
+
found.push(candidate);
|
|
41
|
+
}
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
15
44
|
/** Absolute path of each installed agent CLI, found the way a shell would find it. */
|
|
16
|
-
export async function detectAdapters(env = process.env) {
|
|
45
|
+
export async function detectAdapters(env = process.env, codexDesktop = codexDesktopPaths()) {
|
|
17
46
|
const found = {};
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (await access(candidate, constants.X_OK).then(() => true, () => false)) {
|
|
23
|
-
found[name] = candidate;
|
|
24
|
-
break;
|
|
25
|
-
}
|
|
47
|
+
for (const candidate of onPath("claude", env)) {
|
|
48
|
+
if (await isExecutable(candidate)) {
|
|
49
|
+
found.claude = candidate;
|
|
50
|
+
break;
|
|
26
51
|
}
|
|
27
52
|
}
|
|
53
|
+
const [codex] = await codexCandidates(env, codexDesktop);
|
|
54
|
+
if (codex)
|
|
55
|
+
found.codex = codex;
|
|
28
56
|
return found;
|
|
29
57
|
}
|
|
30
58
|
//# sourceMappingURL=agents.js.map
|
package/dist/machine/policy.js
CHANGED
|
@@ -12,8 +12,8 @@ export async function resolveHome(dir) {
|
|
|
12
12
|
throw new Error(`--home ${absolute} is not a directory.`);
|
|
13
13
|
return absolute;
|
|
14
14
|
}
|
|
15
|
-
export async function resolveRoots(roots) {
|
|
16
|
-
return Promise.all(roots.map((root) => realpath(root)));
|
|
15
|
+
export async function resolveRoots(roots, cwd = process.cwd()) {
|
|
16
|
+
return Promise.all((roots.length ? roots : [cwd]).map((root) => realpath(root)));
|
|
17
17
|
}
|
|
18
18
|
/** `roots` are already realpath-resolved, once at start. */
|
|
19
19
|
export async function resolveCwd(roots, requested) {
|