@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,229 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import { executeWebFetch } from "../web-fetch.js";
4
+
5
+ type RequestExecutor = (
6
+ url: URL,
7
+ requestOptions: {
8
+ signal: AbortSignal;
9
+ headers: Record<string, string>;
10
+ resolvedAddress?: string;
11
+ },
12
+ ) => Promise<Response>;
13
+
14
+ const executeWithMockFetch = (
15
+ input: Record<string, unknown>,
16
+ options?: {
17
+ resolveHostAddresses?: (hostname: string) => Promise<string[]>;
18
+ requestExecutor?: RequestExecutor;
19
+ },
20
+ ) =>
21
+ executeWebFetch(input, {
22
+ resolveHostAddresses:
23
+ options?.resolveHostAddresses ?? (async () => ["93.184.216.34"]),
24
+ requestExecutor:
25
+ options?.requestExecutor ??
26
+ ((url, requestOptions) =>
27
+ globalThis.fetch(url.href, {
28
+ method: "GET",
29
+ redirect: "manual",
30
+ signal: requestOptions.signal,
31
+ headers: requestOptions.headers,
32
+ }) as Promise<Response>),
33
+ });
34
+
35
+ describe("web_fetch activityMetadata", () => {
36
+ let originalFetch: typeof globalThis.fetch;
37
+
38
+ beforeEach(() => {
39
+ originalFetch = globalThis.fetch;
40
+ });
41
+
42
+ afterEach(() => {
43
+ globalThis.fetch = originalFetch;
44
+ });
45
+
46
+ test("populates metadata on a 200 HTML response with a <title>", async () => {
47
+ const body =
48
+ "<!doctype html><html><head><title>Example Domain</title></head>" +
49
+ "<body><p>Hello, world.</p></body></html>";
50
+ globalThis.fetch = (async () =>
51
+ new Response(body, {
52
+ status: 200,
53
+ headers: { "content-type": "text/html; charset=utf-8" },
54
+ })) as unknown as typeof globalThis.fetch;
55
+
56
+ const result = await executeWithMockFetch({ url: "https://example.com/" });
57
+ expect(result.isError).toBe(false);
58
+
59
+ const meta = result.activityMetadata?.webFetch;
60
+ expect(meta).toBeDefined();
61
+ expect(meta?.url).toBe("https://example.com/");
62
+ expect(meta?.finalUrl).toBe("https://example.com/");
63
+ expect(meta?.status).toBe(200);
64
+ expect(meta?.contentType).toContain("text/html");
65
+ expect(meta?.title).toBe("Example Domain");
66
+ expect(meta?.domain).toBe("example.com");
67
+ expect(meta?.byteCount).toBe(Buffer.byteLength(body));
68
+ expect(meta?.charCount).toBeGreaterThan(0);
69
+ expect(meta?.redirectCount).toBe(0);
70
+ expect(meta?.truncated).toBe(false);
71
+ expect(meta?.durationMs).toBeGreaterThanOrEqual(0);
72
+ expect(meta?.errorMessage).toBeUndefined();
73
+ });
74
+
75
+ test("tracks redirect chains in finalUrl and redirectCount", async () => {
76
+ let callCount = 0;
77
+ globalThis.fetch = (async (rawUrl: string) => {
78
+ callCount++;
79
+ if (callCount === 1) {
80
+ expect(rawUrl).toBe("https://example.com/start");
81
+ return new Response("", {
82
+ status: 302,
83
+ headers: { location: "https://example.com/middle" },
84
+ });
85
+ }
86
+ if (callCount === 2) {
87
+ return new Response("", {
88
+ status: 301,
89
+ headers: { location: "https://example.com/final" },
90
+ });
91
+ }
92
+ return new Response("<!doctype html><title>Final</title><p>done</p>", {
93
+ status: 200,
94
+ headers: { "content-type": "text/html; charset=utf-8" },
95
+ });
96
+ }) as unknown as typeof globalThis.fetch;
97
+
98
+ const result = await executeWithMockFetch({
99
+ url: "https://example.com/start",
100
+ });
101
+ expect(result.isError).toBe(false);
102
+
103
+ const meta = result.activityMetadata?.webFetch;
104
+ expect(meta).toBeDefined();
105
+ expect(meta?.url).toBe("https://example.com/start");
106
+ expect(meta?.finalUrl).toBe("https://example.com/final");
107
+ expect(meta?.finalUrl).not.toBe(meta?.url);
108
+ expect(meta?.redirectCount).toBe(2);
109
+ expect(meta?.title).toBe("Final");
110
+ });
111
+
112
+ test("flags truncation when content exceeds max_chars", async () => {
113
+ const longBody = "x".repeat(50_000);
114
+ globalThis.fetch = (async () =>
115
+ new Response(longBody, {
116
+ status: 200,
117
+ headers: { "content-type": "text/plain; charset=utf-8" },
118
+ })) as unknown as typeof globalThis.fetch;
119
+
120
+ const maxChars = 1_000;
121
+ const result = await executeWithMockFetch({
122
+ url: "https://example.com/large",
123
+ max_chars: maxChars,
124
+ });
125
+ expect(result.isError).toBe(false);
126
+
127
+ const meta = result.activityMetadata?.webFetch;
128
+ expect(meta).toBeDefined();
129
+ expect(meta?.truncated).toBe(true);
130
+ expect(meta?.charCount).toBe(maxChars);
131
+ });
132
+
133
+ test("populates errorMessage and status on a 404 response", async () => {
134
+ globalThis.fetch = (async () =>
135
+ new Response("<title>Not Found</title>not here", {
136
+ status: 404,
137
+ statusText: "Not Found",
138
+ headers: { "content-type": "text/html; charset=utf-8" },
139
+ })) as unknown as typeof globalThis.fetch;
140
+
141
+ const result = await executeWithMockFetch({
142
+ url: "https://example.com/missing",
143
+ });
144
+ expect(result.isError).toBe(true);
145
+
146
+ const meta = result.activityMetadata?.webFetch;
147
+ expect(meta).toBeDefined();
148
+ expect(meta?.status).toBe(404);
149
+ expect(meta?.errorMessage).toBeDefined();
150
+ expect(meta?.errorMessage).toContain("HTTP 404");
151
+ expect(meta?.url).toBe("https://example.com/missing");
152
+ expect(meta?.finalUrl).toBe("https://example.com/missing");
153
+ expect(meta?.domain).toBe("example.com");
154
+ });
155
+
156
+ test("flags mayRequireJavaScript when HTML compresses to <5% text and exceeds 10KB", async () => {
157
+ const scriptPayload = `var x = ${JSON.stringify("a".repeat(40_000))};`;
158
+ const body =
159
+ "<!doctype html><html><head><title>App</title></head>" +
160
+ `<body><div id="root"></div><script>${scriptPayload}</script></body></html>`;
161
+ globalThis.fetch = (async () =>
162
+ new Response(body, {
163
+ status: 200,
164
+ headers: { "content-type": "text/html; charset=utf-8" },
165
+ })) as unknown as typeof globalThis.fetch;
166
+
167
+ const result = await executeWithMockFetch({ url: "https://example.com/" });
168
+ expect(result.isError).toBe(false);
169
+
170
+ const meta = result.activityMetadata?.webFetch;
171
+ expect(meta?.mayRequireJavaScript).toBe(true);
172
+ expect(result.status).toContain("Content may be JavaScript-rendered");
173
+ expect(result.status).toContain(`${meta?.byteCount} bytes`);
174
+ });
175
+
176
+ test("flags mayRequireJavaScript when extracted text is under 200 chars", async () => {
177
+ const body =
178
+ '<!doctype html><html><head><title>Tiny</title></head><body><div id="app"></div></body></html>';
179
+ globalThis.fetch = (async () =>
180
+ new Response(body, {
181
+ status: 200,
182
+ headers: { "content-type": "text/html; charset=utf-8" },
183
+ })) as unknown as typeof globalThis.fetch;
184
+
185
+ const result = await executeWithMockFetch({ url: "https://example.com/" });
186
+ expect(result.isError).toBe(false);
187
+
188
+ const meta = result.activityMetadata?.webFetch;
189
+ expect(meta?.mayRequireJavaScript).toBe(true);
190
+ expect(result.status).toContain("Content may be JavaScript-rendered");
191
+ });
192
+
193
+ test("does not flag mayRequireJavaScript for content-heavy HTML", async () => {
194
+ const paragraph =
195
+ "<p>" +
196
+ "The quick brown fox jumps over the lazy dog. ".repeat(20) +
197
+ "</p>";
198
+ const body =
199
+ "<!doctype html><html><head><title>Article</title></head>" +
200
+ `<body>${paragraph.repeat(40)}</body></html>`;
201
+ globalThis.fetch = (async () =>
202
+ new Response(body, {
203
+ status: 200,
204
+ headers: { "content-type": "text/html; charset=utf-8" },
205
+ })) as unknown as typeof globalThis.fetch;
206
+
207
+ const result = await executeWithMockFetch({ url: "https://example.com/" });
208
+ expect(result.isError).toBe(false);
209
+
210
+ const meta = result.activityMetadata?.webFetch;
211
+ expect(meta?.mayRequireJavaScript).toBeUndefined();
212
+ expect(result.status ?? "").not.toContain("JavaScript-rendered");
213
+ });
214
+
215
+ test("populates metadata for blocked private-network targets", async () => {
216
+ const result = await executeWithMockFetch({
217
+ url: "http://127.0.0.1/admin",
218
+ });
219
+ expect(result.isError).toBe(true);
220
+
221
+ const meta = result.activityMetadata?.webFetch;
222
+ expect(meta).toBeDefined();
223
+ expect(meta?.errorMessage).toContain(
224
+ "Refusing to fetch local/private network target",
225
+ );
226
+ expect(meta?.domain).toBe("127.0.0.1");
227
+ expect(meta?.status).toBe(0);
228
+ });
229
+ });
@@ -0,0 +1,346 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Mutable mock state - set per test
4
+ let mockWebSearchProvider: string | undefined = "perplexity";
5
+ let mockBraveSecureKey: string | undefined;
6
+ let mockPerplexitySecureKey: string | undefined;
7
+ let mockTavilySecureKey: string | undefined;
8
+
9
+ // Capture the registered tool
10
+ let capturedTool: any = null;
11
+
12
+ mock.module("../../registry.js", () => ({
13
+ registerTool: (tool: any) => {
14
+ capturedTool = tool;
15
+ },
16
+ }));
17
+
18
+ mock.module("../../../config/loader.js", () => ({
19
+ getConfig: () => ({
20
+ services: {
21
+ "web-search": { provider: mockWebSearchProvider },
22
+ },
23
+ }),
24
+ }));
25
+
26
+ mock.module("../../../security/secure-keys.js", () => ({
27
+ getProviderKeyAsync: async (provider: string) => {
28
+ if (provider === "brave") return mockBraveSecureKey;
29
+ if (provider === "perplexity") return mockPerplexitySecureKey;
30
+ if (provider === "tavily") return mockTavilySecureKey;
31
+ return undefined;
32
+ },
33
+ }));
34
+
35
+ mock.module("../../../util/logger.js", () => ({
36
+ getLogger: () =>
37
+ new Proxy({} as Record<string, unknown>, {
38
+ get: () => () => {},
39
+ }),
40
+ }));
41
+
42
+ mock.module("../../../permissions/types.js", () => ({
43
+ RiskLevel: { Low: "low", Medium: "medium", High: "high" },
44
+ }));
45
+
46
+ // Force the module to load (triggers registerTool)
47
+ await import("../web-search.js");
48
+
49
+ describe("web_search activity metadata", () => {
50
+ let originalFetch: typeof globalThis.fetch;
51
+
52
+ beforeEach(() => {
53
+ originalFetch = globalThis.fetch;
54
+ mockWebSearchProvider = "perplexity";
55
+ mockBraveSecureKey = undefined;
56
+ mockPerplexitySecureKey = undefined;
57
+ mockTavilySecureKey = undefined;
58
+ });
59
+
60
+ afterEach(() => {
61
+ globalThis.fetch = originalFetch;
62
+ });
63
+
64
+ function execute(input: Record<string, unknown>) {
65
+ return capturedTool.execute(input, {} as any);
66
+ }
67
+
68
+ // ---- Brave --------------------------------------------------------------
69
+
70
+ test("Brave populates webSearch metadata on success", async () => {
71
+ mockWebSearchProvider = "brave";
72
+ mockBraveSecureKey = "brave-key";
73
+ globalThis.fetch = (async () =>
74
+ new Response(
75
+ JSON.stringify({
76
+ web: {
77
+ results: [
78
+ {
79
+ title: "Brave One",
80
+ url: "https://example.com/one",
81
+ description: "First Brave result",
82
+ age: "1 day ago",
83
+ },
84
+ {
85
+ title: "Brave Two",
86
+ url: "https://other.example.org/two",
87
+ description: "Second result",
88
+ },
89
+ ],
90
+ },
91
+ }),
92
+ { status: 200, headers: { "content-type": "application/json" } },
93
+ )) as any;
94
+
95
+ const result = await execute({ query: "brave query" });
96
+ const meta = result.activityMetadata?.webSearch;
97
+ expect(meta).toBeDefined();
98
+ expect(meta.provider).toBe("brave");
99
+ expect(meta.query).toBe("brave query");
100
+ expect(meta.resultCount).toBe(2);
101
+ expect(typeof meta.durationMs).toBe("number");
102
+ expect(meta.errorMessage).toBeUndefined();
103
+ expect(meta.results[0].rank).toBe(1);
104
+ expect(meta.results[0].title).toBe("Brave One");
105
+ expect(meta.results[0].url).toBe("https://example.com/one");
106
+ expect(meta.results[0].domain).toBe("example.com");
107
+ expect(meta.results[0].snippet).toBe("First Brave result");
108
+ expect(meta.results[0].age).toBe("1 day ago");
109
+ expect(meta.results[1].rank).toBe(2);
110
+ expect(meta.results[1].domain).toBe("other.example.org");
111
+ });
112
+
113
+ test("Brave populates errorMessage and empty results on auth failure", async () => {
114
+ mockWebSearchProvider = "brave";
115
+ mockBraveSecureKey = "bad-key";
116
+ globalThis.fetch = (async () =>
117
+ new Response("Forbidden", { status: 403 })) as any;
118
+
119
+ const result = await execute({ query: "brave fail" });
120
+ const meta = result.activityMetadata?.webSearch;
121
+ expect(meta).toBeDefined();
122
+ expect(meta.provider).toBe("brave");
123
+ expect(meta.resultCount).toBe(0);
124
+ expect(meta.results).toEqual([]);
125
+ expect(meta.errorMessage).toContain("Invalid or expired Brave Search");
126
+ });
127
+
128
+ // ---- Perplexity ---------------------------------------------------------
129
+
130
+ test("Perplexity populates webSearch metadata from citations", async () => {
131
+ mockPerplexitySecureKey = "pplx-key";
132
+ globalThis.fetch = (async () =>
133
+ new Response(
134
+ JSON.stringify({
135
+ choices: [{ message: { content: "answer text" } }],
136
+ citations: [
137
+ "https://typescriptlang.org/docs",
138
+ "https://example.com/article",
139
+ ],
140
+ }),
141
+ { status: 200, headers: { "content-type": "application/json" } },
142
+ )) as any;
143
+
144
+ const result = await execute({ query: "perplexity query" });
145
+ const meta = result.activityMetadata?.webSearch;
146
+ expect(meta).toBeDefined();
147
+ expect(meta.provider).toBe("perplexity");
148
+ expect(meta.query).toBe("perplexity query");
149
+ expect(meta.resultCount).toBe(2);
150
+ expect(meta.results[0].rank).toBe(1);
151
+ expect(meta.results[0].url).toBe("https://typescriptlang.org/docs");
152
+ expect(meta.results[0].domain).toBe("typescriptlang.org");
153
+ expect(meta.results[0].title).toBe("");
154
+ expect(meta.results[0].snippet).toBeUndefined();
155
+ expect(meta.results[1].domain).toBe("example.com");
156
+ });
157
+
158
+ test("Perplexity populates errorMessage on rate-limit exhaustion", async () => {
159
+ mockPerplexitySecureKey = "pplx-key";
160
+ globalThis.fetch = (async () =>
161
+ new Response("Too Many Requests", {
162
+ status: 429,
163
+ headers: { "retry-after": "0" },
164
+ })) as any;
165
+
166
+ const result = await execute({ query: "rate limited" });
167
+ const meta = result.activityMetadata?.webSearch;
168
+ expect(meta).toBeDefined();
169
+ expect(meta.provider).toBe("perplexity");
170
+ expect(meta.resultCount).toBe(0);
171
+ expect(meta.results).toEqual([]);
172
+ expect(meta.errorMessage).toContain("rate limit exceeded");
173
+ });
174
+
175
+ // ---- Tavily -------------------------------------------------------------
176
+
177
+ test("Tavily populates webSearch metadata with favicon and score", async () => {
178
+ mockWebSearchProvider = "tavily";
179
+ mockTavilySecureKey = "tvly-key";
180
+ globalThis.fetch = (async () =>
181
+ new Response(
182
+ JSON.stringify({
183
+ results: [
184
+ {
185
+ title: "Tavily One",
186
+ url: "https://docs.example.com/page",
187
+ content: "Tavily snippet text",
188
+ score: 0.87,
189
+ favicon: "https://docs.example.com/favicon.ico",
190
+ },
191
+ {
192
+ title: "Tavily Two",
193
+ url: "https://blog.example.org/post",
194
+ content: "Second tavily content",
195
+ score: 0.42,
196
+ },
197
+ ],
198
+ }),
199
+ { status: 200, headers: { "content-type": "application/json" } },
200
+ )) as any;
201
+
202
+ const result = await execute({ query: "tavily query" });
203
+ const meta = result.activityMetadata?.webSearch;
204
+ expect(meta).toBeDefined();
205
+ expect(meta.provider).toBe("tavily");
206
+ expect(meta.resultCount).toBe(2);
207
+ expect(meta.results[0].rank).toBe(1);
208
+ expect(meta.results[0].title).toBe("Tavily One");
209
+ expect(meta.results[0].url).toBe("https://docs.example.com/page");
210
+ expect(meta.results[0].domain).toBe("docs.example.com");
211
+ expect(meta.results[0].faviconUrl).toBe(
212
+ "https://docs.example.com/favicon.ico",
213
+ );
214
+ expect(meta.results[0].snippet).toBe("Tavily snippet text");
215
+ expect(meta.results[0].score).toBe(0.87);
216
+ // PR 5 backfills a synthesized favicon URL via Google s2 when the
217
+ // provider doesn't supply one, so this result now has a faviconUrl too.
218
+ expect(meta.results[1].faviconUrl).toContain(
219
+ "google.com/s2/favicons",
220
+ );
221
+ expect(meta.results[1].score).toBe(0.42);
222
+ });
223
+
224
+ test("Tavily falls back to url for missing title", async () => {
225
+ mockWebSearchProvider = "tavily";
226
+ mockTavilySecureKey = "tvly-key";
227
+ globalThis.fetch = (async () =>
228
+ new Response(
229
+ JSON.stringify({
230
+ results: [
231
+ {
232
+ url: "https://example.net/article",
233
+ content: "No title here",
234
+ },
235
+ ],
236
+ }),
237
+ { status: 200, headers: { "content-type": "application/json" } },
238
+ )) as any;
239
+
240
+ const result = await execute({ query: "no title" });
241
+ const meta = result.activityMetadata?.webSearch;
242
+ expect(meta).toBeDefined();
243
+ expect(meta.results[0].title).toBe("https://example.net/article");
244
+ expect(meta.results[0].domain).toBe("example.net");
245
+ });
246
+
247
+ test("Tavily falls back to url for empty string title", async () => {
248
+ mockWebSearchProvider = "tavily";
249
+ mockTavilySecureKey = "tvly-key";
250
+ globalThis.fetch = (async () =>
251
+ new Response(
252
+ JSON.stringify({
253
+ results: [
254
+ {
255
+ title: "",
256
+ url: "https://example.net/empty-title",
257
+ content: "Empty title",
258
+ },
259
+ ],
260
+ }),
261
+ { status: 200, headers: { "content-type": "application/json" } },
262
+ )) as any;
263
+
264
+ const result = await execute({ query: "empty title" });
265
+ const meta = result.activityMetadata?.webSearch;
266
+ expect(meta).toBeDefined();
267
+ expect(meta.results[0].title).toBe("https://example.net/empty-title");
268
+ });
269
+
270
+ test("Tavily falls back to url for whitespace-only title", async () => {
271
+ mockWebSearchProvider = "tavily";
272
+ mockTavilySecureKey = "tvly-key";
273
+ globalThis.fetch = (async () =>
274
+ new Response(
275
+ JSON.stringify({
276
+ results: [
277
+ {
278
+ title: " ",
279
+ url: "https://example.net/whitespace-title",
280
+ content: "Whitespace title",
281
+ },
282
+ ],
283
+ }),
284
+ { status: 200, headers: { "content-type": "application/json" } },
285
+ )) as any;
286
+
287
+ const result = await execute({ query: "whitespace title" });
288
+ const meta = result.activityMetadata?.webSearch;
289
+ expect(meta).toBeDefined();
290
+ expect(meta.results[0].title).toBe(
291
+ "https://example.net/whitespace-title",
292
+ );
293
+ });
294
+
295
+ test("Tavily populates errorMessage on auth failure", async () => {
296
+ mockWebSearchProvider = "tavily";
297
+ mockTavilySecureKey = "bad-key";
298
+ globalThis.fetch = (async () =>
299
+ new Response("Unauthorized", { status: 401 })) as any;
300
+
301
+ const result = await execute({ query: "tavily fail" });
302
+ const meta = result.activityMetadata?.webSearch;
303
+ expect(meta).toBeDefined();
304
+ expect(meta.provider).toBe("tavily");
305
+ expect(meta.resultCount).toBe(0);
306
+ expect(meta.results).toEqual([]);
307
+ expect(meta.errorMessage).toContain("Invalid or expired Tavily");
308
+ });
309
+
310
+ // ---- Top-level error paths ---------------------------------------------
311
+
312
+ test("top-level catch populates activityMetadata with errorMessage", async () => {
313
+ mockWebSearchProvider = "perplexity";
314
+ mockPerplexitySecureKey = "pplx-key";
315
+ globalThis.fetch = (async () => {
316
+ throw new Error("network down");
317
+ }) as any;
318
+
319
+ const result = await execute({ query: "catch query" });
320
+ expect(result.isError).toBe(true);
321
+ const meta = result.activityMetadata?.webSearch;
322
+ expect(meta).toBeDefined();
323
+ expect(meta.provider).toBe("perplexity");
324
+ expect(meta.query).toBe("catch query");
325
+ expect(meta.resultCount).toBe(0);
326
+ expect(meta.results).toEqual([]);
327
+ expect(meta.errorMessage).toContain("network down");
328
+ expect(typeof meta.durationMs).toBe("number");
329
+ });
330
+
331
+ test("no-API-key branch populates activityMetadata with errorMessage", async () => {
332
+ mockWebSearchProvider = "perplexity";
333
+ // All provider keys remain undefined from beforeEach.
334
+
335
+ const result = await execute({ query: "no key query" });
336
+ expect(result.isError).toBe(true);
337
+ const meta = result.activityMetadata?.webSearch;
338
+ expect(meta).toBeDefined();
339
+ expect(meta.provider).toBe("perplexity");
340
+ expect(meta.query).toBe("no key query");
341
+ expect(meta.resultCount).toBe(0);
342
+ expect(meta.results).toEqual([]);
343
+ expect(meta.errorMessage).toContain("No web search API key configured");
344
+ expect(typeof meta.durationMs).toBe("number");
345
+ });
346
+ });
@@ -80,6 +80,23 @@ export function normalizeDomain(input: string): DomainInfo | null {
80
80
  };
81
81
  }
82
82
 
83
+ /**
84
+ * Best-effort host extractor for URLs surfaced in tool activity metadata
85
+ * (web-search result items, web-fetch metadata). Returns the lowercased
86
+ * host portion of `rawUrl`, or `""` when the input cannot be parsed.
87
+ *
88
+ * Distinct from {@link normalizeDomain} above: callers here only need a
89
+ * stable display string, not a registrable-domain analysis. We return ""
90
+ * (rather than null) so consumers can treat the result as a plain string.
91
+ */
92
+ export function extractDomain(rawUrl: string): string {
93
+ try {
94
+ return new URL(rawUrl).host.toLowerCase();
95
+ } catch {
96
+ return "";
97
+ }
98
+ }
99
+
83
100
  function isIPAddress(hostname: string): boolean {
84
101
  // IPv4
85
102
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) return true;