@stonerzju/opencode 1.2.16-offline.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/AGENTS.md +10 -0
- package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/opencode +179 -0
- package/bunfig.toml +7 -0
- package/drizzle.config.ts +10 -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/package.json +140 -0
- package/package.json.bak +140 -0
- package/parsers-config.ts +254 -0
- package/script/build.ts +224 -0
- package/script/check-migrations.ts +16 -0
- package/script/postinstall.mjs +131 -0
- package/script/publish.ts +181 -0
- package/script/schema.ts +63 -0
- package/script/seed-e2e.ts +50 -0
- package/src/acp/README.md +174 -0
- package/src/acp/agent.ts +1741 -0
- package/src/acp/session.ts +116 -0
- package/src/acp/types.ts +23 -0
- package/src/agent/agent.ts +339 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +14 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/auth/index.ts +68 -0
- package/src/bun/index.ts +131 -0
- package/src/bun/registry.ts +50 -0
- package/src/bus/bus-event.ts +43 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +105 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/acp.ts +70 -0
- package/src/cli/cmd/agent.ts +257 -0
- package/src/cli/cmd/auth.ts +449 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/db.ts +118 -0
- package/src/cli/cmd/debug/agent.ts +167 -0
- package/src/cli/cmd/debug/config.ts +16 -0
- package/src/cli/cmd/debug/file.ts +97 -0
- package/src/cli/cmd/debug/index.ts +48 -0
- package/src/cli/cmd/debug/lsp.ts +52 -0
- package/src/cli/cmd/debug/ripgrep.ts +87 -0
- package/src/cli/cmd/debug/scrap.ts +16 -0
- package/src/cli/cmd/debug/skill.ts +16 -0
- package/src/cli/cmd/debug/snapshot.ts +52 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1631 -0
- package/src/cli/cmd/import.ts +170 -0
- package/src/cli/cmd/mcp.ts +754 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +625 -0
- package/src/cli/cmd/serve.ts +31 -0
- package/src/cli/cmd/session.ts +156 -0
- package/src/cli/cmd/stats.ts +410 -0
- package/src/cli/cmd/tui/app.tsx +845 -0
- package/src/cli/cmd/tui/attach.ts +88 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +147 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +165 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +259 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +167 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/logo.tsx +85 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +667 -0
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1155 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
- package/src/cli/cmd/tui/component/tips.tsx +152 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/context/args.tsx +15 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +53 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +102 -0
- package/src/cli/cmd/tui/context/kv.tsx +52 -0
- package/src/cli/cmd/tui/context/local.tsx +406 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +46 -0
- package/src/cli/cmd/tui/context/sdk.tsx +101 -0
- package/src/cli/cmd/tui/context/sync.tsx +488 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +249 -0
- package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1152 -0
- package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/event.ts +48 -0
- package/src/cli/cmd/tui/routes/home.tsx +145 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +135 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +2219 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +685 -0
- package/src/cli/cmd/tui/routes/session/question.tsx +466 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
- package/src/cli/cmd/tui/thread.ts +199 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +85 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +207 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +80 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +401 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +182 -0
- package/src/cli/cmd/tui/ui/link.tsx +28 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +164 -0
- package/src/cli/cmd/tui/util/editor.ts +33 -0
- package/src/cli/cmd/tui/util/selection.ts +25 -0
- package/src/cli/cmd/tui/util/signal.ts +7 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/util/transcript.ts +98 -0
- package/src/cli/cmd/tui/win32.ts +129 -0
- package/src/cli/cmd/tui/worker.ts +157 -0
- package/src/cli/cmd/uninstall.ts +356 -0
- package/src/cli/cmd/upgrade.ts +73 -0
- package/src/cli/cmd/web.ts +81 -0
- package/src/cli/cmd/workspace-serve.ts +16 -0
- package/src/cli/error.ts +57 -0
- package/src/cli/logo.ts +6 -0
- package/src/cli/network.ts +60 -0
- package/src/cli/ui.ts +116 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +150 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +101 -0
- package/src/config/config.ts +1408 -0
- package/src/config/markdown.ts +99 -0
- package/src/config/migrate-tui-config.ts +155 -0
- package/src/config/paths.ts +174 -0
- package/src/config/tui-schema.ts +34 -0
- package/src/config/tui.ts +118 -0
- package/src/control/control.sql.ts +22 -0
- package/src/control/index.ts +67 -0
- package/src/control-plane/adaptors/index.ts +10 -0
- package/src/control-plane/adaptors/types.ts +7 -0
- package/src/control-plane/adaptors/worktree.ts +26 -0
- package/src/control-plane/config.ts +10 -0
- package/src/control-plane/session-proxy-middleware.ts +46 -0
- package/src/control-plane/sse.ts +66 -0
- package/src/control-plane/workspace-server/routes.ts +33 -0
- package/src/control-plane/workspace-server/server.ts +24 -0
- package/src/control-plane/workspace.sql.ts +12 -0
- package/src/control-plane/workspace.ts +160 -0
- package/src/env/index.ts +28 -0
- package/src/file/ignore.ts +82 -0
- package/src/file/index.ts +646 -0
- package/src/file/ripgrep.ts +372 -0
- package/src/file/time.ts +71 -0
- package/src/file/watcher.ts +128 -0
- package/src/flag/flag.ts +109 -0
- package/src/format/formatter.ts +395 -0
- package/src/format/index.ts +140 -0
- package/src/global/index.ts +54 -0
- package/src/id/id.ts +84 -0
- package/src/ide/index.ts +76 -0
- package/src/index.ts +210 -0
- package/src/installation/index.ts +266 -0
- package/src/lsp/client.ts +251 -0
- package/src/lsp/index.ts +485 -0
- package/src/lsp/language.ts +120 -0
- package/src/lsp/server.ts +2142 -0
- package/src/mcp/auth.ts +130 -0
- package/src/mcp/index.ts +937 -0
- package/src/mcp/oauth-callback.ts +200 -0
- package/src/mcp/oauth-provider.ts +176 -0
- package/src/patch/index.ts +680 -0
- package/src/permission/arity.ts +163 -0
- package/src/permission/index.ts +210 -0
- package/src/permission/next.ts +286 -0
- package/src/plugin/codex.ts +624 -0
- package/src/plugin/copilot.ts +327 -0
- package/src/plugin/index.ts +143 -0
- package/src/project/bootstrap.ts +33 -0
- package/src/project/instance.ts +114 -0
- package/src/project/project.sql.ts +15 -0
- package/src/project/project.ts +441 -0
- package/src/project/state.ts +70 -0
- package/src/project/vcs.ts +76 -0
- package/src/provider/auth.ts +147 -0
- package/src/provider/error.ts +189 -0
- package/src/provider/models.ts +146 -0
- package/src/provider/provider.ts +1338 -0
- package/src/provider/sdk/copilot/README.md +5 -0
- package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +164 -0
- package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
- package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +17 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +780 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +87 -0
- package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
- package/src/provider/sdk/copilot/index.ts +2 -0
- package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
- package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
- package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1732 -0
- package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/copilot/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/copilot/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/copilot/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/copilot/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +955 -0
- package/src/pty/index.ts +324 -0
- package/src/question/index.ts +171 -0
- package/src/scheduler/index.ts +61 -0
- package/src/server/error.ts +36 -0
- package/src/server/event.ts +7 -0
- package/src/server/mdns.ts +60 -0
- package/src/server/routes/config.ts +92 -0
- package/src/server/routes/experimental.ts +270 -0
- package/src/server/routes/file.ts +197 -0
- package/src/server/routes/global.ts +185 -0
- package/src/server/routes/mcp.ts +225 -0
- package/src/server/routes/permission.ts +68 -0
- package/src/server/routes/project.ts +82 -0
- package/src/server/routes/provider.ts +165 -0
- package/src/server/routes/pty.ts +200 -0
- package/src/server/routes/question.ts +98 -0
- package/src/server/routes/session.ts +974 -0
- package/src/server/routes/tui.ts +379 -0
- package/src/server/routes/workspace.ts +104 -0
- package/src/server/server.ts +623 -0
- package/src/session/compaction.ts +261 -0
- package/src/session/index.ts +877 -0
- package/src/session/instruction.ts +192 -0
- package/src/session/llm.ts +279 -0
- package/src/session/message-v2.ts +899 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +421 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex_header.txt +79 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/session/prompt/plan.txt +26 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt/trinity.txt +97 -0
- package/src/session/prompt.ts +1959 -0
- package/src/session/retry.ts +101 -0
- package/src/session/revert.ts +138 -0
- package/src/session/session.sql.ts +88 -0
- package/src/session/status.ts +76 -0
- package/src/session/summary.ts +161 -0
- package/src/session/system.ts +54 -0
- package/src/session/todo.ts +56 -0
- package/src/share/share-next.ts +210 -0
- package/src/share/share.sql.ts +13 -0
- package/src/shell/shell.ts +68 -0
- package/src/skill/discovery.ts +98 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +189 -0
- package/src/snapshot/index.ts +297 -0
- package/src/sql.d.ts +4 -0
- package/src/storage/db.ts +155 -0
- package/src/storage/json-migration.ts +425 -0
- package/src/storage/schema.sql.ts +10 -0
- package/src/storage/schema.ts +5 -0
- package/src/storage/storage.ts +220 -0
- package/src/tool/apply_patch.ts +281 -0
- package/src/tool/apply_patch.txt +33 -0
- package/src/tool/bash.ts +274 -0
- package/src/tool/bash.txt +115 -0
- package/src/tool/batch.ts +181 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/codesearch.ts +132 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +654 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/external-directory.ts +32 -0
- package/src/tool/glob.ts +78 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +156 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +121 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp.ts +97 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/plan-enter.txt +14 -0
- package/src/tool/plan-exit.txt +13 -0
- package/src/tool/plan.ts +131 -0
- package/src/tool/question.ts +33 -0
- package/src/tool/question.txt +10 -0
- package/src/tool/read.ts +293 -0
- package/src/tool/read.txt +14 -0
- package/src/tool/registry.ts +173 -0
- package/src/tool/skill.ts +123 -0
- package/src/tool/task.ts +165 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +53 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +89 -0
- package/src/tool/truncation.ts +107 -0
- package/src/tool/webfetch.ts +206 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +14 -0
- package/src/tool/write.ts +84 -0
- package/src/tool/write.txt +8 -0
- package/src/util/abort.ts +35 -0
- package/src/util/archive.ts +16 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +189 -0
- package/src/util/fn.ts +11 -0
- package/src/util/format.ts +20 -0
- package/src/util/git.ts +35 -0
- package/src/util/glob.ts +34 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +103 -0
- package/src/util/lazy.ts +23 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +182 -0
- package/src/util/process.ts +126 -0
- package/src/util/proxied.ts +3 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +66 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +59 -0
- package/src/worktree/index.ts +643 -0
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +81 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +683 -0
- package/test/agent/agent.test.ts +689 -0
- package/test/bun.test.ts +53 -0
- package/test/cli/github-action.test.ts +197 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/cli/import.test.ts +38 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/tui/transcript.test.ts +322 -0
- package/test/config/agent-color.test.ts +71 -0
- package/test/config/config.test.ts +1886 -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/markdown.test.ts +228 -0
- package/test/config/tui.test.ts +510 -0
- package/test/control-plane/session-proxy-middleware.test.ts +147 -0
- package/test/control-plane/sse.test.ts +56 -0
- package/test/control-plane/workspace-server-sse.test.ts +65 -0
- package/test/control-plane/workspace-sync.test.ts +97 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +394 -0
- package/test/file/path-traversal.test.ts +198 -0
- package/test/file/ripgrep.test.ts +39 -0
- package/test/file/time.test.ts +361 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.ts +45 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
- package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
- package/test/fixture/skills/cloudflare/SKILL.md +211 -0
- package/test/fixture/skills/index.json +6 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/keybind.test.ts +421 -0
- package/test/lsp/client.test.ts +95 -0
- package/test/mcp/headers.test.ts +153 -0
- package/test/mcp/oauth-browser.test.ts +249 -0
- package/test/memory/abort-leak.test.ts +136 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +689 -0
- package/test/permission-task.test.ts +319 -0
- package/test/plugin/auth-override.test.ts +44 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/preload.ts +80 -0
- package/test/project/project.test.ts +348 -0
- package/test/project/worktree-remove.test.ts +65 -0
- package/test/provider/amazon-bedrock.test.ts +446 -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 +262 -0
- package/test/provider/provider.test.ts +2220 -0
- package/test/provider/transform.test.ts +2353 -0
- package/test/pty/pty-output-isolation.test.ts +140 -0
- package/test/question/question.test.ts +300 -0
- package/test/scheduler.test.ts +73 -0
- package/test/server/global-session-list.test.ts +89 -0
- package/test/server/session-list.test.ts +90 -0
- package/test/server/session-select.test.ts +78 -0
- package/test/session/compaction.test.ts +423 -0
- package/test/session/instruction.test.ts +170 -0
- package/test/session/llm.test.ts +667 -0
- package/test/session/message-v2.test.ts +924 -0
- package/test/session/prompt.test.ts +211 -0
- package/test/session/retry.test.ts +188 -0
- package/test/session/revert-compact.test.ts +285 -0
- package/test/session/session.test.ts +71 -0
- package/test/session/structured-output-integration.test.ts +233 -0
- package/test/session/structured-output.test.ts +385 -0
- package/test/skill/discovery.test.ts +110 -0
- package/test/skill/skill.test.ts +388 -0
- package/test/snapshot/snapshot.test.ts +1180 -0
- package/test/storage/json-migration.test.ts +846 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/apply_patch.test.ts +566 -0
- package/test/tool/bash.test.ts +402 -0
- package/test/tool/edit.test.ts +496 -0
- package/test/tool/external-directory.test.ts +127 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +38413 -0
- package/test/tool/grep.test.ts +110 -0
- package/test/tool/question.test.ts +107 -0
- package/test/tool/read.test.ts +504 -0
- package/test/tool/registry.test.ts +122 -0
- package/test/tool/skill.test.ts +112 -0
- package/test/tool/truncation.test.ts +160 -0
- package/test/tool/webfetch.test.ts +100 -0
- package/test/tool/write.test.ts +348 -0
- package/test/util/filesystem.test.ts +443 -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/process.test.ts +59 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/wildcard.test.ts +90 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { ReadTool } from "../../src/tool/read"
|
|
4
|
+
import { Instance } from "../../src/project/instance"
|
|
5
|
+
import { Filesystem } from "../../src/util/filesystem"
|
|
6
|
+
import { tmpdir } from "../fixture/fixture"
|
|
7
|
+
import { PermissionNext } from "../../src/permission/next"
|
|
8
|
+
import { Agent } from "../../src/agent/agent"
|
|
9
|
+
|
|
10
|
+
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
|
|
11
|
+
|
|
12
|
+
const ctx = {
|
|
13
|
+
sessionID: "test",
|
|
14
|
+
messageID: "",
|
|
15
|
+
callID: "",
|
|
16
|
+
agent: "build",
|
|
17
|
+
abort: AbortSignal.any([]),
|
|
18
|
+
messages: [],
|
|
19
|
+
metadata: () => {},
|
|
20
|
+
ask: async () => {},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("tool.read external_directory permission", () => {
|
|
24
|
+
test("allows reading absolute path inside project directory", async () => {
|
|
25
|
+
await using tmp = await tmpdir({
|
|
26
|
+
init: async (dir) => {
|
|
27
|
+
await Bun.write(path.join(dir, "test.txt"), "hello world")
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
await Instance.provide({
|
|
31
|
+
directory: tmp.path,
|
|
32
|
+
fn: async () => {
|
|
33
|
+
const read = await ReadTool.init()
|
|
34
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "test.txt") }, ctx)
|
|
35
|
+
expect(result.output).toContain("hello world")
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("allows reading file in subdirectory inside project directory", async () => {
|
|
41
|
+
await using tmp = await tmpdir({
|
|
42
|
+
init: async (dir) => {
|
|
43
|
+
await Bun.write(path.join(dir, "subdir", "test.txt"), "nested content")
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
await Instance.provide({
|
|
47
|
+
directory: tmp.path,
|
|
48
|
+
fn: async () => {
|
|
49
|
+
const read = await ReadTool.init()
|
|
50
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "test.txt") }, ctx)
|
|
51
|
+
expect(result.output).toContain("nested content")
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("asks for external_directory permission when reading absolute path outside project", async () => {
|
|
57
|
+
await using outerTmp = await tmpdir({
|
|
58
|
+
init: async (dir) => {
|
|
59
|
+
await Bun.write(path.join(dir, "secret.txt"), "secret data")
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
await using tmp = await tmpdir({ git: true })
|
|
63
|
+
await Instance.provide({
|
|
64
|
+
directory: tmp.path,
|
|
65
|
+
fn: async () => {
|
|
66
|
+
const read = await ReadTool.init()
|
|
67
|
+
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
68
|
+
const testCtx = {
|
|
69
|
+
...ctx,
|
|
70
|
+
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
71
|
+
requests.push(req)
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
await read.execute({ filePath: path.join(outerTmp.path, "secret.txt") }, testCtx)
|
|
75
|
+
const extDirReq = requests.find((r) => r.permission === "external_directory")
|
|
76
|
+
expect(extDirReq).toBeDefined()
|
|
77
|
+
expect(extDirReq!.patterns.some((p) => p.includes(outerTmp.path.replaceAll("\\", "/")))).toBe(true)
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("asks for directory-scoped external_directory permission when reading external directory", async () => {
|
|
83
|
+
await using outerTmp = await tmpdir({
|
|
84
|
+
init: async (dir) => {
|
|
85
|
+
await Bun.write(path.join(dir, "external", "a.txt"), "a")
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
await using tmp = await tmpdir({ git: true })
|
|
89
|
+
await Instance.provide({
|
|
90
|
+
directory: tmp.path,
|
|
91
|
+
fn: async () => {
|
|
92
|
+
const read = await ReadTool.init()
|
|
93
|
+
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
94
|
+
const testCtx = {
|
|
95
|
+
...ctx,
|
|
96
|
+
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
97
|
+
requests.push(req)
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
await read.execute({ filePath: path.join(outerTmp.path, "external") }, testCtx)
|
|
101
|
+
const extDirReq = requests.find((r) => r.permission === "external_directory")
|
|
102
|
+
expect(extDirReq).toBeDefined()
|
|
103
|
+
expect(extDirReq!.patterns).toContain(path.join(outerTmp.path, "external", "*").replaceAll("\\", "/"))
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test("asks for external_directory permission when reading relative path outside project", async () => {
|
|
109
|
+
await using tmp = await tmpdir({ git: true })
|
|
110
|
+
await Instance.provide({
|
|
111
|
+
directory: tmp.path,
|
|
112
|
+
fn: async () => {
|
|
113
|
+
const read = await ReadTool.init()
|
|
114
|
+
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
115
|
+
const testCtx = {
|
|
116
|
+
...ctx,
|
|
117
|
+
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
118
|
+
requests.push(req)
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
// This will fail because file doesn't exist, but we can check if permission was asked
|
|
122
|
+
await read.execute({ filePath: "../outside.txt" }, testCtx).catch(() => {})
|
|
123
|
+
const extDirReq = requests.find((r) => r.permission === "external_directory")
|
|
124
|
+
expect(extDirReq).toBeDefined()
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("does not ask for external_directory permission when reading inside project", async () => {
|
|
130
|
+
await using tmp = await tmpdir({
|
|
131
|
+
git: true,
|
|
132
|
+
init: async (dir) => {
|
|
133
|
+
await Bun.write(path.join(dir, "internal.txt"), "internal content")
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
await Instance.provide({
|
|
137
|
+
directory: tmp.path,
|
|
138
|
+
fn: async () => {
|
|
139
|
+
const read = await ReadTool.init()
|
|
140
|
+
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
141
|
+
const testCtx = {
|
|
142
|
+
...ctx,
|
|
143
|
+
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
144
|
+
requests.push(req)
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
await read.execute({ filePath: path.join(tmp.path, "internal.txt") }, testCtx)
|
|
148
|
+
const extDirReq = requests.find((r) => r.permission === "external_directory")
|
|
149
|
+
expect(extDirReq).toBeUndefined()
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe("tool.read env file permissions", () => {
|
|
156
|
+
const cases: [string, boolean][] = [
|
|
157
|
+
[".env", true],
|
|
158
|
+
[".env.local", true],
|
|
159
|
+
[".env.production", true],
|
|
160
|
+
[".env.development.local", true],
|
|
161
|
+
[".env.example", false],
|
|
162
|
+
[".envrc", false],
|
|
163
|
+
["environment.ts", false],
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
describe.each(["build", "plan"])("agent=%s", (agentName) => {
|
|
167
|
+
test.each(cases)("%s asks=%s", async (filename, shouldAsk) => {
|
|
168
|
+
await using tmp = await tmpdir({
|
|
169
|
+
init: (dir) => Bun.write(path.join(dir, filename), "content"),
|
|
170
|
+
})
|
|
171
|
+
await Instance.provide({
|
|
172
|
+
directory: tmp.path,
|
|
173
|
+
fn: async () => {
|
|
174
|
+
const agent = await Agent.get(agentName)
|
|
175
|
+
let askedForEnv = false
|
|
176
|
+
const ctxWithPermissions = {
|
|
177
|
+
...ctx,
|
|
178
|
+
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
179
|
+
for (const pattern of req.patterns) {
|
|
180
|
+
const rule = PermissionNext.evaluate(req.permission, pattern, agent.permission)
|
|
181
|
+
if (rule.action === "ask" && req.permission === "read") {
|
|
182
|
+
askedForEnv = true
|
|
183
|
+
}
|
|
184
|
+
if (rule.action === "deny") {
|
|
185
|
+
throw new PermissionNext.DeniedError(agent.permission)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
const read = await ReadTool.init()
|
|
191
|
+
await read.execute({ filePath: path.join(tmp.path, filename) }, ctxWithPermissions)
|
|
192
|
+
expect(askedForEnv).toBe(shouldAsk)
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe("tool.read truncation", () => {
|
|
200
|
+
test("truncates large file by bytes and sets truncated metadata", async () => {
|
|
201
|
+
await using tmp = await tmpdir({
|
|
202
|
+
init: async (dir) => {
|
|
203
|
+
const base = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json"))
|
|
204
|
+
const target = 60 * 1024
|
|
205
|
+
const content = base.length >= target ? base : base.repeat(Math.ceil(target / base.length))
|
|
206
|
+
await Filesystem.write(path.join(dir, "large.json"), content)
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
await Instance.provide({
|
|
210
|
+
directory: tmp.path,
|
|
211
|
+
fn: async () => {
|
|
212
|
+
const read = await ReadTool.init()
|
|
213
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "large.json") }, ctx)
|
|
214
|
+
expect(result.metadata.truncated).toBe(true)
|
|
215
|
+
expect(result.output).toContain("Output capped at")
|
|
216
|
+
expect(result.output).toContain("Use offset=")
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test("truncates by line count when limit is specified", async () => {
|
|
222
|
+
await using tmp = await tmpdir({
|
|
223
|
+
init: async (dir) => {
|
|
224
|
+
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
|
|
225
|
+
await Bun.write(path.join(dir, "many-lines.txt"), lines)
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
await Instance.provide({
|
|
229
|
+
directory: tmp.path,
|
|
230
|
+
fn: async () => {
|
|
231
|
+
const read = await ReadTool.init()
|
|
232
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "many-lines.txt"), limit: 10 }, ctx)
|
|
233
|
+
expect(result.metadata.truncated).toBe(true)
|
|
234
|
+
expect(result.output).toContain("Showing lines 1-10 of 100")
|
|
235
|
+
expect(result.output).toContain("Use offset=11")
|
|
236
|
+
expect(result.output).toContain("line0")
|
|
237
|
+
expect(result.output).toContain("line9")
|
|
238
|
+
expect(result.output).not.toContain("line10")
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test("does not truncate small file", async () => {
|
|
244
|
+
await using tmp = await tmpdir({
|
|
245
|
+
init: async (dir) => {
|
|
246
|
+
await Bun.write(path.join(dir, "small.txt"), "hello world")
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
await Instance.provide({
|
|
250
|
+
directory: tmp.path,
|
|
251
|
+
fn: async () => {
|
|
252
|
+
const read = await ReadTool.init()
|
|
253
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "small.txt") }, ctx)
|
|
254
|
+
expect(result.metadata.truncated).toBe(false)
|
|
255
|
+
expect(result.output).toContain("End of file")
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test("respects offset parameter", async () => {
|
|
261
|
+
await using tmp = await tmpdir({
|
|
262
|
+
init: async (dir) => {
|
|
263
|
+
const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`).join("\n")
|
|
264
|
+
await Bun.write(path.join(dir, "offset.txt"), lines)
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
await Instance.provide({
|
|
268
|
+
directory: tmp.path,
|
|
269
|
+
fn: async () => {
|
|
270
|
+
const read = await ReadTool.init()
|
|
271
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "offset.txt"), offset: 10, limit: 5 }, ctx)
|
|
272
|
+
expect(result.output).toContain("10: line10")
|
|
273
|
+
expect(result.output).toContain("14: line14")
|
|
274
|
+
expect(result.output).not.toContain("9: line10")
|
|
275
|
+
expect(result.output).not.toContain("15: line15")
|
|
276
|
+
expect(result.output).toContain("line10")
|
|
277
|
+
expect(result.output).toContain("line14")
|
|
278
|
+
expect(result.output).not.toContain("line0")
|
|
279
|
+
expect(result.output).not.toContain("line15")
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test("throws when offset is beyond end of file", async () => {
|
|
285
|
+
await using tmp = await tmpdir({
|
|
286
|
+
init: async (dir) => {
|
|
287
|
+
const lines = Array.from({ length: 3 }, (_, i) => `line${i + 1}`).join("\n")
|
|
288
|
+
await Bun.write(path.join(dir, "short.txt"), lines)
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
await Instance.provide({
|
|
292
|
+
directory: tmp.path,
|
|
293
|
+
fn: async () => {
|
|
294
|
+
const read = await ReadTool.init()
|
|
295
|
+
await expect(
|
|
296
|
+
read.execute({ filePath: path.join(tmp.path, "short.txt"), offset: 4, limit: 5 }, ctx),
|
|
297
|
+
).rejects.toThrow("Offset 4 is out of range for this file (3 lines)")
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test("allows reading empty file at default offset", async () => {
|
|
303
|
+
await using tmp = await tmpdir({
|
|
304
|
+
init: async (dir) => {
|
|
305
|
+
await Bun.write(path.join(dir, "empty.txt"), "")
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
await Instance.provide({
|
|
309
|
+
directory: tmp.path,
|
|
310
|
+
fn: async () => {
|
|
311
|
+
const read = await ReadTool.init()
|
|
312
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "empty.txt") }, ctx)
|
|
313
|
+
expect(result.metadata.truncated).toBe(false)
|
|
314
|
+
expect(result.output).toContain("End of file - total 0 lines")
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test("throws when offset > 1 for empty file", async () => {
|
|
320
|
+
await using tmp = await tmpdir({
|
|
321
|
+
init: async (dir) => {
|
|
322
|
+
await Bun.write(path.join(dir, "empty.txt"), "")
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
await Instance.provide({
|
|
326
|
+
directory: tmp.path,
|
|
327
|
+
fn: async () => {
|
|
328
|
+
const read = await ReadTool.init()
|
|
329
|
+
await expect(read.execute({ filePath: path.join(tmp.path, "empty.txt"), offset: 2 }, ctx)).rejects.toThrow(
|
|
330
|
+
"Offset 2 is out of range for this file (0 lines)",
|
|
331
|
+
)
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test("does not mark final directory page as truncated", async () => {
|
|
337
|
+
await using tmp = await tmpdir({
|
|
338
|
+
init: async (dir) => {
|
|
339
|
+
await Promise.all(
|
|
340
|
+
Array.from({ length: 10 }, (_, i) => Bun.write(path.join(dir, "dir", `file-${i + 1}.txt`), `line${i}`)),
|
|
341
|
+
)
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
await Instance.provide({
|
|
345
|
+
directory: tmp.path,
|
|
346
|
+
fn: async () => {
|
|
347
|
+
const read = await ReadTool.init()
|
|
348
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "dir"), offset: 6, limit: 5 }, ctx)
|
|
349
|
+
expect(result.metadata.truncated).toBe(false)
|
|
350
|
+
expect(result.output).not.toContain("Showing 5 of 10 entries")
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test("truncates long lines", async () => {
|
|
356
|
+
await using tmp = await tmpdir({
|
|
357
|
+
init: async (dir) => {
|
|
358
|
+
const longLine = "x".repeat(3000)
|
|
359
|
+
await Bun.write(path.join(dir, "long-line.txt"), longLine)
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
await Instance.provide({
|
|
363
|
+
directory: tmp.path,
|
|
364
|
+
fn: async () => {
|
|
365
|
+
const read = await ReadTool.init()
|
|
366
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "long-line.txt") }, ctx)
|
|
367
|
+
expect(result.output).toContain("(line truncated to 2000 chars)")
|
|
368
|
+
expect(result.output.length).toBeLessThan(3000)
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test("image files set truncated to false", async () => {
|
|
374
|
+
await using tmp = await tmpdir({
|
|
375
|
+
init: async (dir) => {
|
|
376
|
+
// 1x1 red PNG
|
|
377
|
+
const png = Buffer.from(
|
|
378
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==",
|
|
379
|
+
"base64",
|
|
380
|
+
)
|
|
381
|
+
await Bun.write(path.join(dir, "image.png"), png)
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
await Instance.provide({
|
|
385
|
+
directory: tmp.path,
|
|
386
|
+
fn: async () => {
|
|
387
|
+
const read = await ReadTool.init()
|
|
388
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "image.png") }, ctx)
|
|
389
|
+
expect(result.metadata.truncated).toBe(false)
|
|
390
|
+
expect(result.attachments).toBeDefined()
|
|
391
|
+
expect(result.attachments?.length).toBe(1)
|
|
392
|
+
expect(result.attachments?.[0]).not.toHaveProperty("id")
|
|
393
|
+
expect(result.attachments?.[0]).not.toHaveProperty("sessionID")
|
|
394
|
+
expect(result.attachments?.[0]).not.toHaveProperty("messageID")
|
|
395
|
+
},
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test("large image files are properly attached without error", async () => {
|
|
400
|
+
await Instance.provide({
|
|
401
|
+
directory: FIXTURES_DIR,
|
|
402
|
+
fn: async () => {
|
|
403
|
+
const read = await ReadTool.init()
|
|
404
|
+
const result = await read.execute({ filePath: path.join(FIXTURES_DIR, "large-image.png") }, ctx)
|
|
405
|
+
expect(result.metadata.truncated).toBe(false)
|
|
406
|
+
expect(result.attachments).toBeDefined()
|
|
407
|
+
expect(result.attachments?.length).toBe(1)
|
|
408
|
+
expect(result.attachments?.[0].type).toBe("file")
|
|
409
|
+
expect(result.attachments?.[0]).not.toHaveProperty("id")
|
|
410
|
+
expect(result.attachments?.[0]).not.toHaveProperty("sessionID")
|
|
411
|
+
expect(result.attachments?.[0]).not.toHaveProperty("messageID")
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test(".fbs files (FlatBuffers schema) are read as text, not images", async () => {
|
|
417
|
+
await using tmp = await tmpdir({
|
|
418
|
+
init: async (dir) => {
|
|
419
|
+
// FlatBuffers schema content
|
|
420
|
+
const fbsContent = `namespace MyGame;
|
|
421
|
+
|
|
422
|
+
table Monster {
|
|
423
|
+
pos:Vec3;
|
|
424
|
+
name:string;
|
|
425
|
+
inventory:[ubyte];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
root_type Monster;`
|
|
429
|
+
await Bun.write(path.join(dir, "schema.fbs"), fbsContent)
|
|
430
|
+
},
|
|
431
|
+
})
|
|
432
|
+
await Instance.provide({
|
|
433
|
+
directory: tmp.path,
|
|
434
|
+
fn: async () => {
|
|
435
|
+
const read = await ReadTool.init()
|
|
436
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "schema.fbs") }, ctx)
|
|
437
|
+
// Should be read as text, not as image
|
|
438
|
+
expect(result.attachments).toBeUndefined()
|
|
439
|
+
expect(result.output).toContain("namespace MyGame")
|
|
440
|
+
expect(result.output).toContain("table Monster")
|
|
441
|
+
},
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
describe("tool.read loaded instructions", () => {
|
|
447
|
+
test("loads AGENTS.md from parent directory and includes in metadata", async () => {
|
|
448
|
+
await using tmp = await tmpdir({
|
|
449
|
+
init: async (dir) => {
|
|
450
|
+
await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Test Instructions\nDo something special.")
|
|
451
|
+
await Bun.write(path.join(dir, "subdir", "nested", "test.txt"), "test content")
|
|
452
|
+
},
|
|
453
|
+
})
|
|
454
|
+
await Instance.provide({
|
|
455
|
+
directory: tmp.path,
|
|
456
|
+
fn: async () => {
|
|
457
|
+
const read = await ReadTool.init()
|
|
458
|
+
const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "nested", "test.txt") }, ctx)
|
|
459
|
+
expect(result.output).toContain("test content")
|
|
460
|
+
expect(result.output).toContain("system-reminder")
|
|
461
|
+
expect(result.output).toContain("Test Instructions")
|
|
462
|
+
expect(result.metadata.loaded).toBeDefined()
|
|
463
|
+
expect(result.metadata.loaded).toContain(path.join(tmp.path, "subdir", "AGENTS.md"))
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
describe("tool.read binary detection", () => {
|
|
470
|
+
test("rejects text extension files with null bytes", async () => {
|
|
471
|
+
await using tmp = await tmpdir({
|
|
472
|
+
init: async (dir) => {
|
|
473
|
+
const bytes = Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x77, 0x6f, 0x72, 0x6c, 0x64])
|
|
474
|
+
await Bun.write(path.join(dir, "null-byte.txt"), bytes)
|
|
475
|
+
},
|
|
476
|
+
})
|
|
477
|
+
await Instance.provide({
|
|
478
|
+
directory: tmp.path,
|
|
479
|
+
fn: async () => {
|
|
480
|
+
const read = await ReadTool.init()
|
|
481
|
+
await expect(read.execute({ filePath: path.join(tmp.path, "null-byte.txt") }, ctx)).rejects.toThrow(
|
|
482
|
+
"Cannot read binary file",
|
|
483
|
+
)
|
|
484
|
+
},
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test("rejects known binary extensions", async () => {
|
|
489
|
+
await using tmp = await tmpdir({
|
|
490
|
+
init: async (dir) => {
|
|
491
|
+
await Bun.write(path.join(dir, "module.wasm"), "not really wasm")
|
|
492
|
+
},
|
|
493
|
+
})
|
|
494
|
+
await Instance.provide({
|
|
495
|
+
directory: tmp.path,
|
|
496
|
+
fn: async () => {
|
|
497
|
+
const read = await ReadTool.init()
|
|
498
|
+
await expect(read.execute({ filePath: path.join(tmp.path, "module.wasm") }, ctx)).rejects.toThrow(
|
|
499
|
+
"Cannot read binary file",
|
|
500
|
+
)
|
|
501
|
+
},
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
import { tmpdir } from "../fixture/fixture"
|
|
5
|
+
import { Instance } from "../../src/project/instance"
|
|
6
|
+
import { ToolRegistry } from "../../src/tool/registry"
|
|
7
|
+
|
|
8
|
+
describe("tool.registry", () => {
|
|
9
|
+
test("loads tools from .opencode/tool (singular)", async () => {
|
|
10
|
+
await using tmp = await tmpdir({
|
|
11
|
+
init: async (dir) => {
|
|
12
|
+
const opencodeDir = path.join(dir, ".opencode")
|
|
13
|
+
await fs.mkdir(opencodeDir, { recursive: true })
|
|
14
|
+
|
|
15
|
+
const toolDir = path.join(opencodeDir, "tool")
|
|
16
|
+
await fs.mkdir(toolDir, { recursive: true })
|
|
17
|
+
|
|
18
|
+
await Bun.write(
|
|
19
|
+
path.join(toolDir, "hello.ts"),
|
|
20
|
+
[
|
|
21
|
+
"export default {",
|
|
22
|
+
" description: 'hello tool',",
|
|
23
|
+
" args: {},",
|
|
24
|
+
" execute: async () => {",
|
|
25
|
+
" return 'hello world'",
|
|
26
|
+
" },",
|
|
27
|
+
"}",
|
|
28
|
+
"",
|
|
29
|
+
].join("\n"),
|
|
30
|
+
)
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
await Instance.provide({
|
|
35
|
+
directory: tmp.path,
|
|
36
|
+
fn: async () => {
|
|
37
|
+
const ids = await ToolRegistry.ids()
|
|
38
|
+
expect(ids).toContain("hello")
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("loads tools from .opencode/tools (plural)", async () => {
|
|
44
|
+
await using tmp = await tmpdir({
|
|
45
|
+
init: async (dir) => {
|
|
46
|
+
const opencodeDir = path.join(dir, ".opencode")
|
|
47
|
+
await fs.mkdir(opencodeDir, { recursive: true })
|
|
48
|
+
|
|
49
|
+
const toolsDir = path.join(opencodeDir, "tools")
|
|
50
|
+
await fs.mkdir(toolsDir, { recursive: true })
|
|
51
|
+
|
|
52
|
+
await Bun.write(
|
|
53
|
+
path.join(toolsDir, "hello.ts"),
|
|
54
|
+
[
|
|
55
|
+
"export default {",
|
|
56
|
+
" description: 'hello tool',",
|
|
57
|
+
" args: {},",
|
|
58
|
+
" execute: async () => {",
|
|
59
|
+
" return 'hello world'",
|
|
60
|
+
" },",
|
|
61
|
+
"}",
|
|
62
|
+
"",
|
|
63
|
+
].join("\n"),
|
|
64
|
+
)
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await Instance.provide({
|
|
69
|
+
directory: tmp.path,
|
|
70
|
+
fn: async () => {
|
|
71
|
+
const ids = await ToolRegistry.ids()
|
|
72
|
+
expect(ids).toContain("hello")
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("loads tools with external dependencies without crashing", async () => {
|
|
78
|
+
await using tmp = await tmpdir({
|
|
79
|
+
init: async (dir) => {
|
|
80
|
+
const opencodeDir = path.join(dir, ".opencode")
|
|
81
|
+
await fs.mkdir(opencodeDir, { recursive: true })
|
|
82
|
+
|
|
83
|
+
const toolsDir = path.join(opencodeDir, "tools")
|
|
84
|
+
await fs.mkdir(toolsDir, { recursive: true })
|
|
85
|
+
|
|
86
|
+
await Bun.write(
|
|
87
|
+
path.join(opencodeDir, "package.json"),
|
|
88
|
+
JSON.stringify({
|
|
89
|
+
name: "custom-tools",
|
|
90
|
+
dependencies: {
|
|
91
|
+
"@opencode-ai/plugin": "^0.0.0",
|
|
92
|
+
cowsay: "^1.6.0",
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
await Bun.write(
|
|
98
|
+
path.join(toolsDir, "cowsay.ts"),
|
|
99
|
+
[
|
|
100
|
+
"import { say } from 'cowsay'",
|
|
101
|
+
"export default {",
|
|
102
|
+
" description: 'tool that imports cowsay at top level',",
|
|
103
|
+
" args: { text: { type: 'string' } },",
|
|
104
|
+
" execute: async ({ text }: { text: string }) => {",
|
|
105
|
+
" return say({ text })",
|
|
106
|
+
" },",
|
|
107
|
+
"}",
|
|
108
|
+
"",
|
|
109
|
+
].join("\n"),
|
|
110
|
+
)
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
await Instance.provide({
|
|
115
|
+
directory: tmp.path,
|
|
116
|
+
fn: async () => {
|
|
117
|
+
const ids = await ToolRegistry.ids()
|
|
118
|
+
expect(ids).toContain("cowsay")
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|