@fulldivevr/workcord-cli 3.0.2 → 3.0.3
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
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, 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,17 @@ 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 adapters = pickAdapters(await checkAdapters(detected, opts.adapter, home), "auto", home);
|
|
90
92
|
const baseUrl = credential.baseUrl || (await getBaseUrl());
|
|
91
93
|
const installationId = await readOrCreateInstallationId();
|
|
92
94
|
const release = await acquireMachineLock(getMachineLockPath());
|
|
@@ -105,7 +107,7 @@ export function registerMachine(program) {
|
|
|
105
107
|
...(credential.source === "file" ? { reloadKey: () => savedKeyFor(baseUrl) } : {}),
|
|
106
108
|
});
|
|
107
109
|
const rotation = credential.source === "file" ? scheduleRotation(client) : undefined;
|
|
108
|
-
process.stderr.write(`machine: full access, agents: ${Object.keys(adapters).join(", ") || "none"}, roots: ${roots.
|
|
110
|
+
process.stderr.write(`machine: full access, agents: ${Object.keys(adapters).join(", ") || "none"}, roots: ${roots.join(", ")}\n`);
|
|
109
111
|
process.once("SIGINT", () => client.stop());
|
|
110
112
|
process.once("SIGTERM", () => client.stop());
|
|
111
113
|
const outcome = await client.start().finally(() => {
|
|
@@ -119,7 +121,7 @@ export function registerMachine(program) {
|
|
|
119
121
|
group
|
|
120
122
|
.command("install-service")
|
|
121
123
|
.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, [])
|
|
124
|
+
.option("--root <dir>", "directory a task may work in (repeatable, default: the current directory)", collect, [])
|
|
123
125
|
.option("--adapter <name>", "coding agent that runs tasks: auto, claude or codex", "auto")
|
|
124
126
|
.option("--home <dir>", "home directory the coding agent runs with (HOME, CODEX_HOME=<dir>/.codex, CLAUDE_CONFIG_DIR=<dir>/.claude)")
|
|
125
127
|
.action(async (opts) => {
|
|
@@ -180,6 +182,33 @@ function collect(value, previous) {
|
|
|
180
182
|
return [...previous, value];
|
|
181
183
|
}
|
|
182
184
|
const RUNNERS = { claude: runClaude, codex: runCodex };
|
|
185
|
+
/**
|
|
186
|
+
* Prints the binary each adapter uses and drops one that cannot run tasks, so it is
|
|
187
|
+
* never advertised to the server. A Codex older than `app-server` exits at once.
|
|
188
|
+
*/
|
|
189
|
+
export async function checkAdapters(installed, choice, home, log = (line) => process.stderr.write(line + "\n"), env = process.env) {
|
|
190
|
+
const usable = {};
|
|
191
|
+
for (const [name, binary] of Object.entries(installed)) {
|
|
192
|
+
if (!binary || (choice !== "auto" && choice !== name))
|
|
193
|
+
continue;
|
|
194
|
+
if (name === "codex") {
|
|
195
|
+
const probe = await probeCodex(binary, env, home);
|
|
196
|
+
if (!probe.ok) {
|
|
197
|
+
log(`ERROR: ${probe.message}`);
|
|
198
|
+
if (probe.stderr)
|
|
199
|
+
log(`machine: codex said: ${probe.stderr.replace(/\s+/g, " ").slice(-300)}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (name === "claude" && (await binaryVersion(binary, childEnv(env, home))) === "unknown") {
|
|
204
|
+
log(`ERROR: claude at ${binary} does not answer "claude --version". Reinstall it: npm i -g @anthropic-ai/claude-code@latest`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
log(`machine: ${name} at ${binary}`);
|
|
208
|
+
usable[name] = binary;
|
|
209
|
+
}
|
|
210
|
+
return usable;
|
|
211
|
+
}
|
|
183
212
|
export function pickAdapters(installed, choice, home) {
|
|
184
213
|
const names = choice === "auto" ? ADAPTER_NAMES.filter((name) => installed[name]) : [choice];
|
|
185
214
|
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`;
|
|
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/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) {
|