@geteigen/cli 0.0.0-stage → 3.1.0
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 +154 -2
- package/dist/agent.d.ts +32 -0
- package/dist/agent.js +246 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +33 -0
- package/dist/cli/src/agent.d.ts +24 -0
- package/dist/cli/src/agent.js +263 -0
- package/dist/cli/src/agentTools.d.ts +6 -0
- package/dist/cli/src/agentTools.js +104 -0
- package/dist/cli/src/auth.d.ts +5 -0
- package/dist/cli/src/auth.js +33 -0
- package/dist/cli/src/cliEnv.d.ts +6 -0
- package/dist/cli/src/cliEnv.js +26 -0
- package/dist/cli/src/config.d.ts +11 -0
- package/dist/cli/src/config.js +51 -0
- package/dist/cli/src/cursorAuth.d.ts +10 -0
- package/dist/cli/src/cursorAuth.js +92 -0
- package/dist/cli/src/gitAuthEnv.d.ts +5 -0
- package/dist/cli/src/gitAuthEnv.js +35 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +244 -0
- package/dist/cli/src/listModels.d.ts +14 -0
- package/dist/cli/src/listModels.js +196 -0
- package/dist/cli/src/listSessions.d.ts +9 -0
- package/dist/cli/src/listSessions.js +37 -0
- package/dist/cli/src/machineConfig.d.ts +20 -0
- package/dist/cli/src/machineConfig.js +66 -0
- package/dist/cli/src/pickFolder.d.ts +5 -0
- package/dist/cli/src/pickFolder.js +65 -0
- package/dist/cli/src/proc.d.ts +31 -0
- package/dist/cli/src/proc.js +97 -0
- package/dist/cli/src/service.d.ts +17 -0
- package/dist/cli/src/service.js +255 -0
- package/dist/cli/src/workRoots.d.ts +30 -0
- package/dist/cli/src/workRoots.js +228 -0
- package/dist/cli/src/worker.d.ts +8 -0
- package/dist/cli/src/worker.js +51 -0
- package/dist/cli/src/workerCore.d.ts +47 -0
- package/dist/cli/src/workerCore.js +721 -0
- package/dist/cli/src/workspace.d.ts +10 -0
- package/dist/cli/src/workspace.js +329 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +51 -0
- package/dist/cursorAuth.d.ts +10 -0
- package/dist/cursorAuth.js +95 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/dist/listModels.d.ts +6 -0
- package/dist/listModels.js +62 -0
- package/dist/listSessions.d.ts +9 -0
- package/dist/listSessions.js +37 -0
- package/dist/pickFolder.d.ts +5 -0
- package/dist/pickFolder.js +65 -0
- package/dist/shared/actionDrafts.d.ts +83 -0
- package/dist/shared/actionDrafts.js +169 -0
- package/dist/shared/agentArtifacts.d.ts +49 -0
- package/dist/shared/agentArtifacts.js +169 -0
- package/dist/shared/agentLabel.d.ts +18 -0
- package/dist/shared/agentLabel.js +22 -0
- package/dist/shared/agentMentions.d.ts +38 -0
- package/dist/shared/agentMentions.js +110 -0
- package/dist/shared/agentRepos.d.ts +28 -0
- package/dist/shared/agentRepos.js +60 -0
- package/dist/shared/agentRunStatus.d.ts +12 -0
- package/dist/shared/agentRunStatus.js +17 -0
- package/dist/shared/agentUsage.d.ts +24 -0
- package/dist/shared/agentUsage.js +139 -0
- package/dist/shared/approvals.d.ts +44 -0
- package/dist/shared/approvals.js +97 -0
- package/dist/shared/askEigen.d.ts +190 -0
- package/dist/shared/askEigen.js +76 -0
- package/dist/shared/attribution.d.ts +25 -0
- package/dist/shared/attribution.js +63 -0
- package/dist/shared/automations.d.ts +135 -0
- package/dist/shared/automations.js +553 -0
- package/dist/shared/backends/claudeCode.d.ts +44 -0
- package/dist/shared/backends/claudeCode.js +548 -0
- package/dist/shared/backends/codex.d.ts +15 -0
- package/dist/shared/backends/codex.js +287 -0
- package/dist/shared/backends/cursor.d.ts +111 -0
- package/dist/shared/backends/cursor.js +1216 -0
- package/dist/shared/backends/index.d.ts +18 -0
- package/dist/shared/backends/index.js +58 -0
- package/dist/shared/backends/opencode.d.ts +43 -0
- package/dist/shared/backends/opencode.js +415 -0
- package/dist/shared/backends/types.d.ts +171 -0
- package/dist/shared/backends/types.js +19 -0
- package/dist/shared/backends/usage.d.ts +8 -0
- package/dist/shared/backends/usage.js +71 -0
- package/dist/shared/billingPlans.d.ts +60 -0
- package/dist/shared/billingPlans.js +262 -0
- package/dist/shared/brain.d.ts +353 -0
- package/dist/shared/brain.js +487 -0
- package/dist/shared/clarifyingQuestions.d.ts +13 -0
- package/dist/shared/clarifyingQuestions.js +207 -0
- package/dist/shared/claudeModels.d.ts +10 -0
- package/dist/shared/claudeModels.js +52 -0
- package/dist/shared/cloudBackends.d.ts +21 -0
- package/dist/shared/cloudBackends.js +29 -0
- package/dist/shared/codexModels.d.ts +10 -0
- package/dist/shared/codexModels.js +28 -0
- package/dist/shared/contextWindows.d.ts +2 -0
- package/dist/shared/contextWindows.js +30 -0
- package/dist/shared/cursorMcpConfig.d.ts +14 -0
- package/dist/shared/cursorMcpConfig.js +43 -0
- package/dist/shared/cursorProjectMcp.d.ts +10 -0
- package/dist/shared/cursorProjectMcp.js +42 -0
- package/dist/shared/docs.d.ts +59 -0
- package/dist/shared/docs.js +87 -0
- package/dist/shared/events.d.ts +932 -0
- package/dist/shared/events.js +57 -0
- package/dist/shared/integrations.d.ts +266 -0
- package/dist/shared/integrations.js +449 -0
- package/dist/shared/issueTranscript.d.ts +64 -0
- package/dist/shared/issueTranscript.js +387 -0
- package/dist/shared/issues.d.ts +139 -0
- package/dist/shared/issues.js +217 -0
- package/dist/shared/machines.d.ts +119 -0
- package/dist/shared/machines.js +58 -0
- package/dist/shared/markdownMedia.d.ts +13 -0
- package/dist/shared/markdownMedia.js +41 -0
- package/dist/shared/notificationText.d.ts +6 -0
- package/dist/shared/notificationText.js +38 -0
- package/dist/shared/opencodeModels.d.ts +19 -0
- package/dist/shared/opencodeModels.js +62 -0
- package/dist/shared/opencodeSession.d.ts +7 -0
- package/dist/shared/opencodeSession.js +30 -0
- package/dist/shared/orchestration.d.ts +58 -0
- package/dist/shared/orchestration.js +12 -0
- package/dist/shared/orgs.d.ts +47 -0
- package/dist/shared/orgs.js +49 -0
- package/dist/shared/personalOrg.d.ts +5 -0
- package/dist/shared/personalOrg.js +12 -0
- package/dist/shared/plans.d.ts +3 -0
- package/dist/shared/plans.js +19 -0
- package/dist/shared/providerModels.d.ts +40 -0
- package/dist/shared/providerModels.js +503 -0
- package/dist/shared/roomAttention.d.ts +17 -0
- package/dist/shared/roomAttention.js +36 -0
- package/dist/shared/roomContext.d.ts +120 -0
- package/dist/shared/roomContext.js +133 -0
- package/dist/shared/roomPermissions.d.ts +56 -0
- package/dist/shared/roomPermissions.js +133 -0
- package/dist/shared/sessionSeats.d.ts +29 -0
- package/dist/shared/sessionSeats.js +57 -0
- package/dist/shared/slashCommands.d.ts +39 -0
- package/dist/shared/slashCommands.js +166 -0
- package/dist/shared/space.d.ts +33 -0
- package/dist/shared/space.js +54 -0
- package/dist/shared/spaceFeed.d.ts +78 -0
- package/dist/shared/spaceFeed.js +194 -0
- package/dist/shared/stringifyUnknown.d.ts +1 -0
- package/dist/shared/stringifyUnknown.js +1 -0
- package/dist/shared/swarm.d.ts +320 -0
- package/dist/shared/swarm.js +328 -0
- package/dist/shared/swarmVisual.d.ts +79 -0
- package/dist/shared/swarmVisual.js +212 -0
- package/dist/shared/terminalFilter.d.ts +13 -0
- package/dist/shared/terminalFilter.js +46 -0
- package/dist/shared/toolIntegration.d.ts +56 -0
- package/dist/shared/toolIntegration.js +180 -0
- package/dist/shared/typing.d.ts +9 -0
- package/dist/shared/typing.js +37 -0
- package/dist/shared/uploads.d.ts +31 -0
- package/dist/shared/uploads.js +65 -0
- package/dist/shared/voiceApprovals.d.ts +36 -0
- package/dist/shared/voiceApprovals.js +229 -0
- package/dist/shared/workspace.d.ts +49 -0
- package/dist/shared/workspace.js +53 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +266 -0
- package/package.json +51 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ChildProcess, SpawnOptions } from "child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Spawn an agent CLI (`cursor`, `claude`, `codex`). On Windows those are
|
|
4
|
+
* npm `.cmd` shims, which plain `spawn` can't launch without a shell —
|
|
5
|
+
* cross-spawn resolves the shim and escapes arguments safely.
|
|
6
|
+
*/
|
|
7
|
+
export declare function spawnCli(command: string, args: string[], options: SpawnOptions): ChildProcess;
|
|
8
|
+
/** POSIX-safe single quotes so prompts / MCP JSON survive `zsh -lc`. */
|
|
9
|
+
export declare function posixShellQuote(value: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Spawn an agent CLI the way a terminal would. Finder/Dock Eigen has a
|
|
12
|
+
* different keychain context than Terminal; a login shell is how Claude
|
|
13
|
+
* Foundry / Codex subscription creds actually resolve.
|
|
14
|
+
*/
|
|
15
|
+
export declare function spawnUserCli(command: string, args: string[], options: SpawnOptions): ChildProcess;
|
|
16
|
+
export interface ExecCliOptions {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
timeout?: number;
|
|
20
|
+
maxBuffer?: number;
|
|
21
|
+
}
|
|
22
|
+
/** execFile-style helper on top of spawnCli: resolves `{ stdout, stderr }`, rejects on non-zero exit. */
|
|
23
|
+
export declare function execCli(command: string, args: string[], options?: ExecCliOptions): Promise<{
|
|
24
|
+
stdout: string;
|
|
25
|
+
stderr: string;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Stop a CLI run. On Windows the child is the `.cmd` shim's cmd.exe, so kill
|
|
29
|
+
* the whole tree or the real agent keeps running.
|
|
30
|
+
*/
|
|
31
|
+
export declare function killCli(child: ChildProcess): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import crossSpawn from "cross-spawn";
|
|
2
|
+
/**
|
|
3
|
+
* Spawn an agent CLI (`cursor`, `claude`, `codex`). On Windows those are
|
|
4
|
+
* npm `.cmd` shims, which plain `spawn` can't launch without a shell —
|
|
5
|
+
* cross-spawn resolves the shim and escapes arguments safely.
|
|
6
|
+
*/
|
|
7
|
+
export function spawnCli(command, args, options) {
|
|
8
|
+
return crossSpawn(command, args, options);
|
|
9
|
+
}
|
|
10
|
+
/** POSIX-safe single quotes so prompts / MCP JSON survive `zsh -lc`. */
|
|
11
|
+
export function posixShellQuote(value) {
|
|
12
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Spawn an agent CLI the way a terminal would. Finder/Dock Eigen has a
|
|
16
|
+
* different keychain context than Terminal; a login shell is how Claude
|
|
17
|
+
* Foundry / Codex subscription creds actually resolve.
|
|
18
|
+
*/
|
|
19
|
+
export function spawnUserCli(command, args, options) {
|
|
20
|
+
if (process.platform === "win32")
|
|
21
|
+
return spawnCli(command, args, options);
|
|
22
|
+
const shell = process.env.SHELL && process.env.SHELL.startsWith("/")
|
|
23
|
+
? process.env.SHELL
|
|
24
|
+
: process.platform === "darwin"
|
|
25
|
+
? "/bin/zsh"
|
|
26
|
+
: "/bin/sh";
|
|
27
|
+
const line = [command, ...args].map(posixShellQuote).join(" ");
|
|
28
|
+
return spawnCli(shell, ["-lc", line], options);
|
|
29
|
+
}
|
|
30
|
+
/** execFile-style helper on top of spawnCli: resolves `{ stdout, stderr }`, rejects on non-zero exit. */
|
|
31
|
+
export function execCli(command, args, options = {}) {
|
|
32
|
+
const maxBuffer = options.maxBuffer ?? 1024 * 1024;
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawnCli(command, args, {
|
|
35
|
+
cwd: options.cwd,
|
|
36
|
+
env: options.env,
|
|
37
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
});
|
|
40
|
+
let stdout = "";
|
|
41
|
+
let stderr = "";
|
|
42
|
+
let settled = false;
|
|
43
|
+
const fail = (err) => {
|
|
44
|
+
if (settled)
|
|
45
|
+
return;
|
|
46
|
+
settled = true;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
err.stdout = stdout;
|
|
49
|
+
err.stderr = stderr;
|
|
50
|
+
reject(err);
|
|
51
|
+
};
|
|
52
|
+
const timer = options.timeout
|
|
53
|
+
? setTimeout(() => {
|
|
54
|
+
child.kill();
|
|
55
|
+
fail(new Error(`${command} timed out after ${options.timeout}ms`));
|
|
56
|
+
}, options.timeout)
|
|
57
|
+
: undefined;
|
|
58
|
+
child.stdout?.on("data", (chunk) => {
|
|
59
|
+
stdout += chunk.toString("utf8");
|
|
60
|
+
if (stdout.length > maxBuffer) {
|
|
61
|
+
child.kill();
|
|
62
|
+
fail(new Error(`${command} output exceeded maxBuffer`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
child.stderr?.on("data", (chunk) => {
|
|
66
|
+
stderr += chunk.toString("utf8");
|
|
67
|
+
});
|
|
68
|
+
child.on("error", (err) => fail(err));
|
|
69
|
+
child.on("close", (code) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
if (code === 0) {
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
resolve({ stdout, stderr });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
fail(Object.assign(new Error(`${command} exited with code ${code}`), {
|
|
79
|
+
code,
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Stop a CLI run. On Windows the child is the `.cmd` shim's cmd.exe, so kill
|
|
86
|
+
* the whole tree or the real agent keeps running.
|
|
87
|
+
*/
|
|
88
|
+
export function killCli(child) {
|
|
89
|
+
if (process.platform === "win32" && child.pid) {
|
|
90
|
+
crossSpawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
91
|
+
stdio: "ignore",
|
|
92
|
+
windowsHide: true,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
child.kill("SIGTERM");
|
|
97
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function logPath(): string;
|
|
2
|
+
export declare function readDaemonPid(): number | null;
|
|
3
|
+
/** Re-launch `eigen start <args>` detached, logging to worker.log. */
|
|
4
|
+
export declare function startDaemon(startArgs: string[]): {
|
|
5
|
+
pid: number;
|
|
6
|
+
log: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function stopDaemon(): boolean;
|
|
9
|
+
export declare function installService(startArgs: string[]): string[];
|
|
10
|
+
export declare function uninstallService(): string;
|
|
11
|
+
export declare function serviceStatus(): {
|
|
12
|
+
installed: boolean;
|
|
13
|
+
running: boolean;
|
|
14
|
+
detail: string;
|
|
15
|
+
};
|
|
16
|
+
/** Follow the worker's logs (blocks until Ctrl-C). */
|
|
17
|
+
export declare function followLogs(): void;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, openSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir, platform } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import { getConfigDir } from "./config.js";
|
|
6
|
+
/**
|
|
7
|
+
* Keep the worker running without a terminal: `eigen start --daemon` for a
|
|
8
|
+
* quick background run, `eigen service install` for a systemd / launchd
|
|
9
|
+
* service that restarts on crash and starts at boot.
|
|
10
|
+
*/
|
|
11
|
+
const UNIT = "eigen-worker";
|
|
12
|
+
const LAUNCHD_LABEL = "space.geteigen.worker";
|
|
13
|
+
export function logPath() {
|
|
14
|
+
return join(getConfigDir(), "worker.log");
|
|
15
|
+
}
|
|
16
|
+
function pidPath() {
|
|
17
|
+
return join(getConfigDir(), "worker.pid");
|
|
18
|
+
}
|
|
19
|
+
/** node + this CLI's entry script, as absolute paths. */
|
|
20
|
+
function execArgs() {
|
|
21
|
+
const script = process.argv[1] || "";
|
|
22
|
+
let bin = script;
|
|
23
|
+
try {
|
|
24
|
+
bin = realpathSync(script);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// keep as is
|
|
28
|
+
}
|
|
29
|
+
return { node: process.execPath, bin };
|
|
30
|
+
}
|
|
31
|
+
function run(cmd, args) {
|
|
32
|
+
const res = spawnSync(cmd, args, { encoding: "utf8" });
|
|
33
|
+
return { ok: res.status === 0, out: `${res.stdout || ""}${res.stderr || ""}`.trim() };
|
|
34
|
+
}
|
|
35
|
+
// ── --daemon / stop ─────────────────────────────────────────────────────────
|
|
36
|
+
function pidAlive(pid) {
|
|
37
|
+
try {
|
|
38
|
+
process.kill(pid, 0);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function readDaemonPid() {
|
|
46
|
+
if (!existsSync(pidPath()))
|
|
47
|
+
return null;
|
|
48
|
+
const pid = Number(readFileSync(pidPath(), "utf8").trim());
|
|
49
|
+
return Number.isFinite(pid) && pid > 0 && pidAlive(pid) ? pid : null;
|
|
50
|
+
}
|
|
51
|
+
/** Re-launch `eigen start <args>` detached, logging to worker.log. */
|
|
52
|
+
export function startDaemon(startArgs) {
|
|
53
|
+
const existing = readDaemonPid();
|
|
54
|
+
if (existing)
|
|
55
|
+
throw new Error(`The worker is already running in the background (pid ${existing}). Stop it with \`eigen stop\`.`);
|
|
56
|
+
const { node, bin } = execArgs();
|
|
57
|
+
const log = logPath();
|
|
58
|
+
mkdirSync(dirname(log), { recursive: true });
|
|
59
|
+
const fd = openSync(log, "a");
|
|
60
|
+
const child = spawn(node, [bin, "start", ...startArgs], {
|
|
61
|
+
detached: true,
|
|
62
|
+
stdio: ["ignore", fd, fd],
|
|
63
|
+
env: process.env,
|
|
64
|
+
});
|
|
65
|
+
child.unref();
|
|
66
|
+
if (!child.pid)
|
|
67
|
+
throw new Error("Couldn't start the worker in the background");
|
|
68
|
+
writeFileSync(pidPath(), String(child.pid));
|
|
69
|
+
return { pid: child.pid, log };
|
|
70
|
+
}
|
|
71
|
+
export function stopDaemon() {
|
|
72
|
+
const pid = readDaemonPid();
|
|
73
|
+
if (existsSync(pidPath()))
|
|
74
|
+
rmSync(pidPath());
|
|
75
|
+
if (!pid)
|
|
76
|
+
return false;
|
|
77
|
+
process.kill(pid, "SIGTERM");
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
// ── systemd / launchd ───────────────────────────────────────────────────────
|
|
81
|
+
function isRoot() {
|
|
82
|
+
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
83
|
+
}
|
|
84
|
+
/** Home of the account that ran `sudo` (so the service uses their config). */
|
|
85
|
+
function sudoUserHome(user) {
|
|
86
|
+
const out = run("getent", ["passwd", user]);
|
|
87
|
+
const home = out.ok ? out.out.split(":")[5] : "";
|
|
88
|
+
return home || `/home/${user}`;
|
|
89
|
+
}
|
|
90
|
+
function systemdUnitPath() {
|
|
91
|
+
if (isRoot())
|
|
92
|
+
return { path: `/etc/systemd/system/${UNIT}.service`, system: true };
|
|
93
|
+
return { path: join(homedir(), ".config", "systemd", "user", `${UNIT}.service`), system: false };
|
|
94
|
+
}
|
|
95
|
+
function systemctl(system, args) {
|
|
96
|
+
return run("systemctl", system ? args : ["--user", ...args]);
|
|
97
|
+
}
|
|
98
|
+
function launchdPlistPath() {
|
|
99
|
+
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
100
|
+
}
|
|
101
|
+
function xmlEscape(value) {
|
|
102
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
103
|
+
}
|
|
104
|
+
export function installService(startArgs) {
|
|
105
|
+
const { node, bin } = execArgs();
|
|
106
|
+
const env = { PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin" };
|
|
107
|
+
if (process.env.STEER_SERVER_URL)
|
|
108
|
+
env.STEER_SERVER_URL = process.env.STEER_SERVER_URL;
|
|
109
|
+
if (process.env.EIGEN_WORK_ROOT)
|
|
110
|
+
env.EIGEN_WORK_ROOT = process.env.EIGEN_WORK_ROOT;
|
|
111
|
+
const os = platform();
|
|
112
|
+
if (os === "linux") {
|
|
113
|
+
const { path, system } = systemdUnitPath();
|
|
114
|
+
const sudoUser = system ? process.env.SUDO_USER : undefined;
|
|
115
|
+
const home = sudoUser ? sudoUserHome(sudoUser) : homedir();
|
|
116
|
+
env.HOME = home;
|
|
117
|
+
const exec = [node, bin, "start", ...startArgs].map((p) => (/\s/.test(p) ? `"${p}"` : p)).join(" ");
|
|
118
|
+
const unit = [
|
|
119
|
+
"[Unit]",
|
|
120
|
+
"Description=Eigen agent worker",
|
|
121
|
+
"After=network-online.target",
|
|
122
|
+
"Wants=network-online.target",
|
|
123
|
+
"",
|
|
124
|
+
"[Service]",
|
|
125
|
+
"Type=simple",
|
|
126
|
+
sudoUser ? `User=${sudoUser}` : "",
|
|
127
|
+
`WorkingDirectory=${home}`,
|
|
128
|
+
`ExecStart=${exec}`,
|
|
129
|
+
...Object.entries(env).map(([k, v]) => `Environment="${k}=${v}"`),
|
|
130
|
+
// Not on clean exit: a revoked machine exits 0 and must stay stopped.
|
|
131
|
+
"Restart=on-failure",
|
|
132
|
+
"RestartSec=5",
|
|
133
|
+
"",
|
|
134
|
+
"[Install]",
|
|
135
|
+
`WantedBy=${system ? "multi-user.target" : "default.target"}`,
|
|
136
|
+
"",
|
|
137
|
+
]
|
|
138
|
+
.filter((line, i, all) => line !== "" || all[i - 1] !== "")
|
|
139
|
+
.join("\n");
|
|
140
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
141
|
+
writeFileSync(path, unit);
|
|
142
|
+
systemctl(system, ["daemon-reload"]);
|
|
143
|
+
const enabled = systemctl(system, ["enable", "--now", UNIT]);
|
|
144
|
+
if (!enabled.ok)
|
|
145
|
+
throw new Error(enabled.out || "systemctl enable failed");
|
|
146
|
+
const notes = [`Installed ${path}`, `Logs: ${system ? `journalctl -u ${UNIT} -f` : `journalctl --user -u ${UNIT} -f`}`];
|
|
147
|
+
if (!system) {
|
|
148
|
+
const lingerUser = process.env.SUDO_USER || process.env.USER || "";
|
|
149
|
+
const linger = lingerUser
|
|
150
|
+
? run("sudo", ["-n", "loginctl", "enable-linger", lingerUser])
|
|
151
|
+
: { ok: false, out: "" };
|
|
152
|
+
if (linger.ok) {
|
|
153
|
+
notes.push(`Linger enabled for ${lingerUser} (survives SSH logout).`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
notes.push(`To keep it running after you log out: sudo loginctl enable-linger ${lingerUser || "$USER"}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return notes;
|
|
160
|
+
}
|
|
161
|
+
if (os === "darwin") {
|
|
162
|
+
const path = launchdPlistPath();
|
|
163
|
+
const log = logPath();
|
|
164
|
+
mkdirSync(dirname(log), { recursive: true });
|
|
165
|
+
const args = [node, bin, "start", ...startArgs].map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
166
|
+
const envXml = Object.entries(env)
|
|
167
|
+
.map(([k, v]) => ` <key>${xmlEscape(k)}</key>\n <string>${xmlEscape(v)}</string>`)
|
|
168
|
+
.join("\n");
|
|
169
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
170
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
171
|
+
<plist version="1.0">
|
|
172
|
+
<dict>
|
|
173
|
+
<key>Label</key>
|
|
174
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
175
|
+
<key>ProgramArguments</key>
|
|
176
|
+
<array>
|
|
177
|
+
${args}
|
|
178
|
+
</array>
|
|
179
|
+
<key>EnvironmentVariables</key>
|
|
180
|
+
<dict>
|
|
181
|
+
${envXml}
|
|
182
|
+
</dict>
|
|
183
|
+
<key>RunAtLoad</key>
|
|
184
|
+
<true/>
|
|
185
|
+
<key>KeepAlive</key>
|
|
186
|
+
<dict>
|
|
187
|
+
<key>SuccessfulExit</key>
|
|
188
|
+
<false/>
|
|
189
|
+
</dict>
|
|
190
|
+
<key>StandardOutPath</key>
|
|
191
|
+
<string>${xmlEscape(log)}</string>
|
|
192
|
+
<key>StandardErrorPath</key>
|
|
193
|
+
<string>${xmlEscape(log)}</string>
|
|
194
|
+
</dict>
|
|
195
|
+
</plist>
|
|
196
|
+
`;
|
|
197
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
198
|
+
run("launchctl", ["unload", path]);
|
|
199
|
+
writeFileSync(path, plist);
|
|
200
|
+
const loaded = run("launchctl", ["load", "-w", path]);
|
|
201
|
+
if (!loaded.ok)
|
|
202
|
+
throw new Error(loaded.out || "launchctl load failed");
|
|
203
|
+
return [`Installed ${path}`, `Logs: ${log}`];
|
|
204
|
+
}
|
|
205
|
+
throw new Error("Automatic service install isn't supported on Windows yet. Run `eigen start` from Task Scheduler (At startup, \"Run whether user is logged on or not\"), or use the Docker image.");
|
|
206
|
+
}
|
|
207
|
+
export function uninstallService() {
|
|
208
|
+
const os = platform();
|
|
209
|
+
if (os === "linux") {
|
|
210
|
+
const { path, system } = systemdUnitPath();
|
|
211
|
+
systemctl(system, ["disable", "--now", UNIT]);
|
|
212
|
+
if (existsSync(path))
|
|
213
|
+
rmSync(path);
|
|
214
|
+
systemctl(system, ["daemon-reload"]);
|
|
215
|
+
return `Removed ${path}`;
|
|
216
|
+
}
|
|
217
|
+
if (os === "darwin") {
|
|
218
|
+
const path = launchdPlistPath();
|
|
219
|
+
run("launchctl", ["unload", "-w", path]);
|
|
220
|
+
if (existsSync(path))
|
|
221
|
+
rmSync(path);
|
|
222
|
+
return `Removed ${path}`;
|
|
223
|
+
}
|
|
224
|
+
throw new Error("No service to remove on this platform.");
|
|
225
|
+
}
|
|
226
|
+
export function serviceStatus() {
|
|
227
|
+
const os = platform();
|
|
228
|
+
if (os === "linux") {
|
|
229
|
+
const { path, system } = systemdUnitPath();
|
|
230
|
+
const active = systemctl(system, ["is-active", UNIT]);
|
|
231
|
+
return { installed: existsSync(path), running: active.out === "active", detail: active.out || "unknown" };
|
|
232
|
+
}
|
|
233
|
+
if (os === "darwin") {
|
|
234
|
+
const path = launchdPlistPath();
|
|
235
|
+
const listed = run("launchctl", ["list", LAUNCHD_LABEL]);
|
|
236
|
+
const pid = listed.out.match(/"PID"\s*=\s*(\d+)/)?.[1];
|
|
237
|
+
return { installed: existsSync(path), running: Boolean(pid), detail: pid ? `pid ${pid}` : "not running" };
|
|
238
|
+
}
|
|
239
|
+
return { installed: false, running: false, detail: "not supported on this platform" };
|
|
240
|
+
}
|
|
241
|
+
/** Follow the worker's logs (blocks until Ctrl-C). */
|
|
242
|
+
export function followLogs() {
|
|
243
|
+
const os = platform();
|
|
244
|
+
if (os === "linux" && existsSync(systemdUnitPath().path)) {
|
|
245
|
+
const { system } = systemdUnitPath();
|
|
246
|
+
spawnSync("journalctl", system ? ["-u", UNIT, "-n", "100", "-f"] : ["--user", "-u", UNIT, "-n", "100", "-f"], {
|
|
247
|
+
stdio: "inherit",
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const log = logPath();
|
|
252
|
+
if (!existsSync(log))
|
|
253
|
+
throw new Error(`No log yet at ${log}`);
|
|
254
|
+
spawnSync("tail", ["-n", "100", "-f", log], { stdio: "inherit" });
|
|
255
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MachineDirListing } from "../../shared/machines.js";
|
|
2
|
+
export declare function expandHome(path: string): string;
|
|
3
|
+
export declare class WorkRoots {
|
|
4
|
+
private local;
|
|
5
|
+
private server;
|
|
6
|
+
constructor(localRoots: string[]);
|
|
7
|
+
setServerRoots(roots: string[]): void;
|
|
8
|
+
/** Allowed roots; empty means no limit. */
|
|
9
|
+
effective(): string[];
|
|
10
|
+
/** Where new checkouts go (created on demand). */
|
|
11
|
+
workRoot(): string;
|
|
12
|
+
isAllowed(path: string): boolean;
|
|
13
|
+
listDirs(path?: string): MachineDirListing;
|
|
14
|
+
}
|
|
15
|
+
export declare function prepareRepo(roots: WorkRoots, input: {
|
|
16
|
+
repoUrl: string;
|
|
17
|
+
ref: string;
|
|
18
|
+
dir: string;
|
|
19
|
+
branch?: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
}): Promise<string>;
|
|
22
|
+
export declare function finalizeGit(roots: WorkRoots, input: {
|
|
23
|
+
repoPath: string;
|
|
24
|
+
branch: string;
|
|
25
|
+
message: string;
|
|
26
|
+
token?: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
pushed: boolean;
|
|
29
|
+
branch: string;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, realpathSync, statSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, normalize, resolve, sep } from "path";
|
|
4
|
+
import { execCli } from "./proc.js";
|
|
5
|
+
import { gitCredentialsPath, rememberGithubToken } from "./gitAuthEnv.js";
|
|
6
|
+
/**
|
|
7
|
+
* Folders this machine lets agents work in. Local roots come from
|
|
8
|
+
* `eigen start --root` / EIGEN_WORK_ROOT; server roots from Eigen → Machines.
|
|
9
|
+
* When either is set, runs, folder browsing, and git operations must stay
|
|
10
|
+
* inside their union. With none set, the whole home directory is browsable.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_ENTRIES = 300;
|
|
13
|
+
export function expandHome(path) {
|
|
14
|
+
const trimmed = path.trim();
|
|
15
|
+
if (trimmed === "~")
|
|
16
|
+
return homedir();
|
|
17
|
+
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\"))
|
|
18
|
+
return join(homedir(), trimmed.slice(2));
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
function canonical(path) {
|
|
22
|
+
const abs = resolve(expandHome(path));
|
|
23
|
+
try {
|
|
24
|
+
return realpathSync(abs);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return normalize(abs);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function within(child, root) {
|
|
31
|
+
if (child === root)
|
|
32
|
+
return true;
|
|
33
|
+
const prefix = root.endsWith(sep) ? root : root + sep;
|
|
34
|
+
return child.startsWith(prefix);
|
|
35
|
+
}
|
|
36
|
+
export class WorkRoots {
|
|
37
|
+
local;
|
|
38
|
+
server = [];
|
|
39
|
+
constructor(localRoots) {
|
|
40
|
+
this.local = [...new Set(localRoots.map((r) => r.trim()).filter(Boolean).map(canonical))];
|
|
41
|
+
}
|
|
42
|
+
setServerRoots(roots) {
|
|
43
|
+
this.server = [...new Set((roots || []).map((r) => r.trim()).filter(Boolean).map(canonical))];
|
|
44
|
+
}
|
|
45
|
+
/** Allowed roots; empty means no limit. */
|
|
46
|
+
effective() {
|
|
47
|
+
return [...new Set([...this.local, ...this.server])];
|
|
48
|
+
}
|
|
49
|
+
/** Where new checkouts go (created on demand). */
|
|
50
|
+
workRoot() {
|
|
51
|
+
const root = this.local[0] ?? this.server[0] ?? join(homedir(), "eigen-work");
|
|
52
|
+
mkdirSync(root, { recursive: true });
|
|
53
|
+
return root;
|
|
54
|
+
}
|
|
55
|
+
isAllowed(path) {
|
|
56
|
+
const roots = this.effective();
|
|
57
|
+
if (!roots.length)
|
|
58
|
+
return true;
|
|
59
|
+
const target = canonical(path);
|
|
60
|
+
return roots.some((root) => within(target, root));
|
|
61
|
+
}
|
|
62
|
+
listDirs(path) {
|
|
63
|
+
const roots = this.effective();
|
|
64
|
+
if (!path) {
|
|
65
|
+
const tops = roots.length ? roots : [homedir()];
|
|
66
|
+
return {
|
|
67
|
+
path: "",
|
|
68
|
+
parent: null,
|
|
69
|
+
roots,
|
|
70
|
+
entries: tops.filter((p) => existsSync(p)).map((p) => entryFor(p)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const target = canonical(path);
|
|
74
|
+
if (!this.isAllowed(target))
|
|
75
|
+
throw new Error("That folder is outside this machine's allowed folders");
|
|
76
|
+
if (!statSync(target).isDirectory())
|
|
77
|
+
throw new Error("Not a folder");
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (const dirent of readdirSync(target, { withFileTypes: true })) {
|
|
80
|
+
if (!dirent.isDirectory() || dirent.name.startsWith("."))
|
|
81
|
+
continue;
|
|
82
|
+
entries.push(entryFor(join(target, dirent.name)));
|
|
83
|
+
if (entries.length >= MAX_ENTRIES)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
87
|
+
const atRoot = roots.length ? roots.includes(target) : target === canonical(homedir());
|
|
88
|
+
const parentPath = dirname(target);
|
|
89
|
+
return {
|
|
90
|
+
path: target,
|
|
91
|
+
parent: atRoot || parentPath === target || !this.isAllowed(parentPath) ? null : parentPath,
|
|
92
|
+
roots,
|
|
93
|
+
entries,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function entryFor(path) {
|
|
98
|
+
return { name: basename(path) || path, path, isGitRepo: existsSync(join(path, ".git")) };
|
|
99
|
+
}
|
|
100
|
+
function git(cwd, args, timeout = 120_000) {
|
|
101
|
+
return execCli("git", args, { cwd, timeout, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
|
|
102
|
+
}
|
|
103
|
+
/** https://github.com/o/r → https://x-access-token:TOKEN@github.com/o/r.git */
|
|
104
|
+
function authedUrl(repoUrl, token) {
|
|
105
|
+
const clean = repoUrl.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
106
|
+
if (!token)
|
|
107
|
+
return `${clean}.git`;
|
|
108
|
+
return `${clean.replace(/^https:\/\//, `https://x-access-token:${encodeURIComponent(token)}@`)}.git`;
|
|
109
|
+
}
|
|
110
|
+
async function useStoredGithubCreds(cwd) {
|
|
111
|
+
const file = gitCredentialsPath();
|
|
112
|
+
await git(cwd, ["config", "--local", "--unset-all", "credential.helper"]).catch(() => undefined);
|
|
113
|
+
await git(cwd, ["config", "--local", "credential.helper", `store --file=${file}`]);
|
|
114
|
+
}
|
|
115
|
+
/** Don't leave the access token in origin — `git remote -v` prints it. */
|
|
116
|
+
async function forgetRemoteToken(cwd) {
|
|
117
|
+
try {
|
|
118
|
+
const current = (await git(cwd, ["remote", "get-url", "origin"])).stdout.trim();
|
|
119
|
+
const plain = current.replace(/x-access-token:[^@]+@/, "");
|
|
120
|
+
if (plain !== current)
|
|
121
|
+
await git(cwd, ["remote", "set-url", "origin", plain]);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// no origin / not a repo
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function persistGithubAuth(cwd, token) {
|
|
128
|
+
if (!token || !existsSync(join(cwd, ".git")))
|
|
129
|
+
return;
|
|
130
|
+
rememberGithubToken(token);
|
|
131
|
+
await useStoredGithubCreds(cwd);
|
|
132
|
+
await forgetRemoteToken(cwd);
|
|
133
|
+
}
|
|
134
|
+
/** Strip credentials so errors never echo a token. */
|
|
135
|
+
function scrub(message, token) {
|
|
136
|
+
let out = message.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@");
|
|
137
|
+
if (token)
|
|
138
|
+
out = out.split(token).join("***");
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
export async function prepareRepo(roots, input) {
|
|
142
|
+
const dir = input.dir.trim();
|
|
143
|
+
if (!dir || isAbsolute(dir) || dir.split(/[\\/]/).some((part) => part === ".." || part === "")) {
|
|
144
|
+
throw new Error("Invalid workspace folder");
|
|
145
|
+
}
|
|
146
|
+
const target = join(roots.workRoot(), dir);
|
|
147
|
+
if (!roots.isAllowed(dirname(target)) && roots.effective().length) {
|
|
148
|
+
throw new Error("The work folder is outside this machine's allowed folders");
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
if (!input.repoUrl) {
|
|
152
|
+
// Scratch workspace (no repo): an empty git dir so agents have a cwd.
|
|
153
|
+
mkdirSync(target, { recursive: true });
|
|
154
|
+
if (!existsSync(join(target, ".git")))
|
|
155
|
+
await git(target, ["init", "-q"]);
|
|
156
|
+
return target;
|
|
157
|
+
}
|
|
158
|
+
const remote = authedUrl(input.repoUrl, input.token);
|
|
159
|
+
const ref = input.ref || "main";
|
|
160
|
+
if (existsSync(join(target, ".git"))) {
|
|
161
|
+
await git(target, ["remote", "set-url", "origin", remote]);
|
|
162
|
+
await git(target, ["fetch", "--depth", "50", "origin", ref], 300_000);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
166
|
+
try {
|
|
167
|
+
await git(dirname(target), ["clone", "--depth", "50", "--branch", ref, remote, target], 300_000);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
await git(dirname(target), ["clone", remote, target], 600_000);
|
|
171
|
+
await git(target, ["checkout", ref]);
|
|
172
|
+
}
|
|
173
|
+
await git(target, ["config", "user.name", "Eigen Agent"]);
|
|
174
|
+
await git(target, ["config", "user.email", "agent@users.noreply.github.com"]);
|
|
175
|
+
}
|
|
176
|
+
if (input.branch) {
|
|
177
|
+
await git(target, ["checkout", "-B", input.branch, `origin/${ref}`]).catch(() => git(target, ["checkout", "-B", input.branch]));
|
|
178
|
+
}
|
|
179
|
+
return target;
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
throw new Error(scrub(err.stderr || err.message, input.token));
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await persistGithubAuth(target, input.token);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export async function finalizeGit(roots, input) {
|
|
189
|
+
if (!roots.isAllowed(input.repoPath))
|
|
190
|
+
throw new Error("That folder is outside this machine's allowed folders");
|
|
191
|
+
const cwd = canonical(input.repoPath);
|
|
192
|
+
try {
|
|
193
|
+
await git(cwd, ["add", "-A"]);
|
|
194
|
+
const status = await git(cwd, ["status", "--porcelain"]);
|
|
195
|
+
let committed = false;
|
|
196
|
+
if (status.stdout.trim()) {
|
|
197
|
+
await git(cwd, [
|
|
198
|
+
"-c",
|
|
199
|
+
"user.name=Eigen Agent",
|
|
200
|
+
"-c",
|
|
201
|
+
"user.email=agent@users.noreply.github.com",
|
|
202
|
+
"commit",
|
|
203
|
+
"-q",
|
|
204
|
+
"-m",
|
|
205
|
+
input.message || "Changes from Eigen agent",
|
|
206
|
+
]);
|
|
207
|
+
committed = true;
|
|
208
|
+
}
|
|
209
|
+
if (input.token) {
|
|
210
|
+
const current = (await git(cwd, ["remote", "get-url", "origin"])).stdout.trim();
|
|
211
|
+
const plain = current.replace(/x-access-token:[^@]+@/, "");
|
|
212
|
+
if (/^https:\/\/github\.com\//.test(plain)) {
|
|
213
|
+
await git(cwd, ["remote", "set-url", "origin", authedUrl(plain, input.token)]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const ahead = await git(cwd, ["rev-list", "--count", `origin/${input.branch}..HEAD`]).catch(() => null);
|
|
217
|
+
if (!committed && ahead && ahead.stdout.trim() === "0")
|
|
218
|
+
return { pushed: false, branch: input.branch };
|
|
219
|
+
await git(cwd, ["push", "-u", "origin", `HEAD:refs/heads/${input.branch}`], 300_000);
|
|
220
|
+
return { pushed: true, branch: input.branch };
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
throw new Error(scrub(err.stderr || err.message, input.token));
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
await persistGithubAuth(cwd, input.token);
|
|
227
|
+
}
|
|
228
|
+
}
|