mervexai 0.0.97 → 0.0.99

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 (782) hide show
  1. package/LICENSE +21 -0
  2. package/bin/mervex +1 -1
  3. package/package.json +11 -150
  4. package/AGENTS.md +0 -131
  5. package/Dockerfile +0 -18
  6. package/bunfig.toml +0 -7
  7. package/git +0 -0
  8. package/migration/20260511173437_session-metadata/migration.sql +0 -1
  9. package/migration/20260511173437_session-metadata/snapshot.json +0 -1500
  10. package/parsers-config.ts +0 -1
  11. package/script/bench-search.ts +0 -94
  12. package/script/bench-test-suite.ts +0 -52
  13. package/script/build.ts +0 -257
  14. package/script/generate.ts +0 -14
  15. package/script/httpapi-exercise.ts +0 -1
  16. package/script/profile-test-files.ts +0 -42
  17. package/script/publish.ts +0 -213
  18. package/script/run-workspace-server +0 -106
  19. package/script/schema.ts +0 -77
  20. package/script/time.ts +0 -6
  21. package/script/trace-imports.ts +0 -153
  22. package/specs/effect/error-boundaries-plan.md +0 -235
  23. package/specs/effect/errors.md +0 -207
  24. package/specs/effect/facades.md +0 -218
  25. package/specs/effect/guide.md +0 -247
  26. package/specs/effect/instance-context.md +0 -13
  27. package/specs/effect/loose-ends.md +0 -30
  28. package/specs/effect/migration.md +0 -62
  29. package/specs/effect/routes.md +0 -61
  30. package/specs/effect/schema.md +0 -88
  31. package/specs/effect/server-package.md +0 -58
  32. package/specs/effect/todo.md +0 -241
  33. package/specs/effect/tools.md +0 -88
  34. package/specs/openapi-translation-cleanup.md +0 -204
  35. package/specs/tui-plugins.md +0 -544
  36. package/specs/v2/api.ts +0 -67
  37. package/specs/v2/message-shape.md +0 -136
  38. package/specs/v2/notifications.md +0 -13
  39. package/specs/v2/tui-command-shim.md +0 -67
  40. package/src/account/account.ts +0 -481
  41. package/src/account/repo.ts +0 -171
  42. package/src/account/schema.ts +0 -99
  43. package/src/account/url.ts +0 -8
  44. package/src/acp/agent.ts +0 -95
  45. package/src/acp/config-option.ts +0 -203
  46. package/src/acp/content.ts +0 -269
  47. package/src/acp/directory.ts +0 -212
  48. package/src/acp/error.ts +0 -97
  49. package/src/acp/event.ts +0 -421
  50. package/src/acp/permission.ts +0 -254
  51. package/src/acp/profile.ts +0 -42
  52. package/src/acp/service.ts +0 -1105
  53. package/src/acp/session.ts +0 -232
  54. package/src/acp/tool.ts +0 -364
  55. package/src/acp/usage.ts +0 -243
  56. package/src/agent/agent.ts +0 -508
  57. package/src/agent/generate.txt +0 -75
  58. package/src/agent/prompt/architect.txt +0 -7
  59. package/src/agent/prompt/ask.txt +0 -7
  60. package/src/agent/prompt/compaction.txt +0 -5
  61. package/src/agent/prompt/explore.txt +0 -18
  62. package/src/agent/prompt/summary.txt +0 -11
  63. package/src/agent/prompt/title.txt +0 -44
  64. package/src/agent/subagent-permissions.ts +0 -27
  65. package/src/audio.d.ts +0 -14
  66. package/src/auth/index.ts +0 -97
  67. package/src/background/job.ts +0 -37
  68. package/src/bus/global.ts +0 -22
  69. package/src/cli/bootstrap.ts +0 -11
  70. package/src/cli/cmd/account.ts +0 -264
  71. package/src/cli/cmd/acp.ts +0 -73
  72. package/src/cli/cmd/agent.ts +0 -259
  73. package/src/cli/cmd/attach.ts +0 -148
  74. package/src/cli/cmd/cmd.ts +0 -7
  75. package/src/cli/cmd/db.ts +0 -62
  76. package/src/cli/cmd/debug/agent.handler.ts +0 -193
  77. package/src/cli/cmd/debug/agent.ts +0 -27
  78. package/src/cli/cmd/debug/config.ts +0 -14
  79. package/src/cli/cmd/debug/file.ts +0 -73
  80. package/src/cli/cmd/debug/index.ts +0 -87
  81. package/src/cli/cmd/debug/lsp.ts +0 -50
  82. package/src/cli/cmd/debug/ripgrep.ts +0 -79
  83. package/src/cli/cmd/debug/scrap.ts +0 -16
  84. package/src/cli/cmd/debug/skill.ts +0 -15
  85. package/src/cli/cmd/debug/snapshot.ts +0 -50
  86. package/src/cli/cmd/debug/startup.ts +0 -11
  87. package/src/cli/cmd/debug/v2.ts +0 -42
  88. package/src/cli/cmd/export.ts +0 -292
  89. package/src/cli/cmd/generate.ts +0 -54
  90. package/src/cli/cmd/github.handler.ts +0 -1606
  91. package/src/cli/cmd/github.shared.ts +0 -30
  92. package/src/cli/cmd/github.ts +0 -42
  93. package/src/cli/cmd/import.ts +0 -230
  94. package/src/cli/cmd/mcp.ts +0 -840
  95. package/src/cli/cmd/models.ts +0 -66
  96. package/src/cli/cmd/plug.ts +0 -230
  97. package/src/cli/cmd/pr.ts +0 -115
  98. package/src/cli/cmd/prompt-display.ts +0 -1
  99. package/src/cli/cmd/providers.ts +0 -594
  100. package/src/cli/cmd/repair.ts +0 -231
  101. package/src/cli/cmd/run/demo.ts +0 -1274
  102. package/src/cli/cmd/run/entry.body.ts +0 -205
  103. package/src/cli/cmd/run/footer.command.tsx +0 -1070
  104. package/src/cli/cmd/run/footer.menu.tsx +0 -351
  105. package/src/cli/cmd/run/footer.permission.tsx +0 -472
  106. package/src/cli/cmd/run/footer.prompt.tsx +0 -1306
  107. package/src/cli/cmd/run/footer.question.tsx +0 -573
  108. package/src/cli/cmd/run/footer.subagent.tsx +0 -175
  109. package/src/cli/cmd/run/footer.ts +0 -1129
  110. package/src/cli/cmd/run/footer.view.tsx +0 -945
  111. package/src/cli/cmd/run/footer.width.ts +0 -27
  112. package/src/cli/cmd/run/permission.shared.ts +0 -256
  113. package/src/cli/cmd/run/prompt.editor.ts +0 -157
  114. package/src/cli/cmd/run/prompt.shared.ts +0 -153
  115. package/src/cli/cmd/run/question.shared.ts +0 -340
  116. package/src/cli/cmd/run/runtime.boot.ts +0 -202
  117. package/src/cli/cmd/run/runtime.lifecycle.ts +0 -406
  118. package/src/cli/cmd/run/runtime.queue.ts +0 -349
  119. package/src/cli/cmd/run/runtime.shared.ts +0 -17
  120. package/src/cli/cmd/run/runtime.stdin.ts +0 -37
  121. package/src/cli/cmd/run/runtime.ts +0 -814
  122. package/src/cli/cmd/run/scrollback.shared.ts +0 -92
  123. package/src/cli/cmd/run/scrollback.surface.ts +0 -431
  124. package/src/cli/cmd/run/scrollback.writer.tsx +0 -352
  125. package/src/cli/cmd/run/session-data.ts +0 -1113
  126. package/src/cli/cmd/run/session-replay.ts +0 -374
  127. package/src/cli/cmd/run/session.shared.ts +0 -196
  128. package/src/cli/cmd/run/splash.ts +0 -280
  129. package/src/cli/cmd/run/stream.transport.ts +0 -1462
  130. package/src/cli/cmd/run/stream.ts +0 -175
  131. package/src/cli/cmd/run/subagent-data.ts +0 -876
  132. package/src/cli/cmd/run/theme.ts +0 -690
  133. package/src/cli/cmd/run/tool.ts +0 -1486
  134. package/src/cli/cmd/run/trace.ts +0 -94
  135. package/src/cli/cmd/run/turn-summary.ts +0 -47
  136. package/src/cli/cmd/run/types.ts +0 -350
  137. package/src/cli/cmd/run/variant.shared.ts +0 -216
  138. package/src/cli/cmd/run.ts +0 -1104
  139. package/src/cli/cmd/serve.ts +0 -24
  140. package/src/cli/cmd/session.ts +0 -147
  141. package/src/cli/cmd/stats.ts +0 -393
  142. package/src/cli/cmd/tui.ts +0 -317
  143. package/src/cli/cmd/uninstall.ts +0 -353
  144. package/src/cli/cmd/upgrade.ts +0 -74
  145. package/src/cli/cmd/web.ts +0 -84
  146. package/src/cli/effect/prompt.ts +0 -37
  147. package/src/cli/effect-cmd.ts +0 -96
  148. package/src/cli/error.ts +0 -130
  149. package/src/cli/heap.ts +0 -45
  150. package/src/cli/logo.ts +0 -1
  151. package/src/cli/network.ts +0 -80
  152. package/src/cli/tui/layer.ts +0 -8
  153. package/src/cli/tui/validate-session.ts +0 -29
  154. package/src/cli/tui/worker.ts +0 -80
  155. package/src/cli/ui.ts +0 -131
  156. package/src/cli/upgrade.ts +0 -68
  157. package/src/command/index.ts +0 -328
  158. package/src/command/template/initialize.txt +0 -66
  159. package/src/command/template/review.txt +0 -101
  160. package/src/config/agent.ts +0 -59
  161. package/src/config/command.ts +0 -39
  162. package/src/config/config.ts +0 -720
  163. package/src/config/entry-name.ts +0 -19
  164. package/src/config/managed.ts +0 -69
  165. package/src/config/markdown.ts +0 -36
  166. package/src/config/parse.ts +0 -61
  167. package/src/config/paths.ts +0 -45
  168. package/src/config/plugin.ts +0 -79
  169. package/src/config/tui-cwd.ts +0 -5
  170. package/src/config/tui-host-attention.ts +0 -21
  171. package/src/config/tui-migrate.ts +0 -132
  172. package/src/config/tui.ts +0 -276
  173. package/src/config/variable.ts +0 -91
  174. package/src/control-plane/adapters/index.ts +0 -41
  175. package/src/control-plane/adapters/worktree.ts +0 -96
  176. package/src/control-plane/dev/README.md +0 -19
  177. package/src/control-plane/dev/debug-workspace-plugin.ts +0 -73
  178. package/src/control-plane/types.ts +0 -59
  179. package/src/control-plane/util.ts +0 -39
  180. package/src/control-plane/workspace-adapter-runtime.ts +0 -51
  181. package/src/control-plane/workspace-context.ts +0 -26
  182. package/src/control-plane/workspace.ts +0 -966
  183. package/src/effect/app-node-builder-v1.ts +0 -12
  184. package/src/effect/app-runtime.ts +0 -135
  185. package/src/effect/bootstrap-runtime.ts +0 -19
  186. package/src/effect/bridge.ts +0 -84
  187. package/src/effect/config-service.ts +0 -67
  188. package/src/effect/instance-ref.ts +0 -11
  189. package/src/effect/instance-registry.ts +0 -12
  190. package/src/effect/instance-state.ts +0 -69
  191. package/src/effect/promise.ts +0 -17
  192. package/src/effect/run-service.ts +0 -47
  193. package/src/effect/runner.ts +0 -217
  194. package/src/effect/runtime-flags.ts +0 -78
  195. package/src/env/index.ts +0 -41
  196. package/src/event-manifest.ts +0 -3
  197. package/src/event-v2-bridge.ts +0 -71
  198. package/src/format/formatter.ts +0 -404
  199. package/src/format/index.ts +0 -203
  200. package/src/format/output-style.ts +0 -84
  201. package/src/git/auto-commit.ts +0 -70
  202. package/src/git/index.ts +0 -348
  203. package/src/hooks/index.ts +0 -172
  204. package/src/id/id.ts +0 -80
  205. package/src/ide/index.ts +0 -54
  206. package/src/image/image.ts +0 -172
  207. package/src/index.ts +0 -229
  208. package/src/installation/index.ts +0 -336
  209. package/src/lsp/client.ts +0 -650
  210. package/src/lsp/diagnostic.ts +0 -29
  211. package/src/lsp/language.ts +0 -121
  212. package/src/lsp/launch.ts +0 -21
  213. package/src/lsp/lsp.ts +0 -507
  214. package/src/lsp/server.ts +0 -1983
  215. package/src/markdown.d.ts +0 -4
  216. package/src/mcp/auth.ts +0 -163
  217. package/src/mcp/browser.ts +0 -37
  218. package/src/mcp/catalog.ts +0 -170
  219. package/src/mcp/index.ts +0 -1007
  220. package/src/mcp/oauth-callback.ts +0 -194
  221. package/src/mcp/oauth-provider.ts +0 -259
  222. package/src/node.ts +0 -4
  223. package/src/patch/index.ts +0 -686
  224. package/src/permission/arity.ts +0 -163
  225. package/src/permission/evaluate.ts +0 -1
  226. package/src/permission/index.ts +0 -223
  227. package/src/permission/sandbox.ts +0 -150
  228. package/src/plugin/azure.ts +0 -26
  229. package/src/plugin/cerebras.ts +0 -11
  230. package/src/plugin/cloudflare.ts +0 -65
  231. package/src/plugin/digitalocean.ts +0 -325
  232. package/src/plugin/index.ts +0 -316
  233. package/src/plugin/install.ts +0 -439
  234. package/src/plugin/loader.ts +0 -237
  235. package/src/plugin/meta.ts +0 -188
  236. package/src/plugin/modal/modal.ts +0 -17
  237. package/src/plugin/modal/models.ts +0 -133
  238. package/src/plugin/openai/README.md +0 -31
  239. package/src/plugin/openai/codex.ts +0 -572
  240. package/src/plugin/openai/ws-pool.ts +0 -271
  241. package/src/plugin/openai/ws.ts +0 -383
  242. package/src/plugin/pty-environment.ts +0 -24
  243. package/src/plugin/shared.ts +0 -323
  244. package/src/plugin/snowflake-cortex.ts +0 -507
  245. package/src/plugin/tui/internal.ts +0 -10
  246. package/src/plugin/tui/runtime.ts +0 -1131
  247. package/src/plugin/xai.ts +0 -339
  248. package/src/project/bootstrap-service.ts +0 -9
  249. package/src/project/bootstrap.ts +0 -58
  250. package/src/project/instance-context.ts +0 -24
  251. package/src/project/instance-runtime.ts +0 -16
  252. package/src/project/instance-store.ts +0 -213
  253. package/src/project/project.ts +0 -483
  254. package/src/project/vcs.ts +0 -423
  255. package/src/provider/auth.ts +0 -229
  256. package/src/provider/error.ts +0 -195
  257. package/src/provider/model-status.ts +0 -8
  258. package/src/provider/provider.ts +0 -2399
  259. package/src/provider/transform.ts +0 -1856
  260. package/src/question/index.ts +0 -161
  261. package/src/question/schema.ts +0 -4
  262. package/src/server/auth.ts +0 -48
  263. package/src/server/event.ts +0 -10
  264. package/src/server/global-lifecycle.ts +0 -28
  265. package/src/server/init-projectors.ts +0 -3
  266. package/src/server/mdns.ts +0 -47
  267. package/src/server/projectors.ts +0 -1
  268. package/src/server/proxy-util.ts +0 -48
  269. package/src/server/routes/instance/httpapi/AGENTS.md +0 -39
  270. package/src/server/routes/instance/httpapi/api.ts +0 -97
  271. package/src/server/routes/instance/httpapi/errors.ts +0 -193
  272. package/src/server/routes/instance/httpapi/groups/config.ts +0 -65
  273. package/src/server/routes/instance/httpapi/groups/control-plane.ts +0 -35
  274. package/src/server/routes/instance/httpapi/groups/control.ts +0 -76
  275. package/src/server/routes/instance/httpapi/groups/event.ts +0 -29
  276. package/src/server/routes/instance/httpapi/groups/experimental.ts +0 -275
  277. package/src/server/routes/instance/httpapi/groups/file.ts +0 -185
  278. package/src/server/routes/instance/httpapi/groups/global.ts +0 -163
  279. package/src/server/routes/instance/httpapi/groups/instance.ts +0 -206
  280. package/src/server/routes/instance/httpapi/groups/mcp.ts +0 -156
  281. package/src/server/routes/instance/httpapi/groups/metadata.ts +0 -18
  282. package/src/server/routes/instance/httpapi/groups/permission.ts +0 -61
  283. package/src/server/routes/instance/httpapi/groups/project-copy.ts +0 -32
  284. package/src/server/routes/instance/httpapi/groups/project.ts +0 -93
  285. package/src/server/routes/instance/httpapi/groups/provider.ts +0 -101
  286. package/src/server/routes/instance/httpapi/groups/pty.ts +0 -172
  287. package/src/server/routes/instance/httpapi/groups/query.ts +0 -12
  288. package/src/server/routes/instance/httpapi/groups/question.ts +0 -74
  289. package/src/server/routes/instance/httpapi/groups/session.ts +0 -462
  290. package/src/server/routes/instance/httpapi/groups/sync.ts +0 -113
  291. package/src/server/routes/instance/httpapi/groups/tui.ts +0 -208
  292. package/src/server/routes/instance/httpapi/groups/workspace.ts +0 -141
  293. package/src/server/routes/instance/httpapi/handlers/config.ts +0 -34
  294. package/src/server/routes/instance/httpapi/handlers/control-plane.ts +0 -37
  295. package/src/server/routes/instance/httpapi/handlers/control.ts +0 -43
  296. package/src/server/routes/instance/httpapi/handlers/event.ts +0 -99
  297. package/src/server/routes/instance/httpapi/handlers/experimental.ts +0 -193
  298. package/src/server/routes/instance/httpapi/handlers/file.ts +0 -139
  299. package/src/server/routes/instance/httpapi/handlers/global.ts +0 -144
  300. package/src/server/routes/instance/httpapi/handlers/instance.ts +0 -110
  301. package/src/server/routes/instance/httpapi/handlers/mcp.ts +0 -111
  302. package/src/server/routes/instance/httpapi/handlers/permission.ts +0 -41
  303. package/src/server/routes/instance/httpapi/handlers/project-copy.ts +0 -83
  304. package/src/server/routes/instance/httpapi/handlers/project.ts +0 -63
  305. package/src/server/routes/instance/httpapi/handlers/provider.ts +0 -116
  306. package/src/server/routes/instance/httpapi/handlers/pty.ts +0 -273
  307. package/src/server/routes/instance/httpapi/handlers/question.ts +0 -54
  308. package/src/server/routes/instance/httpapi/handlers/session-errors.ts +0 -21
  309. package/src/server/routes/instance/httpapi/handlers/session.ts +0 -442
  310. package/src/server/routes/instance/httpapi/handlers/sync.ts +0 -89
  311. package/src/server/routes/instance/httpapi/handlers/tui.ts +0 -131
  312. package/src/server/routes/instance/httpapi/handlers/workspace.ts +0 -102
  313. package/src/server/routes/instance/httpapi/lifecycle.ts +0 -54
  314. package/src/server/routes/instance/httpapi/middleware/authorization.ts +0 -150
  315. package/src/server/routes/instance/httpapi/middleware/compression.ts +0 -64
  316. package/src/server/routes/instance/httpapi/middleware/cors-vary.ts +0 -29
  317. package/src/server/routes/instance/httpapi/middleware/error.ts +0 -43
  318. package/src/server/routes/instance/httpapi/middleware/fence.ts +0 -25
  319. package/src/server/routes/instance/httpapi/middleware/instance-context.ts +0 -43
  320. package/src/server/routes/instance/httpapi/middleware/proxy.ts +0 -131
  321. package/src/server/routes/instance/httpapi/middleware/schema-error.ts +0 -41
  322. package/src/server/routes/instance/httpapi/middleware/workspace-routing.ts +0 -250
  323. package/src/server/routes/instance/httpapi/public.ts +0 -537
  324. package/src/server/routes/instance/httpapi/server.ts +0 -325
  325. package/src/server/routes/instance/httpapi/websocket-tracker.ts +0 -60
  326. package/src/server/server.ts +0 -227
  327. package/src/server/shared/fence.ts +0 -60
  328. package/src/server/shared/pty-ticket.ts +0 -15
  329. package/src/server/shared/public-ui.ts +0 -12
  330. package/src/server/shared/tui-control.ts +0 -28
  331. package/src/server/shared/ui.ts +0 -108
  332. package/src/server/shared/workspace-routing.ts +0 -45
  333. package/src/server/tui-event.ts +0 -1
  334. package/src/session/auto-skill.ts +0 -71
  335. package/src/session/compaction.ts +0 -637
  336. package/src/session/instruction.ts +0 -237
  337. package/src/session/llm/AGENTS.md +0 -90
  338. package/src/session/llm/ai-sdk.ts +0 -291
  339. package/src/session/llm/native-request.ts +0 -196
  340. package/src/session/llm/native-runtime.ts +0 -195
  341. package/src/session/llm/request.ts +0 -229
  342. package/src/session/llm.ts +0 -404
  343. package/src/session/message-error.ts +0 -14
  344. package/src/session/message-v2.ts +0 -737
  345. package/src/session/message.ts +0 -148
  346. package/src/session/overflow.ts +0 -69
  347. package/src/session/processor.ts +0 -718
  348. package/src/session/prompt/anthropic.txt +0 -105
  349. package/src/session/prompt/beast.txt +0 -147
  350. package/src/session/prompt/build-switch.txt +0 -5
  351. package/src/session/prompt/codex.txt +0 -79
  352. package/src/session/prompt/copilot-gpt-5.txt +0 -143
  353. package/src/session/prompt/default.txt +0 -95
  354. package/src/session/prompt/gemini.txt +0 -155
  355. package/src/session/prompt/gpt.txt +0 -107
  356. package/src/session/prompt/kimi.txt +0 -95
  357. package/src/session/prompt/meta.txt +0 -65
  358. package/src/session/prompt/plan-mode.txt +0 -70
  359. package/src/session/prompt/plan-reminder-anthropic.txt +0 -67
  360. package/src/session/prompt/plan.txt +0 -26
  361. package/src/session/prompt/trinity.txt +0 -97
  362. package/src/session/prompt.ts +0 -2151
  363. package/src/session/rate-limit.ts +0 -176
  364. package/src/session/reminders.ts +0 -92
  365. package/src/session/retry.ts +0 -211
  366. package/src/session/revert.ts +0 -136
  367. package/src/session/run-state.ts +0 -151
  368. package/src/session/schema.ts +0 -26
  369. package/src/session/session.ts +0 -1050
  370. package/src/session/speculation.ts +0 -88
  371. package/src/session/status.ts +0 -56
  372. package/src/session/summary.ts +0 -164
  373. package/src/session/system.ts +0 -152
  374. package/src/session/todo.ts +0 -74
  375. package/src/session/tools.ts +0 -626
  376. package/src/session/watch.ts +0 -86
  377. package/src/share/session.ts +0 -58
  378. package/src/share/share-next.ts +0 -371
  379. package/src/skill/discovery.ts +0 -140
  380. package/src/skill/index.ts +0 -430
  381. package/src/skill/restriction.ts +0 -62
  382. package/src/snapshot/index.ts +0 -807
  383. package/src/sql.d.ts +0 -4
  384. package/src/storage/schema.ts +0 -5
  385. package/src/storage/storage.ts +0 -327
  386. package/src/sync/README.md +0 -179
  387. package/src/sync/schema.ts +0 -11
  388. package/src/temporary.ts +0 -31
  389. package/src/tool/apply_patch.ts +0 -313
  390. package/src/tool/apply_patch.txt +0 -33
  391. package/src/tool/audio-gen.ts +0 -50
  392. package/src/tool/code-mode.ts +0 -310
  393. package/src/tool/diff-context.ts +0 -64
  394. package/src/tool/edit.ts +0 -749
  395. package/src/tool/edit.txt +0 -10
  396. package/src/tool/external-directory.ts +0 -49
  397. package/src/tool/glob.ts +0 -76
  398. package/src/tool/glob.txt +0 -6
  399. package/src/tool/grep.ts +0 -115
  400. package/src/tool/grep.txt +0 -8
  401. package/src/tool/image-gen.ts +0 -50
  402. package/src/tool/invalid.ts +0 -21
  403. package/src/tool/json-schema.ts +0 -164
  404. package/src/tool/lsp.ts +0 -113
  405. package/src/tool/lsp.txt +0 -24
  406. package/src/tool/mcp-websearch.ts +0 -96
  407. package/src/tool/plan-enter.txt +0 -14
  408. package/src/tool/plan-exit.txt +0 -13
  409. package/src/tool/plan.ts +0 -79
  410. package/src/tool/question.ts +0 -44
  411. package/src/tool/question.txt +0 -10
  412. package/src/tool/read.ts +0 -386
  413. package/src/tool/read.txt +0 -14
  414. package/src/tool/registry.ts +0 -478
  415. package/src/tool/repomap.ts +0 -287
  416. package/src/tool/schema.ts +0 -14
  417. package/src/tool/shell/id.ts +0 -19
  418. package/src/tool/shell/prompt.ts +0 -295
  419. package/src/tool/shell/shell.txt +0 -21
  420. package/src/tool/shell.ts +0 -684
  421. package/src/tool/skill.ts +0 -81
  422. package/src/tool/skill.txt +0 -5
  423. package/src/tool/task.ts +0 -461
  424. package/src/tool/task.txt +0 -21
  425. package/src/tool/todo.ts +0 -46
  426. package/src/tool/todowrite.txt +0 -44
  427. package/src/tool/tool.ts +0 -183
  428. package/src/tool/truncate.ts +0 -156
  429. package/src/tool/truncation-dir.ts +0 -4
  430. package/src/tool/video-gen.ts +0 -50
  431. package/src/tool/webfetch.ts +0 -192
  432. package/src/tool/webfetch.txt +0 -13
  433. package/src/tool/websearch.ts +0 -143
  434. package/src/tool/websearch.txt +0 -14
  435. package/src/tool/write.ts +0 -117
  436. package/src/tool/write.txt +0 -8
  437. package/src/util/archive.ts +0 -17
  438. package/src/util/bom.ts +0 -27
  439. package/src/util/data-url.ts +0 -9
  440. package/src/util/defer.ts +0 -10
  441. package/src/util/effect-http-client.ts +0 -11
  442. package/src/util/error.ts +0 -1
  443. package/src/util/filesystem.ts +0 -251
  444. package/src/util/html.ts +0 -8
  445. package/src/util/iife.ts +0 -3
  446. package/src/util/lazy.ts +0 -20
  447. package/src/util/local-context.ts +0 -25
  448. package/src/util/locale.ts +0 -2
  449. package/src/util/media.ts +0 -26
  450. package/src/util/process.ts +0 -177
  451. package/src/util/proxy-env.ts +0 -72
  452. package/src/util/queue.ts +0 -32
  453. package/src/util/record.ts +0 -1
  454. package/src/util/repository.ts +0 -232
  455. package/src/util/rpc.ts +0 -66
  456. package/src/util/signal.ts +0 -12
  457. package/src/util/timeout.ts +0 -13
  458. package/src/util/token.ts +0 -1
  459. package/src/util/wildcard.ts +0 -59
  460. package/src/verify/verify.ts +0 -106
  461. package/src/worktree/index.ts +0 -623
  462. package/sst-env.d.ts +0 -10
  463. package/test/AGENTS.md +0 -204
  464. package/test/EFFECT_TEST_MIGRATION.md +0 -169
  465. package/test/account/repo.test.ts +0 -355
  466. package/test/account/service.test.ts +0 -524
  467. package/test/acp/config-option.test.ts +0 -229
  468. package/test/acp/content.test.ts +0 -235
  469. package/test/acp/directory.test.ts +0 -188
  470. package/test/acp/error.test.ts +0 -67
  471. package/test/acp/event.test.ts +0 -751
  472. package/test/acp/permission.test.ts +0 -401
  473. package/test/acp/service-session.test.ts +0 -1374
  474. package/test/acp/session.test.ts +0 -201
  475. package/test/acp/tool.test.ts +0 -298
  476. package/test/acp/usage.test.ts +0 -318
  477. package/test/agent/agent.test.ts +0 -755
  478. package/test/agent/plan-mode-subagent-bypass.test.ts +0 -160
  479. package/test/agent/plugin-agent-regression.test.ts +0 -51
  480. package/test/auth/auth.test.ts +0 -75
  481. package/test/background/job.test.ts +0 -244
  482. package/test/cli/account.test.ts +0 -30
  483. package/test/cli/acp/acp-test-client.ts +0 -97
  484. package/test/cli/acp/config-options.test.ts +0 -103
  485. package/test/cli/acp/helpers.ts +0 -96
  486. package/test/cli/acp/initialize-auth.test.ts +0 -61
  487. package/test/cli/acp/lifecycle.test.ts +0 -118
  488. package/test/cli/acp/prompt-content.test.ts +0 -97
  489. package/test/cli/acp/skills.test.ts +0 -38
  490. package/test/cli/cmd/tui/attention.test.ts +0 -484
  491. package/test/cli/effect-cmd-instance-als.test.ts +0 -40
  492. package/test/cli/error.test.ts +0 -95
  493. package/test/cli/github-action.test.ts +0 -199
  494. package/test/cli/github-remote.test.ts +0 -90
  495. package/test/cli/help/__snapshots__/help-snapshots.test.ts.snap +0 -623
  496. package/test/cli/help/help-snapshots.test.ts +0 -140
  497. package/test/cli/import.test.ts +0 -90
  498. package/test/cli/mcp-add.test.ts +0 -74
  499. package/test/cli/plugin-auth-picker.test.ts +0 -120
  500. package/test/cli/run/entry.body.test.ts +0 -536
  501. package/test/cli/run/footer.menu.test.ts +0 -43
  502. package/test/cli/run/footer.view.test.tsx +0 -1411
  503. package/test/cli/run/footer.width.test.ts +0 -35
  504. package/test/cli/run/permission.shared.test.ts +0 -144
  505. package/test/cli/run/prompt.editor.test.ts +0 -101
  506. package/test/cli/run/prompt.shared.test.ts +0 -101
  507. package/test/cli/run/question.shared.test.ts +0 -115
  508. package/test/cli/run/run-process.test.ts +0 -338
  509. package/test/cli/run/runtime.boot.test.ts +0 -283
  510. package/test/cli/run/runtime.queue.test.ts +0 -481
  511. package/test/cli/run/runtime.stdin.test.ts +0 -71
  512. package/test/cli/run/runtime.test.ts +0 -238
  513. package/test/cli/run/scrollback.surface.test.ts +0 -1092
  514. package/test/cli/run/session-data.test.ts +0 -593
  515. package/test/cli/run/session-replay.test.ts +0 -691
  516. package/test/cli/run/session.shared.test.ts +0 -247
  517. package/test/cli/run/stream.test.ts +0 -56
  518. package/test/cli/run/stream.transport.test.ts +0 -2363
  519. package/test/cli/run/subagent-data.test.ts +0 -547
  520. package/test/cli/run/theme.test.ts +0 -177
  521. package/test/cli/run/variant.shared.test.ts +0 -218
  522. package/test/cli/serve/serve-process.test.ts +0 -61
  523. package/test/cli/smokes/read-only.test.ts +0 -115
  524. package/test/cli/tui/attach.test.ts +0 -11
  525. package/test/cli/tui/editor-context-zed.test.ts +0 -379
  526. package/test/cli/tui/editor-context.test.tsx +0 -297
  527. package/test/cli/tui/plugin-add.test.ts +0 -110
  528. package/test/cli/tui/plugin-install.test.ts +0 -87
  529. package/test/cli/tui/plugin-lifecycle.test.ts +0 -224
  530. package/test/cli/tui/plugin-loader-entrypoint.test.ts +0 -485
  531. package/test/cli/tui/plugin-loader-pure.test.ts +0 -72
  532. package/test/cli/tui/plugin-loader.test.ts +0 -1332
  533. package/test/cli/tui/plugin-toggle.test.ts +0 -264
  534. package/test/cli/tui/thread.test.ts +0 -101
  535. package/test/config/agent-color.test.ts +0 -47
  536. package/test/config/config.test.ts +0 -2047
  537. package/test/config/entry-name.test.ts +0 -57
  538. package/test/config/fixtures/empty-frontmatter.md +0 -4
  539. package/test/config/fixtures/frontmatter.md +0 -28
  540. package/test/config/fixtures/markdown-header.md +0 -11
  541. package/test/config/fixtures/no-frontmatter.md +0 -1
  542. package/test/config/fixtures/weird-model-id.md +0 -13
  543. package/test/config/lsp.test.ts +0 -69
  544. package/test/config/markdown.test.ts +0 -228
  545. package/test/config/plugin.test.ts +0 -0
  546. package/test/config/tui.test.ts +0 -894
  547. package/test/control-plane/adapters.test.ts +0 -71
  548. package/test/control-plane/workspace.test.ts +0 -1717
  549. package/test/effect/app-runtime-logger.test.ts +0 -101
  550. package/test/effect/config-service.test.ts +0 -65
  551. package/test/effect/instance-state.test.ts +0 -392
  552. package/test/effect/run-service.test.ts +0 -89
  553. package/test/effect/runner.test.ts +0 -514
  554. package/test/effect/runtime-flags.test.ts +0 -374
  555. package/test/event-manifest.test.ts +0 -24
  556. package/test/fake/account.ts +0 -9
  557. package/test/fake/auth.ts +0 -8
  558. package/test/fake/npm.ts +0 -8
  559. package/test/fake/provider.ts +0 -83
  560. package/test/fake/skill.ts +0 -8
  561. package/test/filesystem/filesystem.test.ts +0 -318
  562. package/test/fixture/agent-plugin.constants.ts +0 -6
  563. package/test/fixture/agent-plugin.ts +0 -12
  564. package/test/fixture/config.ts +0 -24
  565. package/test/fixture/db.ts +0 -11
  566. package/test/fixture/fixture.test.ts +0 -26
  567. package/test/fixture/fixture.ts +0 -228
  568. package/test/fixture/flag.ts +0 -20
  569. package/test/fixture/flock-worker.ts +0 -72
  570. package/test/fixture/lsp/fake-lsp-server.js +0 -249
  571. package/test/fixture/mcp-lifecycle-stdio.ts +0 -26
  572. package/test/fixture/mcp-session-recovery.ts +0 -50
  573. package/test/fixture/plug-worker.ts +0 -93
  574. package/test/fixture/plugin-meta-worker.ts +0 -19
  575. package/test/fixture/plugin.ts +0 -10
  576. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  577. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  578. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  579. package/test/fixture/skills/index.json +0 -6
  580. package/test/fixture/tui-environment.tsx +0 -32
  581. package/test/fixture/tui-plugin.ts +0 -355
  582. package/test/fixture/tui-runtime.ts +0 -56
  583. package/test/fixture/tui-sdk.ts +0 -82
  584. package/test/fixture/workspace.ts +0 -34
  585. package/test/fixtures/recordings/session/native-anthropic-tool-loop.json +0 -49
  586. package/test/fixtures/recordings/session/native-openai-oauth-tool-loop.json +0 -45
  587. package/test/fixtures/recordings/session/native-zen-tool-loop.json +0 -49
  588. package/test/format/format.test.ts +0 -235
  589. package/test/git/auto-commit.test.ts +0 -63
  590. package/test/git/git.test.ts +0 -179
  591. package/test/ide/ide.test.ts +0 -82
  592. package/test/image/fixtures/picture-5mb-base64.png +0 -0
  593. package/test/image/image.test.ts +0 -121
  594. package/test/installation/installation.test.ts +0 -240
  595. package/test/lib/cli-process.ts +0 -535
  596. package/test/lib/effect.ts +0 -177
  597. package/test/lib/filesystem.ts +0 -10
  598. package/test/lib/llm-server.ts +0 -779
  599. package/test/lib/snapshot.ts +0 -73
  600. package/test/lib/test-provider.ts +0 -37
  601. package/test/lib/websocket.ts +0 -46
  602. package/test/lsp/client.test.ts +0 -488
  603. package/test/lsp/index.test.ts +0 -231
  604. package/test/lsp/jdtls-root.test.ts +0 -459
  605. package/test/lsp/launch.test.ts +0 -22
  606. package/test/lsp/lifecycle.test.ts +0 -160
  607. package/test/mcp/auth.test.ts +0 -78
  608. package/test/mcp/catalog.test.ts +0 -107
  609. package/test/mcp/headers.test.ts +0 -102
  610. package/test/mcp/lifecycle.test.ts +0 -560
  611. package/test/mcp/oauth-auto-connect.test.ts +0 -300
  612. package/test/mcp/oauth-browser.test.ts +0 -225
  613. package/test/mcp/oauth-callback.test.ts +0 -114
  614. package/test/mcp/oauth-provider.test.ts +0 -102
  615. package/test/mcp/session-recovery.test.ts +0 -27
  616. package/test/mcp/transport.test.ts +0 -38
  617. package/test/patch/patch.test.ts +0 -384
  618. package/test/permission/arity.test.ts +0 -33
  619. package/test/permission/next.test.ts +0 -1174
  620. package/test/permission/sandbox.test.ts +0 -89
  621. package/test/permission-task.test.ts +0 -319
  622. package/test/plugin/auth-override.test.ts +0 -101
  623. package/test/plugin/cerebras.test.ts +0 -47
  624. package/test/plugin/cloudflare.test.ts +0 -25
  625. package/test/plugin/codex.test.ts +0 -463
  626. package/test/plugin/install-concurrency.test.ts +0 -140
  627. package/test/plugin/install.test.ts +0 -570
  628. package/test/plugin/loader-shared.test.ts +0 -1306
  629. package/test/plugin/meta.test.ts +0 -137
  630. package/test/plugin/modal-models.test.ts +0 -198
  631. package/test/plugin/openai-rollout.test.ts +0 -17
  632. package/test/plugin/openai-ws.test.ts +0 -909
  633. package/test/plugin/shared.test.ts +0 -88
  634. package/test/plugin/snowflake-cortex.test.ts +0 -278
  635. package/test/plugin/trigger.test.ts +0 -108
  636. package/test/plugin/workspace-adapter.test.ts +0 -111
  637. package/test/plugin/xai.test.ts +0 -585
  638. package/test/preload.ts +0 -92
  639. package/test/project/instance-bootstrap.test.ts +0 -115
  640. package/test/project/instance.test.ts +0 -248
  641. package/test/project/migrate-global.test.ts +0 -168
  642. package/test/project/project-directory.test.ts +0 -202
  643. package/test/project/project.test.ts +0 -808
  644. package/test/project/vcs.test.ts +0 -335
  645. package/test/project/worktree-remove.test.ts +0 -128
  646. package/test/project/worktree.test.ts +0 -324
  647. package/test/provider/amazon-bedrock.test.ts +0 -361
  648. package/test/provider/cf-ai-gateway-e2e.test.ts +0 -320
  649. package/test/provider/digitalocean.test.ts +0 -124
  650. package/test/provider/error.test.ts +0 -24
  651. package/test/provider/gitlab-duo.test.ts +0 -412
  652. package/test/provider/header-timeout.test.ts +0 -234
  653. package/test/provider/model-status.test.ts +0 -61
  654. package/test/provider/provider.test.ts +0 -2118
  655. package/test/provider/transform.test.ts +0 -5682
  656. package/test/question/question.test.ts +0 -459
  657. package/test/server/AGENTS.md +0 -15
  658. package/test/server/auth.test.ts +0 -59
  659. package/test/server/global-bus.ts +0 -31
  660. package/test/server/global-session-list.test.ts +0 -108
  661. package/test/server/httpapi-authorization.test.ts +0 -174
  662. package/test/server/httpapi-compression.test.ts +0 -151
  663. package/test/server/httpapi-config.test.ts +0 -110
  664. package/test/server/httpapi-control-plane.test.ts +0 -63
  665. package/test/server/httpapi-cors-vary.test.ts +0 -63
  666. package/test/server/httpapi-cors.test.ts +0 -122
  667. package/test/server/httpapi-error-middleware.test.ts +0 -101
  668. package/test/server/httpapi-event.test.ts +0 -94
  669. package/test/server/httpapi-exercise/assertions.ts +0 -64
  670. package/test/server/httpapi-exercise/backend.ts +0 -144
  671. package/test/server/httpapi-exercise/dsl.ts +0 -210
  672. package/test/server/httpapi-exercise/environment.ts +0 -40
  673. package/test/server/httpapi-exercise/index.ts +0 -1802
  674. package/test/server/httpapi-exercise/report.ts +0 -66
  675. package/test/server/httpapi-exercise/routing.ts +0 -96
  676. package/test/server/httpapi-exercise/runner.ts +0 -267
  677. package/test/server/httpapi-exercise/runtime.ts +0 -52
  678. package/test/server/httpapi-exercise/types.ts +0 -127
  679. package/test/server/httpapi-experimental.test.ts +0 -298
  680. package/test/server/httpapi-file.test.ts +0 -83
  681. package/test/server/httpapi-global.test.ts +0 -90
  682. package/test/server/httpapi-instance-context.test.ts +0 -352
  683. package/test/server/httpapi-instance-route-auth.test.ts +0 -81
  684. package/test/server/httpapi-instance.test.ts +0 -265
  685. package/test/server/httpapi-layer.ts +0 -33
  686. package/test/server/httpapi-listen.test.ts +0 -465
  687. package/test/server/httpapi-mcp-oauth.test.ts +0 -73
  688. package/test/server/httpapi-mcp.test.ts +0 -223
  689. package/test/server/httpapi-mdns.test.ts +0 -79
  690. package/test/server/httpapi-promptasync-context.test.ts +0 -212
  691. package/test/server/httpapi-provider.test.ts +0 -401
  692. package/test/server/httpapi-pty.test.ts +0 -299
  693. package/test/server/httpapi-public-openapi.test.ts +0 -350
  694. package/test/server/httpapi-query-schema-drift.test.ts +0 -330
  695. package/test/server/httpapi-reference.test.ts +0 -63
  696. package/test/server/httpapi-schema-error-body.test.ts +0 -166
  697. package/test/server/httpapi-sdk.test.ts +0 -913
  698. package/test/server/httpapi-session.test.ts +0 -1090
  699. package/test/server/httpapi-sync.test.ts +0 -149
  700. package/test/server/httpapi-ui.test.ts +0 -457
  701. package/test/server/httpapi-v2-location.test.ts +0 -126
  702. package/test/server/httpapi-v2-pty.test.ts +0 -250
  703. package/test/server/httpapi-workspace-routing.test.ts +0 -552
  704. package/test/server/httpapi-workspace.test.ts +0 -506
  705. package/test/server/negative-tokens-regression.test.ts +0 -84
  706. package/test/server/project-copy.test.ts +0 -127
  707. package/test/server/project-init-git.test.ts +0 -118
  708. package/test/server/proxy-util.test.ts +0 -113
  709. package/test/server/sdk-error-shape.test.ts +0 -81
  710. package/test/server/sdk-v1-smoke.test.ts +0 -57
  711. package/test/server/session-actions.test.ts +0 -110
  712. package/test/server/session-diff-missing-patch.test.ts +0 -97
  713. package/test/server/session-list.test.ts +0 -302
  714. package/test/server/session-messages.test.ts +0 -180
  715. package/test/server/session-select.test.ts +0 -67
  716. package/test/server/workspace-proxy.test.ts +0 -181
  717. package/test/server/workspace-routing.test.ts +0 -101
  718. package/test/server/worktree-endpoint-repro.test.ts +0 -307
  719. package/test/session/compaction.test.ts +0 -1975
  720. package/test/session/instruction.test.ts +0 -264
  721. package/test/session/llm-native-recorded.test.ts +0 -413
  722. package/test/session/llm-native.test.ts +0 -761
  723. package/test/session/llm.test.ts +0 -2249
  724. package/test/session/message-v2.test.ts +0 -1729
  725. package/test/session/messages-pagination.test.ts +0 -1057
  726. package/test/session/processor-effect.test.ts +0 -1171
  727. package/test/session/prompt.test.ts +0 -2470
  728. package/test/session/retry.test.ts +0 -521
  729. package/test/session/revert-compact.test.ts +0 -683
  730. package/test/session/schema-decoding.test.ts +0 -313
  731. package/test/session/session-schema.test.ts +0 -78
  732. package/test/session/session.test.ts +0 -285
  733. package/test/session/snapshot-tool-race.test.ts +0 -189
  734. package/test/session/structured-output-integration.test.ts +0 -235
  735. package/test/session/structured-output.test.ts +0 -387
  736. package/test/session/system.test.ts +0 -168
  737. package/test/share/share-next.test.ts +0 -324
  738. package/test/skill/discovery.test.ts +0 -186
  739. package/test/skill/restriction.test.ts +0 -57
  740. package/test/skill/skill.test.ts +0 -588
  741. package/test/snapshot/snapshot.test.ts +0 -1218
  742. package/test/storage/storage.test.ts +0 -297
  743. package/test/tool/__snapshots__/parameters.test.ts.snap +0 -466
  744. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  745. package/test/tool/apply_patch.test.ts +0 -529
  746. package/test/tool/code-mode-integration.test.ts +0 -331
  747. package/test/tool/code-mode.test.ts +0 -730
  748. package/test/tool/edit.test.ts +0 -576
  749. package/test/tool/external-directory.test.ts +0 -156
  750. package/test/tool/fixtures/large-image.png +0 -0
  751. package/test/tool/fixtures/models-api.json +0 -172692
  752. package/test/tool/glob.test.ts +0 -132
  753. package/test/tool/grep.test.ts +0 -223
  754. package/test/tool/lsp.test.ts +0 -184
  755. package/test/tool/parameters.test.ts +0 -290
  756. package/test/tool/question.test.ts +0 -133
  757. package/test/tool/read.test.ts +0 -608
  758. package/test/tool/registry.test.ts +0 -572
  759. package/test/tool/shell.test.ts +0 -1199
  760. package/test/tool/skill.test.ts +0 -134
  761. package/test/tool/task.test.ts +0 -1101
  762. package/test/tool/tool-define.test.ts +0 -154
  763. package/test/tool/truncation.test.ts +0 -266
  764. package/test/tool/webfetch.test.ts +0 -119
  765. package/test/tool/websearch.test.ts +0 -100
  766. package/test/tool/write.test.ts +0 -281
  767. package/test/util/data-url.test.ts +0 -14
  768. package/test/util/error.test.ts +0 -16
  769. package/test/util/filesystem.test.ts +0 -656
  770. package/test/util/glob.test.ts +0 -164
  771. package/test/util/html.test.ts +0 -15
  772. package/test/util/iife.test.ts +0 -36
  773. package/test/util/lazy.test.ts +0 -50
  774. package/test/util/module.test.ts +0 -59
  775. package/test/util/process.test.ts +0 -128
  776. package/test/util/repository.test.ts +0 -93
  777. package/test/util/timeout.test.ts +0 -21
  778. package/test/util/wildcard.test.ts +0 -90
  779. package/test/v2/session-message-updater.test.ts +0 -269
  780. package/test/verify/verify.test.ts +0 -61
  781. package/tsconfig.json +0 -16
  782. /package/{script/postinstall.mjs → postinstall.mjs} +0 -0
@@ -1,2047 +0,0 @@
1
- import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
2
- import { ConfigV1 } from "@msrfteam/core/v1/config/config"
3
- import { LayerNode } from "@msrfteam/core/effect/layer-node"
4
- import { httpClient } from "@msrfteam/core/effect/app-node-platform"
5
- import { Cause, Effect, Exit, Layer, Option } from "effect"
6
- import { NamedError } from "@msrfteam/core/util/error"
7
- import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
8
- import { Config } from "@/config/config"
9
- import { ConfigManaged } from "@/config/managed"
10
- import { ConfigParse } from "../../src/config/parse"
11
- import { Npm } from "@msrfteam/core/npm"
12
-
13
- import { InstanceRef } from "../../src/effect/instance-ref"
14
- import type { InstanceContext } from "../../src/project/instance-context"
15
- import { Auth } from "../../src/auth"
16
- import { Account } from "../../src/account/account"
17
- import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
18
- import { FSUtil } from "@msrfteam/core/fs-util"
19
- import { Env } from "../../src/env"
20
- import {
21
- provideTmpdirInstance,
22
- TestInstance,
23
- tmpdir,
24
- tmpdirScoped,
25
- withTestInstance,
26
- provideInstanceEffect,
27
- testInstanceStoreLayer,
28
- } from "../fixture/fixture"
29
- import { InstanceRuntime } from "@/project/instance-runtime"
30
- import { CrossSpawnSpawner } from "@msrfteam/core/cross-spawn-spawner"
31
- import { testEffect } from "../lib/effect"
32
- import path from "path"
33
- import fs from "fs/promises"
34
- import os from "os"
35
- import { pathToFileURL } from "url"
36
- import { Global } from "@msrfteam/core/global"
37
- import { ProjectV2 } from "@msrfteam/core/project"
38
- import { Filesystem } from "@/util/filesystem"
39
- import { ConfigPlugin } from "@/config/plugin"
40
- import { ConfigPluginV1 } from "@msrfteam/core/v1/config/plugin"
41
- import { AccountTest } from "../fake/account"
42
- import { AuthTest } from "../fake/auth"
43
- import { NpmTest } from "../fake/npm"
44
-
45
- const unexpectedHttp = HttpClient.make((request) =>
46
- Effect.die(`unexpected http request: ${request.method} ${request.url}`),
47
- )
48
-
49
- const json = (request: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
50
- HttpClientResponse.fromWeb(
51
- request,
52
- new Response(JSON.stringify(body), {
53
- status,
54
- headers: { "content-type": "application/json" },
55
- }),
56
- )
57
-
58
- const wellKnownAuth = (url: string) =>
59
- Layer.mock(Auth.Service)({
60
- all: () =>
61
- Effect.succeed({
62
- [url]: new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
63
- }),
64
- })
65
-
66
- function remoteConfigClient(input: {
67
- wellKnown: unknown
68
- remote?: unknown
69
- remoteHtml?: string
70
- seen: { wellKnown?: string; remote?: string; authorization?: string }
71
- }) {
72
- return HttpClient.make((request) => {
73
- if (request.url.includes(".well-known/mervex")) {
74
- input.seen.wellKnown = request.url
75
- return Effect.succeed(json(request, input.wellKnown))
76
- }
77
- if (request.url.includes("config.example.com") && (input.remote !== undefined || input.remoteHtml !== undefined)) {
78
- input.seen.remote = request.url
79
- input.seen.authorization = request.headers.authorization
80
- if (input.remoteHtml !== undefined) {
81
- return Effect.succeed(
82
- HttpClientResponse.fromWeb(
83
- request,
84
- new Response(input.remoteHtml, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }),
85
- ),
86
- )
87
- }
88
- return Effect.succeed(json(request, input.remote))
89
- }
90
- return Effect.succeed(json(request, {}, 404))
91
- })
92
- }
93
-
94
- const configLayer = (
95
- options: {
96
- auth?: Layer.Layer<Auth.Service>
97
- account?: Layer.Layer<Account.Service>
98
- client?: HttpClient.HttpClient
99
- } = {},
100
- ) =>
101
- LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
102
- [Auth.node, options.auth ?? AuthTest.empty],
103
- [Account.node, options.account ?? AccountTest.empty],
104
- [Npm.node, NpmTest.noop],
105
- [httpClient, Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)],
106
- ])
107
-
108
- const layer = configLayer()
109
-
110
- const it = testEffect(layer)
111
- const configIt = (options?: Parameters<typeof configLayer>[0]) => testEffect(configLayer(options))
112
-
113
- const schemaConfig = (config: object) => ({ $schema: "https://mervex.ai/config.json", ...config })
114
-
115
- const provideCurrentInstance = <A, E, R>(effect: Effect.Effect<A, E, R>, ctx: InstanceContext) =>
116
- effect.pipe(Effect.provideService(InstanceRef, ctx))
117
-
118
- const load = (ctx: InstanceContext) =>
119
- Effect.runPromise(
120
- Config.Service.use((svc) => provideCurrentInstance(svc.get(), ctx)).pipe(Effect.scoped, Effect.provide(layer)),
121
- )
122
- const clearEffect = (wait = false) =>
123
- Config.use
124
- .invalidate()
125
- .pipe(
126
- Effect.scoped,
127
- Effect.provide(layer),
128
- Effect.andThen(wait ? Effect.promise(() => InstanceRuntime.disposeAllInstances()) : Effect.void),
129
- )
130
- const clear = (wait = false) => Effect.runPromise(clearEffect(wait))
131
- // Get managed config directory from environment (set in preload.ts)
132
- const managedConfigDir = process.env.MERVEX_TEST_MANAGED_CONFIG_DIR!
133
- const originalTestToken = process.env.TEST_TOKEN
134
- const originalConsoleToken = process.env.MERVEX_CONSOLE_TOKEN
135
-
136
- beforeEach(async () => {
137
- await clear(true)
138
- })
139
-
140
- afterEach(async () => {
141
- await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {})
142
- if (originalTestToken === undefined) delete process.env.TEST_TOKEN
143
- else process.env.TEST_TOKEN = originalTestToken
144
- if (originalConsoleToken === undefined) delete process.env.MERVEX_CONSOLE_TOKEN
145
- else process.env.MERVEX_CONSOLE_TOKEN = originalConsoleToken
146
- await clear(true)
147
- })
148
-
149
- const writeManagedSettingsEffect = (settings: object, filename?: string) =>
150
- FSUtil.use.writeWithDirs(path.join(managedConfigDir, filename ?? "mervex.json"), JSON.stringify(settings))
151
-
152
- async function writeConfig(dir: string, config: object, name = "mervex.json") {
153
- await Filesystem.write(path.join(dir, name), JSON.stringify(config))
154
- }
155
-
156
- const writeConfigEffect = (dir: string, config: object, name = "mervex.json") =>
157
- FSUtil.use.writeWithDirs(path.join(dir, name), JSON.stringify(config))
158
-
159
- const withInstanceDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
160
- effect.pipe(
161
- Effect.provideService(TestInstance, { directory: dir }),
162
- provideInstanceEffect(dir),
163
- Effect.provide(testInstanceStoreLayer),
164
- Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)),
165
- )
166
-
167
- const withGlobalConfigDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
168
- Effect.acquireUseRelease(
169
- Effect.gen(function* () {
170
- const previous = Global.Path.config
171
- ;(Global.Path as { config: string }).config = dir
172
- yield* clearEffect(true)
173
- return previous
174
- }),
175
- () => effect,
176
- (previous) =>
177
- Effect.gen(function* () {
178
- ;(Global.Path as { config: string }).config = previous
179
- yield* clearEffect(true)
180
- }),
181
- )
182
-
183
- const withGlobalConfig = <A, E, R>(
184
- input: { config?: object; name?: string },
185
- fn: (input: { dir: string }) => Effect.Effect<A, E, R>,
186
- ) =>
187
- Effect.gen(function* () {
188
- const dir = yield* tmpdirScoped()
189
- if (input.config) yield* writeConfigEffect(dir, schemaConfig(input.config), input.name)
190
- return yield* withGlobalConfigDir(dir, fn({ dir }))
191
- })
192
-
193
- const withConfigTree = <A, E, R>(
194
- input: { global?: object; project?: object; local?: object },
195
- effect: Effect.Effect<A, E, R>,
196
- ) =>
197
- Effect.gen(function* () {
198
- const root = yield* tmpdirScoped()
199
- const global = yield* tmpdirScoped()
200
- const directory = path.join(root, "project")
201
- yield* Effect.all(
202
- [
203
- input.global ? writeConfigEffect(global, schemaConfig(input.global)) : undefined,
204
- input.project ? writeConfigEffect(directory, schemaConfig(input.project)) : undefined,
205
- input.local ? writeConfigEffect(path.join(directory, ".mervex"), schemaConfig(input.local)) : undefined,
206
- ].filter((effect): effect is Effect.Effect<void, FSUtil.Error, FSUtil.Service> => effect !== undefined),
207
- { concurrency: "unbounded" },
208
- )
209
- return yield* withGlobalConfigDir(global, withInstanceDir(directory, effect))
210
- })
211
-
212
- const wellKnown = (input: {
213
- authUrl?: string
214
- config?: unknown
215
- remoteConfig?: { url: string; headers?: Record<string, string> }
216
- remote?: unknown
217
- remoteHtml?: string
218
- wellKnown?: unknown
219
- }) => {
220
- const seen: { wellKnown?: string; remote?: string; authorization?: string } = {}
221
- const client = remoteConfigClient({
222
- seen,
223
- wellKnown: input.wellKnown ?? {
224
- ...(input.config !== undefined ? { config: input.config } : {}),
225
- ...(input.remoteConfig !== undefined ? { remote_config: input.remoteConfig } : {}),
226
- },
227
- remote: input.remote,
228
- remoteHtml: input.remoteHtml,
229
- })
230
- return {
231
- seen,
232
- it: configIt({ auth: wellKnownAuth(input.authUrl ?? "https://example.com"), client }),
233
- }
234
- }
235
-
236
- function withProcessEnv<A, E, R>(key: string, value: string | undefined, effect: Effect.Effect<A, E, R>) {
237
- return withProcessEnvs({ [key]: value }, effect)
238
- }
239
-
240
- function withProcessEnvs<A, E, R>(entries: Record<string, string | undefined>, effect: Effect.Effect<A, E, R>) {
241
- return Effect.acquireUseRelease(
242
- Effect.sync(() => {
243
- const originals: Record<string, string | undefined> = {}
244
- for (const [key, value] of Object.entries(entries)) {
245
- originals[key] = process.env[key]
246
- if (value === undefined) delete process.env[key]
247
- else process.env[key] = value
248
- }
249
- return originals
250
- }),
251
- () => effect,
252
- (originals) =>
253
- Effect.sync(() => {
254
- for (const [key, original] of Object.entries(originals)) {
255
- if (original !== undefined) process.env[key] = original
256
- else delete process.env[key]
257
- }
258
- }),
259
- )
260
- }
261
-
262
- async function check(map: (dir: string) => string) {
263
- if (process.platform !== "win32") return
264
- await using globalTmp = await tmpdir()
265
- await using tmp = await tmpdir({ git: true, config: { snapshot: true } })
266
- const prev = Global.Path.config
267
- ;(Global.Path as { config: string }).config = globalTmp.path
268
- await clear()
269
- try {
270
- await writeConfig(globalTmp.path, {
271
- $schema: "https://mervex.ai/config.json",
272
- snapshot: false,
273
- })
274
- await withTestInstance({
275
- directory: map(tmp.path),
276
- fn: async (ctx) => {
277
- const cfg = await load(ctx)
278
- expect(cfg.snapshot).toBe(true)
279
- expect(ctx.directory).toBe(Filesystem.resolve(tmp.path))
280
- expect(ctx.project.id).not.toBe(ProjectV2.ID.global)
281
- },
282
- })
283
- } finally {
284
- await InstanceRuntime.disposeAllInstances()
285
- ;(Global.Path as { config: string }).config = prev
286
- await clear()
287
- }
288
- }
289
-
290
- it.instance("loads config with defaults when no files exist", () =>
291
- Effect.gen(function* () {
292
- const config = yield* Config.use.get()
293
- expect(config.username).toBeDefined()
294
- }),
295
- )
296
-
297
- it.instance("falls back to generic username when system user info is unavailable", () =>
298
- Effect.gen(function* () {
299
- const userInfo = spyOn(os, "userInfo").mockImplementation(() => {
300
- throw Object.assign(new Error("missing passwd entry"), { code: "ENOENT" })
301
- })
302
- try {
303
- const config = yield* Config.use.get()
304
- expect(config.username).toBe("user")
305
- } finally {
306
- userInfo.mockRestore()
307
- }
308
- }),
309
- )
310
-
311
- it.effect("creates global jsonc config with schema when no global configs exist", () =>
312
- withGlobalConfig({}, ({ dir }) =>
313
- Effect.gen(function* () {
314
- yield* Config.use.get().pipe(provideInstanceEffect(dir))
315
-
316
- const content = yield* FSUtil.use.readFileString(path.join(dir, "mervex.jsonc"))
317
- expect(content).toContain('"$schema": "https://mervex.ai/config.json"')
318
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
319
- ),
320
- )
321
-
322
- it.effect("does not create global config when MERVEX_CONFIG_DIR is set", () =>
323
- Effect.gen(function* () {
324
- const custom = yield* tmpdirScoped()
325
- yield* withGlobalConfig({}, ({ dir }) =>
326
- withProcessEnv(
327
- "MERVEX_CONFIG_DIR",
328
- custom,
329
- Effect.gen(function* () {
330
- yield* Config.use.get().pipe(provideInstanceEffect(dir))
331
-
332
- expect(yield* FSUtil.use.existsSafe(path.join(dir, "mervex.jsonc"))).toBe(false)
333
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
334
- ),
335
- )
336
- }),
337
- )
338
-
339
- it.instance(
340
- "loads JSON config file",
341
- Effect.gen(function* () {
342
- const config = yield* Config.use.get()
343
- expect(config.model).toBe("test/model")
344
- expect(config.username).toBe("testuser")
345
- }),
346
- { config: { model: "test/model", username: "testuser" } },
347
- )
348
-
349
- it.instance(
350
- "loads shell config field",
351
- Effect.gen(function* () {
352
- const config = yield* Config.use.get()
353
- expect(config.shell).toBe("bash")
354
- }),
355
- { config: { shell: "bash" } },
356
- )
357
-
358
- it.instance("updates config and preserves empty shell sentinel", () =>
359
- Effect.gen(function* () {
360
- const test = yield* TestInstance
361
- yield* writeConfigEffect(
362
- test.directory,
363
- { $schema: "https://mervex.ai/config.json", shell: "bash" },
364
- "config.json",
365
- )
366
-
367
- yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(ConfigV1.Info, { shell: "" }, "test:config")))
368
-
369
- const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
370
- expect(writtenConfig).toMatchObject({ shell: "" })
371
- }),
372
- )
373
-
374
- it.effect("updates global config and omits empty shell key in json", () =>
375
- withGlobalConfig({ config: { shell: "bash" } }, ({ dir }) =>
376
- Effect.gen(function* () {
377
- yield* Config.use.updateGlobal({ shell: "" })
378
-
379
- const writtenConfig = yield* FSUtil.use.readJson(path.join(dir, "mervex.json"))
380
- expect(writtenConfig).not.toHaveProperty("shell")
381
- }),
382
- ),
383
- )
384
-
385
- it.effect("updates global config and omits empty shell key in jsonc", () =>
386
- withGlobalConfig({ config: { shell: "bash", model: "test/model" }, name: "mervex.jsonc" }, ({ dir }) =>
387
- Effect.gen(function* () {
388
- yield* Config.use.updateGlobal({ shell: "" })
389
-
390
- const file = path.join(dir, "mervex.jsonc")
391
- const writtenConfig = yield* FSUtil.use.readFileString(file)
392
- const parsed = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(writtenConfig, file), file)
393
- expect(writtenConfig).not.toContain('"shell"')
394
- expect(parsed.shell).toBeUndefined()
395
- expect(parsed.model).toBe("test/model")
396
- }),
397
- ),
398
- )
399
-
400
- it.instance(
401
- "loads formatter boolean config",
402
- Effect.gen(function* () {
403
- const config = yield* Config.use.get()
404
- expect(config.formatter).toBe(true)
405
- }),
406
- { config: { formatter: true } },
407
- )
408
-
409
- it.instance(
410
- "loads lsp boolean config",
411
- Effect.gen(function* () {
412
- const config = yield* Config.use.get()
413
- expect(config.lsp).toBe(true)
414
- }),
415
- { config: { lsp: true } },
416
- )
417
-
418
- test("loads project config from Git Bash and MSYS2 paths on Windows", async () => {
419
- // Git Bash and MSYS2 both use /<drive>/... paths on Windows.
420
- await check((dir) => {
421
- const drive = dir[0].toLowerCase()
422
- const rest = dir.slice(2).replaceAll("\\", "/")
423
- return `/${drive}${rest}`
424
- })
425
- })
426
-
427
- test("loads project config from Cygwin paths on Windows", async () => {
428
- await check((dir) => {
429
- const drive = dir[0].toLowerCase()
430
- const rest = dir.slice(2).replaceAll("\\", "/")
431
- return `/cygdrive/${drive}${rest}`
432
- })
433
- })
434
-
435
- it.instance("ignores legacy tui keys in mervex config", () =>
436
- Effect.gen(function* () {
437
- const test = yield* TestInstance
438
- yield* writeConfigEffect(test.directory, {
439
- $schema: "https://mervex.ai/config.json",
440
- model: "test/model",
441
- theme: "legacy",
442
- tui: { scroll_speed: 4 },
443
- })
444
-
445
- const config = yield* Config.use.get()
446
- expect(config.model).toBe("test/model")
447
- expect((config as Record<string, unknown>).theme).toBeUndefined()
448
- expect((config as Record<string, unknown>).tui).toBeUndefined()
449
- }),
450
- )
451
-
452
- it.instance("loads JSONC config file", () =>
453
- Effect.gen(function* () {
454
- const test = yield* TestInstance
455
- yield* FSUtil.use.writeWithDirs(
456
- path.join(test.directory, "mervex.jsonc"),
457
- `{
458
- // This is a comment
459
- "$schema": "https://mervex.ai/config.json",
460
- "model": "test/model",
461
- "username": "testuser"
462
- }`,
463
- )
464
- const config = yield* Config.use.get()
465
- expect(config.model).toBe("test/model")
466
- expect(config.username).toBe("testuser")
467
- }),
468
- )
469
-
470
- it.instance("jsonc overrides json in the same directory", () =>
471
- Effect.gen(function* () {
472
- const test = yield* TestInstance
473
- yield* writeConfigEffect(
474
- test.directory,
475
- {
476
- $schema: "https://mervex.ai/config.json",
477
- model: "base",
478
- username: "base",
479
- },
480
- "mervex.jsonc",
481
- )
482
- yield* writeConfigEffect(test.directory, {
483
- $schema: "https://mervex.ai/config.json",
484
- model: "override",
485
- })
486
- const config = yield* Config.use.get()
487
- expect(config.model).toBe("base")
488
- expect(config.username).toBe("base")
489
- }),
490
- )
491
-
492
- it.instance("handles environment variable substitution", () =>
493
- withProcessEnv(
494
- "TEST_VAR",
495
- "test-user",
496
- Effect.gen(function* () {
497
- const test = yield* TestInstance
498
- yield* writeConfigEffect(test.directory, {
499
- $schema: "https://mervex.ai/config.json",
500
- username: "{env:TEST_VAR}",
501
- })
502
- const config = yield* Config.use.get()
503
- expect(config.username).toBe("test-user")
504
- }),
505
- ),
506
- )
507
-
508
- it.instance("preserves env variables when adding $schema to config", () =>
509
- withProcessEnv(
510
- "PRESERVE_VAR",
511
- "secret_value",
512
- Effect.gen(function* () {
513
- const test = yield* TestInstance
514
- // Config without $schema - should trigger auto-add
515
- yield* FSUtil.use.writeWithDirs(
516
- path.join(test.directory, "mervex.json"),
517
- JSON.stringify({ username: "{env:PRESERVE_VAR}" }),
518
- )
519
- const config = yield* Config.use.get()
520
- expect(config.username).toBe("secret_value")
521
-
522
- // Read the file to verify the env variable was preserved
523
- const content = yield* FSUtil.use.readFileString(path.join(test.directory, "mervex.json"))
524
- expect(content).toContain("{env:PRESERVE_VAR}")
525
- expect(content).not.toContain("secret_value")
526
- expect(content).toContain("$schema")
527
- }),
528
- ),
529
- )
530
-
531
- it.instance("handles file inclusion substitution", () =>
532
- Effect.gen(function* () {
533
- const test = yield* TestInstance
534
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "included.txt"), "test-user")
535
- yield* writeConfigEffect(test.directory, {
536
- $schema: "https://mervex.ai/config.json",
537
- username: "{file:included.txt}",
538
- })
539
- const config = yield* Config.use.get()
540
- expect(config.username).toBe("test-user")
541
- }),
542
- )
543
-
544
- it.instance("handles file inclusion with replacement tokens", () =>
545
- Effect.gen(function* () {
546
- const test = yield* TestInstance
547
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "included.md"), "const out = await Bun.$`echo hi`")
548
- yield* writeConfigEffect(test.directory, {
549
- $schema: "https://mervex.ai/config.json",
550
- username: "{file:included.md}",
551
- })
552
- const config = yield* Config.use.get()
553
- expect(config.username).toBe("const out = await Bun.$`echo hi`")
554
- }),
555
- )
556
-
557
- const accountTokenIt = configIt({
558
- account: Layer.mock(Account.Service)({
559
- active: () =>
560
- Effect.succeed(
561
- Option.some({
562
- id: AccountID.make("account-1"),
563
- email: "user@example.com",
564
- url: "https://control.example.com",
565
- active_org_id: OrgID.make("org-1"),
566
- }),
567
- ),
568
- activeOrg: () =>
569
- Effect.succeed(
570
- Option.some({
571
- account: {
572
- id: AccountID.make("account-1"),
573
- email: "user@example.com",
574
- url: "https://control.example.com",
575
- active_org_id: OrgID.make("org-1"),
576
- },
577
- org: {
578
- id: OrgID.make("org-1"),
579
- name: "Example Org",
580
- },
581
- }),
582
- ),
583
- config: () =>
584
- Effect.succeed(
585
- Option.some({
586
- provider: { mervex: { options: { apiKey: "{env:MERVEX_CONSOLE_TOKEN}" } } },
587
- }),
588
- ),
589
- token: () => Effect.succeed(Option.some(AccessToken.make("st_test_token"))),
590
- }),
591
- })
592
-
593
- accountTokenIt.instance("resolves env templates in account config with account token", () =>
594
- Effect.gen(function* () {
595
- const config = yield* Config.use.get()
596
- expect(config.provider?.["mervex"]?.options?.apiKey).toBe("st_test_token")
597
- }),
598
- )
599
-
600
- it.instance("validates config schema and throws on invalid values", () =>
601
- Effect.gen(function* () {
602
- const test = yield* TestInstance
603
- yield* writeConfigEffect(test.directory, {
604
- $schema: "https://mervex.ai/config.json",
605
- model: 42,
606
- })
607
- const exit = yield* Config.use.get().pipe(Effect.exit)
608
- expect(Exit.isFailure(exit)).toBe(true)
609
- }),
610
- )
611
-
612
- it.instance("throws error for invalid JSON", () =>
613
- Effect.gen(function* () {
614
- const test = yield* TestInstance
615
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "mervex.json"), "{ invalid json }")
616
- const exit = yield* Config.use.get().pipe(Effect.exit)
617
- expect(Exit.isFailure(exit)).toBe(true)
618
- }),
619
- )
620
-
621
- it.instance("handles agent configuration", () =>
622
- Effect.gen(function* () {
623
- const test = yield* TestInstance
624
- yield* writeConfigEffect(test.directory, {
625
- $schema: "https://mervex.ai/config.json",
626
- agent: {
627
- test_agent: {
628
- model: "test/model",
629
- temperature: 0.7,
630
- description: "test agent",
631
- },
632
- },
633
- })
634
- const config = yield* Config.use.get()
635
- expect(config.agent?.["test_agent"]).toEqual(
636
- expect.objectContaining({
637
- model: "test/model",
638
- temperature: 0.7,
639
- description: "test agent",
640
- }),
641
- )
642
- }),
643
- )
644
-
645
- it.instance("treats agent variant as model-scoped setting (not provider option)", () =>
646
- Effect.gen(function* () {
647
- const test = yield* TestInstance
648
- yield* writeConfigEffect(test.directory, {
649
- $schema: "https://mervex.ai/config.json",
650
- agent: {
651
- test_agent: {
652
- model: "openai/gpt-5.2",
653
- variant: "xhigh",
654
- max_tokens: 123,
655
- },
656
- },
657
- })
658
- const config = yield* Config.use.get()
659
- const agent = config.agent?.["test_agent"]
660
-
661
- expect(agent?.variant).toBe("xhigh")
662
- expect(agent?.options).toMatchObject({
663
- max_tokens: 123,
664
- })
665
- expect(agent?.options).not.toHaveProperty("variant")
666
- }),
667
- )
668
-
669
- it.instance("handles command configuration", () =>
670
- Effect.gen(function* () {
671
- const test = yield* TestInstance
672
- yield* writeConfigEffect(test.directory, {
673
- $schema: "https://mervex.ai/config.json",
674
- command: {
675
- test_command: {
676
- template: "test template",
677
- description: "test command",
678
- agent: "test_agent",
679
- },
680
- },
681
- })
682
- const config = yield* Config.use.get()
683
- expect(config.command?.["test_command"]).toEqual({
684
- template: "test template",
685
- description: "test command",
686
- agent: "test_agent",
687
- })
688
- }),
689
- )
690
-
691
- it.instance("migrates autoshare to share field", () =>
692
- Effect.gen(function* () {
693
- const test = yield* TestInstance
694
- yield* writeConfigEffect(test.directory, {
695
- $schema: "https://mervex.ai/config.json",
696
- autoshare: true,
697
- })
698
- const config = yield* Config.use.get()
699
- expect(config.share).toBe("auto")
700
- expect(config.autoshare).toBe(true)
701
- }),
702
- )
703
-
704
- it.instance("migrates mode field to agent field", () =>
705
- Effect.gen(function* () {
706
- const test = yield* TestInstance
707
- yield* writeConfigEffect(test.directory, {
708
- $schema: "https://mervex.ai/config.json",
709
- mode: {
710
- test_mode: {
711
- model: "test/model",
712
- temperature: 0.5,
713
- },
714
- },
715
- })
716
- const config = yield* Config.use.get()
717
- expect(config.agent?.["test_mode"]).toEqual({
718
- model: "test/model",
719
- temperature: 0.5,
720
- mode: "primary",
721
- options: {},
722
- permission: {},
723
- })
724
- }),
725
- )
726
-
727
- it.instance("accepts the deprecated reference field", () =>
728
- Effect.gen(function* () {
729
- const test = yield* TestInstance
730
- yield* writeConfigEffect(test.directory, {
731
- $schema: "https://mervex.ai/config.json",
732
- reference: {
733
- local: { path: "../library" },
734
- sdk: { repository: "github.com/example/sdk", branch: "main" },
735
- shorthand: "github.com/example/docs",
736
- },
737
- })
738
- const config = yield* Config.use.get()
739
- expect(config.reference).toEqual({
740
- local: { path: "../library" },
741
- sdk: { repository: "github.com/example/sdk", branch: "main" },
742
- shorthand: "github.com/example/docs",
743
- })
744
- }),
745
- )
746
-
747
- it.instance("loads config from .mervex directory", () =>
748
- Effect.gen(function* () {
749
- const test = yield* TestInstance
750
- yield* FSUtil.use.writeWithDirs(
751
- path.join(test.directory, ".mervex", "agent", "test.md"),
752
- `---
753
- model: test/model
754
- ---
755
- Test agent prompt`,
756
- )
757
-
758
- const config = yield* Config.use.get()
759
- expect(config.agent?.["test"]).toEqual(
760
- expect.objectContaining({
761
- name: "test",
762
- model: "test/model",
763
- prompt: "Test agent prompt",
764
- }),
765
- )
766
- }),
767
- )
768
-
769
- it.instance("agent markdown permission config preserves user key order", () =>
770
- Effect.gen(function* () {
771
- const test = yield* TestInstance
772
- yield* FSUtil.use.writeWithDirs(
773
- path.join(test.directory, ".mervex", "agent", "ordered.md"),
774
- `---
775
- permission:
776
- bash: allow
777
- "*": deny
778
- edit: ask
779
- ---
780
- Ordered permissions`,
781
- )
782
-
783
- const config = yield* Config.use.get()
784
- expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"])
785
- }),
786
- )
787
-
788
- it.instance("loads agents from .mervex/agents (plural)", () =>
789
- Effect.gen(function* () {
790
- const test = yield* TestInstance
791
- yield* FSUtil.use.writeWithDirs(
792
- path.join(test.directory, ".mervex", "agents", "helper.md"),
793
- `---
794
- model: test/model
795
- mode: subagent
796
- ---
797
- Helper agent prompt`,
798
- )
799
-
800
- yield* FSUtil.use.writeWithDirs(
801
- path.join(test.directory, ".mervex", "agents", "nested", "child.md"),
802
- `---
803
- model: test/model
804
- mode: subagent
805
- ---
806
- Nested agent prompt`,
807
- )
808
-
809
- const config = yield* Config.use.get()
810
-
811
- expect(config.agent?.["helper"]).toMatchObject({
812
- name: "helper",
813
- model: "test/model",
814
- mode: "subagent",
815
- prompt: "Helper agent prompt",
816
- })
817
-
818
- expect(config.agent?.["nested/child"]).toMatchObject({
819
- name: "nested/child",
820
- model: "test/model",
821
- mode: "subagent",
822
- prompt: "Nested agent prompt",
823
- })
824
- }),
825
- )
826
-
827
- it.instance("loads commands from .mervex/command (singular)", () =>
828
- Effect.gen(function* () {
829
- const test = yield* TestInstance
830
- yield* FSUtil.use.writeWithDirs(
831
- path.join(test.directory, ".mervex", "command", "hello.md"),
832
- `---
833
- description: Test command
834
- ---
835
- Hello from singular command`,
836
- )
837
-
838
- yield* FSUtil.use.writeWithDirs(
839
- path.join(test.directory, ".mervex", "command", "nested", "child.md"),
840
- `---
841
- description: Nested command
842
- ---
843
- Nested command template`,
844
- )
845
-
846
- const config = yield* Config.use.get()
847
-
848
- expect(config.command?.["hello"]).toEqual({
849
- description: "Test command",
850
- template: "Hello from singular command",
851
- })
852
-
853
- expect(config.command?.["nested/child"]).toEqual({
854
- description: "Nested command",
855
- template: "Nested command template",
856
- })
857
- }),
858
- )
859
-
860
- it.instance("loads commands from .mervex/commands (plural)", () =>
861
- Effect.gen(function* () {
862
- const test = yield* TestInstance
863
- yield* FSUtil.use.writeWithDirs(
864
- path.join(test.directory, ".mervex", "commands", "hello.md"),
865
- `---
866
- description: Test command
867
- ---
868
- Hello from plural commands`,
869
- )
870
-
871
- yield* FSUtil.use.writeWithDirs(
872
- path.join(test.directory, ".mervex", "commands", "nested", "child.md"),
873
- `---
874
- description: Nested command
875
- ---
876
- Nested command template`,
877
- )
878
-
879
- const config = yield* Config.use.get()
880
-
881
- expect(config.command?.["hello"]).toEqual({
882
- description: "Test command",
883
- template: "Hello from plural commands",
884
- })
885
-
886
- expect(config.command?.["nested/child"]).toEqual({
887
- description: "Nested command",
888
- template: "Nested command template",
889
- })
890
- }),
891
- )
892
-
893
- it.instance("updates config and writes to file", () =>
894
- Effect.gen(function* () {
895
- const test = yield* TestInstance
896
- yield* Config.Service.use((svc) =>
897
- svc.update(ConfigParse.schema(ConfigV1.Info, { model: "updated/model" }, "test:config")),
898
- )
899
-
900
- const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
901
- expect(writtenConfig).toMatchObject({ model: "updated/model" })
902
- }),
903
- )
904
-
905
- it.instance("gets config directories", () =>
906
- Effect.gen(function* () {
907
- const dirs = yield* Config.use.directories()
908
- expect(dirs.length).toBeGreaterThanOrEqual(1)
909
- }),
910
- )
911
-
912
- it.effect("does not try to install dependencies in read-only MERVEX_CONFIG_DIR", () =>
913
- Effect.gen(function* () {
914
- if (process.platform === "win32") return
915
-
916
- const dir = yield* tmpdirScoped()
917
- const readonly = path.join(dir, "readonly")
918
- yield* FSUtil.use.ensureDir(readonly)
919
- yield* FSUtil.use.chmod(readonly, 0o555)
920
- yield* Effect.addFinalizer(() => FSUtil.use.chmod(readonly, 0o755).pipe(Effect.ignore))
921
-
922
- yield* withProcessEnv("MERVEX_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir)))
923
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
924
- )
925
-
926
- it.effect("ignores an inaccessible MERVEX_CONFIG_DIR", () =>
927
- Effect.gen(function* () {
928
- if (process.platform === "win32") return
929
-
930
- const dir = yield* tmpdirScoped()
931
- const configDir = path.join(dir, "inaccessible")
932
- yield* FSUtil.use.ensureDir(configDir)
933
- yield* FSUtil.use.chmod(configDir, 0o000)
934
- yield* Effect.addFinalizer(() => FSUtil.use.chmod(configDir, 0o755).pipe(Effect.ignore))
935
-
936
- yield* withProcessEnv("MERVEX_CONFIG_DIR", configDir, Config.use.get().pipe(provideInstanceEffect(dir)))
937
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
938
- )
939
-
940
- it.effect("creates a missing MERVEX_CONFIG_DIR", () =>
941
- Effect.gen(function* () {
942
- const dir = yield* tmpdirScoped()
943
- const configDir = path.join(dir, "configdir")
944
-
945
- yield* withProcessEnv("MERVEX_CONFIG_DIR", configDir, Config.use.get().pipe(provideInstanceEffect(dir)))
946
-
947
- expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("node_modules")
948
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
949
- )
950
-
951
- it.effect("installs dependencies in writable MERVEX_CONFIG_DIR", () =>
952
- Effect.gen(function* () {
953
- const dir = yield* tmpdirScoped()
954
- const configDir = path.join(dir, "configdir")
955
- yield* FSUtil.use.ensureDir(configDir)
956
-
957
- yield* withProcessEnv(
958
- "MERVEX_CONFIG_DIR",
959
- configDir,
960
- Config.Service.use((svc) => svc.get().pipe(Effect.andThen(svc.waitForDependencies()))).pipe(
961
- provideInstanceEffect(dir),
962
- ),
963
- )
964
-
965
- expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
966
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
967
- )
968
-
969
- // Note: deduplication and serialization of npm installs is now handled by the
970
- // core Npm.Service (via EffectFlock). Those behaviors are tested in the core
971
- // package's npm tests, not here.
972
-
973
- it.instance("resolves scoped npm plugins in config", () =>
974
- Effect.gen(function* () {
975
- const test = yield* TestInstance
976
- const pluginDir = path.join(test.directory, "node_modules", "@scope", "plugin")
977
- yield* FSUtil.use.writeWithDirs(
978
- path.join(test.directory, "package.json"),
979
- JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2),
980
- )
981
- yield* FSUtil.use.writeWithDirs(
982
- path.join(pluginDir, "package.json"),
983
- JSON.stringify(
984
- {
985
- name: "@scope/plugin",
986
- version: "1.0.0",
987
- type: "module",
988
- main: "./index.js",
989
- },
990
- null,
991
- 2,
992
- ),
993
- )
994
- yield* FSUtil.use.writeWithDirs(path.join(pluginDir, "index.js"), "export default {}\n")
995
- yield* writeConfigEffect(test.directory, { plugin: ["@scope/plugin"] })
996
-
997
- const config = yield* Config.use.get()
998
- expect(config.plugin ?? []).toContain("@scope/plugin")
999
- }),
1000
- )
1001
-
1002
- it.effect("merges plugin arrays from global and local configs", () =>
1003
- withConfigTree(
1004
- {
1005
- global: { plugin: ["global-plugin-1", "global-plugin-2"] },
1006
- local: { plugin: ["local-plugin-1"] },
1007
- },
1008
- Effect.gen(function* () {
1009
- const plugins = (yield* Config.use.get()).plugin ?? []
1010
-
1011
- expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true)
1012
- expect(plugins.some((p) => p.includes("global-plugin-2"))).toBe(true)
1013
- expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true)
1014
- expect(
1015
- plugins.filter((p) => p.includes("global-plugin") || p.includes("local-plugin")).length,
1016
- ).toBeGreaterThanOrEqual(3)
1017
- }),
1018
- ),
1019
- )
1020
-
1021
- it.effect("global config remains global when project config is disabled", () =>
1022
- withConfigTree(
1023
- {
1024
- global: { model: "global/model", plugin: ["global-plugin"] },
1025
- project: { model: "project/model" },
1026
- local: { model: "local/model" },
1027
- },
1028
- withProcessEnv(
1029
- "MERVEX_DISABLE_PROJECT_CONFIG",
1030
- "true",
1031
- Effect.gen(function* () {
1032
- const config = yield* Config.use.get()
1033
- expect(config.model).toBe("global/model")
1034
- expect(config.plugin_origins?.find((item) => item.spec === "global-plugin")?.scope).toBe("global")
1035
- }),
1036
- ),
1037
- ),
1038
- )
1039
-
1040
- it.instance("does not error when only custom agent is a subagent", () =>
1041
- Effect.gen(function* () {
1042
- const test = yield* TestInstance
1043
- yield* FSUtil.use.writeWithDirs(
1044
- path.join(test.directory, ".mervex", "agent", "helper.md"),
1045
- `---
1046
- model: test/model
1047
- mode: subagent
1048
- ---
1049
- Helper subagent prompt`,
1050
- )
1051
-
1052
- const config = yield* Config.use.get()
1053
- expect(config.agent?.["helper"]).toMatchObject({
1054
- name: "helper",
1055
- model: "test/model",
1056
- mode: "subagent",
1057
- prompt: "Helper subagent prompt",
1058
- })
1059
- }),
1060
- )
1061
-
1062
- it.effect("merges instructions arrays from global and local configs", () =>
1063
- withConfigTree(
1064
- {
1065
- global: { instructions: ["global-instructions.md", "shared-rules.md"] },
1066
- local: { instructions: ["local-instructions.md"] },
1067
- },
1068
- Effect.gen(function* () {
1069
- expect((yield* Config.use.get()).instructions).toEqual([
1070
- "global-instructions.md",
1071
- "shared-rules.md",
1072
- "local-instructions.md",
1073
- ])
1074
- }),
1075
- ),
1076
- )
1077
-
1078
- it.effect("deduplicates duplicate instructions from global and local configs", () =>
1079
- withConfigTree(
1080
- {
1081
- global: { instructions: ["duplicate.md", "global-only.md"] },
1082
- local: { instructions: ["duplicate.md", "local-only.md"] },
1083
- },
1084
- Effect.gen(function* () {
1085
- expect((yield* Config.use.get()).instructions).toEqual(["duplicate.md", "global-only.md", "local-only.md"])
1086
- }),
1087
- ),
1088
- )
1089
-
1090
- it.effect("deduplicates duplicate plugins from global and local configs", () =>
1091
- withConfigTree(
1092
- {
1093
- global: { plugin: ["duplicate-plugin", "global-plugin-1"] },
1094
- local: { plugin: ["duplicate-plugin", "local-plugin-1"] },
1095
- },
1096
- Effect.gen(function* () {
1097
- const plugins = (yield* Config.use.get()).plugin ?? []
1098
-
1099
- expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true)
1100
- expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true)
1101
- expect(plugins.filter((p) => p.includes("duplicate-plugin")).length).toBe(1)
1102
- expect(
1103
- plugins.filter(
1104
- (p) => p.includes("global-plugin") || p.includes("local-plugin") || p.includes("duplicate-plugin"),
1105
- ).length,
1106
- ).toBe(3)
1107
- }),
1108
- ),
1109
- )
1110
-
1111
- it.effect("keeps plugin origins aligned with merged plugin list", () =>
1112
- withConfigTree(
1113
- {
1114
- global: { plugin: [["shared-plugin@1.0.0", { source: "global" }], "global-only@1.0.0"] },
1115
- local: { plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"] },
1116
- },
1117
- Effect.gen(function* () {
1118
- const config = yield* Config.use.get()
1119
- const plugins = config.plugin ?? []
1120
- const origins = config.plugin_origins ?? []
1121
- const names = plugins.map((item) => ConfigPlugin.pluginSpecifier(item))
1122
-
1123
- expect(names).toContain("shared-plugin@2.0.0")
1124
- expect(names).not.toContain("shared-plugin@1.0.0")
1125
- expect(names).toContain("global-only@1.0.0")
1126
- expect(names).toContain("local-only@1.0.0")
1127
- expect(origins.map((item) => item.spec)).toEqual(plugins)
1128
- expect(origins.find((item) => ConfigPlugin.pluginSpecifier(item.spec) === "shared-plugin@2.0.0")?.scope).toBe(
1129
- "local",
1130
- )
1131
- }),
1132
- ),
1133
- )
1134
-
1135
- // Legacy tools migration tests
1136
-
1137
- it.instance("migrates legacy tools config to permissions - allow", () =>
1138
- Effect.gen(function* () {
1139
- const test = yield* TestInstance
1140
- yield* writeConfigEffect(test.directory, {
1141
- $schema: "https://mervex.ai/config.json",
1142
- agent: { test: { tools: { bash: true, read: true } } },
1143
- })
1144
-
1145
- const config = yield* Config.use.get()
1146
- expect(config.agent?.["test"]?.permission).toEqual({
1147
- bash: "allow",
1148
- read: "allow",
1149
- })
1150
- }),
1151
- )
1152
-
1153
- it.instance("migrates legacy tools config to permissions - deny", () =>
1154
- Effect.gen(function* () {
1155
- const test = yield* TestInstance
1156
- yield* writeConfigEffect(test.directory, {
1157
- $schema: "https://mervex.ai/config.json",
1158
- agent: { test: { tools: { bash: false, webfetch: false } } },
1159
- })
1160
-
1161
- const config = yield* Config.use.get()
1162
- expect(config.agent?.["test"]?.permission).toEqual({
1163
- bash: "deny",
1164
- webfetch: "deny",
1165
- })
1166
- }),
1167
- )
1168
-
1169
- it.instance("migrates legacy write tool to edit permission", () =>
1170
- Effect.gen(function* () {
1171
- const test = yield* TestInstance
1172
- yield* writeConfigEffect(test.directory, {
1173
- $schema: "https://mervex.ai/config.json",
1174
- agent: { test: { tools: { write: true } } },
1175
- })
1176
-
1177
- const config = yield* Config.use.get()
1178
- expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" })
1179
- }),
1180
- )
1181
-
1182
- // Managed settings tests
1183
- // Note: preload.ts sets MERVEX_TEST_MANAGED_CONFIG which Global.Path.managedConfig uses
1184
-
1185
- it.instance(
1186
- "managed settings override user settings",
1187
- Effect.gen(function* () {
1188
- yield* writeManagedSettingsEffect({
1189
- $schema: "https://mervex.ai/config.json",
1190
- model: "managed/model",
1191
- share: "disabled",
1192
- })
1193
-
1194
- const config = yield* Config.use.get()
1195
- expect(config.model).toBe("managed/model")
1196
- expect(config.share).toBe("disabled")
1197
- expect(config.username).toBe("testuser")
1198
- }),
1199
- { config: { model: "user/model", share: "auto", username: "testuser" } },
1200
- )
1201
-
1202
- it.instance(
1203
- "managed settings override project settings",
1204
- Effect.gen(function* () {
1205
- yield* writeManagedSettingsEffect({
1206
- $schema: "https://mervex.ai/config.json",
1207
- autoupdate: false,
1208
- disabled_providers: ["openai"],
1209
- })
1210
-
1211
- const config = yield* Config.use.get()
1212
- expect(config.autoupdate).toBe(false)
1213
- expect(config.disabled_providers).toEqual(["openai"])
1214
- }),
1215
- { config: { autoupdate: true, disabled_providers: [] } },
1216
- )
1217
-
1218
- it.instance("managed jsonc settings override managed json settings", () =>
1219
- Effect.gen(function* () {
1220
- yield* writeManagedSettingsEffect({ model: "managed/json" })
1221
- yield* writeManagedSettingsEffect({ model: "managed/jsonc" }, "mervex.jsonc")
1222
-
1223
- const config = yield* Config.use.get()
1224
- expect(config.model).toBe("managed/jsonc")
1225
- }),
1226
- )
1227
-
1228
- it.instance(
1229
- "missing managed settings file is not an error",
1230
- Effect.gen(function* () {
1231
- const config = yield* Config.use.get()
1232
- expect(config.model).toBe("user/model")
1233
- }),
1234
- { config: { model: "user/model" } },
1235
- )
1236
-
1237
- it.instance("migrates legacy edit tool to edit permission", () =>
1238
- Effect.gen(function* () {
1239
- const test = yield* TestInstance
1240
- yield* writeConfigEffect(test.directory, {
1241
- $schema: "https://mervex.ai/config.json",
1242
- agent: { test: { tools: { edit: false } } },
1243
- })
1244
-
1245
- const config = yield* Config.use.get()
1246
- expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny" })
1247
- }),
1248
- )
1249
-
1250
- it.instance("migrates legacy patch tool to edit permission", () =>
1251
- Effect.gen(function* () {
1252
- const test = yield* TestInstance
1253
- yield* writeConfigEffect(test.directory, {
1254
- $schema: "https://mervex.ai/config.json",
1255
- agent: { test: { tools: { patch: true } } },
1256
- })
1257
-
1258
- const config = yield* Config.use.get()
1259
- expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" })
1260
- }),
1261
- )
1262
-
1263
- it.instance("migrates mixed legacy tools config", () =>
1264
- Effect.gen(function* () {
1265
- const test = yield* TestInstance
1266
- yield* writeConfigEffect(test.directory, {
1267
- $schema: "https://mervex.ai/config.json",
1268
- agent: { test: { tools: { bash: true, write: true, read: false, webfetch: true } } },
1269
- })
1270
-
1271
- const config = yield* Config.use.get()
1272
- expect(config.agent?.["test"]?.permission).toEqual({
1273
- bash: "allow",
1274
- edit: "allow",
1275
- read: "deny",
1276
- webfetch: "allow",
1277
- })
1278
- }),
1279
- )
1280
-
1281
- it.instance("merges legacy tools with existing permission config", () =>
1282
- Effect.gen(function* () {
1283
- const test = yield* TestInstance
1284
- yield* writeConfigEffect(test.directory, {
1285
- $schema: "https://mervex.ai/config.json",
1286
- agent: { test: { permission: { glob: "allow" }, tools: { bash: true } } },
1287
- })
1288
-
1289
- const config = yield* Config.use.get()
1290
- expect(config.agent?.["test"]?.permission).toEqual({
1291
- glob: "allow",
1292
- bash: "allow",
1293
- })
1294
- }),
1295
- )
1296
-
1297
- it.instance("permission config preserves user key order", () =>
1298
- // Permission precedence follows the order users write in config, so parsing
1299
- // must not canonicalise known keys ahead of wildcard or custom keys.
1300
- Effect.gen(function* () {
1301
- const test = yield* TestInstance
1302
- yield* writeConfigEffect(test.directory, {
1303
- $schema: "https://mervex.ai/config.json",
1304
- permission: {
1305
- "*": "deny",
1306
- edit: "ask",
1307
- write: "ask",
1308
- external_directory: "ask",
1309
- read: "allow",
1310
- todowrite: "allow",
1311
- "thoughts_*": "allow",
1312
- "reasoning_model_*": "allow",
1313
- "tools_*": "allow",
1314
- "pr_comments_*": "allow",
1315
- },
1316
- })
1317
-
1318
- const config = yield* Config.use.get()
1319
- expect(Object.keys(config.permission!)).toEqual([
1320
- "*",
1321
- "edit",
1322
- "write",
1323
- "external_directory",
1324
- "read",
1325
- "todowrite",
1326
- "thoughts_*",
1327
- "reasoning_model_*",
1328
- "tools_*",
1329
- "pr_comments_*",
1330
- ])
1331
- }),
1332
- )
1333
-
1334
- test("config parser preserves permission order while ignoring unknown top-level keys", () => {
1335
- const config = ConfigParse.schema(
1336
- ConfigV1.Info,
1337
- {
1338
- permission: {
1339
- bash: "allow",
1340
- "*": "deny",
1341
- edit: "ask",
1342
- },
1343
- plugins: ["example"],
1344
- },
1345
- "test",
1346
- )
1347
-
1348
- expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"])
1349
- expect(config).not.toHaveProperty("plugins")
1350
- })
1351
-
1352
- // MCP config merging tests
1353
-
1354
- it.instance("project config can override MCP server enabled status", () =>
1355
- Effect.gen(function* () {
1356
- const test = yield* TestInstance
1357
- // Simulates a base config (like from remote .well-known) with disabled MCP.
1358
- yield* writeConfigEffect(test.directory, {
1359
- $schema: "https://mervex.ai/config.json",
1360
- mcp: {
1361
- jira: {
1362
- type: "remote",
1363
- url: "https://jira.example.com/mcp",
1364
- enabled: false,
1365
- },
1366
- wiki: {
1367
- type: "remote",
1368
- url: "https://wiki.example.com/mcp",
1369
- enabled: false,
1370
- },
1371
- },
1372
- })
1373
- // Project config enables just jira.
1374
- yield* writeConfigEffect(
1375
- test.directory,
1376
- {
1377
- $schema: "https://mervex.ai/config.json",
1378
- mcp: {
1379
- jira: {
1380
- type: "remote",
1381
- url: "https://jira.example.com/mcp",
1382
- enabled: true,
1383
- },
1384
- },
1385
- },
1386
- "mervex.jsonc",
1387
- )
1388
-
1389
- const config = yield* Config.use.get()
1390
- expect(config.mcp?.jira).toEqual({
1391
- type: "remote",
1392
- url: "https://jira.example.com/mcp",
1393
- enabled: true,
1394
- })
1395
- expect(config.mcp?.wiki).toEqual({
1396
- type: "remote",
1397
- url: "https://wiki.example.com/mcp",
1398
- enabled: false,
1399
- })
1400
- }),
1401
- )
1402
-
1403
- it.instance("MCP config deep merges preserving base config properties", () =>
1404
- Effect.gen(function* () {
1405
- const test = yield* TestInstance
1406
- yield* writeConfigEffect(test.directory, {
1407
- $schema: "https://mervex.ai/config.json",
1408
- mcp: {
1409
- myserver: {
1410
- type: "remote",
1411
- url: "https://myserver.example.com/mcp",
1412
- enabled: false,
1413
- headers: {
1414
- "X-Custom-Header": "value",
1415
- },
1416
- },
1417
- },
1418
- })
1419
- yield* writeConfigEffect(
1420
- test.directory,
1421
- {
1422
- $schema: "https://mervex.ai/config.json",
1423
- mcp: {
1424
- myserver: {
1425
- type: "remote",
1426
- url: "https://myserver.example.com/mcp",
1427
- enabled: true,
1428
- },
1429
- },
1430
- },
1431
- "mervex.jsonc",
1432
- )
1433
-
1434
- const config = yield* Config.use.get()
1435
- expect(config.mcp?.myserver).toEqual({
1436
- type: "remote",
1437
- url: "https://myserver.example.com/mcp",
1438
- enabled: true,
1439
- headers: {
1440
- "X-Custom-Header": "value",
1441
- },
1442
- })
1443
- }),
1444
- )
1445
-
1446
- it.instance("local .mervex config can override MCP from project config", () =>
1447
- Effect.gen(function* () {
1448
- const test = yield* TestInstance
1449
- yield* writeConfigEffect(test.directory, {
1450
- $schema: "https://mervex.ai/config.json",
1451
- mcp: {
1452
- docs: {
1453
- type: "remote",
1454
- url: "https://docs.example.com/mcp",
1455
- enabled: false,
1456
- },
1457
- },
1458
- })
1459
- yield* FSUtil.use.ensureDir(path.join(test.directory, ".mervex"))
1460
- yield* writeConfigEffect(
1461
- path.join(test.directory, ".mervex"),
1462
- {
1463
- $schema: "https://mervex.ai/config.json",
1464
- mcp: {
1465
- docs: {
1466
- type: "remote",
1467
- url: "https://docs.example.com/mcp",
1468
- enabled: true,
1469
- },
1470
- },
1471
- },
1472
- "mervex.json",
1473
- )
1474
-
1475
- const config = yield* Config.use.get()
1476
- expect(config.mcp?.docs?.enabled).toBe(true)
1477
- }),
1478
- )
1479
-
1480
- const remoteProjectOverride = wellKnown({
1481
- config: {
1482
- mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false } },
1483
- },
1484
- })
1485
-
1486
- remoteProjectOverride.it.instance(
1487
- "project config overrides remote well-known config",
1488
- () =>
1489
- Effect.gen(function* () {
1490
- const config = yield* Config.use.get()
1491
- expect(remoteProjectOverride.seen.wellKnown).toBe("https://example.com/.well-known/mervex")
1492
- expect(config.mcp?.jira?.enabled).toBe(true)
1493
- }),
1494
- {
1495
- git: true,
1496
- config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } } },
1497
- },
1498
- )
1499
-
1500
- const trailingSlashWellKnown = wellKnown({
1501
- authUrl: "https://example.com/",
1502
- config: {
1503
- mcp: { slack: { type: "remote", url: "https://slack.example.com/mcp", enabled: true } },
1504
- },
1505
- })
1506
-
1507
- trailingSlashWellKnown.it.instance("wellknown URL with trailing slash is normalized", () =>
1508
- Effect.gen(function* () {
1509
- yield* Config.use.get()
1510
- expect(trailingSlashWellKnown.seen.wellKnown).toBe("https://example.com/.well-known/mervex")
1511
- }),
1512
- )
1513
-
1514
- test("remote well-known config can use FetchHttpClient layer", async () => {
1515
- let fetchedUrl: string | undefined
1516
- const server = Bun.serve({
1517
- port: 0,
1518
- fetch: (request) => {
1519
- fetchedUrl = request.url
1520
- return new Response(
1521
- JSON.stringify({
1522
- config: {
1523
- mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } },
1524
- },
1525
- }),
1526
- { status: 200, headers: { "content-type": "application/json" } },
1527
- )
1528
- },
1529
- })
1530
-
1531
- try {
1532
- await provideTmpdirInstance(
1533
- () =>
1534
- Config.Service.use((svc) =>
1535
- Effect.gen(function* () {
1536
- const config = yield* svc.get()
1537
- expect(fetchedUrl).toBe(`${server.url.origin}/.well-known/mervex`)
1538
- expect(config.mcp?.jira?.enabled).toBe(true)
1539
- }),
1540
- ),
1541
- { git: true },
1542
- ).pipe(
1543
- Effect.scoped,
1544
- Effect.provide(
1545
- Layer.mergeAll(
1546
- LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
1547
- [Auth.node, wellKnownAuth(server.url.origin)],
1548
- [Account.node, AccountTest.empty],
1549
- [Npm.node, NpmTest.noop],
1550
- [httpClient, FetchHttpClient.layer],
1551
- ]),
1552
- testInstanceStoreLayer,
1553
- ),
1554
- ),
1555
- Effect.runPromise,
1556
- )
1557
- } finally {
1558
- await server.stop(true)
1559
- }
1560
- })
1561
-
1562
- const templatedHeaderWellKnown = wellKnown({
1563
- remoteConfig: {
1564
- url: "https://config.example.com/mervex.json",
1565
- headers: { Authorization: "Bearer {env:TEST_TOKEN}" },
1566
- },
1567
- remote: {
1568
- mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
1569
- },
1570
- })
1571
-
1572
- templatedHeaderWellKnown.it.instance("wellknown remote_config supports templated env vars in headers", () =>
1573
- Effect.gen(function* () {
1574
- const config = yield* Config.use.get()
1575
- expect(templatedHeaderWellKnown.seen.wellKnown).toBe("https://example.com/.well-known/mervex")
1576
- expect(templatedHeaderWellKnown.seen.remote).toBe("https://config.example.com/mervex.json")
1577
- expect(templatedHeaderWellKnown.seen.authorization).toBe("Bearer test-token")
1578
- expect(config.mcp?.confluence?.enabled).toBe(true)
1579
- }),
1580
- )
1581
-
1582
- const remotePrecedenceWellKnown = wellKnown({
1583
- config: {
1584
- mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: false } },
1585
- },
1586
- remoteConfig: { url: "https://config.example.com/{env:TEST_TOKEN}/mervex.json" },
1587
- remote: {
1588
- config: { mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } } },
1589
- },
1590
- })
1591
-
1592
- remotePrecedenceWellKnown.it.instance(
1593
- "wellknown remote_config url tokens and nested config override embedded config",
1594
- () =>
1595
- Effect.gen(function* () {
1596
- const config = yield* Config.use.get()
1597
- expect(remotePrecedenceWellKnown.seen.remote).toBe("https://config.example.com/test-token/mervex.json")
1598
- expect(config.mcp?.confluence?.enabled).toBe(true)
1599
- }),
1600
- )
1601
-
1602
- const envIsolationWellKnown = wellKnown({
1603
- remoteConfig: {
1604
- url: "https://config.example.com/mervex.json",
1605
- headers: { Authorization: "Bearer {env:TEST_TOKEN}" },
1606
- },
1607
- remote: {
1608
- mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
1609
- },
1610
- })
1611
-
1612
- envIsolationWellKnown.it.instance(
1613
- "wellknown token env substitution does not mutate process env",
1614
- () =>
1615
- Effect.gen(function* () {
1616
- process.env.TEST_TOKEN = "preexisting-token"
1617
- const config = yield* Config.use.get()
1618
- expect(envIsolationWellKnown.seen.authorization).toBe("Bearer test-token")
1619
- expect(config.username).toBe("test-token")
1620
- expect(process.env.TEST_TOKEN).toBe("preexisting-token")
1621
- }),
1622
- { git: true, config: { username: "{env:TEST_TOKEN}" } },
1623
- )
1624
-
1625
- const nullConfigWellKnown = wellKnown({
1626
- wellKnown: {
1627
- config: null,
1628
- remote_config: { url: "https://config.example.com/mervex.json" },
1629
- },
1630
- remote: {
1631
- mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
1632
- },
1633
- })
1634
-
1635
- nullConfigWellKnown.it.instance("wellknown config null is treated as absent", () =>
1636
- Effect.gen(function* () {
1637
- const config = yield* Config.use.get()
1638
- expect(nullConfigWellKnown.seen.remote).toBe("https://config.example.com/mervex.json")
1639
- expect(config.mcp?.confluence?.enabled).toBe(true)
1640
- }),
1641
- )
1642
-
1643
- const invalidRemoteWellKnown = wellKnown({
1644
- remoteConfig: { url: "https://config.example.com/mervex.json" },
1645
- remote: "not an object",
1646
- })
1647
-
1648
- invalidRemoteWellKnown.it.instance("wellknown remote_config rejects non-object config responses", () =>
1649
- Effect.gen(function* () {
1650
- const exit = yield* Config.use.get().pipe(Effect.exit)
1651
- expect(invalidRemoteWellKnown.seen.remote).toBe("https://config.example.com/mervex.json")
1652
- expect(Exit.isFailure(exit)).toBe(true)
1653
- }),
1654
- )
1655
-
1656
- const loginPageWellKnown = wellKnown({
1657
- remoteConfig: { url: "https://config.example.com/mervex.json" },
1658
- remoteHtml: "<!DOCTYPE html><html><head><title>Sign in</title></head><body>Login required</body></html>",
1659
- })
1660
-
1661
- loginPageWellKnown.it.instance(
1662
- "wellknown remote_config surfaces an actionable auth error when the gateway returns an HTML login page",
1663
- () =>
1664
- Effect.gen(function* () {
1665
- const exit = yield* Config.use.get().pipe(Effect.exit)
1666
- expect(loginPageWellKnown.seen.remote).toBe("https://config.example.com/mervex.json")
1667
- expect(Exit.isFailure(exit)).toBe(true)
1668
- const error = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined
1669
- expect(NamedError.hasName(error, "ConfigRemoteAuthError")).toBe(true)
1670
- expect((error as { data?: { url?: string } }).data?.url).toBe("https://example.com")
1671
- }),
1672
- )
1673
-
1674
- describe("resolvePluginSpec", () => {
1675
- test("keeps package specs unchanged", async () => {
1676
- await using tmp = await tmpdir()
1677
- const file = path.join(tmp.path, "mervex.json")
1678
- expect(await ConfigPlugin.resolvePluginSpec("oh-my-mervex@2.4.3", file)).toBe("oh-my-mervex@2.4.3")
1679
- expect(await ConfigPlugin.resolvePluginSpec("@scope/pkg", file)).toBe("@scope/pkg")
1680
- })
1681
-
1682
- test("resolves windows-style relative plugin directory specs", async () => {
1683
- if (process.platform !== "win32") return
1684
-
1685
- await using tmp = await tmpdir({
1686
- init: async (dir) => {
1687
- const plugin = path.join(dir, "plugin")
1688
- await fs.mkdir(plugin, { recursive: true })
1689
- await Filesystem.write(path.join(plugin, "index.ts"), "export default {}")
1690
- },
1691
- })
1692
-
1693
- const file = path.join(tmp.path, "mervex.json")
1694
- const hit = await ConfigPlugin.resolvePluginSpec(".\\plugin", file)
1695
- expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href)
1696
- })
1697
-
1698
- test("resolves relative file plugin paths to file urls", async () => {
1699
- await using tmp = await tmpdir({
1700
- init: async (dir) => {
1701
- await Filesystem.write(path.join(dir, "plugin.ts"), "export default {}")
1702
- },
1703
- })
1704
-
1705
- const file = path.join(tmp.path, "mervex.json")
1706
- const hit = await ConfigPlugin.resolvePluginSpec("./plugin.ts", file)
1707
- expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin.ts")).href)
1708
- })
1709
-
1710
- test("resolves plugin directory paths to directory urls", async () => {
1711
- await using tmp = await tmpdir({
1712
- init: async (dir) => {
1713
- const plugin = path.join(dir, "plugin")
1714
- await fs.mkdir(plugin, { recursive: true })
1715
- await Filesystem.writeJson(path.join(plugin, "package.json"), {
1716
- name: "demo-plugin",
1717
- type: "module",
1718
- main: "./index.ts",
1719
- })
1720
- await Filesystem.write(path.join(plugin, "index.ts"), "export default {}")
1721
- },
1722
- })
1723
-
1724
- const file = path.join(tmp.path, "mervex.json")
1725
- const hit = await ConfigPlugin.resolvePluginSpec("./plugin", file)
1726
- expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin")).href)
1727
- })
1728
-
1729
- test("resolves plugin directories without package.json to index.ts", async () => {
1730
- await using tmp = await tmpdir({
1731
- init: async (dir) => {
1732
- const plugin = path.join(dir, "plugin")
1733
- await fs.mkdir(plugin, { recursive: true })
1734
- await Filesystem.write(path.join(plugin, "index.ts"), "export default {}")
1735
- },
1736
- })
1737
-
1738
- const file = path.join(tmp.path, "mervex.json")
1739
- const hit = await ConfigPlugin.resolvePluginSpec("./plugin", file)
1740
- expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href)
1741
- })
1742
- })
1743
-
1744
- describe("deduplicatePluginOrigins", () => {
1745
- const dedupe = (plugins: ConfigPluginV1.Spec[]) =>
1746
- ConfigPlugin.deduplicatePluginOrigins(
1747
- plugins.map((spec) => ({
1748
- spec,
1749
- source: "",
1750
- scope: "global" as const,
1751
- })),
1752
- ).map((item) => item.spec)
1753
-
1754
- test("removes duplicates keeping higher priority (later entries)", () => {
1755
- const plugins = ["global-plugin@1.0.0", "shared-plugin@1.0.0", "local-plugin@2.0.0", "shared-plugin@2.0.0"]
1756
-
1757
- const result = dedupe(plugins)
1758
-
1759
- expect(result).toContain("global-plugin@1.0.0")
1760
- expect(result).toContain("local-plugin@2.0.0")
1761
- expect(result).toContain("shared-plugin@2.0.0")
1762
- expect(result).not.toContain("shared-plugin@1.0.0")
1763
- expect(result.length).toBe(3)
1764
- })
1765
-
1766
- test("keeps path plugins separate from package plugins", () => {
1767
- const plugins = ["oh-my-mervex@2.4.3", "file:///project/.mervex/plugin/oh-my-mervex.js"]
1768
-
1769
- const result = dedupe(plugins)
1770
-
1771
- expect(result).toEqual(plugins)
1772
- })
1773
-
1774
- test("deduplicates direct path plugins by exact spec", () => {
1775
- const plugins = ["file:///project/.mervex/plugin/demo.ts", "file:///project/.mervex/plugin/demo.ts"]
1776
-
1777
- const result = dedupe(plugins)
1778
-
1779
- expect(result).toEqual(["file:///project/.mervex/plugin/demo.ts"])
1780
- })
1781
-
1782
- test("preserves order of remaining plugins", () => {
1783
- const plugins = ["a-plugin@1.0.0", "b-plugin@1.0.0", "c-plugin@1.0.0"]
1784
-
1785
- const result = dedupe(plugins)
1786
-
1787
- expect(result).toEqual(["a-plugin@1.0.0", "b-plugin@1.0.0", "c-plugin@1.0.0"])
1788
- })
1789
-
1790
- it.effect("loads auto-discovered local plugins as file urls", () =>
1791
- withConfigTree(
1792
- { global: { plugin: ["my-plugin@1.0.0"] } },
1793
- Effect.gen(function* () {
1794
- const test = yield* TestInstance
1795
- yield* FSUtil.use.writeWithDirs(
1796
- path.join(test.directory, ".mervex", "plugin", "my-plugin.js"),
1797
- "export default {}",
1798
- )
1799
-
1800
- const plugins = (yield* Config.use.get()).plugin ?? []
1801
- expect(plugins.some((p) => ConfigPlugin.pluginSpecifier(p) === "my-plugin@1.0.0")).toBe(true)
1802
- expect(plugins.some((p) => ConfigPlugin.pluginSpecifier(p).startsWith("file://"))).toBe(true)
1803
- }),
1804
- ),
1805
- )
1806
- })
1807
-
1808
- describe("MERVEX_DISABLE_PROJECT_CONFIG", () => {
1809
- it.instance(
1810
- "skips project config files when flag is set",
1811
- () =>
1812
- withProcessEnv(
1813
- "MERVEX_DISABLE_PROJECT_CONFIG",
1814
- "true",
1815
- Effect.gen(function* () {
1816
- const config = yield* Config.use.get()
1817
- expect(config.model).not.toBe("project/model")
1818
- expect(config.username).not.toBe("project-user")
1819
- }),
1820
- ),
1821
- { config: { model: "project/model", username: "project-user" } },
1822
- )
1823
-
1824
- it.instance("skips project .mervex/ directories when flag is set", () =>
1825
- withProcessEnv(
1826
- "MERVEX_DISABLE_PROJECT_CONFIG",
1827
- "true",
1828
- Effect.gen(function* () {
1829
- const test = yield* TestInstance
1830
- yield* FSUtil.use.writeWithDirs(
1831
- path.join(test.directory, ".mervex", "command", "test-cmd.md"),
1832
- "# Test Command\nThis is a test command.",
1833
- )
1834
- const directories = yield* Config.use.directories()
1835
- expect(directories.some((d) => d.startsWith(test.directory))).toBe(false)
1836
- }),
1837
- ),
1838
- )
1839
-
1840
- it.instance("still loads global config when flag is set", () =>
1841
- withProcessEnv(
1842
- "MERVEX_DISABLE_PROJECT_CONFIG",
1843
- "true",
1844
- Effect.gen(function* () {
1845
- const config = yield* Config.use.get()
1846
- expect(config).toBeDefined()
1847
- expect(config.username).toBeDefined()
1848
- }),
1849
- ),
1850
- )
1851
-
1852
- it.instance(
1853
- "skips relative instructions with warning when flag is set but no config dir",
1854
- () =>
1855
- withProcessEnvs(
1856
- { MERVEX_CONFIG_DIR: undefined, MERVEX_DISABLE_PROJECT_CONFIG: "true" },
1857
- Effect.gen(function* () {
1858
- const test = yield* TestInstance
1859
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "CUSTOM.md"), "# Custom Instructions")
1860
- // The relative instruction should be skipped without error
1861
- const config = yield* Config.use.get()
1862
- expect(config).toBeDefined()
1863
- }),
1864
- ),
1865
- { config: { instructions: ["./CUSTOM.md"] } },
1866
- )
1867
-
1868
- it.instance(
1869
- "MERVEX_CONFIG_DIR still works when flag is set",
1870
- () =>
1871
- Effect.gen(function* () {
1872
- const configDir = yield* tmpdirScoped({ config: { model: "configdir/model" } })
1873
- yield* withProcessEnvs(
1874
- { MERVEX_DISABLE_PROJECT_CONFIG: "true", MERVEX_CONFIG_DIR: configDir },
1875
- Effect.gen(function* () {
1876
- const config = yield* Config.use.get()
1877
- expect(config.model).toBe("configdir/model")
1878
- }),
1879
- )
1880
- }),
1881
- { config: { model: "project/model" } },
1882
- )
1883
- })
1884
-
1885
- // Regression for #28206: malformed MERVEX_PERMISSION JSON used to crash
1886
- // the app on startup with an unhandled SyntaxError. Loading the config with
1887
- // an invalid JSON value in this env var should not throw.
1888
- describe("MERVEX_PERMISSION env var", () => {
1889
- it.instance("does not crash when MERVEX_PERMISSION contains invalid JSON", () =>
1890
- withProcessEnv(
1891
- "MERVEX_PERMISSION",
1892
- "{invalid",
1893
- Effect.gen(function* () {
1894
- const config = yield* Config.use.get()
1895
- // Regression: load() used to throw before returning anything.
1896
- expect(config).toBeDefined()
1897
- }),
1898
- ),
1899
- )
1900
- })
1901
-
1902
- describe("MERVEX_CONFIG_CONTENT token substitution", () => {
1903
- it.instance("substitutes {env:} tokens in MERVEX_CONFIG_CONTENT", () =>
1904
- withProcessEnv(
1905
- "TEST_CONFIG_VAR",
1906
- "test_api_key_12345",
1907
- withProcessEnv(
1908
- "MERVEX_CONFIG_CONTENT",
1909
- JSON.stringify({
1910
- $schema: "https://mervex.ai/config.json",
1911
- username: "{env:TEST_CONFIG_VAR}",
1912
- }),
1913
- Effect.gen(function* () {
1914
- const config = yield* Config.use.get()
1915
- expect(config.username).toBe("test_api_key_12345")
1916
- }),
1917
- ),
1918
- ),
1919
- )
1920
-
1921
- it.instance("substitutes {file:} tokens in MERVEX_CONFIG_CONTENT", () =>
1922
- Effect.gen(function* () {
1923
- const test = yield* TestInstance
1924
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "api_key.txt"), "secret_key_from_file")
1925
- yield* withProcessEnv(
1926
- "MERVEX_CONFIG_CONTENT",
1927
- JSON.stringify({
1928
- $schema: "https://mervex.ai/config.json",
1929
- username: "{file:./api_key.txt}",
1930
- }),
1931
- Effect.gen(function* () {
1932
- const config = yield* Config.use.get()
1933
- expect(config.username).toBe("secret_key_from_file")
1934
- }),
1935
- )
1936
- }),
1937
- )
1938
- })
1939
-
1940
- // parseManagedPlist unit tests — pure function, no OS interaction
1941
-
1942
- test("parseManagedPlist strips MDM metadata keys", async () => {
1943
- const config = ConfigParse.schema(
1944
- ConfigV1.Info,
1945
- ConfigParse.jsonc(
1946
- await ConfigManaged.parseManagedPlist(
1947
- JSON.stringify({
1948
- PayloadDisplayName: "Mervex Managed",
1949
- PayloadIdentifier: "ai.mervex.managed.test",
1950
- PayloadType: "ai.mervex.managed",
1951
- PayloadUUID: "AAAA-BBBB-CCCC",
1952
- PayloadVersion: 1,
1953
- _manualProfile: true,
1954
- share: "disabled",
1955
- model: "mdm/model",
1956
- }),
1957
- ),
1958
- "test:mobileconfig",
1959
- ),
1960
- "test:mobileconfig",
1961
- )
1962
- expect(config.share).toBe("disabled")
1963
- expect(config.model).toBe("mdm/model")
1964
- // MDM keys must not leak into the parsed config
1965
- expect((config as any).PayloadUUID).toBeUndefined()
1966
- expect((config as any).PayloadType).toBeUndefined()
1967
- expect((config as any)._manualProfile).toBeUndefined()
1968
- })
1969
-
1970
- test("parseManagedPlist parses server settings", async () => {
1971
- const config = ConfigParse.schema(
1972
- ConfigV1.Info,
1973
- ConfigParse.jsonc(
1974
- await ConfigManaged.parseManagedPlist(
1975
- JSON.stringify({
1976
- $schema: "https://mervex.ai/config.json",
1977
- server: { hostname: "127.0.0.1", mdns: false },
1978
- autoupdate: true,
1979
- }),
1980
- ),
1981
- "test:mobileconfig",
1982
- ),
1983
- "test:mobileconfig",
1984
- )
1985
- expect(config.server?.hostname).toBe("127.0.0.1")
1986
- expect(config.server?.mdns).toBe(false)
1987
- expect(config.autoupdate).toBe(true)
1988
- })
1989
-
1990
- test("parseManagedPlist parses permission rules", async () => {
1991
- const config = ConfigParse.schema(
1992
- ConfigV1.Info,
1993
- ConfigParse.jsonc(
1994
- await ConfigManaged.parseManagedPlist(
1995
- JSON.stringify({
1996
- $schema: "https://mervex.ai/config.json",
1997
- permission: {
1998
- "*": "ask",
1999
- bash: { "*": "ask", "rm -rf *": "deny", "curl *": "deny" },
2000
- grep: "allow",
2001
- glob: "allow",
2002
- webfetch: "ask",
2003
- "~/.ssh/*": "deny",
2004
- },
2005
- }),
2006
- ),
2007
- "test:mobileconfig",
2008
- ),
2009
- "test:mobileconfig",
2010
- )
2011
- expect(config.permission?.["*"]).toBe("ask")
2012
- expect(config.permission?.grep).toBe("allow")
2013
- expect(config.permission?.webfetch).toBe("ask")
2014
- expect(config.permission?.["~/.ssh/*"]).toBe("deny")
2015
- const bash = config.permission?.bash as Record<string, string>
2016
- expect(bash?.["rm -rf *"]).toBe("deny")
2017
- expect(bash?.["curl *"]).toBe("deny")
2018
- })
2019
-
2020
- test("parseManagedPlist parses enabled_providers", async () => {
2021
- const config = ConfigParse.schema(
2022
- ConfigV1.Info,
2023
- ConfigParse.jsonc(
2024
- await ConfigManaged.parseManagedPlist(
2025
- JSON.stringify({
2026
- $schema: "https://mervex.ai/config.json",
2027
- enabled_providers: ["anthropic", "google"],
2028
- }),
2029
- ),
2030
- "test:mobileconfig",
2031
- ),
2032
- "test:mobileconfig",
2033
- )
2034
- expect(config.enabled_providers).toEqual(["anthropic", "google"])
2035
- })
2036
-
2037
- test("parseManagedPlist handles empty config", async () => {
2038
- const config = ConfigParse.schema(
2039
- ConfigV1.Info,
2040
- ConfigParse.jsonc(
2041
- await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://mervex.ai/config.json" })),
2042
- "test:mobileconfig",
2043
- ),
2044
- "test:mobileconfig",
2045
- )
2046
- expect(config.$schema).toBe("https://mervex.ai/config.json")
2047
- })