saeeol 1.3.0 → 1.3.1

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 (537) hide show
  1. package/.turbo/turbo-typecheck.log +1 -0
  2. package/AGENTS.md +72 -0
  3. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  4. package/Dockerfile +18 -0
  5. package/assets/saeeol.ico +0 -0
  6. package/bin/saeeol.cjs +0 -0
  7. package/database.db +0 -0
  8. package/drizzle.config.ts +10 -0
  9. package/git +0 -0
  10. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  11. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  12. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  13. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  14. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  15. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  16. package/migration/20260225215848_workspace/migration.sql +7 -0
  17. package/migration/20260225215848_workspace/snapshot.json +959 -0
  18. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  19. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  20. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  21. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  22. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  23. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  24. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  25. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  26. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  27. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  28. package/migration/20260323234822_events/migration.sql +13 -0
  29. package/migration/20260323234822_events/snapshot.json +1271 -0
  30. package/migration/20260410174513_workspace-name/migration.sql +16 -0
  31. package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
  32. package/migration/20260413175956_chief_energizer/migration.sql +13 -0
  33. package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
  34. package/migration/20260423070820_add_icon_url_override/migration.sql +2 -0
  35. package/migration/20260423070820_add_icon_url_override/snapshot.json +1409 -0
  36. package/migration/20260428004200_add_session_path/migration.sql +1 -0
  37. package/migration/20260428004200_add_session_path/snapshot.json +1419 -0
  38. package/npm/bin/saeeol +42 -0
  39. package/npm/package.json +39 -0
  40. package/npm/postinstall.js +162 -0
  41. package/package.json +201 -207
  42. package/parsers-config.ts +289 -0
  43. package/script/build.ts +393 -0
  44. package/script/check-migrations.ts +16 -0
  45. package/script/fix-node-pty.ts +34 -0
  46. package/script/generate.ts +23 -0
  47. package/script/postinstall.mjs +189 -0
  48. package/script/publish.ts +200 -0
  49. package/script/run-workspace-server +106 -0
  50. package/script/schema.ts +63 -0
  51. package/script/test-runner.ts +420 -0
  52. package/script/time.ts +6 -0
  53. package/script/trace-imports.ts +153 -0
  54. package/script/upgrade-opentui.ts +64 -0
  55. package/scripts/diff-sdk-types.sh +52 -0
  56. package/specs/effect/facades.md +221 -0
  57. package/specs/effect/http-api.md +401 -0
  58. package/specs/effect/instance-context.md +309 -0
  59. package/specs/effect/loose-ends.md +34 -0
  60. package/specs/effect/migration.md +299 -0
  61. package/specs/effect/routes.md +64 -0
  62. package/specs/effect/schema.md +399 -0
  63. package/specs/effect/server-package.md +668 -0
  64. package/specs/effect/tools.md +90 -0
  65. package/specs/tui-plugins.md +433 -0
  66. package/specs/v2/api.ts +67 -0
  67. package/specs/v2/keymappings.md +10 -0
  68. package/specs/v2/message-shape.md +136 -0
  69. package/src/acp/agent-message.ts +1 -1
  70. package/src/acp/agent-utils.ts +1 -1
  71. package/src/boxes/ansi.ts +17 -0
  72. package/src/boxes/atomic-write.ts +35 -0
  73. package/src/boxes/b64.ts +58 -0
  74. package/src/boxes/bash-security.ts +129 -0
  75. package/src/boxes/bom.ts +18 -0
  76. package/src/boxes/cancel.ts +16 -0
  77. package/src/boxes/chop.ts +12 -0
  78. package/src/boxes/clamp.ts +3 -0
  79. package/src/boxes/compact.ts +9 -0
  80. package/src/boxes/cost-tracker.ts +116 -0
  81. package/src/boxes/dataurl.ts +29 -0
  82. package/src/boxes/delay.ts +27 -0
  83. package/src/boxes/diff-apply.ts +53 -0
  84. package/src/boxes/disposable.ts +13 -0
  85. package/src/boxes/err.ts +34 -0
  86. package/src/boxes/human.ts +47 -0
  87. package/src/boxes/iife.ts +9 -0
  88. package/src/boxes/latch.ts +8 -0
  89. package/src/boxes/memory.ts +198 -0
  90. package/src/boxes/net.ts +16 -0
  91. package/src/boxes/plural.ts +4 -0
  92. package/src/boxes/puny.ts +21 -0
  93. package/src/boxes/retry.ts +49 -0
  94. package/src/boxes/rwlock.ts +41 -0
  95. package/src/boxes/schedule.ts +71 -0
  96. package/src/boxes/scope.ts +21 -0
  97. package/src/boxes/tokens.ts +9 -0
  98. package/src/boxes/ttl-cache.ts +63 -0
  99. package/src/boxes/typed-event.ts +51 -0
  100. package/src/boxes/uid.ts +50 -0
  101. package/src/boxes/wave6.test.ts +296 -0
  102. package/src/boxes/wildcard.ts +58 -0
  103. package/src/bus/global.ts +1 -1
  104. package/src/cli/cmd/github-run-api.ts +2 -2
  105. package/src/cli/cmd/run-events.ts +2 -2
  106. package/src/cli/cmd/tui/component/logo.tsx +1 -1
  107. package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +2 -2
  108. package/src/cli/cmd/tui/context/app/editor-zed.ts +1 -1
  109. package/src/cli/cmd/tui/context/app/editor.ts +1 -1
  110. package/src/cli/cmd/tui/context/app/theme.tsx +1 -1
  111. package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
  112. package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
  113. package/src/overlay/cost-tracker/format.ts +1 -1
  114. package/src/overlay/cost-tracker/index.ts +4 -4
  115. package/src/overlay/cost-tracker/state.ts +2 -2
  116. package/src/overlay/cost-tracker/types.ts +2 -2
  117. package/src/overlay/memory/age.ts +1 -1
  118. package/src/overlay/memory/index.ts +4 -4
  119. package/src/overlay/memory/paths.ts +2 -2
  120. package/src/overlay/memory/scan.ts +1 -1
  121. package/src/overlay/memory/types.ts +2 -2
  122. package/src/overlay/tool/bash-security.ts +3 -3
  123. package/src/overlay/util/url.ts +1 -1
  124. package/src/plugin/codex-auth.ts +1 -1
  125. package/src/provider/model-cache.ts +2 -2
  126. package/src/provider/provider-resolve.ts +3 -3
  127. package/src/provider/transform-message.ts +1 -1
  128. package/src/server/routes/game.ts +284 -0
  129. package/src/server/server.ts +2 -0
  130. package/src/session/core/compaction/compaction-helpers.ts +1 -1
  131. package/src/session/core/compaction/compaction.ts +1 -1
  132. package/src/session/core/session.ts +2 -0
  133. package/src/sessions/ingest-queue.ts +2 -2
  134. package/src/sessions/remote-ws.ts +1 -1
  135. package/src/tool/workflow/question.ts +1 -1
  136. package/src/util/abort.ts +1 -1
  137. package/src/util/bom.ts +2 -2
  138. package/src/util/color.ts +1 -1
  139. package/src/util/data-url.ts +1 -1
  140. package/src/util/defer.ts +1 -1
  141. package/src/util/error.ts +2 -2
  142. package/src/util/filesystem.ts +2 -2
  143. package/src/util/format.ts +1 -1
  144. package/src/util/iife.ts +1 -1
  145. package/src/util/local-context.ts +1 -1
  146. package/src/util/locale.ts +2 -2
  147. package/src/util/lock.ts +1 -1
  148. package/src/util/network.ts +1 -1
  149. package/src/util/signal.ts +1 -1
  150. package/src/util/token.ts +1 -1
  151. package/src/util/wildcard.ts +1 -1
  152. package/sst-env.d.ts +10 -0
  153. package/test/AGENTS.md +133 -0
  154. package/test/account/repo.test.ts +352 -0
  155. package/test/account/service.test.ts +456 -0
  156. package/test/acp/agent-interface.test.ts +51 -0
  157. package/test/acp/event-subscription.test.ts +725 -0
  158. package/test/agent/agent.test.ts +890 -0
  159. package/test/auth/auth.test.ts +86 -0
  160. package/test/bun/registry.test.ts +75 -0
  161. package/test/bus/bus-effect.test.ts +161 -0
  162. package/test/bus/bus-integration.test.ts +87 -0
  163. package/test/bus/bus.test.ts +219 -0
  164. package/test/cli/account.test.ts +26 -0
  165. package/test/cli/auto-mode.test.ts +75 -0
  166. package/test/cli/bin-saeeol.test.ts +8 -0
  167. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  168. package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
  169. package/test/cli/cmd/tui/sync.test.tsx +159 -0
  170. package/test/cli/error.test.ts +18 -0
  171. package/test/cli/github-action.test.ts +198 -0
  172. package/test/cli/github-remote.test.ts +85 -0
  173. package/test/cli/import.test.ts +97 -0
  174. package/test/cli/install-artifact.test.ts +72 -0
  175. package/test/cli/plugin-auth-picker.test.ts +120 -0
  176. package/test/cli/pr.test.ts +59 -0
  177. package/test/cli/tui/editor-context-zed.test.ts +356 -0
  178. package/test/cli/tui/editor-context.test.tsx +228 -0
  179. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  180. package/test/cli/tui/markdown.test.ts +161 -0
  181. package/test/cli/tui/plugin-add.test.ts +111 -0
  182. package/test/cli/tui/plugin-install.test.ts +87 -0
  183. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  184. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  185. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  186. package/test/cli/tui/plugin-loader.test.ts +816 -0
  187. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  188. package/test/cli/tui/revert-diff.test.ts +35 -0
  189. package/test/cli/tui/slot-replace.test.tsx +47 -0
  190. package/test/cli/tui/theme-store.test.ts +54 -0
  191. package/test/cli/tui/thread.test.ts +28 -0
  192. package/test/cli/tui/transcript.test.ts +426 -0
  193. package/test/cli/tui/usage.test.ts +60 -0
  194. package/test/cli/tui/use-event.test.tsx +175 -0
  195. package/test/config/agent-color.test.ts +67 -0
  196. package/test/config/config.test.ts +2544 -0
  197. package/test/config/fixtures/empty-frontmatter.md +4 -0
  198. package/test/config/fixtures/frontmatter.md +28 -0
  199. package/test/config/fixtures/markdown-header.md +11 -0
  200. package/test/config/fixtures/no-frontmatter.md +1 -0
  201. package/test/config/fixtures/weird-model-id.md +13 -0
  202. package/test/config/lsp.test.ts +87 -0
  203. package/test/config/markdown.test.ts +228 -0
  204. package/test/config/plugin.test.ts +0 -0
  205. package/test/config/tui.test.ts +624 -0
  206. package/test/control-plane/adapters.test.ts +71 -0
  207. package/test/control-plane/workspace.test.ts +1526 -0
  208. package/test/effect/app-runtime-logger.test.ts +98 -0
  209. package/test/effect/config-service.test.ts +65 -0
  210. package/test/effect/instance-state.test.ts +394 -0
  211. package/test/effect/run-service.test.ts +89 -0
  212. package/test/effect/runner.test.ts +523 -0
  213. package/test/fake/provider.ts +82 -0
  214. package/test/file/fsmonitor.test.ts +68 -0
  215. package/test/file/ignore.test.ts +10 -0
  216. package/test/file/index.test.ts +954 -0
  217. package/test/file/path-traversal.test.ts +205 -0
  218. package/test/file/ripgrep.test.ts +226 -0
  219. package/test/file/watcher.test.ts +249 -0
  220. package/test/filesystem/filesystem.test.ts +319 -0
  221. package/test/fixture/db.ts +11 -0
  222. package/test/fixture/fixture.test.ts +26 -0
  223. package/test/fixture/fixture.ts +175 -0
  224. package/test/fixture/flock-worker.ts +72 -0
  225. package/test/fixture/log-init-worker.ts +62 -0
  226. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  227. package/test/fixture/plug-worker.ts +93 -0
  228. package/test/fixture/plugin-meta-worker.ts +19 -0
  229. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  230. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  231. package/test/fixture/skills/index.json +6 -0
  232. package/test/fixture/tui-plugin.ts +323 -0
  233. package/test/fixture/tui-runtime.ts +31 -0
  234. package/test/format/format.test.ts +272 -0
  235. package/test/git/git.test.ts +128 -0
  236. package/test/ide/ide.test.ts +82 -0
  237. package/test/installation/installation.test.ts +168 -0
  238. package/test/keybind.test.ts +421 -0
  239. package/test/lib/effect.ts +53 -0
  240. package/test/lib/filesystem.ts +10 -0
  241. package/test/lib/llm-server.ts +778 -0
  242. package/test/lib/websocket.ts +46 -0
  243. package/test/lsp/client.test.ts +482 -0
  244. package/test/lsp/index.test.ts +160 -0
  245. package/test/lsp/launch.test.ts +22 -0
  246. package/test/lsp/lifecycle.test.ts +184 -0
  247. package/test/ltm/ltm.test.ts +230 -0
  248. package/test/mcp/headers.test.ts +178 -0
  249. package/test/mcp/lifecycle.test.ts +787 -0
  250. package/test/mcp/oauth-auto-connect.test.ts +311 -0
  251. package/test/mcp/oauth-browser.test.ts +276 -0
  252. package/test/mcp/oauth-callback.test.ts +34 -0
  253. package/test/memory/abort-leak-webfetch.ts +49 -0
  254. package/test/memory/abort-leak.test.ts +128 -0
  255. package/test/patch/patch.test.ts +348 -0
  256. package/test/permission/arity.test.ts +33 -0
  257. package/test/permission/next.test.ts +1227 -0
  258. package/test/permission/next.toConfig.test.ts +110 -0
  259. package/test/permission-task.test.ts +326 -0
  260. package/test/plugin/auth-override.test.ts +79 -0
  261. package/test/plugin/cloudflare.test.ts +68 -0
  262. package/test/plugin/codex.test.ts +123 -0
  263. package/test/plugin/github-copilot-models.test.ts +261 -0
  264. package/test/plugin/install-concurrency.test.ts +140 -0
  265. package/test/plugin/install.test.ts +570 -0
  266. package/test/plugin/loader-shared.test.ts +1169 -0
  267. package/test/plugin/meta.test.ts +137 -0
  268. package/test/plugin/plugin-contract.test.ts +291 -0
  269. package/test/plugin/shared.test.ts +88 -0
  270. package/test/plugin/trigger.test.ts +102 -0
  271. package/test/plugin/workspace-adapter.test.ts +109 -0
  272. package/test/preload.ts +77 -0
  273. package/test/project/instance.test.ts +276 -0
  274. package/test/project/migrate-global.test.ts +152 -0
  275. package/test/project/project.test.ts +600 -0
  276. package/test/project/vcs.test.ts +286 -0
  277. package/test/project/worktree-remove.test.ts +126 -0
  278. package/test/project/worktree.test.ts +223 -0
  279. package/test/provider/amazon-bedrock.test.ts +462 -0
  280. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  281. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  282. package/test/provider/gitlab-duo.test.ts +413 -0
  283. package/test/provider/local.test.ts +208 -0
  284. package/test/provider/models.test.ts +261 -0
  285. package/test/provider/provider-category.test.ts +190 -0
  286. package/test/provider/provider.test.ts +2758 -0
  287. package/test/provider/transform.test.ts +3681 -0
  288. package/test/pty/pty-output-isolation.test.ts +147 -0
  289. package/test/pty/pty-session.test.ts +102 -0
  290. package/test/pty/pty-shell.test.ts +104 -0
  291. package/test/question/question.test.ts +490 -0
  292. package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
  293. package/test/saeeol/agent-manager-tool.test.ts +71 -0
  294. package/test/saeeol/agent-permission-overrides.test.ts +75 -0
  295. package/test/saeeol/agent-skill-permissions.test.ts +37 -0
  296. package/test/saeeol/ask-agent-permissions.test.ts +303 -0
  297. package/test/saeeol/bash-hierarchy.test.ts +64 -0
  298. package/test/saeeol/bash-permission-metadata.test.ts +66 -0
  299. package/test/saeeol/bash-security-extended.test.ts +243 -0
  300. package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
  301. package/test/saeeol/boxes-integration.test.ts +415 -0
  302. package/test/saeeol/builtin-skills.test.ts +75 -0
  303. package/test/saeeol/cleanup.ts +28 -0
  304. package/test/saeeol/cli/dev-setup.test.ts +74 -0
  305. package/test/saeeol/cli/roll-call.test.ts +161 -0
  306. package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
  307. package/test/saeeol/codex-auth-refresh.test.ts +124 -0
  308. package/test/saeeol/commit-message/generate.test.ts +188 -0
  309. package/test/saeeol/commit-message/git-context.test.ts +303 -0
  310. package/test/saeeol/commit-message-windows.test.ts +38 -0
  311. package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
  312. package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
  313. package/test/saeeol/compaction-skip-guard.test.ts +224 -0
  314. package/test/saeeol/compaction-smart-select.test.ts +100 -0
  315. package/test/saeeol/config/config.test.ts +166 -0
  316. package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
  317. package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
  318. package/test/saeeol/config-gitignore.test.ts +70 -0
  319. package/test/saeeol/config-injector.test.ts +305 -0
  320. package/test/saeeol/config-resilience.test.ts +234 -0
  321. package/test/saeeol/config-validation.test.ts +183 -0
  322. package/test/saeeol/cost-propagation.test.ts +94 -0
  323. package/test/saeeol/cost-tracker-extended.test.ts +141 -0
  324. package/test/saeeol/cost-tracker.test.ts +64 -0
  325. package/test/saeeol/custom-provider-delete.test.ts +149 -0
  326. package/test/saeeol/diff-full.test.ts +226 -0
  327. package/test/saeeol/edit-permission-filediff.test.ts +223 -0
  328. package/test/saeeol/encoding.test.ts +364 -0
  329. package/test/saeeol/enhance-prompt.test.ts +61 -0
  330. package/test/saeeol/ensure-plan-dir.test.ts +32 -0
  331. package/test/saeeol/errors.test.ts +144 -0
  332. package/test/saeeol/external-directory-boundary.test.ts +96 -0
  333. package/test/saeeol/gateway-headers.test.ts +88 -0
  334. package/test/saeeol/help.test.ts +191 -0
  335. package/test/saeeol/ignore-migrator.test.ts +308 -0
  336. package/test/saeeol/indexing-auth.test.ts +45 -0
  337. package/test/saeeol/indexing-feature.test.ts +44 -0
  338. package/test/saeeol/indexing-label.test.ts +70 -0
  339. package/test/saeeol/indexing-startup.test.ts +381 -0
  340. package/test/saeeol/indexing-worktree.test.ts +73 -0
  341. package/test/saeeol/instruction.test.ts +136 -0
  342. package/test/saeeol/lancedb-runtime.test.ts +116 -0
  343. package/test/saeeol/loader-auth.test.ts +168 -0
  344. package/test/saeeol/local-model.test.ts +621 -0
  345. package/test/saeeol/logo.test.ts +31 -0
  346. package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
  347. package/test/saeeol/mcp-branding.test.ts +33 -0
  348. package/test/saeeol/mcp-docker-rm.test.ts +32 -0
  349. package/test/saeeol/mcp-migrator.test.ts +736 -0
  350. package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
  351. package/test/saeeol/memory-io.test.ts +198 -0
  352. package/test/saeeol/memory-paths.test.ts +87 -0
  353. package/test/saeeol/memory-security.test.ts +166 -0
  354. package/test/saeeol/model-cache-org.test.ts +164 -0
  355. package/test/saeeol/model-info-panel-utils.test.ts +52 -0
  356. package/test/saeeol/model-info-panel.types.test.ts +7 -0
  357. package/test/saeeol/models-401-fallback.test.ts +52 -0
  358. package/test/saeeol/modes-migrator.test.ts +320 -0
  359. package/test/saeeol/nvidia-headers.test.ts +74 -0
  360. package/test/saeeol/patch-jsonc.test.ts +73 -0
  361. package/test/saeeol/patch.test.ts +172 -0
  362. package/test/saeeol/paths.test.ts +265 -0
  363. package/test/saeeol/permission/config-paths.test.ts +174 -0
  364. package/test/saeeol/permission/env-read.test.ts +149 -0
  365. package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
  366. package/test/saeeol/permission/next.always-rules.test.ts +882 -0
  367. package/test/saeeol/permission/next.reply-http.test.ts +205 -0
  368. package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
  369. package/test/saeeol/plan-exit-detection.test.ts +494 -0
  370. package/test/saeeol/plan-followup.test.ts +1376 -0
  371. package/test/saeeol/project-config-update.test.ts +120 -0
  372. package/test/saeeol/project-id.test.ts +455 -0
  373. package/test/saeeol/provider-cost.test.ts +171 -0
  374. package/test/saeeol/provider-list-failed-state.test.ts +100 -0
  375. package/test/saeeol/question-dismiss-all.test.ts +174 -0
  376. package/test/saeeol/read-directory.test.ts +116 -0
  377. package/test/saeeol/rules-migrator.test.ts +257 -0
  378. package/test/saeeol/run-auto.test.ts +176 -0
  379. package/test/saeeol/run-network.test.ts +224 -0
  380. package/test/saeeol/semantic-search.test.ts +186 -0
  381. package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
  382. package/test/saeeol/session/instruction-substitution.test.ts +72 -0
  383. package/test/saeeol/session/platform-attribution.test.ts +118 -0
  384. package/test/saeeol/session/session.test.ts +105 -0
  385. package/test/saeeol/session-compaction-cap.test.ts +399 -0
  386. package/test/saeeol/session-compaction-chunks.test.ts +501 -0
  387. package/test/saeeol/session-compaction-safety.test.ts +481 -0
  388. package/test/saeeol/session-fork-remap.test.ts +251 -0
  389. package/test/saeeol/session-import-service.test.ts +114 -0
  390. package/test/saeeol/session-list.test.ts +47 -0
  391. package/test/saeeol/session-message-metadata.test.ts +128 -0
  392. package/test/saeeol/session-overflow.test.ts +78 -0
  393. package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
  394. package/test/saeeol/session-processor-network-offline.test.ts +204 -0
  395. package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
  396. package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
  397. package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
  398. package/test/saeeol/session-prompt-queue.test.ts +815 -0
  399. package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
  400. package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
  401. package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
  402. package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
  403. package/test/saeeol/sessions/remote-ws.test.ts +367 -0
  404. package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
  405. package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
  406. package/test/saeeol/snapshot-cache.test.ts +84 -0
  407. package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
  408. package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
  409. package/test/saeeol/stats-subagent-cost.test.ts +123 -0
  410. package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
  411. package/test/saeeol/suggestion/suggestion.test.ts +145 -0
  412. package/test/saeeol/suggestion/tool.test.ts +298 -0
  413. package/test/saeeol/summary-file-diff.test.ts +28 -0
  414. package/test/saeeol/system-prompt.test.ts +142 -0
  415. package/test/saeeol/task-nesting.test.ts +193 -0
  416. package/test/saeeol/telemetry/feedback.test.ts +8 -0
  417. package/test/saeeol/todo-view.test.ts +57 -0
  418. package/test/saeeol/tool-encoding.test.ts +455 -0
  419. package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
  420. package/test/saeeol/tool-registry-indexing.test.ts +236 -0
  421. package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
  422. package/test/saeeol/tool-task-model.test.ts +352 -0
  423. package/test/saeeol/transform-opus-4.7.test.ts +89 -0
  424. package/test/saeeol/tui-diff.test.ts +91 -0
  425. package/test/saeeol/tui-sync.test.ts +80 -0
  426. package/test/saeeol/util/url.test.ts +141 -0
  427. package/test/saeeol/workflows-migrator.test.ts +261 -0
  428. package/test/saeeol/worktree-diff-summary.test.ts +64 -0
  429. package/test/saeeol/worktree-diff.test.ts +223 -0
  430. package/test/saeeol/worktree-remove-lock.test.ts +82 -0
  431. package/test/server/AGENTS.md +15 -0
  432. package/test/server/contract.test.ts +357 -0
  433. package/test/server/experimental-session-list.test.ts +157 -0
  434. package/test/server/global-session-list.test.ts +155 -0
  435. package/test/server/httpapi-authorization.test.ts +103 -0
  436. package/test/server/httpapi-bridge.test.ts +440 -0
  437. package/test/server/httpapi-config.test.ts +67 -0
  438. package/test/server/httpapi-cors.test.ts +89 -0
  439. package/test/server/httpapi-event.test.ts +57 -0
  440. package/test/server/httpapi-experimental.test.ts +219 -0
  441. package/test/server/httpapi-file.test.ts +79 -0
  442. package/test/server/httpapi-instance-context.test.ts +237 -0
  443. package/test/server/httpapi-instance.legacy.test.ts +140 -0
  444. package/test/server/httpapi-instance.test.ts +83 -0
  445. package/test/server/httpapi-json-parity.test.ts +263 -0
  446. package/test/server/httpapi-mcp-oauth.test.ts +76 -0
  447. package/test/server/httpapi-mcp.test.ts +189 -0
  448. package/test/server/httpapi-provider.test.ts +153 -0
  449. package/test/server/httpapi-pty-websocket.test.ts +16 -0
  450. package/test/server/httpapi-pty.test.ts +175 -0
  451. package/test/server/httpapi-raw-route-auth.test.ts +89 -0
  452. package/test/server/httpapi-sdk.test.ts +681 -0
  453. package/test/server/httpapi-session.test.ts +464 -0
  454. package/test/server/httpapi-sync.test.ts +130 -0
  455. package/test/server/httpapi-tui.test.ts +121 -0
  456. package/test/server/httpapi-workspace-routing.test.ts +471 -0
  457. package/test/server/httpapi-workspace.test.ts +427 -0
  458. package/test/server/lib/conformance.ts +88 -0
  459. package/test/server/lib/stateful.ts +112 -0
  460. package/test/server/project-init-git.test.ts +113 -0
  461. package/test/server/proxy-util.test.ts +113 -0
  462. package/test/server/session-actions.test.ts +49 -0
  463. package/test/server/session-list.test.ts +238 -0
  464. package/test/server/session-messages.test.ts +167 -0
  465. package/test/server/session-select.test.ts +100 -0
  466. package/test/server/trace-attributes.test.ts +76 -0
  467. package/test/server/workspace-proxy.test.ts +165 -0
  468. package/test/server/workspace-routing.test.ts +85 -0
  469. package/test/session/compaction.test.ts +2420 -0
  470. package/test/session/instruction.test.ts +247 -0
  471. package/test/session/llm.test.ts +1273 -0
  472. package/test/session/message-v2.test.ts +1291 -0
  473. package/test/session/messages-pagination.test.ts +1173 -0
  474. package/test/session/network.test.ts +249 -0
  475. package/test/session/processor-effect.test.ts +847 -0
  476. package/test/session/prompt.test.ts +2131 -0
  477. package/test/session/retry.test.ts +340 -0
  478. package/test/session/revert-compact.test.ts +639 -0
  479. package/test/session/schema-decoding.test.ts +311 -0
  480. package/test/session/session-entry-stepper.test.ts +917 -0
  481. package/test/session/session-schema.test.ts +76 -0
  482. package/test/session/snapshot-tool-race.test.ts +257 -0
  483. package/test/session/structured-output-integration.test.ts +265 -0
  484. package/test/session/structured-output.test.ts +381 -0
  485. package/test/session/system.test.ts +73 -0
  486. package/test/share/share-next.test.ts +333 -0
  487. package/test/shell/shell.test.ts +99 -0
  488. package/test/skill/discovery.test.ts +116 -0
  489. package/test/skill/skill.test.ts +393 -0
  490. package/test/snapshot/snapshot.test.ts +1531 -0
  491. package/test/storage/db.test.ts +23 -0
  492. package/test/storage/json-migration.test.ts +832 -0
  493. package/test/storage/storage.test.ts +293 -0
  494. package/test/suggestion/suggestion.test.ts +1 -0
  495. package/test/sync/index.test.ts +256 -0
  496. package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
  497. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  498. package/test/tool/apply_patch.test.ts +614 -0
  499. package/test/tool/bash.test.ts +1225 -0
  500. package/test/tool/diagnostics-filter.test.ts +55 -0
  501. package/test/tool/edit.test.ts +754 -0
  502. package/test/tool/external-directory.test.ts +169 -0
  503. package/test/tool/fixtures/large-image.png +0 -0
  504. package/test/tool/fixtures/models-api.json +65179 -0
  505. package/test/tool/glob.test.ts +107 -0
  506. package/test/tool/grep.test.ts +114 -0
  507. package/test/tool/lsp.test.ts +187 -0
  508. package/test/tool/parameters.test.ts +243 -0
  509. package/test/tool/question.test.ts +129 -0
  510. package/test/tool/read.test.ts +500 -0
  511. package/test/tool/recall.test.ts +151 -0
  512. package/test/tool/registry.test.ts +203 -0
  513. package/test/tool/skill.test.ts +135 -0
  514. package/test/tool/suggest.test.ts +1 -0
  515. package/test/tool/task.test.ts +612 -0
  516. package/test/tool/tool-define.test.ts +99 -0
  517. package/test/tool/truncation.test.ts +260 -0
  518. package/test/tool/webfetch.test.ts +103 -0
  519. package/test/tool/write.test.ts +291 -0
  520. package/test/util/data-url.test.ts +14 -0
  521. package/test/util/effect-zod.test.ts +754 -0
  522. package/test/util/error.test.ts +38 -0
  523. package/test/util/filesystem.test.ts +656 -0
  524. package/test/util/format.test.ts +59 -0
  525. package/test/util/glob.test.ts +164 -0
  526. package/test/util/iife.test.ts +36 -0
  527. package/test/util/lazy.test.ts +50 -0
  528. package/test/util/lock.test.ts +72 -0
  529. package/test/util/log.test.ts +86 -0
  530. package/test/util/module.test.ts +59 -0
  531. package/test/util/process.test.ts +128 -0
  532. package/test/util/timeout.test.ts +21 -0
  533. package/test/util/which.test.ts +100 -0
  534. package/test/util/wildcard.test.ts +90 -0
  535. package/test/workspace/workspace-restore.test.ts +296 -0
  536. package/src/provider/models-snapshot.d.ts +0 -2
  537. package/src/provider/models-snapshot.js +0 -3
@@ -0,0 +1,172 @@
1
+ // Tests the saeeol-specific patch module guarantees:
2
+ // - Files retain their original encoding after an update (UTF-8 BOM, UTF-16,
3
+ // legacy single-byte, CJK).
4
+ // - Plain UTF-8 files do not gain a spurious BOM.
5
+ // - Moved files keep the original encoding at the new path.
6
+ // These round-trip through Patch.applyPatch directly so we exercise the
7
+ // encoding + BOM integration in patch/index.ts without the tool stack.
8
+
9
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
10
+ import fs from "fs/promises"
11
+ import path from "path"
12
+ import { tmpdir } from "os"
13
+ import iconv from "iconv-lite"
14
+ import { Patch } from "../../src/patch"
15
+
16
+ const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf])
17
+ const UTF16_LE_BOM = Buffer.from([0xff, 0xfe])
18
+
19
+ describe("Patch encoding preservation", () => {
20
+ let dir: string
21
+
22
+ beforeEach(async () => {
23
+ dir = await fs.mkdtemp(path.join(tmpdir(), "saeeol-patch-"))
24
+ })
25
+
26
+ afterEach(async () => {
27
+ await fs.rm(dir, { recursive: true, force: true })
28
+ })
29
+
30
+ test("preserves UTF-8 BOM through update", async () => {
31
+ const file = path.join(dir, "doc.txt")
32
+ await fs.writeFile(file, Buffer.concat([UTF8_BOM, Buffer.from("line 1\nline 2\n", "utf-8")]))
33
+
34
+ const patch = `*** Begin Patch
35
+ *** Update File: ${file}
36
+ @@
37
+ line 1
38
+ -line 2
39
+ +line 2 updated
40
+ *** End Patch`
41
+
42
+ await Patch.applyPatch(patch)
43
+
44
+ const bytes = await fs.readFile(file)
45
+ expect(bytes.subarray(0, 3).equals(UTF8_BOM)).toBe(true)
46
+ expect(bytes.subarray(3).toString("utf-8")).toBe("line 1\nline 2 updated\n")
47
+ })
48
+
49
+ test("does not introduce BOM for plain UTF-8 files", async () => {
50
+ const file = path.join(dir, "plain.txt")
51
+ await fs.writeFile(file, "line 1\nline 2\n", "utf-8")
52
+
53
+ const patch = `*** Begin Patch
54
+ *** Update File: ${file}
55
+ @@
56
+ line 1
57
+ -line 2
58
+ +line 2 updated
59
+ *** End Patch`
60
+
61
+ await Patch.applyPatch(patch)
62
+
63
+ const bytes = await fs.readFile(file)
64
+ expect(bytes[0]).not.toBe(0xef)
65
+ expect(bytes.toString("utf-8")).toBe("line 1\nline 2 updated\n")
66
+ })
67
+
68
+ test("preserves UTF-16 LE encoding through update", async () => {
69
+ const file = path.join(dir, "utf16.txt")
70
+ await fs.writeFile(file, Buffer.concat([UTF16_LE_BOM, iconv.encode("line 1\nline 2\n", "utf-16le")]))
71
+
72
+ const patch = `*** Begin Patch
73
+ *** Update File: ${file}
74
+ @@
75
+ line 1
76
+ -line 2
77
+ +line 2 updated
78
+ *** End Patch`
79
+
80
+ await Patch.applyPatch(patch)
81
+
82
+ const bytes = await fs.readFile(file)
83
+ expect(bytes.subarray(0, 2).equals(UTF16_LE_BOM)).toBe(true)
84
+ expect(iconv.decode(bytes.subarray(2), "utf-16le")).toBe("line 1\nline 2 updated\n")
85
+ })
86
+
87
+ test("preserves iso-8859-1 encoding through update", async () => {
88
+ const file = path.join(dir, "latin1.txt")
89
+ await fs.writeFile(file, iconv.encode("café\nñandú\n", "iso-8859-1"))
90
+
91
+ const patch = `*** Begin Patch
92
+ *** Update File: ${file}
93
+ @@
94
+ café
95
+ -ñandú
96
+ +águila
97
+ *** End Patch`
98
+
99
+ await Patch.applyPatch(patch)
100
+
101
+ const bytes = await fs.readFile(file)
102
+ expect(iconv.decode(bytes, "iso-8859-1")).toBe("café\náguila\n")
103
+ // á and ñ are two bytes in UTF-8, one byte in ISO-8859-1. If the file had
104
+ // been silently re-encoded as UTF-8 the byte length would differ.
105
+ expect(bytes.length).toBe("café\náguila\n".length)
106
+ })
107
+
108
+ test("preserves Shift_JIS encoding through update", async () => {
109
+ const file = path.join(dir, "jp.txt")
110
+ // chardet needs enough characteristic bytes to identify Shift_JIS. A
111
+ // single 19-byte phrase looks like windows-1252, so the sample is padded
112
+ // to match the body of Japanese text the tool tests already rely on.
113
+ const sample = "こんにちは、世界!日本語のテストです。"
114
+ await fs.writeFile(file, iconv.encode(`line1\n${sample}\nline3\n`, "Shift_JIS"))
115
+
116
+ const patch = `*** Begin Patch
117
+ *** Update File: ${file}
118
+ @@
119
+ line1
120
+ -${sample}
121
+ +さようなら、世界!
122
+ line3
123
+ *** End Patch`
124
+
125
+ await Patch.applyPatch(patch)
126
+
127
+ const bytes = await fs.readFile(file)
128
+ expect(iconv.decode(bytes, "Shift_JIS")).toBe("line1\nさようなら、世界!\nline3\n")
129
+ const utf8Rendered = Buffer.from("line1\nさようなら、世界!\nline3\n", "utf-8")
130
+ expect(bytes.equals(utf8Rendered)).toBe(false)
131
+ })
132
+
133
+ test("preserves UTF-8 BOM when file is moved", async () => {
134
+ const from = path.join(dir, "old.txt")
135
+ const to = path.join(dir, "new.txt")
136
+ await fs.writeFile(from, Buffer.concat([UTF8_BOM, Buffer.from("original\n", "utf-8")]))
137
+
138
+ const patch = `*** Begin Patch
139
+ *** Update File: ${from}
140
+ *** Move to: ${to}
141
+ @@
142
+ -original
143
+ +updated
144
+ *** End Patch`
145
+
146
+ await Patch.applyPatch(patch)
147
+
148
+ const moved = await fs.readFile(to)
149
+ expect(moved.subarray(0, 3).equals(UTF8_BOM)).toBe(true)
150
+ expect(moved.subarray(3).toString("utf-8")).toBe("updated\n")
151
+
152
+ const oldExists = await fs
153
+ .access(from)
154
+ .then(() => true)
155
+ .catch(() => false)
156
+ expect(oldExists).toBe(false)
157
+ })
158
+
159
+ test("new files added via patch are written as plain UTF-8", async () => {
160
+ const file = path.join(dir, "new.txt")
161
+ const patch = `*** Begin Patch
162
+ *** Add File: ${file}
163
+ +hello world
164
+ *** End Patch`
165
+
166
+ await Patch.applyPatch(patch)
167
+
168
+ const bytes = await fs.readFile(file)
169
+ expect(bytes[0]).not.toBe(0xef)
170
+ expect(bytes.toString("utf-8")).toBe("hello world")
171
+ })
172
+ })
@@ -0,0 +1,265 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import { SaeeolPaths } from "../../src/overlay/paths"
3
+ import { tmpdir } from "../fixture/fixture"
4
+ import path from "path"
5
+ import fs from "fs/promises"
6
+
7
+ async function withHome<T>(home: string, fn: () => Promise<T>): Promise<T> {
8
+ const prev = process.env.HOME
9
+ process.env.HOME = home
10
+ try {
11
+ return await fn()
12
+ } finally {
13
+ if (prev) process.env.HOME = prev
14
+ else delete process.env.HOME
15
+ }
16
+ }
17
+
18
+ describe("SaeeolPaths", () => {
19
+ describe("skillDirectories", () => {
20
+ test("discovers skills from .saeeol/skills/", async () => {
21
+ await using tmp = await tmpdir({
22
+ init: async (dir) => {
23
+ const skillDir = path.join(dir, ".saeeol", "skills", "test-skill")
24
+ await fs.mkdir(skillDir, { recursive: true })
25
+ await Bun.write(
26
+ path.join(skillDir, "SKILL.md"),
27
+ `---
28
+ name: test-skill
29
+ description: A test skill
30
+ ---
31
+ # Test instructions`,
32
+ )
33
+ },
34
+ })
35
+
36
+ const result = await SaeeolPaths.skillDirectories({
37
+ projectDir: tmp.path,
38
+ worktreeRoot: tmp.path,
39
+ skipGlobalPaths: true,
40
+ })
41
+
42
+ expect(result).toHaveLength(1)
43
+ expect(result[0]).toEndWith(".saeeol")
44
+ })
45
+
46
+ test("returns empty array when no .saeeol/skills/ exists", async () => {
47
+ await using tmp = await tmpdir()
48
+
49
+ const result = await SaeeolPaths.skillDirectories({
50
+ projectDir: tmp.path,
51
+ worktreeRoot: tmp.path,
52
+ skipGlobalPaths: true,
53
+ })
54
+
55
+ expect(result).toHaveLength(0)
56
+ })
57
+
58
+ test("discovers skills from nested .saeeol directories", async () => {
59
+ await using tmp = await tmpdir({
60
+ init: async (dir) => {
61
+ // Root level skill
62
+ const rootSkillDir = path.join(dir, ".saeeol", "skills", "root-skill")
63
+ await fs.mkdir(rootSkillDir, { recursive: true })
64
+ await Bun.write(
65
+ path.join(rootSkillDir, "SKILL.md"),
66
+ `---
67
+ name: root-skill
68
+ description: Root level skill
69
+ ---
70
+ # Root instructions`,
71
+ )
72
+
73
+ // Nested project skill
74
+ const nestedDir = path.join(dir, "packages", "nested")
75
+ const nestedSkillDir = path.join(nestedDir, ".saeeol", "skills", "nested-skill")
76
+ await fs.mkdir(nestedSkillDir, { recursive: true })
77
+ await Bun.write(
78
+ path.join(nestedSkillDir, "SKILL.md"),
79
+ `---
80
+ name: nested-skill
81
+ description: Nested skill
82
+ ---
83
+ # Nested instructions`,
84
+ )
85
+ },
86
+ })
87
+
88
+ // Run from nested directory, should find both
89
+ const nestedPath = path.join(tmp.path, "packages", "nested")
90
+ const result = await SaeeolPaths.skillDirectories({
91
+ projectDir: nestedPath,
92
+ worktreeRoot: tmp.path,
93
+ skipGlobalPaths: true,
94
+ })
95
+
96
+ expect(result).toHaveLength(2)
97
+ const nested = path.join("packages", "nested")
98
+ expect(result.some((d) => d.includes(nested))).toBe(true)
99
+ expect(result.some((d) => !d.includes(nested))).toBe(true)
100
+ })
101
+
102
+ test("handles .saeeol directory without skills subdirectory", async () => {
103
+ await using tmp = await tmpdir({
104
+ init: async (dir) => {
105
+ // Create .saeeol but not skills/
106
+ await fs.mkdir(path.join(dir, ".saeeol"), { recursive: true })
107
+ await Bun.write(path.join(dir, ".saeeol", "config.json"), "{}")
108
+ },
109
+ })
110
+
111
+ const result = await SaeeolPaths.skillDirectories({
112
+ projectDir: tmp.path,
113
+ worktreeRoot: tmp.path,
114
+ skipGlobalPaths: true,
115
+ })
116
+
117
+ expect(result).toHaveLength(0)
118
+ })
119
+
120
+ test("handles symlinked skill directories", async () => {
121
+ await using tmp = await tmpdir({
122
+ init: async (dir) => {
123
+ // Create actual skill in a different location
124
+ const actualDir = path.join(dir, "shared-skills", "my-skill")
125
+ await fs.mkdir(actualDir, { recursive: true })
126
+ await Bun.write(
127
+ path.join(actualDir, "SKILL.md"),
128
+ `---
129
+ name: my-skill
130
+ description: Symlinked skill
131
+ ---
132
+ # Instructions`,
133
+ )
134
+
135
+ // Create .saeeol/skills/ and symlink the skill
136
+ const skillsDir = path.join(dir, ".saeeol", "skills")
137
+ await fs.mkdir(skillsDir, { recursive: true })
138
+ await fs.symlink(actualDir, path.join(skillsDir, "my-skill"))
139
+ },
140
+ })
141
+
142
+ const result = await SaeeolPaths.skillDirectories({
143
+ projectDir: tmp.path,
144
+ worktreeRoot: tmp.path,
145
+ skipGlobalPaths: true,
146
+ })
147
+
148
+ expect(result).toHaveLength(1)
149
+ expect(result[0]).toEndWith(".saeeol")
150
+ })
151
+
152
+ test("discovers skills from .saeeol/skills/", async () => {
153
+ await using tmp = await tmpdir({
154
+ init: async (dir) => {
155
+ const skillDir = path.join(dir, ".saeeol", "skills", "legacy-skill")
156
+ await fs.mkdir(skillDir, { recursive: true })
157
+ await Bun.write(
158
+ path.join(skillDir, "SKILL.md"),
159
+ `---
160
+ name: legacy-skill
161
+ description: A legacy skill
162
+ ---
163
+ # Legacy instructions`,
164
+ )
165
+ },
166
+ })
167
+
168
+ const result = await SaeeolPaths.skillDirectories({
169
+ projectDir: tmp.path,
170
+ worktreeRoot: tmp.path,
171
+ skipGlobalPaths: true,
172
+ })
173
+
174
+ expect(result).toHaveLength(1)
175
+ expect(result[0]).toEndWith(".saeeol")
176
+ })
177
+
178
+ test("returns skill dirs from .saeeol", async () => {
179
+ await using tmp = await tmpdir({
180
+ init: async (dir) => {
181
+ // .saeeol skill
182
+ const saeeolSkillDir = path.join(dir, ".saeeol", "skills", "new-skill")
183
+ await fs.mkdir(saeeolSkillDir, { recursive: true })
184
+ await Bun.write(path.join(saeeolSkillDir, "SKILL.md"), "# New skill")
185
+
186
+ // .saeeol skill
187
+ const legacySkillDir = path.join(dir, ".saeeol", "skills", "old-skill")
188
+ await fs.mkdir(legacySkillDir, { recursive: true })
189
+ await Bun.write(path.join(legacySkillDir, "SKILL.md"), "# Old skill")
190
+ },
191
+ })
192
+
193
+ const result = await SaeeolPaths.skillDirectories({
194
+ projectDir: tmp.path,
195
+ worktreeRoot: tmp.path,
196
+ skipGlobalPaths: true,
197
+ })
198
+
199
+ expect(result).toHaveLength(1)
200
+ expect(result[0]).toEndWith(".saeeol")
201
+ })
202
+
203
+ test("discovers global skills from ~/.saeeol/skills/", async () => {
204
+ await using tmp = await tmpdir({
205
+ init: async (dir) => {
206
+ const skillDir = path.join(dir, ".saeeol", "skills", "global-skill")
207
+ await fs.mkdir(skillDir, { recursive: true })
208
+ await Bun.write(path.join(skillDir, "SKILL.md"), "# Global skill")
209
+ await fs.mkdir(path.join(dir, "repo"), { recursive: true })
210
+ },
211
+ })
212
+
213
+ const result = await withHome(tmp.path, () =>
214
+ SaeeolPaths.skillDirectories({
215
+ projectDir: path.join(tmp.path, "repo"),
216
+ worktreeRoot: path.join(tmp.path, "repo"),
217
+ }),
218
+ )
219
+
220
+ expect(result.some((d) => d.endsWith(".saeeol"))).toBe(true)
221
+ })
222
+
223
+ test("discovers multiple skills in same directory", async () => {
224
+ await using tmp = await tmpdir({
225
+ init: async (dir) => {
226
+ const skillsDir = path.join(dir, ".saeeol", "skills")
227
+
228
+ // First skill
229
+ const skill1 = path.join(skillsDir, "skill-one")
230
+ await fs.mkdir(skill1, { recursive: true })
231
+ await Bun.write(
232
+ path.join(skill1, "SKILL.md"),
233
+ `---
234
+ name: skill-one
235
+ description: First skill
236
+ ---
237
+ # First`,
238
+ )
239
+
240
+ // Second skill
241
+ const skill2 = path.join(skillsDir, "skill-two")
242
+ await fs.mkdir(skill2, { recursive: true })
243
+ await Bun.write(
244
+ path.join(skill2, "SKILL.md"),
245
+ `---
246
+ name: skill-two
247
+ description: Second skill
248
+ ---
249
+ # Second`,
250
+ )
251
+ },
252
+ })
253
+
254
+ const result = await SaeeolPaths.skillDirectories({
255
+ projectDir: tmp.path,
256
+ worktreeRoot: tmp.path,
257
+ skipGlobalPaths: true,
258
+ })
259
+
260
+ // Should return the .saeeol directory (not skills/ subdirectory)
261
+ expect(result).toHaveLength(1)
262
+ expect(result[0]).toEndWith(".saeeol")
263
+ })
264
+ })
265
+ })
@@ -0,0 +1,174 @@
1
+ import path from "path"
2
+ import { describe, expect, test } from "bun:test"
3
+ import { ConfigProtection } from "../../../src/overlay/permission/config-paths"
4
+ import { Global } from "@saeeol/core/global"
5
+ import { SaeeolPaths } from "../../../src/overlay/paths"
6
+
7
+ describe("ConfigProtection.isRequest", () => {
8
+ const config = path.resolve(Global.Path.config)
9
+ const legacy = SaeeolPaths.globalDirs().map((d) => path.resolve(d))
10
+
11
+ // --- external_directory: bash-originated (empty metadata) ---
12
+
13
+ test("returns true for bash external_directory targeting global config", () => {
14
+ const result = ConfigProtection.isRequest({
15
+ permission: "external_directory",
16
+ patterns: [config + "/*"],
17
+ metadata: {},
18
+ })
19
+ expect(result).toBe(true)
20
+ })
21
+
22
+ test("returns true for bash external_directory targeting skill dir", () => {
23
+ const result = ConfigProtection.isRequest({
24
+ permission: "external_directory",
25
+ patterns: [path.join(config, "skills", "my-skill") + "/*"],
26
+ metadata: {},
27
+ })
28
+ expect(result).toBe(true)
29
+ })
30
+
31
+ test("returns true for bash external_directory targeting legacy global dir", () => {
32
+ for (const dir of legacy) {
33
+ const result = ConfigProtection.isRequest({
34
+ permission: "external_directory",
35
+ patterns: [dir + "/*"],
36
+ metadata: {},
37
+ })
38
+ expect(result).toBe(true)
39
+ }
40
+ })
41
+
42
+ // --- external_directory: file-tool-originated (has metadata.filepath) ---
43
+
44
+ test("returns false for file-tool external_directory targeting global config", () => {
45
+ const result = ConfigProtection.isRequest({
46
+ permission: "external_directory",
47
+ patterns: [config + "/*"],
48
+ metadata: { filepath: path.join(config, "saeeol.json"), parentDir: config },
49
+ })
50
+ expect(result).toBe(false)
51
+ })
52
+
53
+ test("returns false for file-tool external_directory targeting global config root dir", () => {
54
+ const result = ConfigProtection.isRequest({
55
+ permission: "external_directory",
56
+ patterns: [config + "/*"],
57
+ metadata: { filepath: config, parentDir: config },
58
+ })
59
+ expect(result).toBe(false)
60
+ })
61
+
62
+ test("returns false for file-tool external_directory targeting readable global command dir", () => {
63
+ const result = ConfigProtection.isRequest({
64
+ permission: "external_directory",
65
+ patterns: [path.join(config, "command") + "/*"],
66
+ metadata: { filepath: path.join(config, "command", "foo.md"), parentDir: path.join(config, "command") },
67
+ })
68
+ expect(result).toBe(false)
69
+ })
70
+
71
+ test("returns false for file-tool external_directory targeting readable global skill dir", () => {
72
+ const result = ConfigProtection.isRequest({
73
+ permission: "external_directory",
74
+ patterns: [path.join(config, "skills") + "/*"],
75
+ metadata: {
76
+ filepath: path.join(config, "skills", "my-skill", "SKILL.md"),
77
+ parentDir: path.join(config, "skills"),
78
+ },
79
+ })
80
+ expect(result).toBe(false)
81
+ })
82
+
83
+ // --- external_directory: non-config dirs ---
84
+
85
+ test("returns false for bash external_directory targeting non-config dir", () => {
86
+ const result = ConfigProtection.isRequest({
87
+ permission: "external_directory",
88
+ patterns: ["/tmp/some-project/*"],
89
+ metadata: {},
90
+ })
91
+ expect(result).toBe(false)
92
+ })
93
+
94
+ // --- edit permission ---
95
+
96
+ test("returns true for edit targeting global config file via metadata.filepath", () => {
97
+ const result = ConfigProtection.isRequest({
98
+ permission: "edit",
99
+ patterns: [],
100
+ metadata: { filepath: path.join(config, "config.json") },
101
+ })
102
+ expect(result).toBe(true)
103
+ })
104
+
105
+ test("returns true for edit targeting skill file via metadata.filepath", () => {
106
+ const result = ConfigProtection.isRequest({
107
+ permission: "edit",
108
+ patterns: [],
109
+ metadata: { filepath: path.join(config, "skills", "my-skill", "SKILL.md") },
110
+ })
111
+ expect(result).toBe(true)
112
+ })
113
+
114
+ test("returns true for edit targeting legacy global dir via metadata.filepath", () => {
115
+ for (const dir of legacy) {
116
+ const result = ConfigProtection.isRequest({
117
+ permission: "edit",
118
+ patterns: [],
119
+ metadata: { filepath: path.join(dir, "config.json") },
120
+ })
121
+ expect(result).toBe(true)
122
+ }
123
+ })
124
+
125
+ test("returns true for edit targeting relative config path via patterns", () => {
126
+ const result = ConfigProtection.isRequest({
127
+ permission: "edit",
128
+ patterns: [".saeeol/command/foo.md"],
129
+ })
130
+ expect(result).toBe(true)
131
+ })
132
+
133
+ test("returns false for edit targeting excluded subdir (plans)", () => {
134
+ const result = ConfigProtection.isRequest({
135
+ permission: "edit",
136
+ patterns: [".saeeol/plans/plan.md"],
137
+ })
138
+ expect(result).toBe(false)
139
+ })
140
+
141
+ test("returns false for read permission", () => {
142
+ const result = ConfigProtection.isRequest({
143
+ permission: "read",
144
+ patterns: [".saeeol/config.json"],
145
+ })
146
+ expect(result).toBe(false)
147
+ })
148
+
149
+ test("returns false for bash permission", () => {
150
+ const result = ConfigProtection.isRequest({
151
+ permission: "bash",
152
+ patterns: ["cat " + path.join(config, "config.json")],
153
+ })
154
+ expect(result).toBe(false)
155
+ })
156
+
157
+ test("returns true for edit targeting root config files", () => {
158
+ for (const file of ["saeeol.json", "saeeol.jsonc", "AGENTS.md"]) {
159
+ const result = ConfigProtection.isRequest({
160
+ permission: "edit",
161
+ patterns: [file],
162
+ })
163
+ expect(result).toBe(true)
164
+ }
165
+ })
166
+
167
+ test("returns false for edit targeting non-config files", () => {
168
+ const result = ConfigProtection.isRequest({
169
+ permission: "edit",
170
+ patterns: ["src/index.ts"],
171
+ })
172
+ expect(result).toBe(false)
173
+ })
174
+ })