Skip to content
Open
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Notes:
- Auto mode summarizes on navigation (incl. SPAs); otherwise use the button.
- Daemon is localhost-only and requires a shared token; rerunning `summarize daemon install --token <TOKEN>` adds another paired browser token instead of invalidating the old one.
- Autostart: macOS (launchd), Linux (systemd user), Windows (Scheduled Task).
- Windows containers: `summarize daemon install` starts the daemon for the current container session but does not register a Scheduled Task. Run it each time the container starts or add that command to your container startup, and publish port `8787` so the host browser can reach the daemon.
- Tip: configure `free` via `summarize refresh-free` (needs `OPENROUTER_API_KEY`). Add `--set-default` to set model=`free`.

More:
Expand Down
6 changes: 6 additions & 0 deletions docs/chrome-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ Dev (repo checkout):
- `summarize daemon install` now tries both launchd domains (`gui/<uid>` then `user/<uid>`).
- Install as your normal user (not root) so HOME + launchd domain match.
- Re-run: `summarize daemon install --token <TOKEN>`.
- Windows containers:
- `summarize daemon install --token <TOKEN>` starts the daemon for the current container session but does not create a Scheduled Task.
- Run that command manually each time the container starts, or add it to your container startup. Also publish the daemon port in `docker-compose.yml`:
`ports: ['8787:8787']`
`command: ['cmd', '/c', 'summarize daemon install --token <TOKEN>']`
- Then restart the container and verify `http://127.0.0.1:8787/health`.
- “Need extension-side traces”:
- Options → Logs → `extension.log` (panel/background events).
- Enable “Extended logging” in Advanced settings for full pipeline traces.
Expand Down
132 changes: 128 additions & 4 deletions src/daemon/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { closeSync, openSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { spawn } from "node:child_process";
import { buildDaemonHelp } from "../run/help.js";
import { resolveCliEntrypointPathForService } from "./cli-entrypoint.js";
import {
Expand All @@ -16,6 +18,7 @@ import {
isLaunchAgentLoaded,
readLaunchAgentProgramArguments,
restartLaunchAgent,
resolveDaemonLogPaths,
uninstallLaunchAgent,
} from "./launchd.js";
import {
Expand All @@ -33,6 +36,7 @@ import {
restartSystemdService,
uninstallSystemdService,
} from "./systemd.js";
import { isWindowsContainerEnvironment } from "./windows-container.js";

type DaemonCliContext = {
normalizedArgv: string[];
Expand Down Expand Up @@ -309,6 +313,61 @@ async function readInstalledDaemonCommand(
return null;
}

function writeWindowsContainerInstallInstructions({
stdout,
port,
configPath,
programArguments,
workingDirectory,
}: {
stdout: NodeJS.WritableStream;
port: number;
configPath: string;
programArguments: string[];
workingDirectory?: string;
}) {
stdout.write("Windows container detected: skipped Scheduled Task registration.\n");
stdout.write(`Daemon config: ${configPath}\n`);
stdout.write(`Daemon command: ${formatProgramArguments(programArguments)}\n`);
if (workingDirectory) {
stdout.write(`Daemon cwd: ${workingDirectory}\n`);
}
stdout.write("Daemon autostart is not available in Windows container mode.\n");
stdout.write(
"Run `summarize daemon install --token <TOKEN>` each time the container starts, or add that command to your container startup.\n",
);
stdout.write(`Publish port ${port}:${port} so the host browser can reach the daemon.\n`);
}

async function startDetachedContainerDaemon({
env,
programArguments,
workingDirectory,
}: {
env: Record<string, string | undefined>;
programArguments: string[];
workingDirectory?: string;
}): Promise<void> {
const { logDir, stdoutPath, stderrPath } = resolveDaemonLogPaths(env);
await fs.mkdir(logDir, { recursive: true });

const stdoutFd = openSync(stdoutPath, "a");
const stderrFd = openSync(stderrPath, "a");
try {
const child = spawn(programArguments[0] ?? process.execPath, programArguments.slice(1), {
cwd: workingDirectory,
detached: true,
env: { ...process.env, ...env },
stdio: ["ignore", stdoutFd, stderrFd],
windowsHide: true,
});
child.unref();
} finally {
closeSync(stdoutFd);
closeSync(stderrFd);
}
}

export async function handleDaemonRequest({
normalizedArgv,
envForRun,
Expand All @@ -325,7 +384,6 @@ export async function handleDaemonRequest({
}

if (sub === "install") {
const service = resolveDaemonService();
const token = readArgValue(normalizedArgv, "--token");
if (!token) throw new Error("Missing --token");
const portRaw = readArgValue(normalizedArgv, "--port");
Expand All @@ -348,8 +406,44 @@ export async function handleDaemonRequest({
},
});

const { programArguments, workingDirectory } = await resolveDaemonProgramArguments({ dev });
const windowsContainerMode =
process.platform === "win32" && isWindowsContainerEnvironment(envForRun);

if (windowsContainerMode) {
const { programArguments, workingDirectory } = await resolveDaemonProgramArguments({ dev });
await startDetachedContainerDaemon({
env: envForRun,
programArguments,
workingDirectory,
});
await waitForHealthWithRetries({
fetchImpl,
port,
attempts: 5,
timeoutMs: 5000,
delayMs: 500,
});
const authed = await checkAuthWithRetries({
fetchImpl,
token: token.trim(),
port,
attempts: 5,
delayMs: 400,
});
if (!authed) throw new Error("Daemon is up but auth failed (token mismatch?)");
writeWindowsContainerInstallInstructions({
stdout,
port,
configPath,
programArguments,
workingDirectory,
});
stdout.write("OK: daemon is running in this container session and authenticated.\n");
return true;
}

const { programArguments, workingDirectory } = await resolveDaemonProgramArguments({ dev });
const service = resolveDaemonService();
await service.install({ env: envForRun, stdout, programArguments, workingDirectory });
await waitForHealthWithRetries({ fetchImpl, port, attempts: 5, timeoutMs: 5000, delayMs: 500 });
const authed = await checkAuthWithRetries({
Expand All @@ -376,13 +470,30 @@ export async function handleDaemonRequest({
}

if (sub === "status") {
const service = resolveDaemonService();
const cfg = await readDaemonConfig({ env: envForRun });
if (!cfg) {
stdout.write("Daemon not installed (missing ~/.summarize/daemon.json)\n");
stdout.write("Run: summarize daemon install --token <token>\n");
return true;
}
if (process.platform === "win32" && isWindowsContainerEnvironment(envForRun)) {
const healthy = await (async () => {
try {
await waitForHealth({ fetchImpl, port: cfg.port, timeoutMs: 1000 });
return true;
} catch {
return false;
}
})();
const authed = healthy
? await checkAuth({ fetchImpl, token: daemonConfigPrimaryToken(cfg), port: cfg.port })
: false;
stdout.write("Autostart: manual (Windows container mode; no Scheduled Task)\n");
stdout.write(`Daemon: ${healthy ? `up on ${DAEMON_HOST}:${cfg.port}` : "down"}\n`);
stdout.write(`Auth: ${authed ? "ok" : "failed"}\n`);
return true;
}
const service = resolveDaemonService();
const loaded = await service.isLoaded({ env: envForRun });
const healthy = await (async () => {
try {
Expand All @@ -403,13 +514,20 @@ export async function handleDaemonRequest({
}

if (sub === "restart") {
const service = resolveDaemonService();
const cfg = await readDaemonConfig({ env: envForRun });
if (!cfg) {
stdout.write("Daemon not installed (missing ~/.summarize/daemon.json)\n");
stdout.write("Run: summarize daemon install --token <token>\n");
return true;
}
if (process.platform === "win32" && isWindowsContainerEnvironment(envForRun)) {
stdout.write("Autostart is manual in Windows container mode; no Scheduled Task is registered.\n");
stdout.write(
"Restart the container or rerun `summarize daemon install --token <token>` to start the daemon again.\n",
);
return true;
}
const service = resolveDaemonService();
const loaded = await service.isLoaded({ env: envForRun });
if (!loaded) {
stdout.write(
Expand Down Expand Up @@ -462,6 +580,12 @@ export async function handleDaemonRequest({
}

if (sub === "uninstall") {
if (process.platform === "win32" && isWindowsContainerEnvironment(envForRun)) {
stdout.write(
"Uninstalled (Windows container mode does not register Scheduled Task autostart). Config left in ~/.summarize/daemon.json\n",
);
return true;
}
const service = resolveDaemonService();
await service.uninstall({ env: envForRun, stdout });
stdout.write(
Expand Down
1 change: 1 addition & 0 deletions src/daemon/schtasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { DAEMON_WINDOWS_TASK_NAME } from "./constants.js";
import { isWindowsContainerEnvironment } from "./windows-container.js";

const execFileAsync = promisify(execFile);

Expand Down
8 changes: 7 additions & 1 deletion src/daemon/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@ import {
toExtractOnlySlidesPayload,
} from "./server-summarize-execution.js";
import { parseSummarizeRequest } from "./server-summarize-request.js";
import { isWindowsContainerEnvironment } from "./windows-container.js";

export { corsHeaders, isTrustedOrigin } from "./server-http.js";

export function resolveDaemonListenHost(env: Record<string, string | undefined>): string {
return isWindowsContainerEnvironment(env) ? "0.0.0.0" : DAEMON_HOST;
}

function createLineWriter(onLine: (line: string) => void) {
let buffer = "";
return new Writable({
Expand Down Expand Up @@ -126,6 +131,7 @@ export async function runDaemonServer({

const processRegistry = new ProcessRegistry();
setProcessObserver(processRegistry.createObserver());
const listenHost = resolveDaemonListenHost(env);

const sessions = new Map<string, Session>();
const refreshSessions = new Map<string, Session>();
Expand Down Expand Up @@ -395,7 +401,7 @@ export async function runDaemonServer({
try {
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(port, DAEMON_HOST, () => {
server.listen(port, listenHost, () => {
const address = server.address();
const actualPort =
address && typeof address === "object" && typeof address.port === "number"
Expand Down
21 changes: 21 additions & 0 deletions src/daemon/windows-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const WINDOWS_CONTAINER_INSTALL_MODE_ENV = "SUMMARIZE_WINDOWS_CONTAINER_MODE";
const WINDOWS_CONTAINER_MARKERS = [
"CONTAINER_SANDBOX_MOUNT_POINT",
"DOTNET_RUNNING_IN_CONTAINER",
"RUNNING_IN_CONTAINER",
] as const;

function isTruthyEnvValue(value: string | undefined): boolean {
if (!value) return false;
const normalized = value.trim().toLowerCase();
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
}

export function isWindowsContainerEnvironment(
env: Record<string, string | undefined>,
): boolean {
const override = env[WINDOWS_CONTAINER_INSTALL_MODE_ENV]?.trim().toLowerCase();
if (override === "container") return true;
if (override === "desktop") return false;
return WINDOWS_CONTAINER_MARKERS.some((key) => isTruthyEnvValue(env[key]));
}
22 changes: 22 additions & 0 deletions src/run/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { spawn } from "node:child_process";
import { accessSync, constants as fsConstants } from "node:fs";
import path from "node:path";
import type { CliProvider, SummarizeConfig } from "../config.js";
Expand Down Expand Up @@ -31,6 +32,27 @@ export function resolveExecutableInPath(
return null;
}

export async function canSpawnCommand({
command,
args = ["--help"],
env,
}: {
command: string;
args?: string[];
env: Record<string, string | undefined>;
}): Promise<boolean> {
if (!command.trim()) return false;
return new Promise((resolve) => {
const proc = spawn(command, args, {
stdio: ["ignore", "ignore", "ignore"],
env,
windowsHide: true,
});
proc.on("error", () => resolve(false));
proc.on("close", (code) => resolve(code === 0));
});
}

export function hasBirdCli(env: Record<string, string | undefined>): boolean {
return resolveExecutableInPath("bird", env) !== null;
}
Expand Down
Loading