saeeol 1.3.0 → 1.4.0

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 (545) hide show
  1. package/AGENTS.md +72 -0
  2. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  3. package/Dockerfile +18 -0
  4. package/assets/saeeol.ico +0 -0
  5. package/bin/saeeol.cjs +3 -1
  6. package/bunfig.toml +7 -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/preflight.ts +138 -0
  112. package/src/cli/cmd/tui/thread.ts +20 -0
  113. package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
  114. package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
  115. package/src/overlay/cost-tracker/format.ts +1 -1
  116. package/src/overlay/cost-tracker/index.ts +4 -4
  117. package/src/overlay/cost-tracker/state.ts +2 -2
  118. package/src/overlay/cost-tracker/types.ts +2 -2
  119. package/src/overlay/memory/age.ts +1 -1
  120. package/src/overlay/memory/index.ts +4 -4
  121. package/src/overlay/memory/paths.ts +2 -2
  122. package/src/overlay/memory/scan.ts +1 -1
  123. package/src/overlay/memory/types.ts +2 -2
  124. package/src/overlay/tool/bash-security.ts +3 -3
  125. package/src/overlay/util/url.ts +1 -1
  126. package/src/plugin/codex-auth.ts +1 -1
  127. package/src/provider/model-cache.ts +2 -2
  128. package/src/provider/provider-resolve.ts +3 -3
  129. package/src/provider/transform-message.ts +1 -1
  130. package/src/server/routes/game.ts +284 -0
  131. package/src/server/server.ts +2 -0
  132. package/src/session/core/compaction/compaction-helpers.ts +1 -1
  133. package/src/session/core/compaction/compaction.ts +1 -1
  134. package/src/session/core/session.ts +2 -0
  135. package/src/sessions/ingest-queue.ts +2 -2
  136. package/src/sessions/remote-ws.ts +1 -1
  137. package/src/tool/workflow/question.ts +1 -1
  138. package/src/util/abort.ts +1 -1
  139. package/src/util/bom.ts +2 -2
  140. package/src/util/color.ts +1 -1
  141. package/src/util/data-url.ts +1 -1
  142. package/src/util/defer.ts +1 -1
  143. package/src/util/error.ts +2 -2
  144. package/src/util/filesystem.ts +2 -2
  145. package/src/util/format.ts +1 -1
  146. package/src/util/iife.ts +1 -1
  147. package/src/util/local-context.ts +1 -1
  148. package/src/util/locale.ts +2 -2
  149. package/src/util/lock.ts +1 -1
  150. package/src/util/network.ts +1 -1
  151. package/src/util/signal.ts +1 -1
  152. package/src/util/token.ts +1 -1
  153. package/src/util/wildcard.ts +1 -1
  154. package/sst-env.d.ts +10 -0
  155. package/test/AGENTS.md +133 -0
  156. package/test/account/repo.test.ts +352 -0
  157. package/test/account/service.test.ts +456 -0
  158. package/test/acp/agent-interface.test.ts +51 -0
  159. package/test/acp/event-subscription.test.ts +725 -0
  160. package/test/agent/agent.test.ts +890 -0
  161. package/test/auth/auth.test.ts +86 -0
  162. package/test/bun/registry.test.ts +75 -0
  163. package/test/bus/bus-effect.test.ts +161 -0
  164. package/test/bus/bus-integration.test.ts +87 -0
  165. package/test/bus/bus.test.ts +219 -0
  166. package/test/cli/account.test.ts +26 -0
  167. package/test/cli/auto-mode.test.ts +75 -0
  168. package/test/cli/bin-saeeol.test.ts +8 -0
  169. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  170. package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
  171. package/test/cli/cmd/tui/sync.test.tsx +159 -0
  172. package/test/cli/error.test.ts +18 -0
  173. package/test/cli/github-action.test.ts +198 -0
  174. package/test/cli/github-remote.test.ts +85 -0
  175. package/test/cli/import.test.ts +97 -0
  176. package/test/cli/install-artifact.test.ts +72 -0
  177. package/test/cli/plugin-auth-picker.test.ts +120 -0
  178. package/test/cli/pr.test.ts +59 -0
  179. package/test/cli/tui/editor-context-zed.test.ts +356 -0
  180. package/test/cli/tui/editor-context.test.tsx +228 -0
  181. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  182. package/test/cli/tui/markdown.test.ts +161 -0
  183. package/test/cli/tui/plugin-add.test.ts +111 -0
  184. package/test/cli/tui/plugin-install.test.ts +87 -0
  185. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  186. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  187. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  188. package/test/cli/tui/plugin-loader.test.ts +816 -0
  189. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  190. package/test/cli/tui/revert-diff.test.ts +35 -0
  191. package/test/cli/tui/slot-replace.test.tsx +47 -0
  192. package/test/cli/tui/theme-store.test.ts +54 -0
  193. package/test/cli/tui/thread.test.ts +28 -0
  194. package/test/cli/tui/transcript.test.ts +426 -0
  195. package/test/cli/tui/usage.test.ts +60 -0
  196. package/test/cli/tui/use-event.test.tsx +175 -0
  197. package/test/config/agent-color.test.ts +67 -0
  198. package/test/config/config.test.ts +2544 -0
  199. package/test/config/fixtures/empty-frontmatter.md +4 -0
  200. package/test/config/fixtures/frontmatter.md +28 -0
  201. package/test/config/fixtures/markdown-header.md +11 -0
  202. package/test/config/fixtures/no-frontmatter.md +1 -0
  203. package/test/config/fixtures/weird-model-id.md +13 -0
  204. package/test/config/lsp.test.ts +87 -0
  205. package/test/config/markdown.test.ts +228 -0
  206. package/test/config/plugin.test.ts +0 -0
  207. package/test/config/tui.test.ts +624 -0
  208. package/test/control-plane/adapters.test.ts +71 -0
  209. package/test/control-plane/workspace.test.ts +1526 -0
  210. package/test/effect/app-runtime-logger.test.ts +98 -0
  211. package/test/effect/config-service.test.ts +65 -0
  212. package/test/effect/instance-state.test.ts +394 -0
  213. package/test/effect/run-service.test.ts +89 -0
  214. package/test/effect/runner.test.ts +523 -0
  215. package/test/fake/provider.ts +82 -0
  216. package/test/file/fsmonitor.test.ts +68 -0
  217. package/test/file/ignore.test.ts +10 -0
  218. package/test/file/index.test.ts +954 -0
  219. package/test/file/path-traversal.test.ts +205 -0
  220. package/test/file/ripgrep.test.ts +226 -0
  221. package/test/file/watcher.test.ts +249 -0
  222. package/test/filesystem/filesystem.test.ts +319 -0
  223. package/test/fixture/db.ts +11 -0
  224. package/test/fixture/fixture.test.ts +26 -0
  225. package/test/fixture/fixture.ts +175 -0
  226. package/test/fixture/flock-worker.ts +72 -0
  227. package/test/fixture/log-init-worker.ts +62 -0
  228. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  229. package/test/fixture/plug-worker.ts +93 -0
  230. package/test/fixture/plugin-meta-worker.ts +19 -0
  231. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  232. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  233. package/test/fixture/skills/index.json +6 -0
  234. package/test/fixture/tui-plugin.ts +323 -0
  235. package/test/fixture/tui-runtime.ts +31 -0
  236. package/test/format/format.test.ts +272 -0
  237. package/test/git/git.test.ts +128 -0
  238. package/test/ide/ide.test.ts +82 -0
  239. package/test/installation/installation.test.ts +168 -0
  240. package/test/keybind.test.ts +421 -0
  241. package/test/lib/effect.ts +53 -0
  242. package/test/lib/filesystem.ts +10 -0
  243. package/test/lib/llm-server.ts +778 -0
  244. package/test/lib/websocket.ts +46 -0
  245. package/test/lsp/client.test.ts +482 -0
  246. package/test/lsp/index.test.ts +160 -0
  247. package/test/lsp/launch.test.ts +22 -0
  248. package/test/lsp/lifecycle.test.ts +184 -0
  249. package/test/ltm/ltm.test.ts +230 -0
  250. package/test/mcp/headers.test.ts +178 -0
  251. package/test/mcp/lifecycle.test.ts +787 -0
  252. package/test/mcp/oauth-auto-connect.test.ts +311 -0
  253. package/test/mcp/oauth-browser.test.ts +276 -0
  254. package/test/mcp/oauth-callback.test.ts +34 -0
  255. package/test/memory/abort-leak-webfetch.ts +49 -0
  256. package/test/memory/abort-leak.test.ts +128 -0
  257. package/test/patch/patch.test.ts +348 -0
  258. package/test/permission/arity.test.ts +33 -0
  259. package/test/permission/next.test.ts +1227 -0
  260. package/test/permission/next.toConfig.test.ts +110 -0
  261. package/test/permission-task.test.ts +326 -0
  262. package/test/plugin/auth-override.test.ts +79 -0
  263. package/test/plugin/cloudflare.test.ts +68 -0
  264. package/test/plugin/codex.test.ts +123 -0
  265. package/test/plugin/github-copilot-models.test.ts +261 -0
  266. package/test/plugin/install-concurrency.test.ts +140 -0
  267. package/test/plugin/install.test.ts +570 -0
  268. package/test/plugin/loader-shared.test.ts +1169 -0
  269. package/test/plugin/meta.test.ts +137 -0
  270. package/test/plugin/plugin-contract.test.ts +291 -0
  271. package/test/plugin/shared.test.ts +88 -0
  272. package/test/plugin/trigger.test.ts +102 -0
  273. package/test/plugin/workspace-adapter.test.ts +109 -0
  274. package/test/preload.ts +77 -0
  275. package/test/project/instance.test.ts +276 -0
  276. package/test/project/migrate-global.test.ts +152 -0
  277. package/test/project/project.test.ts +600 -0
  278. package/test/project/vcs.test.ts +286 -0
  279. package/test/project/worktree-remove.test.ts +126 -0
  280. package/test/project/worktree.test.ts +223 -0
  281. package/test/provider/amazon-bedrock.test.ts +462 -0
  282. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  283. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  284. package/test/provider/gitlab-duo.test.ts +413 -0
  285. package/test/provider/local.test.ts +208 -0
  286. package/test/provider/models.test.ts +261 -0
  287. package/test/provider/provider-category.test.ts +190 -0
  288. package/test/provider/provider.test.ts +2758 -0
  289. package/test/provider/transform.test.ts +3681 -0
  290. package/test/pty/pty-output-isolation.test.ts +147 -0
  291. package/test/pty/pty-session.test.ts +102 -0
  292. package/test/pty/pty-shell.test.ts +104 -0
  293. package/test/question/question.test.ts +490 -0
  294. package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
  295. package/test/saeeol/agent-manager-tool.test.ts +71 -0
  296. package/test/saeeol/agent-permission-overrides.test.ts +75 -0
  297. package/test/saeeol/agent-skill-permissions.test.ts +37 -0
  298. package/test/saeeol/ask-agent-permissions.test.ts +303 -0
  299. package/test/saeeol/bash-hierarchy.test.ts +64 -0
  300. package/test/saeeol/bash-permission-metadata.test.ts +66 -0
  301. package/test/saeeol/bash-security-extended.test.ts +243 -0
  302. package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
  303. package/test/saeeol/boxes-integration.test.ts +415 -0
  304. package/test/saeeol/builtin-skills.test.ts +75 -0
  305. package/test/saeeol/cleanup.ts +28 -0
  306. package/test/saeeol/cli/dev-setup.test.ts +74 -0
  307. package/test/saeeol/cli/roll-call.test.ts +161 -0
  308. package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
  309. package/test/saeeol/codex-auth-refresh.test.ts +124 -0
  310. package/test/saeeol/commit-message/generate.test.ts +188 -0
  311. package/test/saeeol/commit-message/git-context.test.ts +303 -0
  312. package/test/saeeol/commit-message-windows.test.ts +38 -0
  313. package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
  314. package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
  315. package/test/saeeol/compaction-skip-guard.test.ts +224 -0
  316. package/test/saeeol/compaction-smart-select.test.ts +100 -0
  317. package/test/saeeol/config/config.test.ts +166 -0
  318. package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
  319. package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
  320. package/test/saeeol/config-gitignore.test.ts +70 -0
  321. package/test/saeeol/config-injector.test.ts +305 -0
  322. package/test/saeeol/config-resilience.test.ts +234 -0
  323. package/test/saeeol/config-validation.test.ts +183 -0
  324. package/test/saeeol/cost-propagation.test.ts +94 -0
  325. package/test/saeeol/cost-tracker-extended.test.ts +141 -0
  326. package/test/saeeol/cost-tracker.test.ts +64 -0
  327. package/test/saeeol/custom-provider-delete.test.ts +149 -0
  328. package/test/saeeol/diff-full.test.ts +226 -0
  329. package/test/saeeol/edit-permission-filediff.test.ts +223 -0
  330. package/test/saeeol/encoding.test.ts +364 -0
  331. package/test/saeeol/enhance-prompt.test.ts +61 -0
  332. package/test/saeeol/ensure-plan-dir.test.ts +32 -0
  333. package/test/saeeol/errors.test.ts +144 -0
  334. package/test/saeeol/external-directory-boundary.test.ts +96 -0
  335. package/test/saeeol/gateway-headers.test.ts +88 -0
  336. package/test/saeeol/help.test.ts +191 -0
  337. package/test/saeeol/ignore-migrator.test.ts +308 -0
  338. package/test/saeeol/indexing-auth.test.ts +45 -0
  339. package/test/saeeol/indexing-feature.test.ts +44 -0
  340. package/test/saeeol/indexing-label.test.ts +70 -0
  341. package/test/saeeol/indexing-startup.test.ts +381 -0
  342. package/test/saeeol/indexing-worktree.test.ts +73 -0
  343. package/test/saeeol/instruction.test.ts +136 -0
  344. package/test/saeeol/lancedb-runtime.test.ts +116 -0
  345. package/test/saeeol/loader-auth.test.ts +168 -0
  346. package/test/saeeol/local-model.test.ts +621 -0
  347. package/test/saeeol/logo.test.ts +31 -0
  348. package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
  349. package/test/saeeol/mcp-branding.test.ts +33 -0
  350. package/test/saeeol/mcp-docker-rm.test.ts +32 -0
  351. package/test/saeeol/mcp-migrator.test.ts +736 -0
  352. package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
  353. package/test/saeeol/memory-io.test.ts +198 -0
  354. package/test/saeeol/memory-paths.test.ts +87 -0
  355. package/test/saeeol/memory-security.test.ts +166 -0
  356. package/test/saeeol/model-cache-org.test.ts +164 -0
  357. package/test/saeeol/model-info-panel-utils.test.ts +52 -0
  358. package/test/saeeol/model-info-panel.types.test.ts +7 -0
  359. package/test/saeeol/models-401-fallback.test.ts +52 -0
  360. package/test/saeeol/modes-migrator.test.ts +320 -0
  361. package/test/saeeol/nvidia-headers.test.ts +74 -0
  362. package/test/saeeol/patch-jsonc.test.ts +73 -0
  363. package/test/saeeol/patch.test.ts +172 -0
  364. package/test/saeeol/paths.test.ts +265 -0
  365. package/test/saeeol/permission/config-paths.test.ts +174 -0
  366. package/test/saeeol/permission/env-read.test.ts +149 -0
  367. package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
  368. package/test/saeeol/permission/next.always-rules.test.ts +882 -0
  369. package/test/saeeol/permission/next.reply-http.test.ts +205 -0
  370. package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
  371. package/test/saeeol/plan-exit-detection.test.ts +494 -0
  372. package/test/saeeol/plan-followup.test.ts +1376 -0
  373. package/test/saeeol/project-config-update.test.ts +120 -0
  374. package/test/saeeol/project-id.test.ts +455 -0
  375. package/test/saeeol/provider-cost.test.ts +171 -0
  376. package/test/saeeol/provider-list-failed-state.test.ts +100 -0
  377. package/test/saeeol/question-dismiss-all.test.ts +174 -0
  378. package/test/saeeol/read-directory.test.ts +116 -0
  379. package/test/saeeol/rules-migrator.test.ts +257 -0
  380. package/test/saeeol/run-auto.test.ts +176 -0
  381. package/test/saeeol/run-network.test.ts +224 -0
  382. package/test/saeeol/semantic-search.test.ts +186 -0
  383. package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
  384. package/test/saeeol/session/instruction-substitution.test.ts +72 -0
  385. package/test/saeeol/session/platform-attribution.test.ts +118 -0
  386. package/test/saeeol/session/session.test.ts +105 -0
  387. package/test/saeeol/session-compaction-cap.test.ts +399 -0
  388. package/test/saeeol/session-compaction-chunks.test.ts +501 -0
  389. package/test/saeeol/session-compaction-safety.test.ts +481 -0
  390. package/test/saeeol/session-fork-remap.test.ts +251 -0
  391. package/test/saeeol/session-import-service.test.ts +114 -0
  392. package/test/saeeol/session-list.test.ts +47 -0
  393. package/test/saeeol/session-message-metadata.test.ts +128 -0
  394. package/test/saeeol/session-overflow.test.ts +78 -0
  395. package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
  396. package/test/saeeol/session-processor-network-offline.test.ts +204 -0
  397. package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
  398. package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
  399. package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
  400. package/test/saeeol/session-prompt-queue.test.ts +815 -0
  401. package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
  402. package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
  403. package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
  404. package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
  405. package/test/saeeol/sessions/remote-ws.test.ts +367 -0
  406. package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
  407. package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
  408. package/test/saeeol/snapshot-cache.test.ts +84 -0
  409. package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
  410. package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
  411. package/test/saeeol/stats-subagent-cost.test.ts +123 -0
  412. package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
  413. package/test/saeeol/suggestion/suggestion.test.ts +145 -0
  414. package/test/saeeol/suggestion/tool.test.ts +298 -0
  415. package/test/saeeol/summary-file-diff.test.ts +28 -0
  416. package/test/saeeol/system-prompt.test.ts +142 -0
  417. package/test/saeeol/task-nesting.test.ts +193 -0
  418. package/test/saeeol/telemetry/feedback.test.ts +8 -0
  419. package/test/saeeol/todo-view.test.ts +57 -0
  420. package/test/saeeol/tool-encoding.test.ts +455 -0
  421. package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
  422. package/test/saeeol/tool-registry-indexing.test.ts +236 -0
  423. package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
  424. package/test/saeeol/tool-task-model.test.ts +352 -0
  425. package/test/saeeol/transform-opus-4.7.test.ts +89 -0
  426. package/test/saeeol/tui-diff.test.ts +91 -0
  427. package/test/saeeol/tui-sync.test.ts +80 -0
  428. package/test/saeeol/util/url.test.ts +141 -0
  429. package/test/saeeol/workflows-migrator.test.ts +261 -0
  430. package/test/saeeol/worktree-diff-summary.test.ts +64 -0
  431. package/test/saeeol/worktree-diff.test.ts +223 -0
  432. package/test/saeeol/worktree-remove-lock.test.ts +82 -0
  433. package/test/server/AGENTS.md +15 -0
  434. package/test/server/contract.test.ts +357 -0
  435. package/test/server/experimental-session-list.test.ts +157 -0
  436. package/test/server/global-session-list.test.ts +155 -0
  437. package/test/server/httpapi-authorization.test.ts +103 -0
  438. package/test/server/httpapi-bridge.test.ts +440 -0
  439. package/test/server/httpapi-config.test.ts +67 -0
  440. package/test/server/httpapi-cors.test.ts +89 -0
  441. package/test/server/httpapi-event.test.ts +57 -0
  442. package/test/server/httpapi-experimental.test.ts +219 -0
  443. package/test/server/httpapi-file.test.ts +79 -0
  444. package/test/server/httpapi-instance-context.test.ts +237 -0
  445. package/test/server/httpapi-instance.legacy.test.ts +140 -0
  446. package/test/server/httpapi-instance.test.ts +83 -0
  447. package/test/server/httpapi-json-parity.test.ts +263 -0
  448. package/test/server/httpapi-mcp-oauth.test.ts +76 -0
  449. package/test/server/httpapi-mcp.test.ts +189 -0
  450. package/test/server/httpapi-provider.test.ts +153 -0
  451. package/test/server/httpapi-pty-websocket.test.ts +16 -0
  452. package/test/server/httpapi-pty.test.ts +175 -0
  453. package/test/server/httpapi-raw-route-auth.test.ts +89 -0
  454. package/test/server/httpapi-sdk.test.ts +681 -0
  455. package/test/server/httpapi-session.test.ts +464 -0
  456. package/test/server/httpapi-sync.test.ts +130 -0
  457. package/test/server/httpapi-tui.test.ts +121 -0
  458. package/test/server/httpapi-workspace-routing.test.ts +471 -0
  459. package/test/server/httpapi-workspace.test.ts +427 -0
  460. package/test/server/lib/conformance.ts +88 -0
  461. package/test/server/lib/stateful.ts +112 -0
  462. package/test/server/project-init-git.test.ts +113 -0
  463. package/test/server/proxy-util.test.ts +113 -0
  464. package/test/server/session-actions.test.ts +49 -0
  465. package/test/server/session-list.test.ts +238 -0
  466. package/test/server/session-messages.test.ts +167 -0
  467. package/test/server/session-select.test.ts +100 -0
  468. package/test/server/trace-attributes.test.ts +76 -0
  469. package/test/server/workspace-proxy.test.ts +165 -0
  470. package/test/server/workspace-routing.test.ts +85 -0
  471. package/test/session/compaction.test.ts +2420 -0
  472. package/test/session/instruction.test.ts +247 -0
  473. package/test/session/llm.test.ts +1273 -0
  474. package/test/session/message-v2.test.ts +1291 -0
  475. package/test/session/messages-pagination.test.ts +1173 -0
  476. package/test/session/network.test.ts +249 -0
  477. package/test/session/processor-effect.test.ts +847 -0
  478. package/test/session/prompt.test.ts +2131 -0
  479. package/test/session/retry.test.ts +340 -0
  480. package/test/session/revert-compact.test.ts +639 -0
  481. package/test/session/schema-decoding.test.ts +311 -0
  482. package/test/session/session-entry-stepper.test.ts +917 -0
  483. package/test/session/session-schema.test.ts +76 -0
  484. package/test/session/snapshot-tool-race.test.ts +257 -0
  485. package/test/session/structured-output-integration.test.ts +265 -0
  486. package/test/session/structured-output.test.ts +381 -0
  487. package/test/session/system.test.ts +73 -0
  488. package/test/share/share-next.test.ts +333 -0
  489. package/test/shell/shell.test.ts +99 -0
  490. package/test/skill/discovery.test.ts +116 -0
  491. package/test/skill/skill.test.ts +393 -0
  492. package/test/smoke/.tui-debug-output.txt +1 -0
  493. package/test/smoke/.tui-debug-plain.txt +1 -0
  494. package/test/smoke/.tui-walkthrough-report.txt +122 -0
  495. package/test/smoke/smoke-tui-pty.test.ts +123 -0
  496. package/test/smoke/smoke-tui.mjs +83 -0
  497. package/test/smoke/tui-walkthrough.test.ts +520 -0
  498. package/test/snapshot/snapshot.test.ts +1531 -0
  499. package/test/storage/db.test.ts +23 -0
  500. package/test/storage/json-migration.test.ts +832 -0
  501. package/test/storage/storage.test.ts +293 -0
  502. package/test/suggestion/suggestion.test.ts +1 -0
  503. package/test/sync/index.test.ts +256 -0
  504. package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
  505. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  506. package/test/tool/apply_patch.test.ts +614 -0
  507. package/test/tool/bash.test.ts +1225 -0
  508. package/test/tool/diagnostics-filter.test.ts +55 -0
  509. package/test/tool/edit.test.ts +754 -0
  510. package/test/tool/external-directory.test.ts +169 -0
  511. package/test/tool/fixtures/large-image.png +0 -0
  512. package/test/tool/fixtures/models-api.json +65179 -0
  513. package/test/tool/glob.test.ts +107 -0
  514. package/test/tool/grep.test.ts +114 -0
  515. package/test/tool/lsp.test.ts +187 -0
  516. package/test/tool/parameters.test.ts +243 -0
  517. package/test/tool/question.test.ts +129 -0
  518. package/test/tool/read.test.ts +500 -0
  519. package/test/tool/recall.test.ts +151 -0
  520. package/test/tool/registry.test.ts +203 -0
  521. package/test/tool/skill.test.ts +135 -0
  522. package/test/tool/suggest.test.ts +1 -0
  523. package/test/tool/task.test.ts +612 -0
  524. package/test/tool/tool-define.test.ts +99 -0
  525. package/test/tool/truncation.test.ts +260 -0
  526. package/test/tool/webfetch.test.ts +103 -0
  527. package/test/tool/write.test.ts +291 -0
  528. package/test/util/data-url.test.ts +14 -0
  529. package/test/util/effect-zod.test.ts +754 -0
  530. package/test/util/error.test.ts +38 -0
  531. package/test/util/filesystem.test.ts +656 -0
  532. package/test/util/format.test.ts +59 -0
  533. package/test/util/glob.test.ts +164 -0
  534. package/test/util/iife.test.ts +36 -0
  535. package/test/util/lazy.test.ts +50 -0
  536. package/test/util/lock.test.ts +72 -0
  537. package/test/util/log.test.ts +86 -0
  538. package/test/util/module.test.ts +59 -0
  539. package/test/util/process.test.ts +128 -0
  540. package/test/util/timeout.test.ts +21 -0
  541. package/test/util/which.test.ts +100 -0
  542. package/test/util/wildcard.test.ts +90 -0
  543. package/test/workspace/workspace-restore.test.ts +296 -0
  544. package/src/provider/models-snapshot.d.ts +0 -2
  545. package/src/provider/models-snapshot.js +0 -3
@@ -0,0 +1,136 @@
1
+ # Message Shape
2
+
3
+ Problem:
4
+
5
+ - stored messages need enough data to replay and resume a session later
6
+ - prompt hooks often just want to append a synthetic user/assistant message
7
+ - today that means faking ids, timestamps, and request metadata
8
+
9
+ ## Option 1: Two Message Shapes
10
+
11
+ Keep `User` / `Assistant` for stored history, but clean them up.
12
+
13
+ ```ts
14
+ type User = {
15
+ role: "user"
16
+ time: { created: number }
17
+ request: {
18
+ agent: string
19
+ model: ModelRef
20
+ variant?: string
21
+ format?: OutputFormat
22
+ system?: string
23
+ tools?: Record<string, boolean>
24
+ }
25
+ }
26
+
27
+ type Assistant = {
28
+ role: "assistant"
29
+ run: { agent: string; model: ModelRef; path: { cwd: string; root: string } }
30
+ usage: { cost: number; tokens: Tokens }
31
+ result: { finish?: string; error?: Error; structured?: unknown; kind: "reply" | "summary" }
32
+ }
33
+ ```
34
+
35
+ Add a separate transient `PromptMessage` for prompt surgery.
36
+
37
+ ```ts
38
+ type PromptMessage = {
39
+ role: "user" | "assistant"
40
+ parts: PromptPart[]
41
+ }
42
+ ```
43
+
44
+ Plugin hook example:
45
+
46
+ ```ts
47
+ prompt.push({
48
+ role: "user",
49
+ parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
50
+ })
51
+ ```
52
+
53
+ Tradeoff: prompt hooks get easy lightweight messages, but there are now two message shapes.
54
+
55
+ ## Option 2: Prompt Mutators
56
+
57
+ Keep `User` / `Assistant` as the stored history model.
58
+
59
+ Prompt hooks do not build messages directly. The runtime gives them prompt mutators.
60
+
61
+ ```ts
62
+ type PromptEditor = {
63
+ append(input: { role: "user" | "assistant"; parts: PromptPart[] }): void
64
+ prepend(input: { role: "user" | "assistant"; parts: PromptPart[] }): void
65
+ appendTo(target: "last-user" | "last-assistant", parts: PromptPart[]): void
66
+ insertAfter(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void
67
+ insertBefore(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void
68
+ }
69
+ ```
70
+
71
+ Plugin hook examples:
72
+
73
+ ```ts
74
+ prompt.append({
75
+ role: "user",
76
+ parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
77
+ })
78
+ ```
79
+
80
+ ```ts
81
+ prompt.appendTo("last-user", [{ type: "text", text: BUILD_SWITCH }])
82
+ ```
83
+
84
+ Tradeoff: avoids a second full message type and avoids fake ids/timestamps, but moves more magic into the hook API.
85
+
86
+ ## Option 3: Separate Turn State
87
+
88
+ Move execution settings out of `User` and into a separate turn/request object.
89
+
90
+ ```ts
91
+ type Turn = {
92
+ id: string
93
+ request: {
94
+ agent: string
95
+ model: ModelRef
96
+ variant?: string
97
+ format?: OutputFormat
98
+ system?: string
99
+ tools?: Record<string, boolean>
100
+ }
101
+ }
102
+
103
+ type User = {
104
+ role: "user"
105
+ turnID: string
106
+ time: { created: number }
107
+ }
108
+
109
+ type Assistant = {
110
+ role: "assistant"
111
+ turnID: string
112
+ usage: { cost: number; tokens: Tokens }
113
+ result: { finish?: string; error?: Error; structured?: unknown; kind: "reply" | "summary" }
114
+ }
115
+ ```
116
+
117
+ Examples:
118
+
119
+ ```ts
120
+ const turn = {
121
+ request: {
122
+ agent: "build",
123
+ model: { providerID: "openai", modelID: "gpt-5" },
124
+ },
125
+ }
126
+ ```
127
+
128
+ ```ts
129
+ const msg = {
130
+ role: "user",
131
+ turnID: turn.id,
132
+ parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
133
+ }
134
+ ```
135
+
136
+ Tradeoff: stored messages get much smaller and cleaner, but replay now has to join messages with turn state and prompt hooks still need a way to pick which turn they belong to.
@@ -1,4 +1,4 @@
1
- import { parse as parseDataUrl } from "@saeeol/boxes/dataurl"
1
+ import { parse as parseDataUrl } from "@/boxes/dataurl"
2
2
  import { pathToFileURL } from "url"
3
3
  import type { Role, ToolCallContent } from "@agentclientprotocol/sdk"
4
4
  import type { SessionMessageResponse, ToolPart } from "@saeeol/sdk/v2"
@@ -1,4 +1,4 @@
1
- import { parse as parseDataUrl } from "@saeeol/boxes/dataurl"
1
+ import { parse as parseDataUrl } from "@/boxes/dataurl"
2
2
  import { RequestError, type Role, type ToolKind, type Usage, type PlanEntry } from "@agentclientprotocol/sdk"
3
3
  import * as Log from "@saeeol/core/util/log"
4
4
  import { pathToFileURL } from "url"
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ansi.ts — Terminal color: hex → ANSI escape sequence
3
+ * Zero deps.
4
+ *
5
+ * bold("#ff6600") → "\x1b[38;2;255;102;0m\x1b[1m"
6
+ */
7
+ export function valid(hex?: string): hex is string { return !!hex && /^#[0-9a-fA-F]{6}$/.test(hex) }
8
+
9
+ export function rgb(hex: string) {
10
+ return { r: parseInt(hex.slice(1, 3), 16), g: parseInt(hex.slice(3, 5), 16), b: parseInt(hex.slice(5, 7), 16) }
11
+ }
12
+
13
+ export function bold(hex?: string): string | undefined {
14
+ if (!valid(hex)) return undefined
15
+ const { r, g, b } = rgb(hex)
16
+ return `\x1b[38;2;${r};${g};${b}m\x1b[1m`
17
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * atomic-write.ts — tmp→rename atomic file write with ENOENT recovery
3
+ * Pattern from abtop (MIT)
4
+ * Deps: fs/promises, path (Node built-ins)
5
+ */
6
+
7
+ import { writeFile, rename, unlink, mkdir } from "fs/promises"
8
+ import { join, extname, dirname } from "path"
9
+
10
+ function isEnoent(e: unknown): e is { code: "ENOENT" } {
11
+ return typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "ENOENT"
12
+ }
13
+
14
+ export async function atomicWrite(filePath: string, content: string | Buffer, mode?: number): Promise<void> {
15
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`
16
+ const doWrite = async () => {
17
+ if (mode) {
18
+ await writeFile(tmp, content, { mode })
19
+ } else {
20
+ await writeFile(tmp, content)
21
+ }
22
+ await rename(tmp, filePath)
23
+ }
24
+ try {
25
+ await doWrite()
26
+ } catch (e) {
27
+ if (isEnoent(e)) {
28
+ await mkdir(dirname(filePath), { recursive: true })
29
+ await doWrite()
30
+ return
31
+ }
32
+ try { await unlink(tmp) } catch { /* ignore */ }
33
+ throw e
34
+ }
35
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * b64.ts — URL-safe Base64 + FNV-1a checksum
3
+ *
4
+ * ⚠ b64Enc normalizes `\` → `/` for cross-platform path fingerprinting.
5
+ * If `\` must be preserved, pre-encode manually.
6
+ * ⚠ Uses atob/btoa (Bun/Node safe, may chunk-fail on >100KB in browsers).
7
+ */
8
+
9
+ export function b64Enc(val: string): string {
10
+ const norm = val.replace(/\\/g, "/") // path normalization
11
+ const bytes = new TextEncoder().encode(norm)
12
+ const bin = Array.from(bytes, (b) => String.fromCharCode(b)).join("")
13
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
14
+ }
15
+
16
+ export function b64EncBytes(buffer: ArrayBuffer): string {
17
+ const bytes = new Uint8Array(buffer)
18
+ const bin = String.fromCharCode(...bytes)
19
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
20
+ }
21
+
22
+ export function b64Dec(val: string): string {
23
+ const bin = atob(val.replace(/-/g, "+").replace(/_/g, "/"))
24
+ const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0))
25
+ return new TextDecoder().decode(bytes)
26
+ }
27
+
28
+ /** FNV-1a 32-bit hash as base-36. Returns undefined for empty input. */
29
+ export function fnv1a(s: string): string | undefined {
30
+ if (!s) return undefined
31
+ let h = 0x811c9dc5
32
+ for (let i = 0; i < s.length; i++) {
33
+ h ^= s.charCodeAt(i)
34
+ h = Math.imul(h, 0x01000193)
35
+ }
36
+ return (h >>> 0).toString(36)
37
+ }
38
+
39
+ /** Hashes 5 evenly-spaced 4KB windows for large strings. Returns `"len:h1:h2:..."`. */
40
+ export function sampledHash(s: string, limit = 500_000): string | undefined {
41
+ if (!s) return undefined
42
+ if (s.length <= limit) return fnv1a(s)
43
+ const sz = 4096
44
+ const pts = [
45
+ 0,
46
+ (s.length * 0.25) | 0,
47
+ (s.length * 0.5) | 0,
48
+ (s.length * 0.75) | 0,
49
+ s.length - sz,
50
+ ]
51
+ const parts = pts
52
+ .map((p) => {
53
+ const start = Math.max(0, Math.min(s.length - sz, p - (sz >> 1)))
54
+ return fnv1a(s.slice(start, start + sz)) ?? ""
55
+ })
56
+ .join(":")
57
+ return `${s.length}:${parts}`
58
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * bash-security.ts — 23-item shell command security validator
3
+ * Deps: none (pure TS + regex)
4
+ * Ported from Claude Code bashSecurity.ts
5
+ */
6
+
7
+ const BLOCKED = new Set([
8
+ "dd", "shred", "wipefs", "sudo", "su", "pkexec", "gksudo", "kdesudo",
9
+ "sudoedit", "keylogger", "insmod", "rmmod", "modprobe",
10
+ ])
11
+ const BLOCKED_PREFIXES = ["mkfs"]
12
+ const DANGEROUS = new Set([
13
+ "rm", "rmdir", "format", "del", "erase", "remove-item", "rd",
14
+ "kill", "killall", "pkill", "taskkill",
15
+ "curl", "wget", "nc", "ncat", "socat",
16
+ "npm", "npx", "pip", "pip3", "gem", "cargo", "go",
17
+ "docker", "aws", "gcloud", "az",
18
+ ])
19
+
20
+ const INJECTION = [
21
+ /;\s*(rm|sudo|mkfs|dd|shred|format|del)\b/i,
22
+ /&&\s*(rm|sudo|mkfs|dd|shred|format|del)\b/i,
23
+ /\|\s*(rm|sudo|mkfs|dd|shred|format|del)\b/i,
24
+ /\$\([^)]*\)/, /`[^`]*`/, />\s*\/dev\//i,
25
+ /chmod\s+777/i, /chmod\s+[+-].*s/i, /chown\s+.*root/i,
26
+ /\\x[0-9a-f]{2}/i, /\\u[0-9a-f]{4}/i, /\\[0-7]{3}/, /\$'\x/,
27
+ />\s*\/etc\/passwd/i, />\s*\/etc\/shadow/i,
28
+ /cat\s+\/etc\/shadow/i, /cat\s+\/etc\/passwd.*>/i,
29
+ ]
30
+ const TRAVERSAL = [/\.\.[\\/]/, /\.\.[\\/]|\.\.$/]
31
+ const EXFIL = [
32
+ /curl.*\$\{?HOME/i, /curl.*\$HOME/i, /wget.*\$HOME/i,
33
+ /curl.*\$\{?AWS/i, /curl.*\$\{?API_KEY/i, /curl.*\$\{?SECRET/i,
34
+ /curl.*\$\{?TOKEN/i, /curl.*\$\{?PASSWORD/i,
35
+ /echo.*\$.*\|.*nc/i, /echo.*\$.*\|.*curl/i,
36
+ ]
37
+ const SHELL_ATK = [
38
+ /exec\s+rm/i, /eval\s+.*rm/i, /\bsource\s+\/dev\//i, /\.\s+\/dev\//i,
39
+ /unset\s+PATH/i, /PATH\s*=\s*""/i, /export\s+PATH\s*=\s*""/i, /\bsudoedit\b/i,
40
+ ]
41
+
42
+ export interface SecurityResult {
43
+ safe: boolean
44
+ blocked: boolean
45
+ reasons: string[]
46
+ risk: "low" | "medium" | "high" | "critical"
47
+ }
48
+
49
+ function baseCmd(cmd: string): string {
50
+ const m = cmd.trim().match(/^([A-Za-z_.\-/][A-Za-z0-9_.\-/]*)/)
51
+ if (!m) return cmd.trim().split(/\s+/)[0]?.toLowerCase() ?? ""
52
+ const parts = m[1].split("/")
53
+ return parts[parts.length - 1].toLowerCase()
54
+ }
55
+
56
+ function pipedDestructive(cmd: string): boolean {
57
+ return cmd.split(/[|;&]/).some(p => {
58
+ const b = baseCmd(p)
59
+ if (BLOCKED.has(b)) return true
60
+ return BLOCKED_PREFIXES.some(pre => b.startsWith(pre + ".") || b === pre)
61
+ })
62
+ }
63
+
64
+ function sensitivePath(t: string): boolean {
65
+ const paths = [
66
+ "/etc/passwd", "/etc/shadow", "/etc/ssh", "/root/.ssh", "/.ssh",
67
+ "\\ssh", "/boot/", "\\boot\\", "/efi/", "\\efi\\",
68
+ "/proc/sys/", "/sys/", "\\sys\\", "/dev/", "\\dev\\",
69
+ "C:\\Windows\\System32", "C:\\Windows\\SysWOW64",
70
+ ]
71
+ const lower = t.toLowerCase()
72
+ return paths.some(s => lower.includes(s.toLowerCase()))
73
+ }
74
+
75
+ function matchPats(cmd: string, pats: RegExp[], label: string, reasons: string[]): boolean {
76
+ return pats.some(p => { if (p.test(cmd)) { reasons.push(`${label}: matched ${p.source}`); return true }; return false })
77
+ }
78
+
79
+ export function validate(command: string): SecurityResult {
80
+ const reasons: string[] = []
81
+ let risk: SecurityResult["risk"] = "low"
82
+ const base = baseCmd(command)
83
+
84
+ if (BLOCKED.has(base))
85
+ return { safe: false, blocked: true, reasons: [`Blocked command: "${base}" is not allowed`], risk: "critical" }
86
+ for (const pre of BLOCKED_PREFIXES)
87
+ if (base.startsWith(pre + ".") || base === pre)
88
+ return { safe: false, blocked: true, reasons: [`Blocked command: "${base}" matches blocked prefix "${pre}"`], risk: "critical" }
89
+ if (pipedDestructive(command))
90
+ return { safe: false, blocked: true, reasons: ["Destructive command detected in pipe/chain"], risk: "critical" }
91
+ if (matchPats(command, INJECTION, "Potential injection", reasons)) risk = "high"
92
+ if (matchPats(command, SHELL_ATK, "Shell attack vector", reasons)) risk = "high"
93
+ if (matchPats(command, EXFIL, "Credential exfiltration", reasons)) risk = "critical"
94
+ if (DANGEROUS.has(base) && TRAVERSAL.some(p => p.test(command))) { reasons.push("Path traversal in destructive command context"); risk = "high" }
95
+ if (sensitivePath(command) && (base === "rm" || base === "remove-item" || base === "del")) { reasons.push("Attempting to modify sensitive system path"); risk = "critical" }
96
+
97
+ const recursive = (base === "rm" && /-rf\b|--recursive\b/.test(command)) || (base === "remove-item" && /-recurse\b/.test(command))
98
+ if (recursive) {
99
+ if (/[/~\\]\s*$/.test(command.trim()) || sensitivePath(command)) { reasons.push("Recursive delete targeting root or sensitive directory"); risk = "critical" }
100
+ else { if (risk === "low") risk = "medium"; reasons.push("Recursive delete — verify target") }
101
+ }
102
+
103
+ if (DANGEROUS.has(base) && /--force\b|-f\b/i.test(command)) { if (risk === "low") risk = "medium"; reasons.push("Force flag on potentially destructive command") }
104
+ if ((base === "curl" || base === "wget") && /\|\s*(bash|sh|zsh|fish|powershell|pwsh|cmd)/i.test(command)) { reasons.push("Download and execute pattern detected"); risk = "critical" }
105
+ if (base === "docker" && /--privileged\b/.test(command)) { reasons.push("Docker privileged mode — host access risk"); risk = "high" }
106
+ if (base === "docker" && /-v\s+\/:/i.test(command)) { reasons.push("Docker mounting root filesystem"); risk = "critical" }
107
+ if (/export\s+LD_PRELOAD/i.test(command) || /LD_PRELOAD\s*=/i.test(command)) { reasons.push("LD_PRELOAD manipulation — code injection risk"); risk = "critical" }
108
+ if (/crontab\s+-r/i.test(command)) { reasons.push("Removing all crontab entries"); risk = "high" }
109
+ if (/ssh-keygen/i.test(command) && /-N\s+""/i.test(command)) { reasons.push("Generating SSH key with empty passphrase"); if (risk === "low") risk = "medium" }
110
+ if (/\biptables\b/.test(command) && /-F\b|--flush\b/.test(command)) { reasons.push("Flushing all firewall rules"); risk = "high" }
111
+ if (/\bsystemctl\b/.test(command) && /(?:stop|disable|mask)\s+/i.test(command) && /\bsshd?\b|\bfirewalld?\b/i.test(command)) { reasons.push("Stopping/disabling critical system service"); risk = "high" }
112
+ if (/\bdd\s+.*of=\/dev\//i.test(command)) { reasons.push("dd writing directly to device — disk destruction risk"); risk = "critical" }
113
+ if (/encodedcommand/i.test(command) || (/\b-enc\b/i.test(command) && /\bpowershell\b|\bpwsh\b/i.test(command))) { reasons.push("PowerShell encoded command — obfuscation risk"); risk = "high" }
114
+ if (/executionpolicy\s+bypass/i.test(command) || /executionpolicy\s+unrestricted/i.test(command)) { reasons.push("PowerShell execution policy bypass"); risk = "high" }
115
+ if (/>\s*.*\/\.bashrc/i.test(command) || />\s*.*\/\.zshrc/i.test(command) || />\s*.*\/\.profile/i.test(command) || />\s*.*\/\.bash_profile/i.test(command)) { reasons.push("Overwriting shell configuration file"); risk = "high" }
116
+ if ((/\/etc\/resolv\.conf/i.test(command) || /\/etc\/hosts/i.test(command)) && (base === "rm" || base === "remove-item" || />/.test(command))) { reasons.push("Modifying DNS/network configuration"); risk = "high" }
117
+
118
+ return { safe: reasons.length === 0, blocked: risk === "critical", reasons, risk }
119
+ }
120
+
121
+ export function isCommandSafe(cmd: string) { return validate(cmd).safe }
122
+
123
+ export function getSecurityReport(cmd: string): string {
124
+ const r = validate(cmd)
125
+ if (r.safe) return "✓ No security concerns"
126
+ const lines = [`Security: ${r.risk.toUpperCase()}`, ...r.reasons.map(x => ` - ${x}`)]
127
+ if (r.blocked) lines.push(" BLOCKED: This command cannot be executed.")
128
+ return lines.join("\n")
129
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * bom.ts — UTF-8 BOM split/join
3
+ * Zero deps.
4
+ *
5
+ * split("\uFEFFhello") → { bom: true, text: "hello" }
6
+ * join("hello", true) → "\uFEFFhello"
7
+ */
8
+ const BOM = 0xfeff
9
+
10
+ export function split(text: string) {
11
+ if (text.charCodeAt(0) !== BOM) return { bom: false as const, text }
12
+ return { bom: true as const, text: text.slice(1) }
13
+ }
14
+
15
+ export function join(text: string, bom: boolean) {
16
+ const t = split(text).text
17
+ return bom ? String.fromCharCode(BOM) + t : t
18
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * cancel.ts — Auto-aborting AbortController + signal combiner
3
+ * Zero deps.
4
+ *
5
+ * const { signal, clearTimeout } = cancelAfter(5000)
6
+ */
7
+ export function cancelAfter(ms: number) {
8
+ const ctrl = new AbortController()
9
+ const id = setTimeout(ctrl.abort.bind(ctrl), ms)
10
+ return { controller: ctrl, signal: ctrl.signal, clearTimeout: () => globalThis.clearTimeout(id) }
11
+ }
12
+
13
+ export function cancelAny(ms: number, ...signals: AbortSignal[]) {
14
+ const t = cancelAfter(ms)
15
+ return { signal: AbortSignal.any([t.signal, ...signals]), clearTimeout: t.clearTimeout }
16
+ }
@@ -0,0 +1,12 @@
1
+ export function chop(s: string, max: number, marker = "…"): string {
2
+ if (s.length <= max) return s
3
+ return s.slice(0, max - marker.length) + marker
4
+ }
5
+
6
+ export function chopMid(s: string, max = 35): string {
7
+ if (s.length <= max) return s
8
+ const half = max - 1
9
+ const a = Math.ceil(half / 2)
10
+ const b = Math.floor(half / 2)
11
+ return s.slice(0, a) + "…" + s.slice(-b)
12
+ }
@@ -0,0 +1,3 @@
1
+ export function clamp(v: number, lo: number, hi: number): number {
2
+ return Math.min(hi, Math.max(lo, v))
3
+ }
@@ -0,0 +1,9 @@
1
+ export function compact(n: number): string {
2
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"
3
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + "K"
4
+ return String(n)
5
+ }
6
+
7
+ export function filterTruthy<T>(arr: (T | false | null | undefined | 0 | "")[]): T[] {
8
+ return arr.filter(Boolean as any) as T[]
9
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * cost-tracker.ts — Token usage & USD cost per model (session-scoped)
3
+ * Deps: none (pure TS)
4
+ * Ported from Claude Code cost-tracker.ts
5
+ */
6
+
7
+ export interface TokenUsage {
8
+ input_tokens: number
9
+ output_tokens: number
10
+ cache_read_input_tokens?: number
11
+ cache_creation_input_tokens?: number
12
+ server_tool_use?: { web_search_requests?: number }
13
+ }
14
+
15
+ export interface ModelUsage {
16
+ inputTokens: number
17
+ outputTokens: number
18
+ cacheReadInputTokens: number
19
+ cacheCreationInputTokens: number
20
+ webSearchRequests: number
21
+ costUSD: number
22
+ contextWindow: number
23
+ maxOutputTokens: number
24
+ }
25
+
26
+ interface CostState {
27
+ totalCostUSD: number
28
+ totalAPIDuration: number
29
+ totalAPIDurationWithoutRetries: number
30
+ totalToolDuration: number
31
+ totalLinesAdded: number
32
+ totalLinesRemoved: number
33
+ totalDuration: number
34
+ lastDuration: number | undefined
35
+ modelUsage: Record<string, ModelUsage>
36
+ }
37
+
38
+ function empty(): CostState {
39
+ return {
40
+ totalCostUSD: 0, totalAPIDuration: 0, totalAPIDurationWithoutRetries: 0,
41
+ totalToolDuration: 0, totalLinesAdded: 0, totalLinesRemoved: 0,
42
+ totalDuration: 0, lastDuration: undefined, modelUsage: {},
43
+ }
44
+ }
45
+
46
+ const state: CostState = empty()
47
+
48
+ export function reset() { Object.assign(state, empty()) }
49
+ export function getTotalCost() { return state.totalCostUSD }
50
+ export function getDuration() { return state.totalDuration }
51
+ export function getAPIDuration() { return state.totalAPIDuration }
52
+ export function getLines() { return { added: state.totalLinesAdded, removed: state.totalLinesRemoved } }
53
+ export function getModelUsage() { return state.modelUsage }
54
+ export function getUsageForModel(model: string) { return state.modelUsage[model] }
55
+ export function addLines(added: number, removed: number) {
56
+ state.totalLinesAdded += added; state.totalLinesRemoved += removed
57
+ }
58
+ export function addAPIDuration(ms: number, withoutRetries: number) {
59
+ state.totalAPIDuration += ms; state.totalAPIDurationWithoutRetries += withoutRetries
60
+ }
61
+ export function addToolDuration(ms: number) { state.totalToolDuration += ms }
62
+
63
+ export function addSessionCost(cost: number, usage: TokenUsage, model: string): number {
64
+ if (!state.modelUsage[model]) {
65
+ state.modelUsage[model] = {
66
+ inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0,
67
+ cacheCreationInputTokens: 0, webSearchRequests: 0,
68
+ costUSD: 0, contextWindow: 0, maxOutputTokens: 0,
69
+ }
70
+ }
71
+ const m = state.modelUsage[model]
72
+ m.inputTokens += usage.input_tokens
73
+ m.outputTokens += usage.output_tokens
74
+ m.cacheReadInputTokens += usage.cache_read_input_tokens ?? 0
75
+ m.cacheCreationInputTokens += usage.cache_creation_input_tokens ?? 0
76
+ m.webSearchRequests += usage.server_tool_use?.web_search_requests ?? 0
77
+ m.costUSD += cost
78
+ state.totalCostUSD += cost
79
+ return cost
80
+ }
81
+
82
+ export function formatCost(cost: number, decimals = 4) {
83
+ return `$${cost > 0.5 ? cost.toFixed(2) : cost.toFixed(decimals)}`
84
+ }
85
+
86
+ function fmtDur(ms: number) {
87
+ if (ms < 1000) return `${Math.round(ms)}ms`
88
+ const s = ms / 1000
89
+ if (s < 60) return `${s.toFixed(1)}s`
90
+ return `${Math.floor(s / 60)}m${Math.round(s % 60)}s`
91
+ }
92
+
93
+ function fmtNum(n: number) { return n.toLocaleString() }
94
+
95
+ export function formatTotalCost() {
96
+ const l = state
97
+ const usageLines = Object.keys(l.modelUsage).length === 0
98
+ ? "Usage: 0 input, 0 output, 0 cache read, 0 cache write"
99
+ : [
100
+ "Usage by model:",
101
+ ...Object.entries(l.modelUsage).map(([model, u]) =>
102
+ `${model}:`.padStart(21) +
103
+ `${fmtNum(u.inputTokens)} input, ${fmtNum(u.outputTokens)} output, ` +
104
+ `${fmtNum(u.cacheReadInputTokens)} cache read, ${fmtNum(u.cacheCreationInputTokens)} cache write` +
105
+ (u.webSearchRequests > 0 ? `, ${fmtNum(u.webSearchRequests)} web search` : "") +
106
+ ` (${formatCost(u.costUSD)})`
107
+ ),
108
+ ].join("\n")
109
+ return [
110
+ `Total cost: ${formatCost(l.totalCostUSD)}`,
111
+ `Total duration (API): ${fmtDur(l.totalAPIDuration)}`,
112
+ `Total duration (wall): ${fmtDur(l.totalDuration)}`,
113
+ `Total code changes: ${l.totalLinesAdded} ${l.totalLinesAdded === 1 ? "line" : "lines"} added, ${l.totalLinesRemoved} ${l.totalLinesRemoved === 1 ? "line" : "lines"} removed`,
114
+ usageLines,
115
+ ].join("\n")
116
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * dataurl.ts — Encode/decode data: URLs
3
+ * Zero deps (uses Buffer for base64).
4
+ *
5
+ * decode("data:text/plain;base64,aGVsbG8=") → "hello"
6
+ * encode("text/plain", buf) → "data:text/plain;base64,aGVsbG8="
7
+ * parse("data:text/plain;base64,aGVsbG8=") → { mime: "text/plain", base64: "aGVsbG8=" }
8
+ */
9
+ export function decode(url: string): string {
10
+ const i = url.indexOf(",")
11
+ if (i === -1) return ""
12
+ const head = url.slice(0, i)
13
+ const body = url.slice(i + 1)
14
+ if (head.includes(";base64")) return Buffer.from(body, "base64").toString("utf8")
15
+ return decodeURIComponent(body)
16
+ }
17
+
18
+ export function encode(mime: string, data: string | ArrayBuffer | Uint8Array): string {
19
+ const base64 = typeof data === "string"
20
+ ? Buffer.from(data).toString("base64")
21
+ : Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data)).toString("base64")
22
+ return `data:${mime};base64,${base64}`
23
+ }
24
+
25
+ export function parse(url: string): { mime: string; base64: string; data: string } | undefined {
26
+ const match = url.match(/^data:([^;]+);base64,(.*)$/)
27
+ if (!match) return undefined
28
+ return { mime: match[1], base64: match[2], data: decode(url) }
29
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * delay.ts — Abort-aware setTimeout
3
+ * Ported from gemini-cli (Apache-2.0)
4
+ * Deps: none
5
+ */
6
+ export function createAbortError(): Error {
7
+ const e = new Error("Aborted")
8
+ e.name = "AbortError"
9
+ return e
10
+ }
11
+
12
+ export function delay(ms: number, signal?: AbortSignal): Promise<void> {
13
+ if (!signal) return new Promise(r => setTimeout(r, ms))
14
+ if (signal.aborted) return Promise.reject(createAbortError())
15
+ return new Promise((resolve, reject) => {
16
+ const onAbort = () => {
17
+ clearTimeout(tid)
18
+ signal.removeEventListener("abort", onAbort)
19
+ reject(createAbortError())
20
+ }
21
+ const tid = setTimeout(() => {
22
+ signal.removeEventListener("abort", onAbort)
23
+ resolve()
24
+ }, ms)
25
+ signal.addEventListener("abort", onAbort, { once: true })
26
+ })
27
+ }