From 92b92a81c88c27202a463d1bb6b9b7ed2fbfe2f3 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 17 Sep 2026 15:18:12 +0200 Subject: [PATCH] perf(desktop): reduce daemon management memory overhead (#5007) Desktop and CLI helpers imported daemon bootstrap through the server root, while supervisor configuration retained the full wire schema tree. Expose existing capabilities through narrow package entrypoints and share configuration schemas without loading message definitions. Guard the dependency trees without tree-shaking so unused exports cannot hide the runtime cost. --- docs/architecture.md | 9 ++ packages/cli/src/commands/agent/logs.ts | 2 +- packages/cli/src/commands/agent/run.ts | 5 +- packages/cli/src/commands/daemon/config.ts | 4 +- .../src/commands/daemon/lifecycle.e2e.test.ts | 2 +- .../cli/src/commands/daemon/local-daemon.ts | 2 +- packages/cli/src/commands/daemon/pair.ts | 6 +- packages/cli/src/commands/daemon/restart.ts | 6 +- .../src/commands/daemon/runtime-toolchain.ts | 2 +- .../cli/src/commands/daemon/set-password.ts | 6 +- packages/cli/src/commands/daemon/start.ts | 2 +- packages/cli/src/commands/daemon/status.ts | 5 +- packages/cli/src/commands/daemon/stop.ts | 2 +- packages/cli/src/commands/onboard.ts | 5 +- packages/cli/src/commands/open.ts | 2 +- packages/cli/src/utils/client-id.ts | 2 +- packages/cli/src/utils/client.ts | 6 +- packages/cli/src/utils/daemon-target.ts | 2 +- .../25-daemon-restart-supervisor.test.ts | 2 +- .../cli/tests/32-daemon-set-password.test.ts | 2 +- packages/cli/tests/e2e/relay-host.test.ts | 2 +- .../daemon-lifecycle-renderer.electron.mjs | 2 +- packages/desktop/e2e/daemon-lifecycle.e2e.mjs | 4 +- .../desktop/src/daemon/cli/external.test.ts | 2 +- packages/desktop/src/daemon/cli/external.ts | 2 +- .../desktop/src/daemon/daemon-manager.test.ts | 2 +- packages/desktop/src/daemon/daemon-manager.ts | 2 +- .../desktop/src/daemon/runtime-paths.test.ts | 4 - packages/desktop/src/features/attachments.ts | 2 +- packages/protocol/src/agent-profile.ts | 36 ++++++++ packages/protocol/src/messages.ts | 82 ++++--------------- packages/protocol/src/plugin-config.ts | 18 ++++ packages/protocol/src/terminal-profile.ts | 13 +++ packages/server/package.json | 35 ++++++++ packages/server/src/server/config.ts | 9 ++ packages/server/src/server/daemon-instance.ts | 4 + packages/server/src/server/exports.test.ts | 55 +++++++++++++ .../server/src/server/persisted-config.ts | 10 +-- 38 files changed, 246 insertions(+), 112 deletions(-) create mode 100644 packages/protocol/src/agent-profile.ts create mode 100644 packages/protocol/src/plugin-config.ts create mode 100644 packages/protocol/src/terminal-profile.ts diff --git a/docs/architecture.md b/docs/architecture.md index 6d21103cb6..fdf7ec909a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -54,6 +54,15 @@ The heart of Paseo. A Node.js process that: All paths are under `packages/server/src/`. +Desktop and CLI import server capabilities through explicit package subpaths such as +`@getpaseo/server/daemon-control`, `/configuration`, and `/process`. The root export +loads daemon bootstrap eagerly, even when a caller only uses a path or process helper. +Keep process-management and configuration dependencies independent of bootstrap and +WebSocket message schemas. Shared configuration schemas belong in protocol leaf modules; +`messages` imports and re-exports them. This prevents every supervising process from +retaining the daemon's runtime and wire-schema allocations. The server export tests guard +these dependency trees with tree-shaking disabled to match unbundled production imports. + Project identity is daemon-global rather than session-owned. After registry bootstrap, the daemon's project Git observer keeps one non-recursive watch on each lexically equivalent active project root and listens only for the root `.git` entry, with a slow rescan as a missed-event fallback. It runs diff --git a/packages/cli/src/commands/agent/logs.ts b/packages/cli/src/commands/agent/logs.ts index 0226ed2b06..6e32690b39 100644 --- a/packages/cli/src/commands/agent/logs.ts +++ b/packages/cli/src/commands/agent/logs.ts @@ -7,7 +7,7 @@ import { } from "../../utils/timeline.js"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types"; -import { curateAgentActivity } from "@getpaseo/server"; +import { curateAgentActivity } from "@getpaseo/server/agent-activity"; export function addLogsOptions(cmd: Command): Command { return cmd diff --git a/packages/cli/src/commands/agent/run.ts b/packages/cli/src/commands/agent/run.ts index c72049347d..a8b140de1e 100644 --- a/packages/cli/src/commands/agent/run.ts +++ b/packages/cli/src/commands/agent/run.ts @@ -1,5 +1,8 @@ import { Command, Option } from "commander"; -import { getStructuredAgentResponse, StructuredAgentResponseError } from "@getpaseo/server"; +import { + getStructuredAgentResponse, + StructuredAgentResponseError, +} from "@getpaseo/server/agent-response"; import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; import { connectToDaemon } from "../../utils/client.js"; import type { diff --git a/packages/cli/src/commands/daemon/config.ts b/packages/cli/src/commands/daemon/config.ts index c07354c5d6..c4f5cbf321 100644 --- a/packages/cli/src/commands/daemon/config.ts +++ b/packages/cli/src/commands/daemon/config.ts @@ -1,11 +1,11 @@ import { addLocalDaemonOptions } from "../../utils/command-options.js"; import { Command } from "commander"; +import { readDaemonInstance } from "@getpaseo/server/daemon-control"; import { - readDaemonInstance, readPersistedConfig, getPersistedConfigValue, editPersistedConfig, -} from "@getpaseo/server"; +} from "@getpaseo/server/configuration"; import { connectToDaemon } from "../../utils/client.js"; import { withOutput, type CommandOptions } from "../../output/index.js"; diff --git a/packages/cli/src/commands/daemon/lifecycle.e2e.test.ts b/packages/cli/src/commands/daemon/lifecycle.e2e.test.ts index 6c2da1a350..b020c27e55 100644 --- a/packages/cli/src/commands/daemon/lifecycle.e2e.test.ts +++ b/packages/cli/src/commands/daemon/lifecycle.e2e.test.ts @@ -5,7 +5,7 @@ import net from "node:net"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { startDaemonInstance, readDaemonInstance } from "@getpaseo/server"; +import { startDaemonInstance, readDaemonInstance } from "@getpaseo/server/daemon-control"; import { expect, test } from "vitest"; import { connectToDaemon } from "../../utils/client.js"; diff --git a/packages/cli/src/commands/daemon/local-daemon.ts b/packages/cli/src/commands/daemon/local-daemon.ts index 049cfd0bd9..5391957ce1 100644 --- a/packages/cli/src/commands/daemon/local-daemon.ts +++ b/packages/cli/src/commands/daemon/local-daemon.ts @@ -2,7 +2,7 @@ import { Command, Option } from "commander"; import { existsSync, readFileSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; -import { startDaemonInstance, resolvePaseoHome } from "@getpaseo/server"; +import { startDaemonInstance, resolvePaseoHome } from "@getpaseo/server/daemon-control"; const require = createRequire(import.meta.url); function resolveServerRunnerFromDir(currentDir: string): string | null { const packageJsonPath = path.join(currentDir, "package.json"); diff --git a/packages/cli/src/commands/daemon/pair.ts b/packages/cli/src/commands/daemon/pair.ts index 1d9e090ee8..4c030a44f5 100644 --- a/packages/cli/src/commands/daemon/pair.ts +++ b/packages/cli/src/commands/daemon/pair.ts @@ -1,13 +1,13 @@ import { confirm, isCancel, log } from "@clack/prompts"; import { Command } from "commander"; import chalk from "chalk"; +import { generateLocalPairingOffer } from "@getpaseo/server/pairing"; +import { readDaemonInstance } from "@getpaseo/server/daemon-control"; import { - generateLocalPairingOffer, - readDaemonInstance, readPersistedConfig, editPersistedConfig, resolveConfigFromPersisted, -} from "@getpaseo/server"; +} from "@getpaseo/server/configuration"; import { connectToDaemon } from "../../utils/client.js"; import type { DaemonTarget } from "../../utils/daemon-target.js"; import { addJsonAndDaemonHostOptions, withGlobalOptions } from "../../utils/command-options.js"; diff --git a/packages/cli/src/commands/daemon/restart.ts b/packages/cli/src/commands/daemon/restart.ts index 5394fc4b53..1fdbd7137c 100644 --- a/packages/cli/src/commands/daemon/restart.ts +++ b/packages/cli/src/commands/daemon/restart.ts @@ -1,5 +1,9 @@ import { Command } from "commander"; -import { readDaemonInstance, isSameDaemonInstance, DaemonInstanceError } from "@getpaseo/server"; +import { + readDaemonInstance, + isSameDaemonInstance, + DaemonInstanceError, +} from "@getpaseo/server/daemon-control"; import { setTimeout as delay } from "node:timers/promises"; import { connectToDaemon } from "../../utils/client.js"; import { withOutput, type CommandOptions } from "../../output/index.js"; diff --git a/packages/cli/src/commands/daemon/runtime-toolchain.ts b/packages/cli/src/commands/daemon/runtime-toolchain.ts index be17439651..23fbd3bff7 100644 --- a/packages/cli/src/commands/daemon/runtime-toolchain.ts +++ b/packages/cli/src/commands/daemon/runtime-toolchain.ts @@ -1,5 +1,5 @@ import { platform } from "node:os"; -import { execCommand } from "@getpaseo/server"; +import { execCommand } from "@getpaseo/server/process"; export interface NodePathFromPidResult { nodePath: string | null; diff --git a/packages/cli/src/commands/daemon/set-password.ts b/packages/cli/src/commands/daemon/set-password.ts index cf190327e0..d572269ad1 100644 --- a/packages/cli/src/commands/daemon/set-password.ts +++ b/packages/cli/src/commands/daemon/set-password.ts @@ -1,13 +1,13 @@ import path from "node:path"; import type { Command } from "commander"; import { isCancel, password as passwordPrompt } from "@clack/prompts"; +import { hashDaemonPassword } from "@getpaseo/server/auth"; import { - hashDaemonPassword, readPersistedConfig, - resolvePaseoHome, savePersistedConfig, type PersistedConfig, -} from "@getpaseo/server"; +} from "@getpaseo/server/configuration"; +import { resolvePaseoHome } from "@getpaseo/server/daemon-control"; import type { CommandError, CommandOptions, diff --git a/packages/cli/src/commands/daemon/start.ts b/packages/cli/src/commands/daemon/start.ts index ae36e0eed0..47871e4ad3 100644 --- a/packages/cli/src/commands/daemon/start.ts +++ b/packages/cli/src/commands/daemon/start.ts @@ -1,6 +1,6 @@ import { addLocalDaemonOptions } from "../../utils/command-options.js"; import { Command } from "commander"; -import { daemonLogPath } from "@getpaseo/server"; +import { daemonLogPath } from "@getpaseo/server/daemon-control"; import { launchLocalDaemon, parseTimeoutMs, rejectRemovedLaunchFlags } from "./local-daemon.js"; import { withOutput, type CommandOptions } from "../../output/index.js"; diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts index c44e01524e..d053ef11ea 100644 --- a/packages/cli/src/commands/daemon/status.ts +++ b/packages/cli/src/commands/daemon/status.ts @@ -2,12 +2,11 @@ import { Command } from "commander"; import { DaemonConnectionError } from "@getpaseo/client/internal/daemon-client"; import { readDaemonInstance, - readPersistedConfig, - resolveConfigFromPersisted, daemonLogPath, isSameDaemonInstance, DaemonInstanceError, -} from "@getpaseo/server"; +} from "@getpaseo/server/daemon-control"; +import { readPersistedConfig, resolveConfigFromPersisted } from "@getpaseo/server/configuration"; import { connectToDaemon, buildDaemonConnectionCommandError } from "../../utils/client.js"; import { withOutput, toCommandError, type CommandOptions } from "../../output/index.js"; import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; diff --git a/packages/cli/src/commands/daemon/stop.ts b/packages/cli/src/commands/daemon/stop.ts index a94eab24d4..f2bf8b1255 100644 --- a/packages/cli/src/commands/daemon/stop.ts +++ b/packages/cli/src/commands/daemon/stop.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { stopDaemonInstance, type DaemonInstance } from "@getpaseo/server"; +import { stopDaemonInstance, type DaemonInstance } from "@getpaseo/server/daemon-control"; import { connectToDaemon } from "../../utils/client.js"; import { withOutput, type CommandOptions } from "../../output/index.js"; import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; diff --git a/packages/cli/src/commands/onboard.ts b/packages/cli/src/commands/onboard.ts index 4719e2823f..75bcee7478 100644 --- a/packages/cli/src/commands/onboard.ts +++ b/packages/cli/src/commands/onboard.ts @@ -5,10 +5,9 @@ import path from "node:path"; import { readPersistedConfig as loadPersistedConfig, savePersistedConfig, - readDaemonInstance, - waitForDaemonReady, type PersistedConfig, -} from "@getpaseo/server"; +} from "@getpaseo/server/configuration"; +import { readDaemonInstance, waitForDaemonReady } from "@getpaseo/server/daemon-control"; import { withGlobalOptions } from "../utils/command-options.js"; import type { CommandOptions } from "../output/index.js"; import { launchLocalDaemon, parseTimeoutMs } from "./daemon/local-daemon.js"; diff --git a/packages/cli/src/commands/open.ts b/packages/cli/src/commands/open.ts index 51656436e9..512a677811 100644 --- a/packages/cli/src/commands/open.ts +++ b/packages/cli/src/commands/open.ts @@ -1,7 +1,7 @@ import { existsSync } from "node:fs"; import { homedir } from "node:os"; import path from "node:path"; -import { spawnProcess } from "@getpaseo/server"; +import { spawnProcess } from "@getpaseo/server/process"; import { buildAgentDeepLink, type AgentDeepLinkTarget } from "@getpaseo/protocol/agent-deep-link"; function findDesktopApp(): string | null { diff --git a/packages/cli/src/utils/client-id.ts b/packages/cli/src/utils/client-id.ts index 145b698884..84a04b3951 100644 --- a/packages/cli/src/utils/client-id.ts +++ b/packages/cli/src/utils/client-id.ts @@ -1,4 +1,4 @@ -import { ensurePrivateDirectory } from "@getpaseo/server"; +import { ensurePrivateDirectory } from "@getpaseo/server/daemon-control"; import { readFile, writeFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { join } from "node:path"; diff --git a/packages/cli/src/utils/client.ts b/packages/cli/src/utils/client.ts index f2eae4b6a2..38d5b26571 100644 --- a/packages/cli/src/utils/client.ts +++ b/packages/cli/src/utils/client.ts @@ -1,4 +1,8 @@ -import { waitForDaemonReady, resolvePaseoHome, type DaemonInstance } from "@getpaseo/server"; +import { + waitForDaemonReady, + resolvePaseoHome, + type DaemonInstance, +} from "@getpaseo/server/daemon-control"; import { describeDaemonTarget, type DaemonTarget } from "./daemon-target.js"; export type { DaemonTarget } from "./daemon-target.js"; import { diff --git a/packages/cli/src/utils/daemon-target.ts b/packages/cli/src/utils/daemon-target.ts index 2705168159..cfe8db8a15 100644 --- a/packages/cli/src/utils/daemon-target.ts +++ b/packages/cli/src/utils/daemon-target.ts @@ -1,4 +1,4 @@ -import { resolvePaseoHome } from "@getpaseo/server"; +import { resolvePaseoHome } from "@getpaseo/server/daemon-control"; export type DaemonTarget = { kind: "instance"; home: string } | { kind: "endpoint"; host: string }; diff --git a/packages/cli/tests/25-daemon-restart-supervisor.test.ts b/packages/cli/tests/25-daemon-restart-supervisor.test.ts index d02371f722..f38caa5601 100644 --- a/packages/cli/tests/25-daemon-restart-supervisor.test.ts +++ b/packages/cli/tests/25-daemon-restart-supervisor.test.ts @@ -16,7 +16,7 @@ import { DaemonConnectionError, type WebSocketLike, } from "@getpaseo/client/internal/daemon-client"; -import { readDaemonInstance, isSameDaemonInstance } from "@getpaseo/server"; +import { readDaemonInstance, isSameDaemonInstance } from "@getpaseo/server/daemon-control"; import { runLocalPaseo } from "./helpers/local-cli.ts"; import { getAvailablePort } from "./helpers/network.ts"; diff --git a/packages/cli/tests/32-daemon-set-password.test.ts b/packages/cli/tests/32-daemon-set-password.test.ts index 5756cfe0a0..7b174e1631 100644 --- a/packages/cli/tests/32-daemon-set-password.test.ts +++ b/packages/cli/tests/32-daemon-set-password.test.ts @@ -5,7 +5,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Command } from "commander"; -import { isBearerTokenValid } from "@getpaseo/server"; +import { isBearerTokenValid } from "@getpaseo/server/auth"; import { runSetPasswordCommand, setDaemonPasswordInConfig, diff --git a/packages/cli/tests/e2e/relay-host.test.ts b/packages/cli/tests/e2e/relay-host.test.ts index 1c5e4bd8b9..bfba930f7d 100644 --- a/packages/cli/tests/e2e/relay-host.test.ts +++ b/packages/cli/tests/e2e/relay-host.test.ts @@ -6,7 +6,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildRelayWebSocketUrl } from "@getpaseo/protocol/daemon-endpoints"; import { parseConnectionOfferFromUrl } from "@getpaseo/protocol/connection-offer"; -import { generateLocalPairingOffer } from "@getpaseo/server"; +import { generateLocalPairingOffer } from "@getpaseo/server/pairing"; import { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import { WebSocket } from "ws"; import { getAvailablePort } from "../helpers/network.ts"; diff --git a/packages/desktop/e2e/daemon-lifecycle-renderer.electron.mjs b/packages/desktop/e2e/daemon-lifecycle-renderer.electron.mjs index 82f94d5d2e..d32b7d65b8 100644 --- a/packages/desktop/e2e/daemon-lifecycle-renderer.electron.mjs +++ b/packages/desktop/e2e/daemon-lifecycle-renderer.electron.mjs @@ -6,7 +6,7 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; import net from "node:net"; import { _electron as electron, expect } from "playwright/test"; -import { readDaemonInstance } from "@getpaseo/server"; +import { readDaemonInstance } from "@getpaseo/server/daemon-control"; // Run under the test's private Xvfb display; native dialog input never targets // a user's desktop. All daemon operations still cross the real preload/IPC. diff --git a/packages/desktop/e2e/daemon-lifecycle.e2e.mjs b/packages/desktop/e2e/daemon-lifecycle.e2e.mjs index 459877351c..870e0f7db5 100644 --- a/packages/desktop/e2e/daemon-lifecycle.e2e.mjs +++ b/packages/desktop/e2e/daemon-lifecycle.e2e.mjs @@ -7,12 +7,12 @@ import path from "node:path"; import net from "node:net"; import { fileURLToPath, pathToFileURL } from "node:url"; import { _electron as electron, expect } from "playwright/test"; +import { savePersistedConfig } from "@getpaseo/server/configuration"; import { - savePersistedConfig, startDaemonInstance, stopDaemonInstance, readDaemonInstance, -} from "@getpaseo/server"; +} from "@getpaseo/server/daemon-control"; const repo = fileURLToPath(new URL("../../..", import.meta.url)); const root = await mkdtemp(path.join(tmpdir(), "paseo desktop lifecycle ")); diff --git a/packages/desktop/src/daemon/cli/external.test.ts b/packages/desktop/src/daemon/cli/external.test.ts index 3a17c62916..5649c9f5c5 100644 --- a/packages/desktop/src/daemon/cli/external.test.ts +++ b/packages/desktop/src/daemon/cli/external.test.ts @@ -15,7 +15,7 @@ const mocks = vi.hoisted(() => ({ spawnProcess: vi.fn(), })); -vi.mock("@getpaseo/server", () => ({ +vi.mock("@getpaseo/server/process", () => ({ spawnProcess: mocks.spawnProcess, })); diff --git a/packages/desktop/src/daemon/cli/external.ts b/packages/desktop/src/daemon/cli/external.ts index 424ef1d163..6c9deeccb1 100644 --- a/packages/desktop/src/daemon/cli/external.ts +++ b/packages/desktop/src/daemon/cli/external.ts @@ -1,4 +1,4 @@ -import { spawnProcess } from "@getpaseo/server"; +import { spawnProcess } from "@getpaseo/server/process"; import log from "electron-log/main"; import type { NodeEntrypointInvocation } from "../node-entrypoint-launcher.js"; import { createNodeEntrypointInvocation } from "../runtime-paths.js"; diff --git a/packages/desktop/src/daemon/daemon-manager.test.ts b/packages/desktop/src/daemon/daemon-manager.test.ts index 1bd3beed33..31e0f208ef 100644 --- a/packages/desktop/src/daemon/daemon-manager.test.ts +++ b/packages/desktop/src/daemon/daemon-manager.test.ts @@ -51,7 +51,7 @@ vi.mock("electron-log/main", () => ({ }, })); -vi.mock("@getpaseo/server", () => ({ +vi.mock("@getpaseo/server/daemon-control", () => ({ resolvePaseoHome: vi.fn(() => mocks.paseoHome), spawnProcess: mocks.spawnProcess, })); diff --git a/packages/desktop/src/daemon/daemon-manager.ts b/packages/desktop/src/daemon/daemon-manager.ts index c54a252d81..a95e349647 100644 --- a/packages/desktop/src/daemon/daemon-manager.ts +++ b/packages/desktop/src/daemon/daemon-manager.ts @@ -10,7 +10,7 @@ import { readDaemonInstance, isSameDaemonInstance, type DaemonInstance, -} from "@getpaseo/server"; +} from "@getpaseo/server/daemon-control"; import { copyAttachmentFileToManagedStorage, deleteManagedAttachmentFile, diff --git a/packages/desktop/src/daemon/runtime-paths.test.ts b/packages/desktop/src/daemon/runtime-paths.test.ts index dbb456df87..7ed6e901ee 100644 --- a/packages/desktop/src/daemon/runtime-paths.test.ts +++ b/packages/desktop/src/daemon/runtime-paths.test.ts @@ -21,10 +21,6 @@ vi.mock("electron-log/main", () => ({ default: { warn: vi.fn() }, })); -vi.mock("@getpaseo/server", () => ({ - spawnProcess: vi.fn(), -})); - const originalPlatform = process.platform; const originalExecPath = process.execPath; const originalResourcesPath = process.resourcesPath; diff --git a/packages/desktop/src/features/attachments.ts b/packages/desktop/src/features/attachments.ts index 96e9aea6c9..82554a5d5a 100644 --- a/packages/desktop/src/features/attachments.ts +++ b/packages/desktop/src/features/attachments.ts @@ -1,6 +1,6 @@ import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import path from "node:path"; -import { resolvePaseoHome } from "@getpaseo/server"; +import { resolvePaseoHome } from "@getpaseo/server/daemon-control"; const ATTACHMENTS_DIRNAME = "desktop-attachments"; const ATTACHMENT_ID_PATTERN = /^[A-Za-z0-9_-]+$/; diff --git a/packages/protocol/src/agent-profile.ts b/packages/protocol/src/agent-profile.ts new file mode 100644 index 0000000000..2534984240 --- /dev/null +++ b/packages/protocol/src/agent-profile.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +/** + * A named launch bundle: a provider plus the agent-config values a client would + * otherwise set one control at a time. Field names mirror `AgentSessionConfig` + * so applying a profile is a copy rather than a translation table. + * + * There is deliberately no system prompt here. `AgentSessionConfig.systemPrompt` + * is creation-only, so a profile carrying one would apply when starting a new + * agent and silently do nothing when applied to a running one. + */ +export const AgentProfileSchema = z + .object({ + id: z.string(), + name: z.string(), + /** A key into the client's icon registry, not a glyph. Unknown keys draw the default. */ + icon: z.string().optional(), + /** An identity colour name shared with host badges. Unknown values draw unthemed. */ + color: z.string().optional(), + provider: z.string(), + model: z.string().optional(), + modeId: z.string().optional(), + thinkingOptionId: z.string().optional(), + featureValues: z.record(z.string(), z.unknown()).optional(), + /** Free text, surfaced to orchestrating agents by the `list_profiles` MCP tool. */ + notes: z.string().optional(), + }) + .passthrough(); + +export type AgentProfile = z.infer; + +export const AgentSkillSelectionSchema = z.discriminatedUnion("mode", [ + z.object({ mode: z.literal("all") }).strict(), + z.object({ mode: z.literal("custom"), skills: z.array(z.string()) }).strict(), +]); +export type AgentSkillSelection = z.infer; diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 5eb84d5850..f174a2dfe0 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -1,3 +1,21 @@ +import { AgentProfileSchema, AgentSkillSelectionSchema } from "./agent-profile.js"; +export { + AgentProfileSchema, + AgentSkillSelectionSchema, + type AgentProfile, + type AgentSkillSelection, +} from "./agent-profile.js"; +import { PluginIdSchema, PluginRequirementsSchema, PluginSourceSchema } from "./plugin-config.js"; +export { + PluginIdSchema, + PluginRequirementsSchema, + DirectoryPluginSourceSchema, + PluginSourceSchema, + type PluginRequirements, + type PluginSource, +} from "./plugin-config.js"; +import { TerminalProfileSchema } from "./terminal-profile.js"; +export { TerminalProfileSchema, type TerminalProfile } from "./terminal-profile.js"; import { z } from "zod"; import { TerminalActivitySchema } from "./terminal-activity.js"; import { CLIENT_CAPS } from "./client-capabilities.js"; @@ -140,47 +158,6 @@ const MutableMetadataGenerationConfigSchema = z }) .passthrough(); -export const TerminalProfileSchema = z - .object({ - id: z.string(), - name: z.string(), - command: z.string(), - args: z.array(z.string()).optional(), - icon: z.string().optional(), - }) - .passthrough(); - -export type TerminalProfile = z.infer; - -/** - * A named launch bundle: a provider plus the agent-config values a client would - * otherwise set one control at a time. Field names mirror `AgentSessionConfig` - * so applying a profile is a copy rather than a translation table. - * - * There is deliberately no system prompt here. `AgentSessionConfig.systemPrompt` - * is creation-only, so a profile carrying one would apply when starting a new - * agent and silently do nothing when applied to a running one. - */ -export const AgentProfileSchema = z - .object({ - id: z.string(), - name: z.string(), - /** A key into the client's icon registry, not a glyph. Unknown keys draw the default. */ - icon: z.string().optional(), - /** An identity colour name shared with host badges. Unknown values draw unthemed. */ - color: z.string().optional(), - provider: z.string(), - model: z.string().optional(), - modeId: z.string().optional(), - thinkingOptionId: z.string().optional(), - featureValues: z.record(z.string(), z.unknown()).optional(), - /** Free text, surfaced to orchestrating agents by the `list_profiles` MCP tool. */ - notes: z.string().optional(), - }) - .passthrough(); - -export type AgentProfile = z.infer; - const MutableBrowserToolsConfigSchema = z .object({ enabled: z.boolean().default(false), @@ -192,29 +169,6 @@ const MutableRelayConfigSchema = z }) .passthrough(); -export const PluginIdSchema = z.string().regex(/^[a-z][a-z0-9-]*$/); -// Semver validation belongs at the manifest/runtime boundary, not on the wire. -export const PluginRequirementsSchema = z.object({ paseo: z.string().optional() }); -export type PluginRequirements = z.infer; - -export const DirectoryPluginSourceSchema = z - .object({ - source: z.literal("directory"), - path: z.string().min(1), - enabled: z.boolean().optional(), - }) - .strict(); - -export const PluginSourceSchema = z.discriminatedUnion("source", [DirectoryPluginSourceSchema]); - -export type PluginSource = z.infer; - -export const AgentSkillSelectionSchema = z.discriminatedUnion("mode", [ - z.object({ mode: z.literal("all") }).strict(), - z.object({ mode: z.literal("custom"), skills: z.array(z.string()) }).strict(), -]); -export type AgentSkillSelection = z.infer; - export const MutableDaemonConfigSchema = z .object({ // COMPAT(relayConfig): added in v0.2.6, remove after 2027-01-31 when old daemons are unsupported. diff --git a/packages/protocol/src/plugin-config.ts b/packages/protocol/src/plugin-config.ts new file mode 100644 index 0000000000..69816e539e --- /dev/null +++ b/packages/protocol/src/plugin-config.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const PluginIdSchema = z.string().regex(/^[a-z][a-z0-9-]*$/); +// Semver validation belongs at the manifest/runtime boundary, not on the wire. +export const PluginRequirementsSchema = z.object({ paseo: z.string().optional() }); +export type PluginRequirements = z.infer; + +export const DirectoryPluginSourceSchema = z + .object({ + source: z.literal("directory"), + path: z.string().min(1), + enabled: z.boolean().optional(), + }) + .strict(); + +export const PluginSourceSchema = z.discriminatedUnion("source", [DirectoryPluginSourceSchema]); + +export type PluginSource = z.infer; diff --git a/packages/protocol/src/terminal-profile.ts b/packages/protocol/src/terminal-profile.ts new file mode 100644 index 0000000000..282084c0c5 --- /dev/null +++ b/packages/protocol/src/terminal-profile.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const TerminalProfileSchema = z + .object({ + id: z.string(), + name: z.string(), + command: z.string(), + args: z.array(z.string()).optional(), + icon: z.string().optional(), + }) + .passthrough(); + +export type TerminalProfile = z.infer; diff --git a/packages/server/package.json b/packages/server/package.json index 0b64fa516c..2fef02847d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -27,6 +27,41 @@ "types": "./dist/server/terminal/agent-hooks/provider-registry.d.ts", "source": "./src/terminal/agent-hooks/provider-registry.ts", "default": "./dist/server/terminal/agent-hooks/provider-registry.js" + }, + "./daemon-control": { + "types": "./dist/server/server/daemon-instance.d.ts", + "source": "./src/server/daemon-instance.ts", + "default": "./dist/server/server/daemon-instance.js" + }, + "./configuration": { + "types": "./dist/server/server/config.d.ts", + "source": "./src/server/config.ts", + "default": "./dist/server/server/config.js" + }, + "./process": { + "types": "./dist/server/utils/spawn.d.ts", + "source": "./src/utils/spawn.ts", + "default": "./dist/server/utils/spawn.js" + }, + "./auth": { + "types": "./dist/server/server/auth.d.ts", + "source": "./src/server/auth.ts", + "default": "./dist/server/server/auth.js" + }, + "./pairing": { + "types": "./dist/server/server/pairing-offer.d.ts", + "source": "./src/server/pairing-offer.ts", + "default": "./dist/server/server/pairing-offer.js" + }, + "./agent-activity": { + "types": "./dist/server/server/agent/activity-curator.d.ts", + "source": "./src/server/agent/activity-curator.ts", + "default": "./dist/server/server/agent/activity-curator.js" + }, + "./agent-response": { + "types": "./dist/server/server/agent/agent-response-loop.d.ts", + "source": "./src/server/agent/agent-response-loop.ts", + "default": "./dist/server/server/agent/agent-response-loop.js" } }, "publishConfig": { diff --git a/packages/server/src/server/config.ts b/packages/server/src/server/config.ts index 8a8eb39a67..a683ec69fb 100644 --- a/packages/server/src/server/config.ts +++ b/packages/server/src/server/config.ts @@ -26,6 +26,15 @@ import type { RequestedSpeechProviders } from "./speech/speech-types.js"; import { mergeHostnames, parseHostnamesEnv, type HostnamesConfig } from "./hostnames.js"; import { resolveGitProcessPolicy } from "../utils/git-process-scheduler.js"; +export { + loadPersistedConfig, + readPersistedConfig, + savePersistedConfig, + getPersistedConfigValue, + editPersistedConfig, + type PersistedConfig, +} from "./persisted-config.js"; + const DEFAULT_PORT = 6767; const DEFAULT_RELAY_ENDPOINT = "relay.paseo.sh:443"; const DEFAULT_APP_BASE_URL = "https://app.paseo.sh"; diff --git a/packages/server/src/server/daemon-instance.ts b/packages/server/src/server/daemon-instance.ts index f3101f3b28..6ef315e58e 100644 --- a/packages/server/src/server/daemon-instance.ts +++ b/packages/server/src/server/daemon-instance.ts @@ -23,6 +23,10 @@ const killTree = (pid: number, signal: string): Promise => }), ); +// Process ownership only: desktop and CLI load this entry in their own processes. +// Keep daemon bootstrap and WebSocket schemas out of its dependency tree. +export { resolvePaseoHome } from "./paseo-home.js"; +export { ensurePrivateDirectory } from "./private-files.js"; export { daemonLaunchEnvironment } from "./config-environment.js"; export { isSamePidLock as isSameDaemonInstance, diff --git a/packages/server/src/server/exports.test.ts b/packages/server/src/server/exports.test.ts index 9c9a5af9bb..a67f77bc7b 100644 --- a/packages/server/src/server/exports.test.ts +++ b/packages/server/src/server/exports.test.ts @@ -17,3 +17,58 @@ test("keeps daemon-client APIs out of the server public entry", async () => { expect(serverExports).not.toHaveProperty(name); } }); + +// Keep the emitted dependency tree intact: production runs unbundled modules, +// so tree-shaking must not hide the cost of an unused re-export. +test.each([ + ["desktop daemon management", "../../../desktop/src/daemon/daemon-manager.ts"], + ["supervisor", "../../scripts/supervisor-entrypoint.ts"], + ["daemon control", "@getpaseo/server/daemon-control"], + ["configuration", "@getpaseo/server/configuration"], +])("%s does not load the daemon runtime or wire schemas", async (_name, entry) => { + const inputs = await runtimeDependencies(entry); + expect(inputs.filter((file) => /\/(?:bootstrap|messages)\.[jt]s$/.test(file))).toEqual([]); +}); + +test("CLI command registration does not load the daemon implementation", async () => { + const inputs = await runtimeDependencies("../../../cli/src/cli.ts"); + expect( + inputs.filter((file) => /\/(?:bootstrap|agent-manager|session)\.[jt]s$/.test(file)), + ).toEqual([]); +}); + +async function runtimeDependencies(entry: string): Promise { + const { build } = await import("esbuild"); + const { fileURLToPath } = await import("node:url"); + const { isAbsolute } = await import("node:path"); + const result = await build({ + stdin: { + contents: `import ${JSON.stringify(entry.startsWith("@") ? entry : fileURLToPath(new URL(entry, import.meta.url)))};`, + resolveDir: fileURLToPath(new URL(".", import.meta.url)), + }, + bundle: true, + write: false, + metafile: true, + treeShaking: false, + platform: "node", + // Server unit CI builds dependency workspaces, then tests server source. + conditions: ["source"], + format: "esm", + logLevel: "silent", + plugins: [ + { + name: "external-vendors", + setup(builder) { + builder.onResolve({ filter: /^[^./]/ }, ({ path }) => { + if (!isAbsolute(path) && !path.startsWith("@getpaseo/")) { + return { path, external: true }; + } + }); + }, + }, + ], + }); + const inputs = Object.keys(result.metafile.inputs); + expect(inputs.length).toBeGreaterThan(1); + return inputs; +} diff --git a/packages/server/src/server/persisted-config.ts b/packages/server/src/server/persisted-config.ts index 678e72370d..953dde62d2 100644 --- a/packages/server/src/server/persisted-config.ts +++ b/packages/server/src/server/persisted-config.ts @@ -9,13 +9,9 @@ import { } from "./agent/provider-launch-config.js"; import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js"; import { ensurePrivateFile, writePrivateFileAtomicSync } from "./private-files.js"; -import { - AgentProfileSchema, - AgentSkillSelectionSchema, - PluginIdSchema, - PluginSourceSchema, - TerminalProfileSchema, -} from "@getpaseo/protocol/messages"; +import { AgentProfileSchema, AgentSkillSelectionSchema } from "@getpaseo/protocol/agent-profile"; +import { PluginIdSchema, PluginSourceSchema } from "@getpaseo/protocol/plugin-config"; +import { TerminalProfileSchema } from "@getpaseo/protocol/terminal-profile"; import { PaseoServicePortAllocationSchema } from "@getpaseo/protocol/paseo-config-schema"; export const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);