@getpaseo/server 0.1.101 → 0.1.102

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.
Files changed (141) hide show
  1. package/.env.example +8 -0
  2. package/dist/scripts/supervisor.js +26 -8
  3. package/dist/server/server/agent/activity-curator.d.ts +17 -0
  4. package/dist/server/server/agent/activity-curator.js +101 -24
  5. package/dist/server/server/agent/agent-manager.js +5 -1
  6. package/dist/server/server/agent/agent-sdk-types.d.ts +7 -2
  7. package/dist/server/server/agent/provider-registry.js +12 -11
  8. package/dist/server/server/agent/provider-snapshot-manager.d.ts +8 -1
  9. package/dist/server/server/agent/provider-snapshot-manager.js +78 -33
  10. package/dist/server/server/agent/providers/acp-agent.d.ts +13 -0
  11. package/dist/server/server/agent/providers/acp-agent.js +37 -1
  12. package/dist/server/server/agent/providers/claude/agent.js +51 -14
  13. package/dist/server/server/agent/providers/claude/query.d.ts +3 -0
  14. package/dist/server/server/agent/providers/claude/query.js +4 -2
  15. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +2 -1
  16. package/dist/server/server/agent/providers/generic-acp-agent.js +2 -1
  17. package/dist/server/server/agent/providers/kiro-acp-agent.d.ts +17 -0
  18. package/dist/server/server/agent/providers/kiro-acp-agent.js +78 -0
  19. package/dist/server/server/agent/providers/mock-load-test-agent.js +8 -0
  20. package/dist/server/server/agent/providers/opencode/paths.d.ts +2 -0
  21. package/dist/server/server/agent/providers/opencode/paths.js +7 -0
  22. package/dist/server/server/agent/providers/opencode/server-manager.d.ts +2 -0
  23. package/dist/server/server/agent/providers/opencode/server-manager.js +34 -5
  24. package/dist/server/server/agent/providers/opencode-agent.d.ts +4 -0
  25. package/dist/server/server/agent/providers/opencode-agent.js +14 -2
  26. package/dist/server/server/agent/providers/pi/agent.d.ts +3 -0
  27. package/dist/server/server/agent/providers/pi/agent.js +9 -3
  28. package/dist/server/server/agent/providers/provider-image-output.js +11 -6
  29. package/dist/server/server/agent/tools/paseo-tools.d.ts +1 -1
  30. package/dist/server/server/agent/tools/paseo-tools.js +0 -2
  31. package/dist/server/server/bootstrap.d.ts +7 -1
  32. package/dist/server/server/bootstrap.js +18 -0
  33. package/dist/server/server/config.d.ts +2 -0
  34. package/dist/server/server/config.js +57 -1
  35. package/dist/server/server/daemon-worker.js +19 -7
  36. package/dist/server/server/lifecycle-reasons.d.ts +4 -0
  37. package/dist/server/server/lifecycle-reasons.js +6 -0
  38. package/dist/server/server/persisted-config.d.ts +13 -2
  39. package/dist/server/server/persisted-config.js +30 -11
  40. package/dist/server/server/process-diagnostics.d.ts +17 -0
  41. package/dist/server/server/process-diagnostics.js +22 -0
  42. package/dist/server/server/relay-transport.js +1 -0
  43. package/dist/server/server/resolve-worktree-creation-intent.js +3 -1
  44. package/dist/server/server/session/daemon/daemon-self-update-session-controller.d.ts +32 -0
  45. package/dist/server/server/session/daemon/daemon-self-update-session-controller.js +88 -0
  46. package/dist/server/server/session/daemon/daemon-self-updater.d.ts +32 -0
  47. package/dist/server/server/session/daemon/daemon-self-updater.js +56 -0
  48. package/dist/server/server/session/daemon/daemon-session.d.ts +12 -0
  49. package/dist/server/server/session/daemon/daemon-session.js +12 -0
  50. package/dist/server/server/session/daemon/diagnostics.js +10 -0
  51. package/dist/server/server/session/daemon/install-origin.d.ts +7 -0
  52. package/dist/server/server/session/daemon/install-origin.js +64 -0
  53. package/dist/server/server/session/daemon/npm-global-cli.d.ts +29 -0
  54. package/dist/server/server/session/daemon/npm-global-cli.js +98 -0
  55. package/dist/server/server/session/provider/provider-catalog-session.js +8 -4
  56. package/dist/server/server/session.d.ts +5 -3
  57. package/dist/server/server/session.js +74 -32
  58. package/dist/server/server/speech/providers/openai/config.d.ts +0 -2
  59. package/dist/server/server/speech/providers/openai/config.js +71 -34
  60. package/dist/server/server/speech/providers/openai/runtime.js +4 -4
  61. package/dist/server/server/web-ui.d.ts +10 -0
  62. package/dist/server/server/web-ui.js +205 -0
  63. package/dist/server/server/websocket/runtime-metrics.d.ts +3 -0
  64. package/dist/server/server/websocket-server.d.ts +3 -0
  65. package/dist/server/server/websocket-server.js +190 -32
  66. package/dist/server/services/quota-fetcher/manifest.js +5 -0
  67. package/dist/server/services/quota-fetcher/providers/minimax.d.ts +29 -0
  68. package/dist/server/services/quota-fetcher/providers/minimax.js +227 -0
  69. package/dist/server/terminal/agent-hooks/agent-hook-installer.js +2 -2
  70. package/dist/server/utils/checkout-git.js +156 -3
  71. package/dist/server/utils/directory-suggestions.js +1 -4
  72. package/dist/server/utils/path.d.ts +2 -0
  73. package/dist/server/utils/path.js +13 -0
  74. package/dist/server/utils/worktree.d.ts +1 -0
  75. package/dist/server/utils/worktree.js +92 -11
  76. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css +1 -0
  77. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.br +0 -0
  78. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.gz +0 -0
  79. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-5fa9347afb28c7d61bbf73684f3bdafa.js +1 -0
  80. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-5fa9347afb28c7d61bbf73684f3bdafa.js.br +0 -0
  81. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-5fa9347afb28c7d61bbf73684f3bdafa.js.gz +0 -0
  82. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-35159ed50cf218b741c5dab5a6f4a889.js +1 -0
  83. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-35159ed50cf218b741c5dab5a6f4a889.js.br +0 -0
  84. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-35159ed50cf218b741c5dab5a6f4a889.js.gz +0 -0
  85. package/dist/server/web-ui/_expo/static/js/web/index-cf63ba3f4de4164d2d9c3738e1b791b2.js +16157 -0
  86. package/dist/server/web-ui/_expo/static/js/web/index-cf63ba3f4de4164d2d9c3738e1b791b2.js.br +0 -0
  87. package/dist/server/web-ui/_expo/static/js/web/index-cf63ba3f4de4164d2d9c3738e1b791b2.js.gz +0 -0
  88. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-6a6cfdd9f06d4fd2b05adc1a8771c58c.js +1 -0
  89. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-6a6cfdd9f06d4fd2b05adc1a8771c58c.js.br +0 -0
  90. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-6a6cfdd9f06d4fd2b05adc1a8771c58c.js.gz +0 -0
  91. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-b31888f9a7de4f0c52d6a58fcddbcb20.js +3 -0
  92. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-b31888f9a7de4f0c52d6a58fcddbcb20.js.br +0 -0
  93. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-b31888f9a7de4f0c52d6a58fcddbcb20.js.gz +0 -0
  94. package/dist/server/web-ui/apple-touch-icon.png +0 -0
  95. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
  96. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
  97. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
  98. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
  99. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
  100. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
  101. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
  102. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
  103. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
  104. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
  105. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
  106. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
  107. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
  108. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
  109. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
  110. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
  111. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
  112. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
  113. package/dist/server/web-ui/assets/assets/images/editor-apps/antigravity.6e91a685c33435e0b466a56db86cf141.png +0 -0
  114. package/dist/server/web-ui/assets/assets/images/editor-apps/cursor.c31d6bce4fe9aadc3fe59962f4c4fcf3.png +0 -0
  115. package/dist/server/web-ui/assets/assets/images/editor-apps/file-explorer.3e15e8f72c825c85ce336bcb0cdef776.png +0 -0
  116. package/dist/server/web-ui/assets/assets/images/editor-apps/finder.7f68fc2c475621a672e1be09309d5567.png +0 -0
  117. package/dist/server/web-ui/assets/assets/images/editor-apps/vscode.832bdb4c685d930f1c864c793703600b.png +0 -0
  118. package/dist/server/web-ui/assets/assets/images/editor-apps/webstorm.aa5dc2cd8c20cc0a155c4c5c5ab3c5f5.png +0 -0
  119. package/dist/server/web-ui/assets/assets/images/editor-apps/zed.f3a670b7f9aa226da4fe53fb86f1abbd.png +0 -0
  120. package/dist/server/web-ui/assets/assets/images/favicon-dark-attention.882b3a27dcb2073e9e31b334f9ed9728.png +0 -0
  121. package/dist/server/web-ui/assets/assets/images/favicon-dark-running.8112342ff0d39e047a7f8d4fad9402f3.png +0 -0
  122. package/dist/server/web-ui/assets/assets/images/favicon-dark.8005ed36ac07a5a7c60de25780897bd4.png +0 -0
  123. package/dist/server/web-ui/assets/assets/images/favicon-light-attention.882b3a27dcb2073e9e31b334f9ed9728.png +0 -0
  124. package/dist/server/web-ui/assets/assets/images/favicon-light-running.8112342ff0d39e047a7f8d4fad9402f3.png +0 -0
  125. package/dist/server/web-ui/assets/assets/images/favicon-light.8005ed36ac07a5a7c60de25780897bd4.png +0 -0
  126. package/dist/server/web-ui/assets/assets/images/notification-icon.3bf81d33ddbf380606bdd248ba83e158.png +0 -0
  127. package/dist/server/web-ui/favicon.ico +0 -0
  128. package/dist/server/web-ui/index.html +90 -0
  129. package/dist/server/web-ui/index.html.br +0 -0
  130. package/dist/server/web-ui/index.html.gz +0 -0
  131. package/dist/server/web-ui/manifest.json +27 -0
  132. package/dist/server/web-ui/manifest.json.br +0 -0
  133. package/dist/server/web-ui/manifest.json.gz +0 -0
  134. package/dist/server/web-ui/metadata.json +1 -0
  135. package/dist/server/web-ui/metadata.json.br +1 -0
  136. package/dist/server/web-ui/metadata.json.gz +0 -0
  137. package/dist/server/web-ui/pwa-icon-192.png +0 -0
  138. package/dist/server/web-ui/pwa-icon-512.png +0 -0
  139. package/dist/server/web-ui/robots.txt +2 -0
  140. package/dist/src/server/persisted-config.js +30 -11
  141. package/package.json +7 -7
@@ -1,4 +1,5 @@
1
1
  import { createOpencodeClient, } from "@opencode-ai/sdk/v2/client";
2
+ import fs from "node:fs/promises";
2
3
  import { createPathEquivalenceMatcher } from "../../../utils/path.js";
3
4
  import pLimit from "p-limit";
4
5
  import { z } from "zod";
@@ -11,6 +12,7 @@ import { execCommand } from "../../../utils/spawn.js";
11
12
  import { buildToolCallDisplayModel } from "@getpaseo/protocol/tool-call-display";
12
13
  import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
13
14
  import { OpenCodeServerManager, } from "./opencode/server-manager.js";
15
+ import { resolveOpenCodeHomeDir } from "./opencode/paths.js";
14
16
  import { formatProviderDiagnostic, formatProviderDiagnosticError, buildBinaryDiagnosticRows, buildCommandResolutionDiagnosticRows, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
15
17
  import { runProviderTurn } from "./provider-runner.js";
16
18
  import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
@@ -847,6 +849,7 @@ export const __openCodeInternals = {
847
849
  resolveOpenCodeSelectedModelContextWindow,
848
850
  isSelectableOpenCodeAgent,
849
851
  mapOpenCodeAgentToMode,
852
+ resolveOpenCodeHomeDir,
850
853
  get OpenCodeAgentSession() {
851
854
  return OpenCodeAgentSession;
852
855
  },
@@ -867,8 +870,10 @@ export class OpenCodeAgentClient {
867
870
  deps.serverManager ??
868
871
  OpenCodeServerManager.getInstance(this.logger, runtimeSettings, {
869
872
  managedProcesses: deps.managedProcesses,
873
+ resolveHomeDir: deps.resolveHomeDir,
870
874
  });
871
875
  this.createOpenCodeClient = deps.createClient ?? createSdkOpenCodeClient;
876
+ this.resolveHomeDir = deps.resolveHomeDir ?? resolveOpenCodeHomeDir;
872
877
  }
873
878
  async createSession(config, launchContext, options) {
874
879
  const openCodeConfig = this.assertConfig(config);
@@ -930,9 +935,16 @@ export class OpenCodeAgentClient {
930
935
  ? await this.serverManager.acquireNew()
931
936
  : await this.serverManager.acquireCurrent();
932
937
  const { url } = acquisition.server;
933
- const directory = options.cwd;
934
- const client = this.createOpenCodeClient({ baseUrl: url, directory });
938
+ const isGlobalCatalog = options.scope === "global";
935
939
  try {
940
+ // OpenCode treats the catalog directory as a workspace. The global catalog
941
+ // is not a project, so use the neutral OpenCode home instead of user home.
942
+ const directory = isGlobalCatalog ? this.resolveHomeDir() : options.cwd;
943
+ if (isGlobalCatalog) {
944
+ await fs.mkdir(directory, { recursive: true });
945
+ this.logger.debug({ directory }, "opencode catalog refresh: using opencode-home for global provider catalog");
946
+ }
947
+ const client = this.createOpenCodeClient({ baseUrl: url, directory });
936
948
  const [models, modes] = await Promise.all([
937
949
  this.fetchModelsFromClient(client, directory),
938
950
  this.fetchModesFromClient(client, directory),
@@ -6,6 +6,7 @@ import type { PiRuntime, PiRuntimeSession } from "./runtime.js";
6
6
  import type { PiCommandsRpcType, PiSessionState } from "./rpc-types.js";
7
7
  export declare const PiProviderParamsSchema: z.ZodObject<{
8
8
  sessionDir: z.ZodOptional<z.ZodString>;
9
+ extensionTimeoutMs: z.ZodDefault<z.ZodNumber>;
9
10
  }, z.core.$strict>;
10
11
  interface PiRpcAgentClientOptions {
11
12
  logger: Logger;
@@ -23,6 +24,7 @@ interface PiRpcAgentSessionOptions {
23
24
  initialState: PiSessionState;
24
25
  capabilities: AgentCapabilityFlags;
25
26
  cleanup?: () => void;
27
+ extensionTimeoutMs?: number;
26
28
  }
27
29
  export declare function transformPiModels(models: AgentModelDefinition[]): AgentModelDefinition[];
28
30
  export declare class PiRpcAgentSession implements AgentSession {
@@ -50,6 +52,7 @@ export declare class PiRpcAgentSession implements AgentSession {
50
52
  private readonly runtimeSession;
51
53
  private readonly config;
52
54
  private readonly cleanup?;
55
+ private readonly extensionTimeoutMs;
53
56
  get id(): string | null;
54
57
  run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult>;
55
58
  startTurn(prompt: AgentPromptInput, _options?: AgentRunOptions): Promise<StartTurnResult>;
@@ -22,7 +22,7 @@ const PASEO_PI_TREE_EXTENSION_COMMAND = "paseo_tree";
22
22
  const PASEO_PI_CAPTURE_EXTENSION_COMMAND = "paseo_capture_entries";
23
23
  const PASEO_PI_ENTRY_CAPTURE_MARKER = "PASEO_ENTRY_CAPTURE";
24
24
  const PASEO_PI_COMMAND_RESULT_MARKER = "PASEO_COMMAND_RESULT";
25
- const PASEO_PI_EXTENSION_RESULT_TIMEOUT_MS = 10000;
25
+ const DEFAULT_PI_EXTENSION_RESULT_TIMEOUT_MS = 30000;
26
26
  const QUESTION_RESPONSE_HEADER = "Response";
27
27
  const QUESTION_COMMENT_HEADER = "Comment";
28
28
  const PI_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
@@ -30,6 +30,7 @@ const COMBINED_ASK_USER_METADATA = "ask_user_select_optional_comment";
30
30
  export const PiProviderParamsSchema = z
31
31
  .object({
32
32
  sessionDir: z.string().min(1).optional(),
33
+ extensionTimeoutMs: z.number().int().positive().default(DEFAULT_PI_EXTENSION_RESULT_TIMEOUT_MS),
33
34
  })
34
35
  .strict();
35
36
  const PI_HANDLED_BUILTIN_SLASH_COMMANDS = [
@@ -715,6 +716,7 @@ export class PiRpcAgentSession {
715
716
  normalizePiThinkingOption(options.config.thinkingOptionId) ??
716
717
  this.state.thinkingLevel ??
717
718
  null;
719
+ this.extensionTimeoutMs = options.extensionTimeoutMs ?? DEFAULT_PI_EXTENSION_RESULT_TIMEOUT_MS;
718
720
  this.runtimeSession.onEvent((event) => {
719
721
  this.handleRuntimeEvent(event);
720
722
  });
@@ -1065,7 +1067,7 @@ export class PiRpcAgentSession {
1065
1067
  const timer = setTimeout(() => {
1066
1068
  this.pendingExtensionResults.delete(requestId);
1067
1069
  reject(new Error(`Pi extension result timed out for request ${requestId}`));
1068
- }, PASEO_PI_EXTENSION_RESULT_TIMEOUT_MS);
1070
+ }, this.extensionTimeoutMs);
1069
1071
  this.pendingExtensionResults.set(requestId, { resolve, reject, timer });
1070
1072
  });
1071
1073
  }
@@ -1489,6 +1491,7 @@ export class PiRpcAgentClient {
1489
1491
  initialState: await runtimeSession.getState(),
1490
1492
  capabilities: withPiMcpCapability(mcpConfig !== null),
1491
1493
  cleanup: combineCleanup([mcpConfig?.cleanup, paseoExtension.cleanup]),
1494
+ extensionTimeoutMs: this.providerParams.extensionTimeoutMs,
1492
1495
  });
1493
1496
  }
1494
1497
  catch (error) {
@@ -1531,6 +1534,7 @@ export class PiRpcAgentClient {
1531
1534
  initialState: await runtimeSession.getState(),
1532
1535
  capabilities: withPiMcpCapability(mcpConfig !== null),
1533
1536
  cleanup: combineCleanup([mcpConfig?.cleanup, paseoExtension.cleanup]),
1537
+ extensionTimeoutMs: this.providerParams.extensionTimeoutMs,
1534
1538
  });
1535
1539
  }
1536
1540
  catch (error) {
@@ -1541,7 +1545,9 @@ export class PiRpcAgentClient {
1541
1545
  }
1542
1546
  }
1543
1547
  async fetchCatalog(options) {
1544
- const runtimeSession = await this.runtime.startSession({ cwd: options.cwd });
1548
+ const runtimeSession = await this.runtime.startSession({
1549
+ cwd: options.scope === "global" ? homedir() : options.cwd,
1550
+ });
1545
1551
  try {
1546
1552
  const models = transformPiModels((await runtimeSession.getAvailableModels(PI_CATALOG_REQUEST_TIMEOUT_MS)).map(mapPiModel));
1547
1553
  return { models, modes: [] };
@@ -44,11 +44,10 @@ export function materializeProviderImage(image) {
44
44
  fsSync.writeFileSync(filePath, bytes);
45
45
  return { path: filePath };
46
46
  }
47
- // Recognizes the markdown renderProviderImageOutputAsAssistantMarkdown emits for a materialized
48
- // provider image: its source is a content-hashed file in the attachments dir. Matching the full
49
- // <hash>.<ext> shape (not just a leading "![") keeps user-authored text from being mistaken for a
50
- // provider image when it reaches the history-replay filter. The separator class allows one-or-more
51
- // because on Windows the path uses "\\" and escapeMarkdownImageSource doubles each backslash.
47
+ // Recognizes markdown rendered for a materialized provider image: its source is a content-hashed
48
+ // file in the attachments dir. Matching the full <hash>.<ext> shape (not just a leading "![")
49
+ // keeps user-authored text from being mistaken for a provider image during history replay. The
50
+ // separator still accepts old doubled-backslash Windows history; new Windows output uses file URIs.
52
51
  const PROVIDER_IMAGE_MARKDOWN = new RegExp(`^!\\[[^\\]]*\\]\\([^)]*${PROVIDER_IMAGE_ATTACHMENT_DIR}[/\\\\]+[0-9a-f]{64}\\.[a-z0-9]+\\)`);
53
52
  export function isProviderImageMarkdown(text) {
54
53
  return PROVIDER_IMAGE_MARKDOWN.test(text);
@@ -63,8 +62,14 @@ function isDataImageSource(source) {
63
62
  function escapeMarkdownImageAlt(value) {
64
63
  return value.replace(/\\/g, "\\\\").replace(/\]/g, "\\]");
65
64
  }
65
+ function markdownImageSource(value) {
66
+ if (/^[A-Za-z]:[\\/]/.test(value)) {
67
+ return `file:///${value.replace(/\\/g, "/")}`;
68
+ }
69
+ return value;
70
+ }
66
71
  function escapeMarkdownImageSource(value) {
67
- return value.replace(/\\/g, "\\\\").replace(/\)/g, "\\)");
72
+ return markdownImageSource(value).replace(/\\/g, "\\\\").replace(/\)/g, "\\)");
68
73
  }
69
74
  export function renderProviderImageOutputAsAssistantMarkdown(image, options = {}) {
70
75
  const source = nonEmptyString(image.path) ?? nonEmptyString(image.url);
@@ -6,7 +6,7 @@ import type { VoiceCallerContext, VoiceSpeakHandler } from "../../voice-types.js
6
6
  import type { TerminalManager } from "../../../terminal/terminal-manager.js";
7
7
  import type { CreatePaseoWorktreeWorkflowFn } from "../../worktree-session.js";
8
8
  import type { ScheduleService } from "../../schedule/service.js";
9
- import { type ProviderSnapshotManager } from "../provider-snapshot-manager.js";
9
+ import type { ProviderSnapshotManager } from "../provider-snapshot-manager.js";
10
10
  import type { GitHubService } from "../../../services/github-service.js";
11
11
  import type { WorkspaceGitService } from "../../workspace-git-service.js";
12
12
  import type { PaseoToolCatalog } from "./types.js";
@@ -11,7 +11,6 @@ import { WaitForAgentTracker } from "../wait-for-agent-tracker.js";
11
11
  import { createAgentCommand } from "../create-agent/create.js";
12
12
  import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../../path-utils.js";
13
13
  import { ScheduleRunSchema, ScheduleSummarySchema, StoredScheduleSchema, } from "@getpaseo/protocol/schedule/types";
14
- import { resolveSnapshotCwd } from "../provider-snapshot-manager.js";
15
14
  import { AgentModelSchema, AgentProviderEnum, AgentStatusEnum, ProviderModeSchema, ProviderSummarySchema, parseDurationString, resolveRequiredProviderModel, sanitizePermissionRequest, serializeSnapshotWithMetadata, toScheduleSummary, waitForAgentWithTimeout, } from "../mcp-shared.js";
16
15
  import { sendPromptToAgent, setupFinishNotification } from "../agent-prompt.js";
17
16
  import { respondToAgentPermission } from "../permission-response.js";
@@ -1760,7 +1759,6 @@ export function createPaseoToolCatalog(options) {
1760
1759
  },
1761
1760
  }, async ({ provider }) => {
1762
1761
  const models = await providerSnapshotManager.listModels({
1763
- cwd: resolveSnapshotCwd(),
1764
1762
  provider,
1765
1763
  wait: true,
1766
1764
  });
@@ -44,11 +44,12 @@ export type DaemonLifecycleIntent = {
44
44
  type: "shutdown";
45
45
  clientId: string;
46
46
  requestId: string;
47
+ reason: string;
47
48
  } | {
48
49
  type: "restart";
49
50
  clientId: string;
50
51
  requestId: string;
51
- reason?: string;
52
+ reason: string;
52
53
  };
53
54
  export interface PaseoDaemonConfig {
54
55
  listen: string;
@@ -57,6 +58,7 @@ export interface PaseoDaemonConfig {
57
58
  corsAllowedOrigins: string[];
58
59
  allowedHosts?: HostnamesConfig;
59
60
  hostnames?: HostnamesConfig;
61
+ trustedProxies?: true | string[];
60
62
  mcpEnabled?: boolean;
61
63
  mcpInjectIntoAgents?: boolean;
62
64
  autoArchiveAfterMerge?: boolean;
@@ -77,6 +79,10 @@ export interface PaseoDaemonConfig {
77
79
  publicBaseUrl: string | null;
78
80
  standaloneListen: string | null;
79
81
  };
82
+ webUi?: {
83
+ enabled: boolean;
84
+ distDir: string | null;
85
+ };
80
86
  appBaseUrl?: string;
81
87
  auth?: DaemonAuthConfig;
82
88
  openai?: PaseoOpenAIConfig;
@@ -113,6 +113,7 @@ import { createManagedProcessRegistry, createSystemManagedProcessTable, } from "
113
113
  import { terminateWithTreeKill } from "../utils/tree-kill.js";
114
114
  import { isHostnameAllowed } from "./hostnames.js";
115
115
  import { createRequireBearerMiddleware, isAgentMcpRequestAuthorized, } from "./auth.js";
116
+ import { createWebUiMiddleware } from "./web-ui.js";
116
117
  const MAX_MCP_DEBUG_BATCH_ITEMS = 10;
117
118
  const REDACTED_LOG_VALUE = "[redacted]";
118
119
  const DOWNLOAD_OPEN_FLAGS = process.platform === "win32" ? constants.O_RDONLY : constants.O_RDONLY | constants.O_NOFOLLOW;
@@ -230,6 +231,17 @@ async function reconcileManagedProcessLedger(managedProcesses, logger) {
230
231
  logger.info(reapResult, "Managed helper process ledger reconciled");
231
232
  }
232
233
  }
234
+ function mountWebUi(app, config, logger) {
235
+ app.use(createWebUiMiddleware({
236
+ enabled: config.webUi?.enabled ?? false,
237
+ distDir: config.webUi?.distDir ?? null,
238
+ label: getHostname(),
239
+ logger,
240
+ }));
241
+ }
242
+ function resolveExpressTrustProxySetting(config) {
243
+ return config.trustedProxies ?? ["loopback"];
244
+ }
233
245
  export async function createPaseoDaemon(config, rootLogger) {
234
246
  const logger = rootLogger.child({ module: "bootstrap" });
235
247
  const bootstrapStart = performance.now();
@@ -279,6 +291,7 @@ export async function createPaseoDaemon(config, rootLogger) {
279
291
  const agentMcpAuthToken = randomUUID();
280
292
  const listenTarget = parseListenString(config.listen);
281
293
  const app = express();
294
+ app.set("trust proxy", resolveExpressTrustProxySetting(config));
282
295
  let boundListenTarget = null;
283
296
  let workspaceRegistry = null;
284
297
  const terminalManager = createConfiguredTerminalManager({
@@ -363,6 +376,11 @@ export async function createPaseoDaemon(config, rootLogger) {
363
376
  });
364
377
  // Local, harmless, and token-gated; deliberately skips daemon auth.
365
378
  app.post("/api/terminal-activity", express.json(), createTerminalActivityRouteHandler(terminalManager));
379
+ // Serve the bundled browser web UI when enabled. Mounted after service-proxy
380
+ // classification and host/CORS handling, but before daemon bearer auth, so
381
+ // static app files load without the daemon password while API/WebSocket calls
382
+ // remain protected.
383
+ mountWebUi(app, config, logger);
366
384
  app.use(createRequireBearerMiddleware(config.auth, (context) => {
367
385
  logger.warn(context, "Rejected HTTP request with invalid daemon password");
368
386
  }));
@@ -1,11 +1,13 @@
1
1
  import type { PaseoDaemonConfig } from "./bootstrap.js";
2
2
  import { type HostnamesConfig } from "./hostnames.js";
3
+ export declare function resolveBundledWebUiDistDir(moduleUrl?: string | URL): string;
3
4
  export type CliConfigOverrides = Partial<{
4
5
  listen: string;
5
6
  relayEnabled: boolean;
6
7
  relayUseTls: boolean;
7
8
  mcpEnabled: boolean;
8
9
  mcpInjectIntoAgents: boolean;
10
+ webUiEnabled: boolean;
9
11
  hostnames: HostnamesConfig;
10
12
  }>;
11
13
  export declare function loadConfig(paseoHome: string, options?: {
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { resolvePaseoNodeEnv } from "./paseo-env.js";
3
4
  import { z } from "zod";
4
5
  import { expandTilde } from "../utils/path.js";
@@ -11,6 +12,20 @@ import { mergeHostnames, parseHostnamesEnv } from "./hostnames.js";
11
12
  const DEFAULT_PORT = 6767;
12
13
  const DEFAULT_RELAY_ENDPOINT = "relay.paseo.sh:443";
13
14
  const DEFAULT_APP_BASE_URL = "https://app.paseo.sh";
15
+ const DEFAULT_TRUSTED_PROXIES = ["loopback"];
16
+ export function resolveBundledWebUiDistDir(moduleUrl = import.meta.url) {
17
+ const moduleDir = path.dirname(fileURLToPath(moduleUrl));
18
+ if (path.basename(moduleDir) === "server" && path.basename(path.dirname(moduleDir)) === "src") {
19
+ return path.resolve(moduleDir, "..", "..", "dist", "server", "web-ui");
20
+ }
21
+ if (path.basename(moduleDir) === "server" &&
22
+ path.basename(path.dirname(moduleDir)) === "server" &&
23
+ path.basename(path.dirname(path.dirname(moduleDir))) === "dist") {
24
+ return path.resolve(moduleDir, "..", "web-ui");
25
+ }
26
+ return path.resolve(moduleDir, "web-ui");
27
+ }
28
+ const BUNDLED_WEB_UI_DIST_DIR = resolveBundledWebUiDistDir();
14
29
  function parseBooleanEnv(value) {
15
30
  if (value === undefined) {
16
31
  return undefined;
@@ -137,6 +152,21 @@ function resolveServiceProxyConfig(env, persisted) {
137
152
  : null;
138
153
  return { publicBaseUrl, standaloneListen };
139
154
  }
155
+ function resolveWebUiConfig(paseoHome, env, cli, persisted) {
156
+ const enabled = cli?.webUiEnabled ??
157
+ parseBooleanEnv(env.PASEO_WEB_UI_ENABLED) ??
158
+ persisted.features?.webUi?.enabled ??
159
+ false;
160
+ const rawDistDir = env.PASEO_WEB_UI_DIST_DIR ?? persisted.features?.webUi?.distDir;
161
+ const trimmedDistDir = rawDistDir?.trim();
162
+ const distDir = trimmedDistDir
163
+ ? path.resolve(path.isAbsolute(trimmedDistDir) ? trimmedDistDir : paseoHome, trimmedDistDir)
164
+ : BUNDLED_WEB_UI_DIST_DIR;
165
+ return {
166
+ enabled,
167
+ distDir,
168
+ };
169
+ }
140
170
  function resolveVoiceLlmConfig(env, persisted) {
141
171
  const envVoiceLlmProvider = parseOptionalVoiceLlmProvider(env.PASEO_VOICE_LLM_PROVIDER);
142
172
  const persistedVoiceLlmProvider = parseOptionalVoiceLlmProvider(persisted.features?.voiceMode?.llm?.provider);
@@ -153,6 +183,28 @@ function resolveCorsAllowedOrigins(env, persisted) {
153
183
  const persistedCorsOrigins = persisted.daemon?.cors?.allowedOrigins ?? [];
154
184
  return Array.from(new Set([...persistedCorsOrigins, ...envCorsOrigins].filter((s) => s.length > 0)));
155
185
  }
186
+ function parseTrustedProxiesEnv(value) {
187
+ const trimmed = value?.trim();
188
+ if (!trimmed) {
189
+ return undefined;
190
+ }
191
+ const normalized = trimmed.toLowerCase();
192
+ if (["1", "true", "yes", "on"].includes(normalized)) {
193
+ return true;
194
+ }
195
+ if (["0", "false", "no", "off"].includes(normalized)) {
196
+ return [];
197
+ }
198
+ return trimmed
199
+ .split(",")
200
+ .map((proxy) => proxy.trim())
201
+ .filter((proxy) => proxy.length > 0);
202
+ }
203
+ function resolveTrustedProxiesConfig(env, persisted) {
204
+ return (parseTrustedProxiesEnv(env.PASEO_TRUSTED_PROXIES) ??
205
+ persisted.daemon?.trustedProxies ??
206
+ DEFAULT_TRUSTED_PROXIES);
207
+ }
156
208
  // PASEO_LISTEN can be:
157
209
  // - host:port (TCP)
158
210
  // - /path/to/socket (Unix socket)
@@ -198,6 +250,7 @@ function resolveStaticLoadConfigSettings(env, cli, persisted) {
198
250
  parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
199
251
  cli?.hostnames,
200
252
  ]),
253
+ trustedProxies: resolveTrustedProxiesConfig(env, persisted),
201
254
  appBaseUrl: env.PASEO_APP_BASE_URL ?? persisted.app?.baseUrl ?? DEFAULT_APP_BASE_URL,
202
255
  };
203
256
  }
@@ -205,7 +258,7 @@ export function loadConfig(paseoHome, options) {
205
258
  const env = options?.env ?? process.env;
206
259
  const persisted = loadPersistedConfig(paseoHome);
207
260
  const listen = resolveListenAddress(env, options?.cli, persisted);
208
- const { mcpEnabled, mcpInjectIntoAgents, autoArchiveAfterMerge, appendSystemPrompt, terminalProfiles, hostnames, appBaseUrl, } = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
261
+ const { mcpEnabled, mcpInjectIntoAgents, autoArchiveAfterMerge, appendSystemPrompt, terminalProfiles, hostnames, trustedProxies, appBaseUrl, } = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
209
262
  const relay = resolveRelayConfig({
210
263
  env,
211
264
  persisted,
@@ -213,6 +266,7 @@ export function loadConfig(paseoHome, options) {
213
266
  cliRelayUseTls: options?.cli?.relayUseTls,
214
267
  });
215
268
  const serviceProxy = resolveServiceProxyConfig(env, persisted);
269
+ const webUi = resolveWebUiConfig(paseoHome, env, options?.cli, persisted);
216
270
  const { openai, speech } = resolveSpeechConfig({
217
271
  paseoHome,
218
272
  env,
@@ -226,6 +280,7 @@ export function loadConfig(paseoHome, options) {
226
280
  worktreesRoot: resolveWorktreesRoot(paseoHome, persisted),
227
281
  corsAllowedOrigins: resolveCorsAllowedOrigins(env, persisted),
228
282
  hostnames,
283
+ trustedProxies,
229
284
  mcpEnabled,
230
285
  mcpInjectIntoAgents,
231
286
  autoArchiveAfterMerge,
@@ -243,6 +298,7 @@ export function loadConfig(paseoHome, options) {
243
298
  relayUseTls: relay.useTls,
244
299
  relayPublicUseTls: relay.publicUseTls,
245
300
  serviceProxy,
301
+ webUi,
246
302
  appBaseUrl,
247
303
  auth: resolveAuthConfig(env, persisted),
248
304
  openai,
@@ -4,6 +4,7 @@ import { createPaseoDaemon } from "./bootstrap.js";
4
4
  import { loadConfig } from "./config.js";
5
5
  import { resolvePaseoHome } from "./paseo-home.js";
6
6
  import { createRootLogger } from "./logger.js";
7
+ import { getProcessDiagnostics } from "./process-diagnostics.js";
7
8
  process.title = "Paseo Daemon";
8
9
  function isPidAlive(pid) {
9
10
  try {
@@ -60,6 +61,12 @@ function applyCliFlagOverrides(config) {
60
61
  if (process.argv.includes("--no-inject-mcp")) {
61
62
  config.mcpInjectIntoAgents = false;
62
63
  }
64
+ if (process.argv.includes("--web-ui")) {
65
+ config.webUi = { ...(config.webUi ?? { distDir: null }), enabled: true };
66
+ }
67
+ if (process.argv.includes("--no-web-ui")) {
68
+ config.webUi = { ...(config.webUi ?? { distDir: null }), enabled: false };
69
+ }
63
70
  }
64
71
  async function main() {
65
72
  const { paseoHome, logger, config } = bootstrapFromEnvironment();
@@ -77,11 +84,12 @@ async function main() {
77
84
  });
78
85
  };
79
86
  const beginShutdown = (signal, options) => {
87
+ const reason = options?.reason ?? `worker_received_${signal}`;
80
88
  if (!shutdownPromise) {
81
- logger.info(`${signal} received, shutting down gracefully...`);
89
+ logger.info({ signal, reason, ...getProcessDiagnostics() }, `${signal} received, shutting down gracefully...`);
82
90
  shutdownPromise = (async () => {
83
91
  const forceExit = setTimeout(() => {
84
- logger.warn("Forcing shutdown - HTTP server didn't close in time");
92
+ logger.warn({ signal, reason, ...getProcessDiagnostics() }, "Forcing shutdown - HTTP server didn't close in time");
85
93
  process.exit(1);
86
94
  }, 10000);
87
95
  try {
@@ -103,7 +111,7 @@ async function main() {
103
111
  })();
104
112
  }
105
113
  else {
106
- logger.info(`${signal} received while shutdown is already in progress`);
114
+ logger.info({ signal, reason, ...getProcessDiagnostics() }, `${signal} received while shutdown is already in progress`);
107
115
  }
108
116
  installExitHook();
109
117
  };
@@ -122,11 +130,11 @@ async function main() {
122
130
  };
123
131
  const handleLifecycleIntent = (intent) => {
124
132
  if (intent.type === "shutdown") {
125
- logger.warn({ clientId: intent.clientId, requestId: intent.requestId }, "Shutdown requested via websocket");
126
- if (sendSupervisorLifecycleMessage({ type: "paseo:shutdown" })) {
133
+ logger.warn({ clientId: intent.clientId, requestId: intent.requestId, reason: intent.reason }, "Shutdown requested via websocket");
134
+ if (sendSupervisorLifecycleMessage({ type: "paseo:shutdown", reason: intent.reason })) {
127
135
  return;
128
136
  }
129
- beginShutdown("shutdown lifecycle intent");
137
+ beginShutdown("shutdown lifecycle intent", { reason: intent.reason });
130
138
  return;
131
139
  }
132
140
  logger.warn({ clientId: intent.clientId, requestId: intent.requestId, reason: intent.reason }, "Restart requested via websocket");
@@ -136,7 +144,10 @@ async function main() {
136
144
  })) {
137
145
  return;
138
146
  }
139
- beginShutdown("restart lifecycle intent", { successExitCode: 0 });
147
+ beginShutdown("restart lifecycle intent", {
148
+ reason: intent.reason,
149
+ successExitCode: 0,
150
+ });
140
151
  };
141
152
  const installSupervisorLivenessGuard = () => {
142
153
  if (typeof process.send !== "function") {
@@ -152,6 +163,7 @@ async function main() {
152
163
  supervisorExitRequested = true;
153
164
  writeWorkerLifecycleLog(paseoHome, "Supervisor liveness lost; worker exiting", {
154
165
  reason,
166
+ ...getProcessDiagnostics(),
155
167
  supervisorPid,
156
168
  currentParentPid: process.ppid,
157
169
  ipcConnected: typeof process.connected === "boolean" ? process.connected : null,
@@ -0,0 +1,4 @@
1
+ export declare const CLIENT_SHUTDOWN_RPC_REASON = "client_shutdown_rpc";
2
+ export declare const DEFAULT_CLIENT_RESTART_RPC_REASON = "client_restart_rpc";
3
+ export declare function normalizeClientRestartRpcReason(reason: string | undefined): string;
4
+ //# sourceMappingURL=lifecycle-reasons.d.ts.map
@@ -0,0 +1,6 @@
1
+ export const CLIENT_SHUTDOWN_RPC_REASON = "client_shutdown_rpc";
2
+ export const DEFAULT_CLIENT_RESTART_RPC_REASON = "client_restart_rpc";
3
+ export function normalizeClientRestartRpcReason(reason) {
4
+ return reason?.trim() || DEFAULT_CLIENT_RESTART_RPC_REASON;
5
+ }
6
+ //# sourceMappingURL=lifecycle-reasons.js.map
@@ -19,6 +19,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
19
19
  listen: z.ZodOptional<z.ZodString>;
20
20
  hostnames: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodArray<z.ZodString>]>>;
21
21
  allowedHosts: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodArray<z.ZodString>]>>;
22
+ trustedProxies: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodArray<z.ZodString>]>>;
22
23
  mcp: z.ZodOptional<z.ZodObject<{
23
24
  enabled: z.ZodOptional<z.ZodBoolean>;
24
25
  injectIntoAgents: z.ZodOptional<z.ZodBoolean>;
@@ -54,6 +55,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
54
55
  }, z.core.$strict>, z.ZodTransform<{
55
56
  listen?: string | undefined;
56
57
  hostnames?: true | string[] | undefined;
58
+ trustedProxies?: true | string[] | undefined;
57
59
  mcp?: {
58
60
  [x: string]: unknown;
59
61
  enabled?: boolean | undefined;
@@ -92,6 +94,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
92
94
  listen?: string | undefined;
93
95
  hostnames?: true | string[] | undefined;
94
96
  allowedHosts?: true | string[] | undefined;
97
+ trustedProxies?: true | string[] | undefined;
95
98
  mcp?: {
96
99
  [x: string]: unknown;
97
100
  enabled?: boolean | undefined;
@@ -133,11 +136,15 @@ export declare const PersistedConfigSchema: z.ZodObject<{
133
136
  providers: z.ZodOptional<z.ZodObject<{
134
137
  openai: z.ZodOptional<z.ZodObject<{
135
138
  apiKey: z.ZodOptional<z.ZodString>;
136
- voice: z.ZodOptional<z.ZodObject<{
139
+ baseUrl: z.ZodOptional<z.ZodString>;
140
+ stt: z.ZodOptional<z.ZodObject<{
141
+ apiKey: z.ZodOptional<z.ZodString>;
142
+ baseUrl: z.ZodOptional<z.ZodString>;
143
+ }, z.core.$strict>>;
144
+ tts: z.ZodOptional<z.ZodObject<{
137
145
  apiKey: z.ZodOptional<z.ZodString>;
138
146
  baseUrl: z.ZodOptional<z.ZodString>;
139
147
  }, z.core.$strict>>;
140
- baseUrl: z.ZodOptional<z.ZodString>;
141
148
  }, z.core.$strict>>;
142
149
  local: z.ZodOptional<z.ZodObject<{
143
150
  modelsDir: z.ZodOptional<z.ZodString>;
@@ -241,6 +248,10 @@ export declare const PersistedConfigSchema: z.ZodObject<{
241
248
  speed: z.ZodOptional<z.ZodNumber>;
242
249
  }, z.core.$strict>>;
243
250
  }, z.core.$strict>>;
251
+ webUi: z.ZodOptional<z.ZodObject<{
252
+ enabled: z.ZodOptional<z.ZodBoolean>;
253
+ distDir: z.ZodOptional<z.ZodString>;
254
+ }, z.core.$strict>>;
244
255
  }, z.core.$strict>>;
245
256
  log: z.ZodOptional<z.ZodObject<{
246
257
  level: z.ZodOptional<z.ZodEnum<{
@@ -34,7 +34,7 @@ const LogConfigSchema = z
34
34
  .optional(),
35
35
  })
36
36
  .strict();
37
- const OpenAiVoiceProviderSchema = z
37
+ const OpenAiSpeechEndpointSchema = z
38
38
  .object({
39
39
  apiKey: z.string().trim().min(1).optional(),
40
40
  baseUrl: z.string().trim().min(1).optional(),
@@ -43,8 +43,9 @@ const OpenAiVoiceProviderSchema = z
43
43
  const OpenAiProviderSchema = z
44
44
  .object({
45
45
  apiKey: z.string().min(1).optional(),
46
- voice: OpenAiVoiceProviderSchema.optional(),
47
46
  baseUrl: z.string().trim().min(1).optional(),
47
+ stt: OpenAiSpeechEndpointSchema.optional(),
48
+ tts: OpenAiSpeechEndpointSchema.optional(),
48
49
  })
49
50
  .strict();
50
51
  const LocalSpeechProviderSchema = z
@@ -126,6 +127,12 @@ const FeatureVoiceModeSchema = z
126
127
  .optional(),
127
128
  })
128
129
  .strict();
130
+ const FeatureWebUiSchema = z
131
+ .object({
132
+ enabled: z.boolean().optional(),
133
+ distDir: z.string().min(1).optional(),
134
+ })
135
+ .strict();
129
136
  const StructuredGenerationProviderConfigSchema = z
130
137
  .object({
131
138
  provider: z.string().min(1),
@@ -187,6 +194,7 @@ export const PersistedConfigSchema = z
187
194
  listen: z.string().optional(),
188
195
  hostnames: z.union([z.literal(true), z.array(z.string())]).optional(),
189
196
  allowedHosts: z.union([z.literal(true), z.array(z.string())]).optional(),
197
+ trustedProxies: z.union([z.literal(true), z.array(z.string())]).optional(),
190
198
  mcp: z
191
199
  .object({
192
200
  enabled: z.boolean().optional(),
@@ -252,6 +260,7 @@ export const PersistedConfigSchema = z
252
260
  .object({
253
261
  dictation: FeatureDictationSchema.optional(),
254
262
  voiceMode: FeatureVoiceModeSchema.optional(),
263
+ webUi: FeatureWebUiSchema.optional(),
255
264
  })
256
265
  .strict()
257
266
  .optional(),
@@ -280,7 +289,11 @@ function getConfigPath(paseoHome) {
280
289
  function getLogger(logger) {
281
290
  return logger?.child({ module: "config" });
282
291
  }
283
- function stripDeprecatedLocalSpeechConfigFields(parsed) {
292
+ // Removed config fields are stripped before parsing so the strict schema does not
293
+ // reject a config written by an older release. The stripped values are discarded,
294
+ // not migrated — there is no back-compat for the removed `providers.openai.voice`
295
+ // block (use `providers.openai.stt` / `providers.openai.tts`).
296
+ function stripRemovedConfigFields(parsed) {
284
297
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
285
298
  return parsed;
286
299
  }
@@ -291,15 +304,21 @@ function stripDeprecatedLocalSpeechConfigFields(parsed) {
291
304
  }
292
305
  const providersRecord = { ...providers };
293
306
  const local = providersRecord.local;
294
- if (!local || typeof local !== "object" || Array.isArray(local)) {
295
- root.providers = providersRecord;
296
- return root;
297
- }
298
- const localRecord = { ...local };
299
- if ("autoDownload" in localRecord) {
307
+ if (local && typeof local === "object" && !Array.isArray(local)) {
308
+ const localRecord = { ...local };
300
309
  delete localRecord.autoDownload;
310
+ providersRecord.local = localRecord;
311
+ }
312
+ const openai = providersRecord.openai;
313
+ if (openai && typeof openai === "object" && !Array.isArray(openai)) {
314
+ const openaiRecord = { ...openai };
315
+ // COMPAT(openaiVoiceConfig): added 2026-06-30, remove after 2026-12-30.
316
+ // Drop a `providers.openai.voice` block left by an older release so the strict
317
+ // schema doesn't reject it. The value is discarded, not migrated — there is no
318
+ // back-compat; configure `providers.openai.stt` / `providers.openai.tts` instead.
319
+ delete openaiRecord.voice;
320
+ providersRecord.openai = openaiRecord;
301
321
  }
302
- providersRecord.local = localRecord;
303
322
  root.providers = providersRecord;
304
323
  return root;
305
324
  }
@@ -337,7 +356,7 @@ export function loadPersistedConfig(paseoHome, logger) {
337
356
  cause: err,
338
357
  });
339
358
  }
340
- const migrated = stripDeprecatedLocalSpeechConfigFields(parsed);
359
+ const migrated = stripRemovedConfigFields(parsed);
341
360
  const result = PersistedConfigSchema.safeParse(migrated);
342
361
  if (!result.success) {
343
362
  const issues = result.error.issues