Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Docs: fix broken docs index links by setting an empty Jekyll `baseurl` (#113, thanks @Youpen-y).
- Models: preserve model id casing after the provider prefix so OpenAI-compatible proxies can route exact names correctly (#128, thanks @WinnCook).
- Cache: give extract entries with unavailable transcripts the same short retry TTL as negative transcript cache entries, so transient Apify failures can recover (#115, thanks @gluneau).
- Daemon: apply the saved env snapshot to `process.env` before `daemon run` starts so child tools inherit the right PATH and API/tool config under launchd/systemd (#99, thanks @heyalchang).

## 0.11.0 - 2026-02-14

Expand Down
8 changes: 8 additions & 0 deletions src/daemon/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,14 @@ export async function handleDaemonRequest({
throw new Error("Daemon not configured");
}
const mergedEnv = mergeDaemonEnv({ envForRun, snapshot: cfg.env });
// Apply snapshot env to process.env so child processes (yt-dlp, ffmpeg,
// deno, tesseract) inherit the correct PATH and tool config under
// launchd/systemd where the default environment is minimal.
for (const [key, value] of Object.entries(cfg.env)) {
if (typeof value === "string") {
process.env[key] = value;
}
}
await runDaemonServer({ env: mergedEnv, fetchImpl, config: cfg });
return true;
}
Expand Down
90 changes: 90 additions & 0 deletions tests/daemon.cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { PassThrough } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
readDaemonConfig: vi.fn(),
writeDaemonConfig: vi.fn(),
runDaemonServer: vi.fn(),
}));

vi.mock("../src/daemon/config.js", () => ({
readDaemonConfig: mocks.readDaemonConfig,
writeDaemonConfig: mocks.writeDaemonConfig,
}));

vi.mock("../src/daemon/server.js", () => ({
runDaemonServer: mocks.runDaemonServer,
}));

import { handleDaemonRequest } from "../src/daemon/cli.js";

describe("daemon cli", () => {
const originalPath = process.env.PATH;
const originalOpenAiKey = process.env.OPENAI_API_KEY;
const originalHome = process.env.HOME;

beforeEach(() => {
vi.clearAllMocks();
process.env.PATH = "/usr/bin:/bin";
process.env.OPENAI_API_KEY = "from-process";
process.env.HOME = "/tmp/original-home";
});

afterEach(() => {
if (originalPath === undefined) delete process.env.PATH;
else process.env.PATH = originalPath;
if (originalOpenAiKey === undefined) delete process.env.OPENAI_API_KEY;
else process.env.OPENAI_API_KEY = originalOpenAiKey;
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
});

it("applies daemon snapshot env to process.env for child processes on run (#99)", async () => {
mocks.readDaemonConfig.mockResolvedValueOnce({
token: "test-token",
port: 8787,
env: {
PATH: "/opt/homebrew/bin:/usr/bin:/bin",
OPENAI_API_KEY: "from-snapshot",
},
});
mocks.runDaemonServer.mockResolvedValueOnce(undefined);

const envForRun = {
HOME: "/Users/peter",
PATH: "/usr/bin:/bin",
OPENAI_API_KEY: "from-run",
};

const handled = await handleDaemonRequest({
normalizedArgv: ["daemon", "run"],
envForRun,
fetchImpl: fetch,
stdout: new PassThrough(),
stderr: new PassThrough(),
});

expect(handled).toBe(true);
expect(mocks.readDaemonConfig).toHaveBeenCalledWith({ env: envForRun });
expect(mocks.runDaemonServer).toHaveBeenCalledWith({
env: {
HOME: "/Users/peter",
PATH: "/opt/homebrew/bin:/usr/bin:/bin",
OPENAI_API_KEY: "from-snapshot",
},
fetchImpl: fetch,
config: {
token: "test-token",
port: 8787,
env: {
PATH: "/opt/homebrew/bin:/usr/bin:/bin",
OPENAI_API_KEY: "from-snapshot",
},
},
});

expect(process.env.PATH).toBe("/opt/homebrew/bin:/usr/bin:/bin");
expect(process.env.OPENAI_API_KEY).toBe("from-snapshot");
expect(process.env.HOME).toBe("/tmp/original-home");
});
});