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"]);