@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa

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 (295) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +26 -3
  3. package/docs/automatic-done.md +32 -0
  4. package/docs/desktop-browser-cli.md +3 -2
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
  10. package/node_modules/@vellumai/environments/src/shell.ts +22 -1
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  15. package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
  16. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
  17. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  18. package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  19. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  21. package/openapi.yaml +270 -10
  22. package/package.json +1 -1
  23. package/src/__tests__/agent-loop.test.ts +76 -0
  24. package/src/__tests__/anthropic-provider.test.ts +2 -0
  25. package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
  26. package/src/__tests__/config-get-vision-flag.test.ts +1 -1
  27. package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
  28. package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
  29. package/src/__tests__/conversation-evictor.test.ts +54 -0
  30. package/src/__tests__/conversation-pairing.test.ts +112 -0
  31. package/src/__tests__/document-append-idempotency.test.ts +13 -0
  32. package/src/__tests__/document-create-dedupe.test.ts +13 -0
  33. package/src/__tests__/document-find-replace.test.ts +13 -0
  34. package/src/__tests__/document-revision-history.test.ts +390 -0
  35. package/src/__tests__/document-revision.test.ts +425 -0
  36. package/src/__tests__/document-tool-security.test.ts +13 -0
  37. package/src/__tests__/document-update-default-surface.test.ts +13 -0
  38. package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
  39. package/src/__tests__/gateway-flag-listener.test.ts +59 -6
  40. package/src/__tests__/headless-browser-navigate.test.ts +20 -30
  41. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  42. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  43. package/src/__tests__/host-shell-tool.test.ts +6 -2
  44. package/src/__tests__/identity-routes.test.ts +4 -3
  45. package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
  46. package/src/__tests__/llm-catalog-parity.test.ts +4 -4
  47. package/src/__tests__/llm-schema.test.ts +1 -1
  48. package/src/__tests__/notification-platform-adapter.test.ts +83 -1
  49. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  50. package/src/__tests__/openai-provider.test.ts +38 -11
  51. package/src/__tests__/pricing.test.ts +11 -0
  52. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  53. package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
  54. package/src/__tests__/skill-execute-input.test.ts +11 -0
  55. package/src/__tests__/tool-approval-handler.test.ts +4 -0
  56. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
  57. package/src/__tests__/usage-attribution.test.ts +31 -0
  58. package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
  59. package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
  60. package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
  61. package/src/acp/agent-process.ts +6 -1
  62. package/src/acp/client-handler.ts +6 -1
  63. package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
  64. package/src/agent/loop.ts +161 -17
  65. package/src/api/events/document-editor-show.ts +5 -0
  66. package/src/api/events/document-editor-update.ts +10 -0
  67. package/src/api/events/tool-result.ts +1 -0
  68. package/src/calls/__tests__/call-controller.test.ts +273 -1
  69. package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
  70. package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
  71. package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
  72. package/src/calls/call-constants.ts +10 -0
  73. package/src/calls/call-controller.ts +359 -25
  74. package/src/calls/channel-admission-reader.ts +15 -9
  75. package/src/calls/media-stream-server.ts +18 -0
  76. package/src/calls/media-stream-stt-session.ts +30 -9
  77. package/src/calls/twilio-provider.ts +14 -31
  78. package/src/calls/voice-session-bridge.ts +18 -1
  79. package/src/cli/commands/browser.ts +1 -2
  80. package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
  81. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  82. package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
  83. package/src/cli/lib/bundled-marketplace.json +54 -2
  84. package/src/cli/lib/inspect-plugin.ts +15 -15
  85. package/src/cli/lib/list-installed-plugins.ts +29 -28
  86. package/src/cli/lib/plugin-artifact.ts +74 -63
  87. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  88. package/src/cli/lib/plugin-details.ts +27 -42
  89. package/src/cli/lib/plugin-marketplace.ts +6 -8
  90. package/src/cli/lib/plugin-surfaces.ts +8 -7
  91. package/src/cli.ts +1 -3
  92. package/src/config/__tests__/chat-settings.test.ts +121 -0
  93. package/src/config/assistant-feature-flags.ts +28 -23
  94. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  95. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  96. package/src/config/chat-settings.ts +100 -0
  97. package/src/config/default-profile-catalog.ts +1 -1
  98. package/src/config/feature-flag-cache.ts +7 -25
  99. package/src/config/feature-flag-registry.json +9 -9
  100. package/src/config/loader.ts +5 -1
  101. package/src/config/profile-text-generation.ts +25 -21
  102. package/src/config/schemas/conversations.ts +14 -0
  103. package/src/config/schemas/llm.ts +1 -0
  104. package/src/config/schemas/notifications.ts +8 -1
  105. package/src/contacts/gateway-channel-read.ts +1 -1
  106. package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
  107. package/src/conversations/__tests__/auto-archive.test.ts +304 -0
  108. package/src/conversations/auto-archive-activity.ts +61 -0
  109. package/src/conversations/auto-archive.ts +201 -0
  110. package/src/daemon/computer-use-observation.test.ts +81 -0
  111. package/src/daemon/computer-use-observation.ts +23 -0
  112. package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
  113. package/src/daemon/conversation-agent-loop.ts +14 -6
  114. package/src/daemon/conversation-evictor.ts +50 -16
  115. package/src/daemon/conversation-messaging.ts +4 -0
  116. package/src/daemon/conversation-process.ts +15 -0
  117. package/src/daemon/conversation-queue-manager.ts +2 -0
  118. package/src/daemon/conversation.ts +2 -0
  119. package/src/daemon/doordash-steps.ts +1 -3
  120. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
  121. package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
  122. package/src/daemon/handlers/config-a2a.ts +75 -2
  123. package/src/daemon/handlers/shared.ts +13 -2
  124. package/src/daemon/host-cu-proxy.ts +34 -3
  125. package/src/daemon/lifecycle.ts +15 -2
  126. package/src/daemon/process-message.ts +5 -0
  127. package/src/daemon/shutdown-handlers.ts +3 -0
  128. package/src/daemon/turn-finalization.ts +4 -0
  129. package/src/desktop/desktop-browser-endpoint.ts +11 -2
  130. package/src/desktop/desktop-chrome-policy.test.ts +1 -0
  131. package/src/desktop/desktop-chrome-policy.ts +7 -1
  132. package/src/desktop/desktop-computer-use.test.ts +14 -4
  133. package/src/desktop/desktop-session-manager.ts +5 -3
  134. package/src/documents/document-revisions-store.ts +153 -0
  135. package/src/documents/document-store.ts +471 -133
  136. package/src/followups/followup-store.ts +0 -4
  137. package/src/i18n/__tests__/i18n.test.ts +25 -0
  138. package/src/i18n/assistant-text.ts +18 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/messages.ts +11 -0
  141. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  142. package/src/inbound/oauth-callback-url.ts +30 -1
  143. package/src/inbound/public-ingress-urls.ts +12 -2
  144. package/src/ipc/gateway-client.test.ts +40 -0
  145. package/src/ipc/gateway-client.ts +12 -29
  146. package/src/ipc/gateway-flag-listener.ts +7 -10
  147. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
  148. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  149. package/src/live-voice/__tests__/protocol.test.ts +77 -0
  150. package/src/live-voice/__tests__/share-targets.test.ts +163 -0
  151. package/src/live-voice/live-voice-session.ts +23 -6
  152. package/src/live-voice/protocol.ts +22 -0
  153. package/src/live-voice/share-targets.ts +225 -0
  154. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  155. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  156. package/src/mcp/client.ts +1 -19
  157. package/src/mcp/mcp-oauth-provider.ts +66 -49
  158. package/src/media/openrouter-image-service.ts +39 -22
  159. package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
  160. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
  161. package/src/monitoring/control.ts +2 -1
  162. package/src/monitoring/oom-inheritance-sweep.ts +96 -0
  163. package/src/monitoring/oom-kill-reporter.ts +469 -0
  164. package/src/monitoring/resource-sampler.ts +30 -0
  165. package/src/notifications/README.md +53 -4
  166. package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
  167. package/src/notifications/__tests__/broadcaster.test.ts +558 -6
  168. package/src/notifications/__tests__/completion-policy.test.ts +37 -0
  169. package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
  170. package/src/notifications/adapters/platform.ts +14 -0
  171. package/src/notifications/assistant-initiated-thread.ts +101 -0
  172. package/src/notifications/broadcaster.ts +94 -16
  173. package/src/notifications/chat-reply-policy.ts +11 -0
  174. package/src/notifications/completion-policy.ts +12 -2
  175. package/src/notifications/conversation-pairing.ts +15 -70
  176. package/src/notifications/emit-signal.ts +34 -3
  177. package/src/notifications/types.ts +2 -0
  178. package/src/oauth/connect-orchestrator.ts +1 -2
  179. package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
  180. package/src/persistence/conversation-auto-archive.ts +170 -0
  181. package/src/persistence/conversation-crud.ts +2 -1
  182. package/src/persistence/conversation-queries.ts +15 -8
  183. package/src/persistence/embeddings/embed.test.ts +96 -0
  184. package/src/persistence/embeddings/embed.ts +20 -2
  185. package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
  186. package/src/persistence/embeddings/embedding-gemini.ts +39 -1
  187. package/src/persistence/embeddings/embedding-local.ts +6 -0
  188. package/src/persistence/embeddings/qdrant-manager.ts +5 -0
  189. package/src/persistence/job-utils.ts +10 -0
  190. package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
  191. package/src/persistence/migrations/384-add-document-revision.ts +21 -0
  192. package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
  193. package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
  194. package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
  195. package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
  196. package/src/persistence/schema/conversations.ts +1 -0
  197. package/src/persistence/schema/documents.ts +20 -0
  198. package/src/persistence/steps.ts +6 -0
  199. package/src/platform/managed-speech.ts +42 -46
  200. package/src/plugin-api/vision-support.test.ts +21 -21
  201. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
  202. package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
  203. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  204. package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
  205. package/src/providers/anthropic/client.ts +1 -1
  206. package/src/providers/fireworks/client.test.ts +168 -0
  207. package/src/providers/fireworks/client.ts +24 -0
  208. package/src/providers/inference/adapter-factory.ts +6 -0
  209. package/src/providers/ionet/client.test.ts +143 -0
  210. package/src/providers/ionet/client.ts +37 -0
  211. package/src/providers/model-catalog.ts +141 -84
  212. package/src/providers/model-intents.ts +4 -4
  213. package/src/providers/openai/chat-completions-provider.ts +33 -2
  214. package/src/providers/retry.ts +56 -4
  215. package/src/providers/search-provider-catalog.ts +10 -0
  216. package/src/providers/types.ts +10 -2
  217. package/src/providers/unparseable-tool-args.ts +29 -5
  218. package/src/providers/usage-tracking.ts +5 -3
  219. package/src/runtime/assistant-event-hub.ts +23 -0
  220. package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
  221. package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
  222. package/src/runtime/channel-reply-delivery.ts +2 -0
  223. package/src/runtime/channel-retry-sweep.ts +1 -2
  224. package/src/runtime/http-types.ts +3 -0
  225. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  226. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  227. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  228. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  229. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  230. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  231. package/src/runtime/pending-interactions.ts +20 -2
  232. package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
  233. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
  234. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
  235. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
  236. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
  237. package/src/runtime/routes/contact-routes.ts +2 -2
  238. package/src/runtime/routes/conversation-list-routes.ts +10 -2
  239. package/src/runtime/routes/conversation-query-routes.ts +48 -10
  240. package/src/runtime/routes/conversation-routes.ts +7 -0
  241. package/src/runtime/routes/documents-routes.ts +185 -47
  242. package/src/runtime/routes/events-routes.ts +1 -3
  243. package/src/runtime/routes/host-app-control-routes.ts +1 -0
  244. package/src/runtime/routes/host-bash-routes.ts +1 -0
  245. package/src/runtime/routes/host-browser-routes.ts +15 -0
  246. package/src/runtime/routes/host-cu-routes.ts +4 -1
  247. package/src/runtime/routes/host-file-routes.ts +1 -0
  248. package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
  249. package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
  250. package/src/runtime/routes/host-transfer-routes.ts +1 -0
  251. package/src/runtime/routes/identity-routes.ts +3 -3
  252. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  253. package/src/runtime/routes/integrations/a2a.ts +4 -1
  254. package/src/runtime/routes/migration-routes.ts +3 -1
  255. package/src/runtime/routes/notification-routes.ts +1 -2
  256. package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
  257. package/src/runtime/routes/user-route-dispatcher.ts +5 -1
  258. package/src/runtime/verification-templates.ts +1 -2
  259. package/src/sequence/analytics.ts +0 -4
  260. package/src/sequence/engine.ts +1 -11
  261. package/src/sequence/guardrails.ts +1 -6
  262. package/src/sequence/importer.ts +0 -8
  263. package/src/sequence/store.ts +0 -10
  264. package/src/signals/user-message.ts +0 -6
  265. package/src/skills/catalog-install.ts +52 -23
  266. package/src/tools/browser/browser-execution.ts +20 -23
  267. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
  268. package/src/tools/document/document-tool.ts +13 -11
  269. package/src/tools/executor.ts +1 -2
  270. package/src/tools/host-terminal/host-shell.ts +4 -2
  271. package/src/tools/network/__tests__/web-search.test.ts +105 -0
  272. package/src/tools/network/web-fetch.ts +2 -4
  273. package/src/tools/network/web-search.ts +239 -12
  274. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  275. package/src/tools/shared/shell-output.test.ts +42 -0
  276. package/src/tools/shared/shell-output.ts +27 -4
  277. package/src/tools/skills/execute.ts +10 -7
  278. package/src/tools/terminal/shell.ts +3 -3
  279. package/src/tools/tool-approval-handler.ts +9 -1
  280. package/src/tools/ui-surface/visual-validation.ts +1 -2
  281. package/src/usage/attribution.ts +18 -1
  282. package/src/util/__tests__/oom-priority.test.ts +55 -0
  283. package/src/util/__tests__/worker-memory.test.ts +16 -0
  284. package/src/util/debounce.ts +0 -4
  285. package/src/util/oom-priority.ts +79 -0
  286. package/src/util/platform.ts +0 -2
  287. package/src/util/plugin-manifest.ts +5 -7
  288. package/src/util/pricing.ts +25 -0
  289. package/src/util/worker-memory.ts +9 -3
  290. package/src/util/worker-process.ts +8 -0
  291. package/src/workspace/byok-default-profile-ensure.ts +11 -0
  292. package/src/workspace/hatched-date.ts +11 -4
  293. package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
  294. package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
  295. package/src/workspace/migrations/registry.ts +4 -0
@@ -52,21 +52,76 @@ const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
52
52
  * the digest must be canonical lowercase hex so client-side comparison is a
53
53
  * plain string equality with no normalization step.
54
54
  */
55
- const PluginArtifactSchema = z.object({
56
- url: z
57
- .string()
58
- .url()
59
- .refine((u) => u.startsWith("https://"), {
60
- message: "artifact url must be an https:// URL",
61
- }),
62
- sha256: z
63
- .string()
64
- .regex(SHA256_HEX_RE, "artifact sha256 must be 64 lowercase hex chars"),
65
- // Optional, non-critical metadata: a malformed label must never nullify an
66
- // otherwise-valid `url` + `sha256`, so a wrong-typed value falls back to
67
- // `undefined` rather than failing the whole descriptor.
68
- label: z.string().optional().catch(undefined),
69
- });
55
+ const PluginArtifactSchema = z
56
+ .object({
57
+ url: z
58
+ .string()
59
+ .url()
60
+ .refine((u) => u.startsWith("https://"), {
61
+ message: "artifact url must be an https:// URL",
62
+ }),
63
+ sha256: z
64
+ .string()
65
+ .regex(SHA256_HEX_RE, "artifact sha256 must be 64 lowercase hex chars"),
66
+ // Optional, non-critical metadata: a malformed label must never nullify an
67
+ // otherwise-valid `url` + `sha256`, so a wrong-typed value falls back to
68
+ // `undefined` rather than failing the whole descriptor.
69
+ label: z
70
+ .string()
71
+ .transform((label) => label.trim())
72
+ .optional()
73
+ .catch(undefined),
74
+ })
75
+ .transform(({ label, ...artifact }) =>
76
+ label ? { ...artifact, label } : artifact,
77
+ );
78
+
79
+ /** Upper bound on `vellum.icon`, in Unicode code points. */
80
+ const MAX_ICON_CODE_POINTS = 16;
81
+
82
+ const PluginIconSchema = z
83
+ .string()
84
+ .transform((icon) => icon.trim())
85
+ .refine(
86
+ (icon) => {
87
+ const codePoints = [...icon].length;
88
+ return codePoints >= 1 && codePoints <= MAX_ICON_CODE_POINTS;
89
+ },
90
+ { message: "plugin icon must contain 1 to 16 Unicode code points" },
91
+ );
92
+
93
+ const LicenseSchema = z
94
+ .union([
95
+ z.string(),
96
+ z.object({ type: z.string() }).transform(({ type }) => type),
97
+ z.undefined(),
98
+ ])
99
+ .catch(undefined);
100
+
101
+ export const PluginPackageJsonSchema = z
102
+ .object({
103
+ name: z.string().optional().catch(undefined),
104
+ version: z.string().optional().catch(undefined),
105
+ description: z.string().optional().catch(undefined),
106
+ homepage: z.string().optional().catch(undefined),
107
+ license: LicenseSchema.optional(),
108
+ peerDependencies: z
109
+ .record(z.string(), z.string())
110
+ .optional()
111
+ .catch(undefined),
112
+ vellum: z
113
+ .object({
114
+ artifact: PluginArtifactSchema.optional().catch(undefined),
115
+ icon: PluginIconSchema.optional().catch(undefined),
116
+ })
117
+ .optional()
118
+ .catch(undefined),
119
+ })
120
+ .transform(({ vellum, ...packageJson }) => ({
121
+ ...packageJson,
122
+ artifact: vellum?.artifact,
123
+ icon: vellum?.icon,
124
+ }));
70
125
 
71
126
  /**
72
127
  * Read `vellum.artifact` from an already-parsed `package.json` value and
@@ -78,35 +133,10 @@ const PluginArtifactSchema = z.object({
78
133
  export function parsePluginArtifact(
79
134
  packageJson: unknown,
80
135
  ): PluginArtifact | null {
81
- if (
82
- typeof packageJson !== "object" ||
83
- packageJson === null ||
84
- Array.isArray(packageJson)
85
- ) {
86
- return null;
87
- }
88
- const vellum = (packageJson as Record<string, unknown>).vellum;
89
- if (typeof vellum !== "object" || vellum === null || Array.isArray(vellum)) {
90
- return null;
91
- }
92
- const artifact = (vellum as Record<string, unknown>).artifact;
93
- const parsed = PluginArtifactSchema.safeParse(artifact);
94
- if (!parsed.success) {
95
- return null;
96
- }
97
- // A blank or whitespace-only label is treated as absent so it never
98
- // invalidates an otherwise well-formed `url` + `sha256` descriptor.
99
- const label = parsed.data.label?.trim();
100
- return {
101
- url: parsed.data.url,
102
- sha256: parsed.data.sha256,
103
- ...(label ? { label } : {}),
104
- };
136
+ const parsed = PluginPackageJsonSchema.safeParse(packageJson);
137
+ return parsed.success ? (parsed.data.artifact ?? null) : null;
105
138
  }
106
139
 
107
- /** Upper bound on `vellum.icon`, in Unicode code points. */
108
- const MAX_ICON_CODE_POINTS = 16;
109
-
110
140
  /**
111
141
  * Read `vellum.icon` from an already-parsed `package.json` value and return
112
142
  * it only when it is a short, non-empty glyph (an emoji or similar). The
@@ -115,25 +145,6 @@ const MAX_ICON_CODE_POINTS = 16;
115
145
  * type, empty, or too long — yields `undefined` rather than throwing.
116
146
  */
117
147
  export function parsePluginIcon(packageJson: unknown): string | undefined {
118
- if (
119
- typeof packageJson !== "object" ||
120
- packageJson === null ||
121
- Array.isArray(packageJson)
122
- ) {
123
- return undefined;
124
- }
125
- const vellum = (packageJson as Record<string, unknown>).vellum;
126
- if (typeof vellum !== "object" || vellum === null || Array.isArray(vellum)) {
127
- return undefined;
128
- }
129
- const icon = (vellum as Record<string, unknown>).icon;
130
- if (typeof icon !== "string") {
131
- return undefined;
132
- }
133
- const trimmed = icon.trim();
134
- const codePoints = [...trimmed].length;
135
- if (codePoints < 1 || codePoints > MAX_ICON_CODE_POINTS) {
136
- return undefined;
137
- }
138
- return trimmed;
148
+ const parsed = PluginPackageJsonSchema.safeParse(packageJson);
149
+ return parsed.success ? parsed.data.icon : undefined;
139
150
  }
@@ -10,6 +10,7 @@ const MCP_CATALOG_QA_INTEGRATION_NAMES = new Set([
10
10
  "ramp",
11
11
  "semrush",
12
12
  "typeform",
13
+ "upwork",
13
14
  ]);
14
15
 
15
16
  export type PluginCatalogFeatureFlagResolver = (key: string) => boolean;
@@ -32,15 +32,16 @@
32
32
  import { existsSync, readdirSync, readFileSync } from "node:fs";
33
33
  import { join } from "node:path";
34
34
 
35
+ import { z } from "zod";
36
+
35
37
  import { getWorkspacePluginsDir } from "../../util/platform.js";
36
38
  import { readPluginManifest } from "../../util/plugin-manifest.js";
37
39
  import { readBundledPluginFile } from "./bundled-plugin-packages.js";
38
40
  import type { FetchLike } from "./fetch-like.js";
39
41
  import { sanitizePluginName } from "./install-from-github.js";
40
42
  import {
41
- parsePluginArtifact,
42
- parsePluginIcon,
43
43
  type PluginArtifact,
44
+ PluginPackageJsonSchema,
44
45
  } from "./plugin-artifact.js";
45
46
  import {
46
47
  findCatalogEntry,
@@ -61,12 +62,16 @@ import {
61
62
  const README_RE = /^readme(\.md|\.markdown)?$/i;
62
63
 
63
64
  /** Entry shape returned by the GitHub Contents API for a directory listing. */
64
- interface GitHubContentEntry {
65
- readonly name: string;
66
- readonly path: string;
67
- readonly type: "file" | "dir" | "symlink" | "submodule";
68
- readonly download_url: string | null;
69
- }
65
+ const GitHubContentEntrySchema = z.object({
66
+ name: z.string(),
67
+ path: z.string(),
68
+ type: z.enum(["file", "dir", "symlink", "submodule"]),
69
+ download_url: z.string().nullable(),
70
+ });
71
+
72
+ const GitHubContentListingSchema = z.array(GitHubContentEntrySchema);
73
+
74
+ type GitHubContentEntry = z.infer<typeof GitHubContentEntrySchema>;
70
75
 
71
76
  /** The subset of `package.json` fields the detail view surfaces. */
72
77
  interface PluginManifestFields {
@@ -249,11 +254,14 @@ function readLocalPlugin(pluginsDir: string, name: string): LocalPlugin {
249
254
  manifest = parseManifest(readFileSync(packagePath, "utf8"));
250
255
  } else {
251
256
  const selected = readPluginManifest(target);
257
+ const parsedPackageJson = PluginPackageJsonSchema.safeParse(selected.raw);
252
258
  manifest = {
253
259
  version: selected.version ?? null,
254
260
  description: selected.description ?? null,
255
261
  homepage: selected.homepage ?? null,
256
- license: normalizeLicense(selected.license),
262
+ license: parsedPackageJson.success
263
+ ? (parsedPackageJson.data.license ?? null)
264
+ : null,
257
265
  artifact: null,
258
266
  icon: null,
259
267
  };
@@ -365,11 +373,8 @@ async function listDirSafe(
365
373
  if (!res.ok) {
366
374
  return null;
367
375
  }
368
- const body = (await res.json()) as unknown;
369
- if (!Array.isArray(body)) {
370
- return null;
371
- }
372
- return body as readonly GitHubContentEntry[];
376
+ const parsed = GitHubContentListingSchema.safeParse(await res.json());
377
+ return parsed.success ? parsed.data : null;
373
378
  } catch {
374
379
  return null;
375
380
  }
@@ -502,36 +507,16 @@ function safeParseManifest(raw: string): PluginManifestFields {
502
507
 
503
508
  /** Extract the surfaced fields from a `package.json` body. Throws on bad JSON. */
504
509
  function parseManifest(raw: string): PluginManifestFields {
505
- const parsed: unknown = JSON.parse(raw);
506
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
510
+ const parsed = PluginPackageJsonSchema.safeParse(JSON.parse(raw));
511
+ if (!parsed.success) {
507
512
  return emptyManifest();
508
513
  }
509
- const meta = parsed as Record<string, unknown>;
510
514
  return {
511
- version: typeof meta.version === "string" ? meta.version : null,
512
- description: typeof meta.description === "string" ? meta.description : null,
513
- homepage: typeof meta.homepage === "string" ? meta.homepage : null,
514
- license: normalizeLicense(meta.license),
515
- artifact: parsePluginArtifact(parsed),
516
- icon: parsePluginIcon(parsed) ?? null,
515
+ version: parsed.data.version ?? null,
516
+ description: parsed.data.description ?? null,
517
+ homepage: parsed.data.homepage ?? null,
518
+ license: parsed.data.license ?? null,
519
+ artifact: parsed.data.artifact ?? null,
520
+ icon: parsed.data.icon ?? null,
517
521
  };
518
522
  }
519
-
520
- /**
521
- * `package.json#license` is usually an SPDX string but the legacy object form
522
- * `{ "type": "MIT" }` still appears in the wild — surface its `type`.
523
- */
524
- function normalizeLicense(value: unknown): string | null {
525
- if (typeof value === "string") {
526
- return value;
527
- }
528
- if (
529
- typeof value === "object" &&
530
- value !== null &&
531
- "type" in value &&
532
- typeof (value as { type: unknown }).type === "string"
533
- ) {
534
- return (value as { type: string }).type;
535
- }
536
- return null;
537
- }
@@ -183,15 +183,13 @@ const fetchedMarketplaceManifestSchema = marketplaceManifestBaseSchema.extend({
183
183
  plugins: z.array(z.unknown()),
184
184
  });
185
185
 
186
+ const marketplaceSourceDiscriminatorSchema = z.object({
187
+ source: z.object({ source: z.unknown() }),
188
+ });
189
+
186
190
  function readSourceDiscriminator(entry: unknown): unknown {
187
- if (typeof entry !== "object" || entry === null) {
188
- return null;
189
- }
190
- const source = (entry as Record<string, unknown>).source;
191
- if (typeof source !== "object" || source === null) {
192
- return null;
193
- }
194
- return (source as Record<string, unknown>).source;
191
+ const parsed = marketplaceSourceDiscriminatorSchema.safeParse(entry);
192
+ return parsed.success ? parsed.data.source.source : null;
195
193
  }
196
194
 
197
195
  /** A single reviewed plugin entry. */
@@ -23,6 +23,8 @@
23
23
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
24
24
  import { join } from "node:path";
25
25
 
26
+ import { z } from "zod";
27
+
26
28
  /** A schedule the plugin declares under `schedules/`. */
27
29
  export interface PluginScheduleSurface {
28
30
  /** Schedule name: the declaration directory's name. */
@@ -124,15 +126,14 @@ function listSkillIds(skillsDir: string): string[] {
124
126
  /** Matches a `---` delimited YAML frontmatter block at the start of a file. */
125
127
  const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
126
128
 
129
+ const ScheduleConfigSchema = z.object({
130
+ expression: z.string().refine((expression) => expression.trim() !== ""),
131
+ });
132
+
127
133
  /** Read the raw `expression` string from a parsed schedule config, or `null`. */
128
134
  function readConfigExpression(config: unknown): string | null {
129
- if (typeof config !== "object" || config === null || Array.isArray(config)) {
130
- return null;
131
- }
132
- const expression = (config as Record<string, unknown>).expression;
133
- return typeof expression === "string" && expression.trim() !== ""
134
- ? expression
135
- : null;
135
+ const parsed = ScheduleConfigSchema.safeParse(config);
136
+ return parsed.success ? parsed.data.expression : null;
136
137
  }
137
138
 
138
139
  /**
package/src/cli.ts CHANGED
@@ -157,7 +157,7 @@ export async function startCli(): Promise<void> {
157
157
  }),
158
158
  );
159
159
 
160
- const result = await new Promise<{
160
+ return await new Promise<{
161
161
  ok: boolean;
162
162
  error?: string;
163
163
  message?: string;
@@ -208,8 +208,6 @@ export async function startCli(): Promise<void> {
208
208
  checkResult();
209
209
  }
210
210
  });
211
-
212
- return result;
213
211
  } catch {
214
212
  return { ok: false };
215
213
  }
@@ -0,0 +1,121 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { getWorkspaceConfigPath } from "../../util/platform.js";
5
+ import { loadRawConfig, saveRawConfig } from "../loader.js";
6
+ import { AssistantConfigSchema } from "../schema.js";
7
+ import { AutoArchiveConfigSchema } from "../schemas/conversations.js";
8
+
9
+ describe("chat settings config", () => {
10
+ test("missing settings preserve existing behavior", () => {
11
+ const config = AssistantConfigSchema.parse({});
12
+ expect(config.conversations.autoArchive).toEqual({
13
+ enabled: false,
14
+ afterDays: 7,
15
+ });
16
+ expect(config.notifications.newMessageEnabled).toBe(true);
17
+ });
18
+
19
+ test.each([1, 7, 14, 30])(
20
+ "accepts a %i-day inactivity interval",
21
+ (afterDays) => {
22
+ expect(AutoArchiveConfigSchema.parse({ afterDays })).toEqual({
23
+ enabled: false,
24
+ afterDays,
25
+ });
26
+ },
27
+ );
28
+
29
+ test.each([
30
+ { conversations: { autoArchive: { enabled: "false" } } },
31
+ { conversations: { autoArchive: { afterDays: 2 } } },
32
+ { conversations: { autoArchive: { afterDays: 1.5 } } },
33
+ { conversations: { autoArchive: { enabled: null } } },
34
+ { conversations: { autoArchive: [] } },
35
+ { conversations: null },
36
+ { notifications: { newMessageEnabled: 1 } },
37
+ { notifications: { newMessageEnabled: null } },
38
+ { notifications: [] },
39
+ ])(
40
+ "raw writes reject invalid settings before changing disk: %j",
41
+ (invalid) => {
42
+ const original = {
43
+ conversations: { autoArchive: { enabled: true, afterDays: 14 } },
44
+ notifications: { newMessageEnabled: false },
45
+ };
46
+ saveRawConfig(original);
47
+ expect(() => saveRawConfig(invalid)).toThrow();
48
+ expect(loadRawConfig()).toEqual(original);
49
+ },
50
+ );
51
+
52
+ test.each([
53
+ { conversations: false },
54
+ { conversations: [] },
55
+ { conversations: null },
56
+ { conversations: { autoArchive: "legacy" } },
57
+ { conversations: { autoArchive: [] } },
58
+ { conversations: { autoArchive: null } },
59
+ { conversations: { autoArchive: { enabled: "true", afterDays: 2 } } },
60
+ { conversations: { autoArchive: { enabled: null, afterDays: null } } },
61
+ { notifications: "legacy" },
62
+ { notifications: [] },
63
+ { notifications: null },
64
+ { notifications: { newMessageEnabled: "false" } },
65
+ { notifications: { newMessageEnabled: null } },
66
+ ])("raw unrelated writes preserve untouched legacy values: %j", (legacy) => {
67
+ const original = { ...legacy, maxStepsPerSession: 75 };
68
+ writeFileSync(getWorkspaceConfigPath(), JSON.stringify(original));
69
+
70
+ const updated = { ...loadRawConfig(), maxStepsPerSession: 100 };
71
+ saveRawConfig(updated);
72
+
73
+ expect(loadRawConfig()).toEqual({ ...original, maxStepsPerSession: 100 });
74
+ });
75
+
76
+ test.each([
77
+ [{ conversations: false }, { conversations: [] }],
78
+ [
79
+ { conversations: [] },
80
+ { conversations: { autoArchive: { enabled: "true" } } },
81
+ ],
82
+ [
83
+ { conversations: { autoArchive: null } },
84
+ { conversations: { autoArchive: { afterDays: 2 } } },
85
+ ],
86
+ [
87
+ { notifications: { newMessageEnabled: "false" } },
88
+ { notifications: { newMessageEnabled: "true" } },
89
+ ],
90
+ [
91
+ { conversations: { autoArchive: { enabled: "true", afterDays: 2 } } },
92
+ { conversations: { autoArchive: { enabled: "true", afterDays: 3 } } },
93
+ ],
94
+ ])("raw writes reject changed invalid values: %j -> %j", (previous, next) => {
95
+ const configPath = getWorkspaceConfigPath();
96
+ writeFileSync(configPath, JSON.stringify(previous));
97
+ const originalBytes = readFileSync(configPath, "utf-8");
98
+
99
+ expect(() => saveRawConfig(next)).toThrow();
100
+
101
+ expect(readFileSync(configPath, "utf-8")).toBe(originalBytes);
102
+ });
103
+
104
+ test("raw writes can repair one leaf while preserving other legacy values", () => {
105
+ writeFileSync(
106
+ getWorkspaceConfigPath(),
107
+ JSON.stringify({
108
+ conversations: { autoArchive: { enabled: "true", afterDays: 2 } },
109
+ notifications: null,
110
+ }),
111
+ );
112
+ const repaired = {
113
+ conversations: { autoArchive: { enabled: true, afterDays: 2 } },
114
+ notifications: null,
115
+ };
116
+
117
+ saveRawConfig(repaired);
118
+
119
+ expect(loadRawConfig()).toEqual(repaired);
120
+ });
121
+ });
@@ -16,6 +16,8 @@
16
16
  import { existsSync, readFileSync } from "node:fs";
17
17
  import { dirname, join } from "node:path";
18
18
 
19
+ import { z } from "zod";
20
+
19
21
  import { ipcGetFeatureFlags } from "../ipc/gateway-client.js";
20
22
  import { getLogger } from "../util/logger.js";
21
23
  import {
@@ -47,6 +49,22 @@ export type FeatureFlagDefaultsRegistry = Record<string, FeatureFlagDefault>;
47
49
  let cachedDefaults: FeatureFlagDefaultsRegistry | undefined;
48
50
 
49
51
  const REGISTRY_FILENAME = "feature-flag-registry.json";
52
+ const FeatureFlagRegistryEntrySchema = z
53
+ .object({
54
+ scope: z.enum(["assistant", "both"]).optional().catch(undefined),
55
+ key: z.string().optional().catch(undefined),
56
+ defaultEnabled: z
57
+ .union([z.boolean(), z.string()])
58
+ .optional()
59
+ .catch(undefined),
60
+ description: z.string().catch(""),
61
+ label: z.string().catch(""),
62
+ })
63
+ .optional()
64
+ .catch(undefined);
65
+ const FeatureFlagRegistrySchema = z.object({
66
+ flags: z.array(FeatureFlagRegistryEntrySchema),
67
+ });
50
68
 
51
69
  function loadDefaultsRegistry(): FeatureFlagDefaultsRegistry {
52
70
  if (cachedDefaults) {
@@ -100,39 +118,26 @@ function loadDefaultsRegistry(): FeatureFlagDefaultsRegistry {
100
118
  * filtering to flags the backend consumes (`assistant`- and `both`-scope).
101
119
  */
102
120
  function parseRegistryToDefaults(parsed: unknown): FeatureFlagDefaultsRegistry {
103
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
104
- return {};
105
- }
106
-
107
- const registry = parsed as { version?: number; flags?: unknown[] };
108
- if (!Array.isArray(registry.flags)) {
121
+ const registry = FeatureFlagRegistrySchema.safeParse(parsed);
122
+ if (!registry.success) {
109
123
  return {};
110
124
  }
111
125
 
112
126
  const result: FeatureFlagDefaultsRegistry = {};
113
- for (const flag of registry.flags) {
114
- if (!flag || typeof flag !== "object" || Array.isArray(flag)) {
115
- continue;
116
- }
117
- const entry = flag as Record<string, unknown>;
118
- if (entry.scope !== "assistant" && entry.scope !== "both") {
119
- continue;
120
- }
121
- if (typeof entry.key !== "string") {
122
- continue;
123
- }
127
+ for (const entry of registry.data.flags) {
124
128
  if (
125
- typeof entry.defaultEnabled !== "boolean" &&
126
- typeof entry.defaultEnabled !== "string"
129
+ !entry ||
130
+ entry.scope === undefined ||
131
+ entry.key === undefined ||
132
+ entry.defaultEnabled === undefined
127
133
  ) {
128
134
  continue;
129
135
  }
130
136
 
131
- result[entry.key as string] = {
137
+ result[entry.key] = {
132
138
  defaultEnabled: entry.defaultEnabled,
133
- description:
134
- typeof entry.description === "string" ? entry.description : "",
135
- label: typeof entry.label === "string" ? entry.label : "",
139
+ description: entry.description,
140
+ label: entry.label,
136
141
  };
137
142
  }
138
143
  return result;
@@ -37,6 +37,26 @@ The tree is walked to a limited depth to keep steps fast, and says when it was
37
37
  cut off. If the element you need is not in it, call `computer_use_observe` with
38
38
  `full_tree: true`.
39
39
 
40
+ ## Coordinates and live editing
41
+
42
+ Prefer element IDs from the latest observation. Click, scroll and drag
43
+ coordinates are **screen points**, not screenshot pixels. For a desktop
44
+ screenshot, use the per-axis conversion in its observation metadata. If you
45
+ crop or resize the image, map back to the original screenshot first. Observe
46
+ again after the layout changes; an older image is not a current target.
47
+
48
+ During live editing, use the app's zoom and track-height controls, then inspect
49
+ the fresh screenshot. Avoid installing image libraries or repeatedly cropping
50
+ and measuring screenshot pixels just to locate a UI control or waveform.
51
+ Reserve scripted image or media analysis for tasks that actually require it.
52
+ If an action does not produce the expected visible result, check focus and the
53
+ coordinate mapping before repeating it.
54
+
55
+ Waveform pixels do not reveal spoken words. If an edit depends on audio you
56
+ cannot hear or a boundary you cannot see, ask for a timestamp, a user-positioned
57
+ playhead, or the source media. Do not guess the cut point or claim an edit
58
+ succeeded without observing its result.
59
+
40
60
  ## Scripting apps (macOS)
41
61
 
42
62
  Reach for `computer_use_run_applescript` first when an app can be driven by
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "full_tree": {
23
23
  "type": "boolean",
24
- "description": "On connected personal computers, walk the accessibility tree to full depth. The virtual desktop uses a bounded tree; use screenshot coordinates when an element is not listed."
24
+ "description": "On connected personal computers, walk the accessibility tree to full depth. The virtual desktop uses a bounded tree; use coordinates converted from the screenshot metadata when an element is not listed."
25
25
  },
26
26
  "target_client_id": {
27
27
  "type": "string",
@@ -57,11 +57,11 @@
57
57
  },
58
58
  "x": {
59
59
  "type": "integer",
60
- "description": "X coordinate on screen (fallback when no element_id)"
60
+ "description": "X in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no element_id)."
61
61
  },
62
62
  "y": {
63
63
  "type": "integer",
64
- "description": "Y coordinate on screen (fallback when no element_id)"
64
+ "description": "Y in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no element_id)."
65
65
  },
66
66
  "reasoning": {
67
67
  "type": "string",
@@ -170,11 +170,11 @@
170
170
  },
171
171
  "x": {
172
172
  "type": "integer",
173
- "description": "X coordinate on screen (fallback when no element_id)"
173
+ "description": "X in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no element_id)."
174
174
  },
175
175
  "y": {
176
176
  "type": "integer",
177
- "description": "Y coordinate on screen (fallback when no element_id)"
177
+ "description": "Y in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no element_id)."
178
178
  },
179
179
  "direction": {
180
180
  "type": "string",
@@ -222,11 +222,11 @@
222
222
  },
223
223
  "x": {
224
224
  "type": "integer",
225
- "description": "Source X coordinate (fallback when no element_id)"
225
+ "description": "Source X in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no element_id)."
226
226
  },
227
227
  "y": {
228
228
  "type": "integer",
229
- "description": "Source Y coordinate (fallback when no element_id)"
229
+ "description": "Source Y in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no element_id)."
230
230
  },
231
231
  "to_element_id": {
232
232
  "type": "integer",
@@ -234,11 +234,11 @@
234
234
  },
235
235
  "to_x": {
236
236
  "type": "integer",
237
- "description": "Destination X coordinate (fallback when no to_element_id)"
237
+ "description": "Destination X in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no to_element_id)."
238
238
  },
239
239
  "to_y": {
240
240
  "type": "integer",
241
- "description": "Destination Y coordinate (fallback when no to_element_id)"
241
+ "description": "Destination Y in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no to_element_id)."
242
242
  },
243
243
  "reasoning": {
244
244
  "type": "string",
@@ -408,11 +408,11 @@
408
408
  },
409
409
  "x": {
410
410
  "type": "integer",
411
- "description": "click, double_click, right_click, scroll: X coordinate on screen (fallback when no element_id)"
411
+ "description": "click, double_click, right_click, scroll: X in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no element_id)."
412
412
  },
413
413
  "y": {
414
414
  "type": "integer",
415
- "description": "click, double_click, right_click, scroll: Y coordinate on screen (fallback when no element_id)"
415
+ "description": "click, double_click, right_click, scroll: Y in screen points, not screenshot pixels. Apply the latest observation's coordinate mapping (fallback when no element_id)."
416
416
  },
417
417
  "direction": {
418
418
  "type": "string",