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.
- package/.turbo/turbo-typecheck.log +1 -0
- package/AGENTS.md +72 -0
- package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
- package/Dockerfile +18 -0
- package/assets/saeeol.ico +0 -0
- package/bin/saeeol.cjs +0 -0
- package/database.db +0 -0
- package/drizzle.config.ts +10 -0
- package/git +0 -0
- package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
- package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
- package/migration/20260211171708_add_project_commands/migration.sql +1 -0
- package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
- package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
- package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
- package/migration/20260225215848_workspace/migration.sql +7 -0
- package/migration/20260225215848_workspace/snapshot.json +959 -0
- package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
- package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
- package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
- package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
- package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
- package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
- package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
- package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
- package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
- package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
- package/migration/20260323234822_events/migration.sql +13 -0
- package/migration/20260323234822_events/snapshot.json +1271 -0
- package/migration/20260410174513_workspace-name/migration.sql +16 -0
- package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
- package/migration/20260413175956_chief_energizer/migration.sql +13 -0
- package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
- package/migration/20260423070820_add_icon_url_override/migration.sql +2 -0
- package/migration/20260423070820_add_icon_url_override/snapshot.json +1409 -0
- package/migration/20260428004200_add_session_path/migration.sql +1 -0
- package/migration/20260428004200_add_session_path/snapshot.json +1419 -0
- package/npm/bin/saeeol +42 -0
- package/npm/package.json +39 -0
- package/npm/postinstall.js +162 -0
- package/package.json +201 -207
- package/parsers-config.ts +289 -0
- package/script/build.ts +393 -0
- package/script/check-migrations.ts +16 -0
- package/script/fix-node-pty.ts +34 -0
- package/script/generate.ts +23 -0
- package/script/postinstall.mjs +189 -0
- package/script/publish.ts +200 -0
- package/script/run-workspace-server +106 -0
- package/script/schema.ts +63 -0
- package/script/test-runner.ts +420 -0
- package/script/time.ts +6 -0
- package/script/trace-imports.ts +153 -0
- package/script/upgrade-opentui.ts +64 -0
- package/scripts/diff-sdk-types.sh +52 -0
- package/specs/effect/facades.md +221 -0
- package/specs/effect/http-api.md +401 -0
- package/specs/effect/instance-context.md +309 -0
- package/specs/effect/loose-ends.md +34 -0
- package/specs/effect/migration.md +299 -0
- package/specs/effect/routes.md +64 -0
- package/specs/effect/schema.md +399 -0
- package/specs/effect/server-package.md +668 -0
- package/specs/effect/tools.md +90 -0
- package/specs/tui-plugins.md +433 -0
- package/specs/v2/api.ts +67 -0
- package/specs/v2/keymappings.md +10 -0
- package/specs/v2/message-shape.md +136 -0
- package/src/acp/agent-message.ts +1 -1
- package/src/acp/agent-utils.ts +1 -1
- package/src/boxes/ansi.ts +17 -0
- package/src/boxes/atomic-write.ts +35 -0
- package/src/boxes/b64.ts +58 -0
- package/src/boxes/bash-security.ts +129 -0
- package/src/boxes/bom.ts +18 -0
- package/src/boxes/cancel.ts +16 -0
- package/src/boxes/chop.ts +12 -0
- package/src/boxes/clamp.ts +3 -0
- package/src/boxes/compact.ts +9 -0
- package/src/boxes/cost-tracker.ts +116 -0
- package/src/boxes/dataurl.ts +29 -0
- package/src/boxes/delay.ts +27 -0
- package/src/boxes/diff-apply.ts +53 -0
- package/src/boxes/disposable.ts +13 -0
- package/src/boxes/err.ts +34 -0
- package/src/boxes/human.ts +47 -0
- package/src/boxes/iife.ts +9 -0
- package/src/boxes/latch.ts +8 -0
- package/src/boxes/memory.ts +198 -0
- package/src/boxes/net.ts +16 -0
- package/src/boxes/plural.ts +4 -0
- package/src/boxes/puny.ts +21 -0
- package/src/boxes/retry.ts +49 -0
- package/src/boxes/rwlock.ts +41 -0
- package/src/boxes/schedule.ts +71 -0
- package/src/boxes/scope.ts +21 -0
- package/src/boxes/tokens.ts +9 -0
- package/src/boxes/ttl-cache.ts +63 -0
- package/src/boxes/typed-event.ts +51 -0
- package/src/boxes/uid.ts +50 -0
- package/src/boxes/wave6.test.ts +296 -0
- package/src/boxes/wildcard.ts +58 -0
- package/src/bus/global.ts +1 -1
- package/src/cli/cmd/github-run-api.ts +2 -2
- package/src/cli/cmd/run-events.ts +2 -2
- package/src/cli/cmd/tui/component/logo.tsx +1 -1
- package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +2 -2
- package/src/cli/cmd/tui/context/app/editor-zed.ts +1 -1
- package/src/cli/cmd/tui/context/app/editor.ts +1 -1
- package/src/cli/cmd/tui/context/app/theme.tsx +1 -1
- package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
- package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
- package/src/overlay/cost-tracker/format.ts +1 -1
- package/src/overlay/cost-tracker/index.ts +4 -4
- package/src/overlay/cost-tracker/state.ts +2 -2
- package/src/overlay/cost-tracker/types.ts +2 -2
- package/src/overlay/memory/age.ts +1 -1
- package/src/overlay/memory/index.ts +4 -4
- package/src/overlay/memory/paths.ts +2 -2
- package/src/overlay/memory/scan.ts +1 -1
- package/src/overlay/memory/types.ts +2 -2
- package/src/overlay/tool/bash-security.ts +3 -3
- package/src/overlay/util/url.ts +1 -1
- package/src/plugin/codex-auth.ts +1 -1
- package/src/provider/model-cache.ts +2 -2
- package/src/provider/provider-resolve.ts +3 -3
- package/src/provider/transform-message.ts +1 -1
- package/src/server/routes/game.ts +284 -0
- package/src/server/server.ts +2 -0
- package/src/session/core/compaction/compaction-helpers.ts +1 -1
- package/src/session/core/compaction/compaction.ts +1 -1
- package/src/session/core/session.ts +2 -0
- package/src/sessions/ingest-queue.ts +2 -2
- package/src/sessions/remote-ws.ts +1 -1
- package/src/tool/workflow/question.ts +1 -1
- package/src/util/abort.ts +1 -1
- package/src/util/bom.ts +2 -2
- package/src/util/color.ts +1 -1
- package/src/util/data-url.ts +1 -1
- package/src/util/defer.ts +1 -1
- package/src/util/error.ts +2 -2
- package/src/util/filesystem.ts +2 -2
- package/src/util/format.ts +1 -1
- package/src/util/iife.ts +1 -1
- package/src/util/local-context.ts +1 -1
- package/src/util/locale.ts +2 -2
- package/src/util/lock.ts +1 -1
- package/src/util/network.ts +1 -1
- package/src/util/signal.ts +1 -1
- package/src/util/token.ts +1 -1
- package/src/util/wildcard.ts +1 -1
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +133 -0
- package/test/account/repo.test.ts +352 -0
- package/test/account/service.test.ts +456 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +725 -0
- package/test/agent/agent.test.ts +890 -0
- package/test/auth/auth.test.ts +86 -0
- package/test/bun/registry.test.ts +75 -0
- package/test/bus/bus-effect.test.ts +161 -0
- package/test/bus/bus-integration.test.ts +87 -0
- package/test/bus/bus.test.ts +219 -0
- package/test/cli/account.test.ts +26 -0
- package/test/cli/auto-mode.test.ts +75 -0
- package/test/cli/bin-saeeol.test.ts +8 -0
- package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
- package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
- package/test/cli/cmd/tui/sync.test.tsx +159 -0
- package/test/cli/error.test.ts +18 -0
- package/test/cli/github-action.test.ts +198 -0
- package/test/cli/github-remote.test.ts +85 -0
- package/test/cli/import.test.ts +97 -0
- package/test/cli/install-artifact.test.ts +72 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/pr.test.ts +59 -0
- package/test/cli/tui/editor-context-zed.test.ts +356 -0
- package/test/cli/tui/editor-context.test.tsx +228 -0
- package/test/cli/tui/keybind-plugin.test.ts +90 -0
- package/test/cli/tui/markdown.test.ts +161 -0
- package/test/cli/tui/plugin-add.test.ts +111 -0
- package/test/cli/tui/plugin-install.test.ts +87 -0
- package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
- package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
- package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
- package/test/cli/tui/plugin-loader.test.ts +816 -0
- package/test/cli/tui/plugin-toggle.test.ts +157 -0
- package/test/cli/tui/revert-diff.test.ts +35 -0
- package/test/cli/tui/slot-replace.test.tsx +47 -0
- package/test/cli/tui/theme-store.test.ts +54 -0
- package/test/cli/tui/thread.test.ts +28 -0
- package/test/cli/tui/transcript.test.ts +426 -0
- package/test/cli/tui/usage.test.ts +60 -0
- package/test/cli/tui/use-event.test.tsx +175 -0
- package/test/config/agent-color.test.ts +67 -0
- package/test/config/config.test.ts +2544 -0
- package/test/config/fixtures/empty-frontmatter.md +4 -0
- package/test/config/fixtures/frontmatter.md +28 -0
- package/test/config/fixtures/markdown-header.md +11 -0
- package/test/config/fixtures/no-frontmatter.md +1 -0
- package/test/config/fixtures/weird-model-id.md +13 -0
- package/test/config/lsp.test.ts +87 -0
- package/test/config/markdown.test.ts +228 -0
- package/test/config/plugin.test.ts +0 -0
- package/test/config/tui.test.ts +624 -0
- package/test/control-plane/adapters.test.ts +71 -0
- package/test/control-plane/workspace.test.ts +1526 -0
- package/test/effect/app-runtime-logger.test.ts +98 -0
- package/test/effect/config-service.test.ts +65 -0
- package/test/effect/instance-state.test.ts +394 -0
- package/test/effect/run-service.test.ts +89 -0
- package/test/effect/runner.test.ts +523 -0
- package/test/fake/provider.ts +82 -0
- package/test/file/fsmonitor.test.ts +68 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +954 -0
- package/test/file/path-traversal.test.ts +205 -0
- package/test/file/ripgrep.test.ts +226 -0
- package/test/file/watcher.test.ts +249 -0
- package/test/filesystem/filesystem.test.ts +319 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.test.ts +26 -0
- package/test/fixture/fixture.ts +175 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/log-init-worker.ts +62 -0
- package/test/fixture/lsp/fake-lsp-server.js +249 -0
- package/test/fixture/plug-worker.ts +93 -0
- package/test/fixture/plugin-meta-worker.ts +19 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
- package/test/fixture/skills/cloudflare/SKILL.md +211 -0
- package/test/fixture/skills/index.json +6 -0
- package/test/fixture/tui-plugin.ts +323 -0
- package/test/fixture/tui-runtime.ts +31 -0
- package/test/format/format.test.ts +272 -0
- package/test/git/git.test.ts +128 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/installation/installation.test.ts +168 -0
- package/test/keybind.test.ts +421 -0
- package/test/lib/effect.ts +53 -0
- package/test/lib/filesystem.ts +10 -0
- package/test/lib/llm-server.ts +778 -0
- package/test/lib/websocket.ts +46 -0
- package/test/lsp/client.test.ts +482 -0
- package/test/lsp/index.test.ts +160 -0
- package/test/lsp/launch.test.ts +22 -0
- package/test/lsp/lifecycle.test.ts +184 -0
- package/test/ltm/ltm.test.ts +230 -0
- package/test/mcp/headers.test.ts +178 -0
- package/test/mcp/lifecycle.test.ts +787 -0
- package/test/mcp/oauth-auto-connect.test.ts +311 -0
- package/test/mcp/oauth-browser.test.ts +276 -0
- package/test/mcp/oauth-callback.test.ts +34 -0
- package/test/memory/abort-leak-webfetch.ts +49 -0
- package/test/memory/abort-leak.test.ts +128 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +1227 -0
- package/test/permission/next.toConfig.test.ts +110 -0
- package/test/permission-task.test.ts +326 -0
- package/test/plugin/auth-override.test.ts +79 -0
- package/test/plugin/cloudflare.test.ts +68 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/plugin/github-copilot-models.test.ts +261 -0
- package/test/plugin/install-concurrency.test.ts +140 -0
- package/test/plugin/install.test.ts +570 -0
- package/test/plugin/loader-shared.test.ts +1169 -0
- package/test/plugin/meta.test.ts +137 -0
- package/test/plugin/plugin-contract.test.ts +291 -0
- package/test/plugin/shared.test.ts +88 -0
- package/test/plugin/trigger.test.ts +102 -0
- package/test/plugin/workspace-adapter.test.ts +109 -0
- package/test/preload.ts +77 -0
- package/test/project/instance.test.ts +276 -0
- package/test/project/migrate-global.test.ts +152 -0
- package/test/project/project.test.ts +600 -0
- package/test/project/vcs.test.ts +286 -0
- package/test/project/worktree-remove.test.ts +126 -0
- package/test/project/worktree.test.ts +223 -0
- package/test/provider/amazon-bedrock.test.ts +462 -0
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
- package/test/provider/gitlab-duo.test.ts +413 -0
- package/test/provider/local.test.ts +208 -0
- package/test/provider/models.test.ts +261 -0
- package/test/provider/provider-category.test.ts +190 -0
- package/test/provider/provider.test.ts +2758 -0
- package/test/provider/transform.test.ts +3681 -0
- package/test/pty/pty-output-isolation.test.ts +147 -0
- package/test/pty/pty-session.test.ts +102 -0
- package/test/pty/pty-shell.test.ts +104 -0
- package/test/question/question.test.ts +490 -0
- package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
- package/test/saeeol/agent-manager-tool.test.ts +71 -0
- package/test/saeeol/agent-permission-overrides.test.ts +75 -0
- package/test/saeeol/agent-skill-permissions.test.ts +37 -0
- package/test/saeeol/ask-agent-permissions.test.ts +303 -0
- package/test/saeeol/bash-hierarchy.test.ts +64 -0
- package/test/saeeol/bash-permission-metadata.test.ts +66 -0
- package/test/saeeol/bash-security-extended.test.ts +243 -0
- package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
- package/test/saeeol/boxes-integration.test.ts +415 -0
- package/test/saeeol/builtin-skills.test.ts +75 -0
- package/test/saeeol/cleanup.ts +28 -0
- package/test/saeeol/cli/dev-setup.test.ts +74 -0
- package/test/saeeol/cli/roll-call.test.ts +161 -0
- package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
- package/test/saeeol/codex-auth-refresh.test.ts +124 -0
- package/test/saeeol/commit-message/generate.test.ts +188 -0
- package/test/saeeol/commit-message/git-context.test.ts +303 -0
- package/test/saeeol/commit-message-windows.test.ts +38 -0
- package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
- package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
- package/test/saeeol/compaction-skip-guard.test.ts +224 -0
- package/test/saeeol/compaction-smart-select.test.ts +100 -0
- package/test/saeeol/config/config.test.ts +166 -0
- package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
- package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
- package/test/saeeol/config-gitignore.test.ts +70 -0
- package/test/saeeol/config-injector.test.ts +305 -0
- package/test/saeeol/config-resilience.test.ts +234 -0
- package/test/saeeol/config-validation.test.ts +183 -0
- package/test/saeeol/cost-propagation.test.ts +94 -0
- package/test/saeeol/cost-tracker-extended.test.ts +141 -0
- package/test/saeeol/cost-tracker.test.ts +64 -0
- package/test/saeeol/custom-provider-delete.test.ts +149 -0
- package/test/saeeol/diff-full.test.ts +226 -0
- package/test/saeeol/edit-permission-filediff.test.ts +223 -0
- package/test/saeeol/encoding.test.ts +364 -0
- package/test/saeeol/enhance-prompt.test.ts +61 -0
- package/test/saeeol/ensure-plan-dir.test.ts +32 -0
- package/test/saeeol/errors.test.ts +144 -0
- package/test/saeeol/external-directory-boundary.test.ts +96 -0
- package/test/saeeol/gateway-headers.test.ts +88 -0
- package/test/saeeol/help.test.ts +191 -0
- package/test/saeeol/ignore-migrator.test.ts +308 -0
- package/test/saeeol/indexing-auth.test.ts +45 -0
- package/test/saeeol/indexing-feature.test.ts +44 -0
- package/test/saeeol/indexing-label.test.ts +70 -0
- package/test/saeeol/indexing-startup.test.ts +381 -0
- package/test/saeeol/indexing-worktree.test.ts +73 -0
- package/test/saeeol/instruction.test.ts +136 -0
- package/test/saeeol/lancedb-runtime.test.ts +116 -0
- package/test/saeeol/loader-auth.test.ts +168 -0
- package/test/saeeol/local-model.test.ts +621 -0
- package/test/saeeol/logo.test.ts +31 -0
- package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
- package/test/saeeol/mcp-branding.test.ts +33 -0
- package/test/saeeol/mcp-docker-rm.test.ts +32 -0
- package/test/saeeol/mcp-migrator.test.ts +736 -0
- package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
- package/test/saeeol/memory-io.test.ts +198 -0
- package/test/saeeol/memory-paths.test.ts +87 -0
- package/test/saeeol/memory-security.test.ts +166 -0
- package/test/saeeol/model-cache-org.test.ts +164 -0
- package/test/saeeol/model-info-panel-utils.test.ts +52 -0
- package/test/saeeol/model-info-panel.types.test.ts +7 -0
- package/test/saeeol/models-401-fallback.test.ts +52 -0
- package/test/saeeol/modes-migrator.test.ts +320 -0
- package/test/saeeol/nvidia-headers.test.ts +74 -0
- package/test/saeeol/patch-jsonc.test.ts +73 -0
- package/test/saeeol/patch.test.ts +172 -0
- package/test/saeeol/paths.test.ts +265 -0
- package/test/saeeol/permission/config-paths.test.ts +174 -0
- package/test/saeeol/permission/env-read.test.ts +149 -0
- package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
- package/test/saeeol/permission/next.always-rules.test.ts +882 -0
- package/test/saeeol/permission/next.reply-http.test.ts +205 -0
- package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
- package/test/saeeol/plan-exit-detection.test.ts +494 -0
- package/test/saeeol/plan-followup.test.ts +1376 -0
- package/test/saeeol/project-config-update.test.ts +120 -0
- package/test/saeeol/project-id.test.ts +455 -0
- package/test/saeeol/provider-cost.test.ts +171 -0
- package/test/saeeol/provider-list-failed-state.test.ts +100 -0
- package/test/saeeol/question-dismiss-all.test.ts +174 -0
- package/test/saeeol/read-directory.test.ts +116 -0
- package/test/saeeol/rules-migrator.test.ts +257 -0
- package/test/saeeol/run-auto.test.ts +176 -0
- package/test/saeeol/run-network.test.ts +224 -0
- package/test/saeeol/semantic-search.test.ts +186 -0
- package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
- package/test/saeeol/session/instruction-substitution.test.ts +72 -0
- package/test/saeeol/session/platform-attribution.test.ts +118 -0
- package/test/saeeol/session/session.test.ts +105 -0
- package/test/saeeol/session-compaction-cap.test.ts +399 -0
- package/test/saeeol/session-compaction-chunks.test.ts +501 -0
- package/test/saeeol/session-compaction-safety.test.ts +481 -0
- package/test/saeeol/session-fork-remap.test.ts +251 -0
- package/test/saeeol/session-import-service.test.ts +114 -0
- package/test/saeeol/session-list.test.ts +47 -0
- package/test/saeeol/session-message-metadata.test.ts +128 -0
- package/test/saeeol/session-overflow.test.ts +78 -0
- package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
- package/test/saeeol/session-processor-network-offline.test.ts +204 -0
- package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
- package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
- package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
- package/test/saeeol/session-prompt-queue.test.ts +815 -0
- package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
- package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
- package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
- package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
- package/test/saeeol/sessions/remote-ws.test.ts +367 -0
- package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
- package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
- package/test/saeeol/snapshot-cache.test.ts +84 -0
- package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
- package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
- package/test/saeeol/stats-subagent-cost.test.ts +123 -0
- package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
- package/test/saeeol/suggestion/suggestion.test.ts +145 -0
- package/test/saeeol/suggestion/tool.test.ts +298 -0
- package/test/saeeol/summary-file-diff.test.ts +28 -0
- package/test/saeeol/system-prompt.test.ts +142 -0
- package/test/saeeol/task-nesting.test.ts +193 -0
- package/test/saeeol/telemetry/feedback.test.ts +8 -0
- package/test/saeeol/todo-view.test.ts +57 -0
- package/test/saeeol/tool-encoding.test.ts +455 -0
- package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
- package/test/saeeol/tool-registry-indexing.test.ts +236 -0
- package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
- package/test/saeeol/tool-task-model.test.ts +352 -0
- package/test/saeeol/transform-opus-4.7.test.ts +89 -0
- package/test/saeeol/tui-diff.test.ts +91 -0
- package/test/saeeol/tui-sync.test.ts +80 -0
- package/test/saeeol/util/url.test.ts +141 -0
- package/test/saeeol/workflows-migrator.test.ts +261 -0
- package/test/saeeol/worktree-diff-summary.test.ts +64 -0
- package/test/saeeol/worktree-diff.test.ts +223 -0
- package/test/saeeol/worktree-remove-lock.test.ts +82 -0
- package/test/server/AGENTS.md +15 -0
- package/test/server/contract.test.ts +357 -0
- package/test/server/experimental-session-list.test.ts +157 -0
- package/test/server/global-session-list.test.ts +155 -0
- package/test/server/httpapi-authorization.test.ts +103 -0
- package/test/server/httpapi-bridge.test.ts +440 -0
- package/test/server/httpapi-config.test.ts +67 -0
- package/test/server/httpapi-cors.test.ts +89 -0
- package/test/server/httpapi-event.test.ts +57 -0
- package/test/server/httpapi-experimental.test.ts +219 -0
- package/test/server/httpapi-file.test.ts +79 -0
- package/test/server/httpapi-instance-context.test.ts +237 -0
- package/test/server/httpapi-instance.legacy.test.ts +140 -0
- package/test/server/httpapi-instance.test.ts +83 -0
- package/test/server/httpapi-json-parity.test.ts +263 -0
- package/test/server/httpapi-mcp-oauth.test.ts +76 -0
- package/test/server/httpapi-mcp.test.ts +189 -0
- package/test/server/httpapi-provider.test.ts +153 -0
- package/test/server/httpapi-pty-websocket.test.ts +16 -0
- package/test/server/httpapi-pty.test.ts +175 -0
- package/test/server/httpapi-raw-route-auth.test.ts +89 -0
- package/test/server/httpapi-sdk.test.ts +681 -0
- package/test/server/httpapi-session.test.ts +464 -0
- package/test/server/httpapi-sync.test.ts +130 -0
- package/test/server/httpapi-tui.test.ts +121 -0
- package/test/server/httpapi-workspace-routing.test.ts +471 -0
- package/test/server/httpapi-workspace.test.ts +427 -0
- package/test/server/lib/conformance.ts +88 -0
- package/test/server/lib/stateful.ts +112 -0
- package/test/server/project-init-git.test.ts +113 -0
- package/test/server/proxy-util.test.ts +113 -0
- package/test/server/session-actions.test.ts +49 -0
- package/test/server/session-list.test.ts +238 -0
- package/test/server/session-messages.test.ts +167 -0
- package/test/server/session-select.test.ts +100 -0
- package/test/server/trace-attributes.test.ts +76 -0
- package/test/server/workspace-proxy.test.ts +165 -0
- package/test/server/workspace-routing.test.ts +85 -0
- package/test/session/compaction.test.ts +2420 -0
- package/test/session/instruction.test.ts +247 -0
- package/test/session/llm.test.ts +1273 -0
- package/test/session/message-v2.test.ts +1291 -0
- package/test/session/messages-pagination.test.ts +1173 -0
- package/test/session/network.test.ts +249 -0
- package/test/session/processor-effect.test.ts +847 -0
- package/test/session/prompt.test.ts +2131 -0
- package/test/session/retry.test.ts +340 -0
- package/test/session/revert-compact.test.ts +639 -0
- package/test/session/schema-decoding.test.ts +311 -0
- package/test/session/session-entry-stepper.test.ts +917 -0
- package/test/session/session-schema.test.ts +76 -0
- package/test/session/snapshot-tool-race.test.ts +257 -0
- package/test/session/structured-output-integration.test.ts +265 -0
- package/test/session/structured-output.test.ts +381 -0
- package/test/session/system.test.ts +73 -0
- package/test/share/share-next.test.ts +333 -0
- package/test/shell/shell.test.ts +99 -0
- package/test/skill/discovery.test.ts +116 -0
- package/test/skill/skill.test.ts +393 -0
- package/test/snapshot/snapshot.test.ts +1531 -0
- package/test/storage/db.test.ts +23 -0
- package/test/storage/json-migration.test.ts +832 -0
- package/test/storage/storage.test.ts +293 -0
- package/test/suggestion/suggestion.test.ts +1 -0
- package/test/sync/index.test.ts +256 -0
- package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/apply_patch.test.ts +614 -0
- package/test/tool/bash.test.ts +1225 -0
- package/test/tool/diagnostics-filter.test.ts +55 -0
- package/test/tool/edit.test.ts +754 -0
- package/test/tool/external-directory.test.ts +169 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +65179 -0
- package/test/tool/glob.test.ts +107 -0
- package/test/tool/grep.test.ts +114 -0
- package/test/tool/lsp.test.ts +187 -0
- package/test/tool/parameters.test.ts +243 -0
- package/test/tool/question.test.ts +129 -0
- package/test/tool/read.test.ts +500 -0
- package/test/tool/recall.test.ts +151 -0
- package/test/tool/registry.test.ts +203 -0
- package/test/tool/skill.test.ts +135 -0
- package/test/tool/suggest.test.ts +1 -0
- package/test/tool/task.test.ts +612 -0
- package/test/tool/tool-define.test.ts +99 -0
- package/test/tool/truncation.test.ts +260 -0
- package/test/tool/webfetch.test.ts +103 -0
- package/test/tool/write.test.ts +291 -0
- package/test/util/data-url.test.ts +14 -0
- package/test/util/effect-zod.test.ts +754 -0
- package/test/util/error.test.ts +38 -0
- package/test/util/filesystem.test.ts +656 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/glob.test.ts +164 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/lock.test.ts +72 -0
- package/test/util/log.test.ts +86 -0
- package/test/util/module.test.ts +59 -0
- package/test/util/process.test.ts +128 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/which.test.ts +100 -0
- package/test/util/wildcard.test.ts +90 -0
- package/test/workspace/workspace-restore.test.ts +296 -0
- package/src/provider/models-snapshot.d.ts +0 -2
- 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.
|
package/src/acp/agent-message.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse as parseDataUrl } from "
|
|
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"
|
package/src/acp/agent-utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse as parseDataUrl } from "
|
|
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
|
+
}
|
package/src/boxes/b64.ts
ADDED
|
@@ -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
|
+
}
|
package/src/boxes/bom.ts
ADDED
|
@@ -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,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
|
+
}
|