@fulldivevr/workcord-cli 3.0.0 → 3.0.1
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/README.md +3 -3
- package/dist/commands/machine.js +22 -8
- package/dist/machine/adapters/claude.js +1 -1
- package/dist/machine/adapters/codex.js +5 -3
- package/dist/machine/agents.js +10 -2
- package/dist/machine/policy.js +14 -2
- package/dist/machine/relay.js +10 -1
- package/dist/machine/service.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,11 +91,11 @@ Email login returns `accessToken`; phone/social login returns `token`. The CLI
|
|
|
91
91
|
emits both fields on either path so callers can read one consistently — either
|
|
92
92
|
value works as `WORKCORD_TOKEN`.
|
|
93
93
|
|
|
94
|
-
Email auth is
|
|
94
|
+
Email auth is allowlist-restricted. WorkCord prod and VIA both allow
|
|
95
95
|
`fulldive.com`, `via.com`, `via.com.tw`, `viatech.com.cn`, `*.tvbs.com`,
|
|
96
96
|
`tvbs.*.com`, `tvbs.com.*`, and `*.tvbs.com.tw` (`EMAIL_AUTH_ALLOWED_DOMAINS` in
|
|
97
|
-
`.sc/stacks/wize-rooms-api/client.yaml`).
|
|
98
|
-
`
|
|
97
|
+
`.sc/stacks/wize-rooms-api/client.yaml`). WorkCord prod also allows individually
|
|
98
|
+
approved addresses from `email_auth_allowlist`.
|
|
99
99
|
|
|
100
100
|
`auth login --phone` is for local and RoomCord only. Deployed WorkCord does not
|
|
101
101
|
set `AUTH_PHONE_OTP_DEV_BYPASS`, so `POST /auth/send-otp` returns 503
|
package/dist/commands/machine.js
CHANGED
|
@@ -14,7 +14,7 @@ import { pairComputer } from "../machine/pair.js";
|
|
|
14
14
|
import { nextRotationDelay, rotateMachineKey, ROTATE_EVERY_MS } from "../machine/rotate.js";
|
|
15
15
|
import { installService, serviceArgs, servicePaths, uninstallService } from "../machine/service.js";
|
|
16
16
|
import { acquireMachineLock } from "../machine/lock.js";
|
|
17
|
-
import { resolveRoots } from "../machine/policy.js";
|
|
17
|
+
import { resolveHome, resolveRoots } from "../machine/policy.js";
|
|
18
18
|
import { ConversationThreads } from "../machine/threads.js";
|
|
19
19
|
import { MachineRelayClient } from "../machine/relay.js";
|
|
20
20
|
import { getOrCreateGroup } from "./_group.js";
|
|
@@ -44,7 +44,12 @@ export function registerMachine(program) {
|
|
|
44
44
|
const enrolled = (await call({
|
|
45
45
|
method: "POST",
|
|
46
46
|
path: "/api/v1/machine-agents",
|
|
47
|
-
body: {
|
|
47
|
+
body: {
|
|
48
|
+
name: opts.name,
|
|
49
|
+
platform: `${process.platform}-${process.arch}`,
|
|
50
|
+
cliVersion: version,
|
|
51
|
+
installationId: await readOrCreateInstallationId(),
|
|
52
|
+
},
|
|
48
53
|
asUserId: actingUserId(program),
|
|
49
54
|
}));
|
|
50
55
|
await saveMachineFile({
|
|
@@ -75,11 +80,13 @@ export function registerMachine(program) {
|
|
|
75
80
|
.description("Connect this computer and run your tasks on it until stopped (foreground). Tasks run with full access, without approvals.")
|
|
76
81
|
.option("--root <dir>", "directory a task may work in (repeatable); without one, only session search runs", collect, [])
|
|
77
82
|
.option("--adapter <name>", "coding agent that runs tasks: auto, claude or codex", "auto")
|
|
83
|
+
.option("--home <dir>", "home directory the coding agent runs with (HOME, CODEX_HOME=<dir>/.codex, CLAUDE_CONFIG_DIR=<dir>/.claude)")
|
|
78
84
|
.action(async (opts) => {
|
|
79
85
|
const credential = resolveMachineCredential(process.env, await readMachineFile());
|
|
80
86
|
const roots = await resolveRoots(opts.root);
|
|
87
|
+
const home = await resolveHome(opts.home);
|
|
81
88
|
const installed = await detectAdapters();
|
|
82
|
-
const adapters = pickAdapters(installed, opts.adapter);
|
|
89
|
+
const adapters = pickAdapters(installed, opts.adapter, home);
|
|
83
90
|
const baseUrl = credential.baseUrl || (await getBaseUrl());
|
|
84
91
|
const installationId = await readOrCreateInstallationId();
|
|
85
92
|
const release = await acquireMachineLock(getMachineLockPath());
|
|
@@ -94,7 +101,8 @@ export function registerMachine(program) {
|
|
|
94
101
|
handle: (rpc, signal, onEvent) => handleTask(rpc, {
|
|
95
102
|
roots, adapters, adapter: Object.keys(adapters)[0], contexts, threads, signal, onEvent,
|
|
96
103
|
}),
|
|
97
|
-
probe: () => probeMachine(baseUrl,
|
|
104
|
+
probe: (key) => probeMachine(baseUrl, key, installationId),
|
|
105
|
+
...(credential.source === "file" ? { reloadKey: () => savedKeyFor(baseUrl) } : {}),
|
|
98
106
|
});
|
|
99
107
|
const rotation = credential.source === "file" ? scheduleRotation(client) : undefined;
|
|
100
108
|
process.stderr.write(`machine: full access, agents: ${Object.keys(adapters).join(", ") || "none"}, roots: ${roots.length ? roots.join(", ") : "none (session search only)"}\n`);
|
|
@@ -113,17 +121,19 @@ export function registerMachine(program) {
|
|
|
113
121
|
.description("Keep `machine start` running in the background (launchd on macOS, systemd on Linux).")
|
|
114
122
|
.option("--root <dir>", "directory a task may work in (repeatable)", collect, [])
|
|
115
123
|
.option("--adapter <name>", "coding agent that runs tasks: auto, claude or codex", "auto")
|
|
124
|
+
.option("--home <dir>", "home directory the coding agent runs with (HOME, CODEX_HOME=<dir>/.codex, CLAUDE_CONFIG_DIR=<dir>/.claude)")
|
|
116
125
|
.action(async (opts) => {
|
|
117
126
|
await readMachineFile().then((file) => resolveMachineCredential({}, file));
|
|
118
127
|
const roots = await resolveRoots(opts.root);
|
|
128
|
+
const home = await resolveHome(opts.home);
|
|
119
129
|
const installed = await detectAdapters();
|
|
120
|
-
pickAdapters(installed, opts.adapter);
|
|
130
|
+
pickAdapters(installed, opts.adapter, home);
|
|
121
131
|
const paths = servicePaths();
|
|
122
132
|
const pathDirs = [...new Set([dirname(process.execPath), ...Object.values(installed).map((path) => dirname(path)), "/usr/bin", "/bin"])];
|
|
123
133
|
const installedService = await installService({
|
|
124
134
|
node: process.execPath,
|
|
125
135
|
cli: await realpath(process.argv[1]),
|
|
126
|
-
args: serviceArgs({ roots, adapter: opts.adapter }),
|
|
136
|
+
args: serviceArgs({ roots, adapter: opts.adapter, home }),
|
|
127
137
|
pathDirs,
|
|
128
138
|
logPath: paths.log,
|
|
129
139
|
home: homedir(),
|
|
@@ -170,7 +180,7 @@ function collect(value, previous) {
|
|
|
170
180
|
return [...previous, value];
|
|
171
181
|
}
|
|
172
182
|
const RUNNERS = { claude: runClaude, codex: runCodex };
|
|
173
|
-
export function pickAdapters(installed, choice) {
|
|
183
|
+
export function pickAdapters(installed, choice, home) {
|
|
174
184
|
const names = choice === "auto" ? ADAPTER_NAMES.filter((name) => installed[name]) : [choice];
|
|
175
185
|
const picked = {};
|
|
176
186
|
for (const name of names) {
|
|
@@ -179,10 +189,14 @@ export function pickAdapters(installed, choice) {
|
|
|
179
189
|
const binary = installed[name];
|
|
180
190
|
if (!binary)
|
|
181
191
|
throw new Error(`${name} is not installed on this computer (not on PATH).`);
|
|
182
|
-
picked[name] = (input) => RUNNERS[name]({ ...input, binary });
|
|
192
|
+
picked[name] = (input) => RUNNERS[name]({ ...input, binary, ...(home ? { home } : {}) });
|
|
183
193
|
}
|
|
184
194
|
return picked;
|
|
185
195
|
}
|
|
196
|
+
async function savedKeyFor(baseUrl) {
|
|
197
|
+
const file = await readMachineFile();
|
|
198
|
+
return file && normalizeBaseUrl(file.baseUrl || baseUrl) === normalizeBaseUrl(baseUrl) ? file.key : undefined;
|
|
199
|
+
}
|
|
186
200
|
function scheduleRotation(client) {
|
|
187
201
|
const handle = { timer: undefined };
|
|
188
202
|
const run = async () => {
|
|
@@ -62,7 +62,7 @@ function runOnce(input, signal, attempt) {
|
|
|
62
62
|
cwd: input.cwd,
|
|
63
63
|
// One process per turn: a background command would outlive the turn and its answer.
|
|
64
64
|
// This also lifts Claude Code's block on a long foreground `sleep`, which offers only the background.
|
|
65
|
-
env: { ...childEnv(input.env), CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" },
|
|
65
|
+
env: { ...childEnv(input.env, input.home), CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" },
|
|
66
66
|
stdio: ["ignore", "pipe", "pipe"],
|
|
67
67
|
});
|
|
68
68
|
const onAbort = () => stopTree(child);
|
|
@@ -47,7 +47,7 @@ export function codexItemEvent(method, item) {
|
|
|
47
47
|
export async function runCodex(input) {
|
|
48
48
|
const child = spawn(input.binary ?? "codex", ["app-server"], {
|
|
49
49
|
cwd: input.cwd,
|
|
50
|
-
env: childEnv(input.env),
|
|
50
|
+
env: childEnv(input.env, input.home),
|
|
51
51
|
stdio: ["pipe", "pipe", "pipe"],
|
|
52
52
|
});
|
|
53
53
|
child.stdout.setEncoding("utf8");
|
|
@@ -122,11 +122,13 @@ export async function runCodex(input) {
|
|
|
122
122
|
child.on("error", (error) => {
|
|
123
123
|
settle.reject(new Error(error.code === "ENOENT" ? "Codex is not installed on this machine (no `codex` on PATH)." : error.message));
|
|
124
124
|
});
|
|
125
|
+
// "close" fires after stdout and stderr have ended, so the stderr tail is complete here.
|
|
125
126
|
child.on("close", (code) => {
|
|
127
|
+
const exited = new Error(`Codex exited with ${code}: ${stderr.trim().slice(0, 500)}`);
|
|
126
128
|
for (const waiter of pending.values())
|
|
127
|
-
waiter.reject(
|
|
129
|
+
waiter.reject(exited);
|
|
128
130
|
pending.clear();
|
|
129
|
-
settle.reject(
|
|
131
|
+
settle.reject(exited);
|
|
130
132
|
});
|
|
131
133
|
const signal = AbortSignal.any([input.signal, AbortSignal.timeout(TASK_TIMEOUT_MS)]);
|
|
132
134
|
const onAbort = () => {
|
package/dist/machine/agents.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { access, constants } from "node:fs/promises";
|
|
2
2
|
import { delimiter, join } from "node:path";
|
|
3
3
|
export const ADAPTER_NAMES = ["claude", "codex"];
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The env every adapter spawns its CLI with. With a home, each tool's own config
|
|
6
|
+
* variable is pinned under it too, so one inherited from the parent shell cannot
|
|
7
|
+
* point the tool at another home.
|
|
8
|
+
*/
|
|
9
|
+
export function childEnv(env = process.env, home) {
|
|
10
|
+
const inherited = Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith("WORKCORD_")));
|
|
11
|
+
if (!home)
|
|
12
|
+
return inherited;
|
|
13
|
+
return { ...inherited, HOME: home, CODEX_HOME: join(home, ".codex"), CLAUDE_CONFIG_DIR: join(home, ".claude") };
|
|
6
14
|
}
|
|
7
15
|
/** Absolute path of each installed agent CLI, found the way a shell would find it. */
|
|
8
16
|
export async function detectAdapters(env = process.env) {
|
package/dist/machine/policy.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import { realpath } from "node:fs/promises";
|
|
2
|
-
import {
|
|
1
|
+
import { realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
|
+
export async function resolveHome(dir) {
|
|
5
|
+
if (!dir)
|
|
6
|
+
return undefined;
|
|
7
|
+
const absolute = resolve(dir === "~" ? homedir() : dir.startsWith("~/") ? join(homedir(), dir.slice(2)) : dir);
|
|
8
|
+
const found = await stat(absolute).catch(() => undefined);
|
|
9
|
+
if (!found)
|
|
10
|
+
throw new Error(`--home ${absolute} does not exist.`);
|
|
11
|
+
if (!found.isDirectory())
|
|
12
|
+
throw new Error(`--home ${absolute} is not a directory.`);
|
|
13
|
+
return absolute;
|
|
14
|
+
}
|
|
3
15
|
export async function resolveRoots(roots) {
|
|
4
16
|
return Promise.all(roots.map((root) => realpath(root)));
|
|
5
17
|
}
|
package/dist/machine/relay.js
CHANGED
|
@@ -155,10 +155,19 @@ export class MachineRelayClient {
|
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
this.setStatus("errored");
|
|
158
|
-
const verdict = await this.options.probe().catch(() => "retry");
|
|
158
|
+
const verdict = await this.options.probe(this.key).catch(() => "retry");
|
|
159
159
|
if (this.stopped)
|
|
160
160
|
return;
|
|
161
161
|
if (verdict === "unauthorized") {
|
|
162
|
+
const saved = await this.options.reloadKey?.().catch(() => undefined);
|
|
163
|
+
if (this.stopped)
|
|
164
|
+
return;
|
|
165
|
+
if (saved && saved !== this.key) {
|
|
166
|
+
this.key = saved;
|
|
167
|
+
this.attempt = 0;
|
|
168
|
+
this.scheduleReconnect("this computer was paired again");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
162
171
|
const reason = "the server says this machine key is revoked or invalid. Pair it again with `workcord machine pair` (or `workcord machine enroll`).";
|
|
163
172
|
this.log(`machine: ${reason}`);
|
|
164
173
|
this.end({ ok: false, reason });
|
package/dist/machine/service.js
CHANGED
|
@@ -7,7 +7,10 @@ const exec = promisify(execFile);
|
|
|
7
7
|
export const LAUNCHD_LABEL = "com.fulldive.workcord.machine";
|
|
8
8
|
const SYSTEMD_UNIT = "workcord-machine.service";
|
|
9
9
|
export function serviceArgs(input) {
|
|
10
|
-
return [
|
|
10
|
+
return [
|
|
11
|
+
"machine", "start", ...input.roots.flatMap((root) => ["--root", root]), "--adapter", input.adapter,
|
|
12
|
+
...(input.home ? ["--home", input.home] : []),
|
|
13
|
+
];
|
|
11
14
|
}
|
|
12
15
|
export function launchdPlist(spec) {
|
|
13
16
|
const args = [spec.node, spec.cli, ...spec.args].map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
|