@stonerzju/opencode 1.2.17 → 1.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/bin/opencode +29 -157
  2. package/package.json +29 -29
  3. package/src/acp/agent.ts +4 -4
  4. package/src/acp/session.ts +1 -1
  5. package/src/agent/agent.ts +3 -3
  6. package/src/bun/index.ts +2 -2
  7. package/src/cli/cmd/acp.ts +3 -3
  8. package/src/cli/cmd/debug/file.ts +1 -1
  9. package/src/cli/cmd/github.ts +2 -2
  10. package/src/cli/cmd/pr.ts +1 -1
  11. package/src/cli/cmd/tui/app.tsx +24 -24
  12. package/src/cli/cmd/tui/attach.ts +3 -3
  13. package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
  14. package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
  15. package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
  16. package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
  17. package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
  18. package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
  19. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
  20. package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
  21. package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
  22. package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
  23. package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
  24. package/src/cli/cmd/tui/component/logo.tsx +2 -2
  25. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
  26. package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
  27. package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
  28. package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
  29. package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
  30. package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
  31. package/src/cli/cmd/tui/component/tips.tsx +1 -1
  32. package/src/cli/cmd/tui/context/directory.ts +1 -1
  33. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  34. package/src/cli/cmd/tui/context/keybind.tsx +2 -2
  35. package/src/cli/cmd/tui/context/kv.tsx +2 -2
  36. package/src/cli/cmd/tui/context/local.tsx +6 -6
  37. package/src/cli/cmd/tui/context/sync.tsx +4 -4
  38. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  39. package/src/cli/cmd/tui/context/theme.tsx +2 -2
  40. package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
  41. package/src/cli/cmd/tui/event.ts +2 -2
  42. package/src/cli/cmd/tui/routes/home.tsx +6 -6
  43. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
  44. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
  45. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
  46. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
  47. package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
  48. package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
  49. package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
  50. package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
  51. package/src/cli/cmd/tui/thread.ts +9 -9
  52. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
  53. package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
  54. package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
  55. package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
  56. package/src/cli/cmd/tui/ui/toast.tsx +1 -1
  57. package/src/cli/cmd/tui/util/editor.ts +3 -3
  58. package/src/cli/cmd/tui/util/transcript.ts +1 -1
  59. package/src/cli/cmd/tui/worker.ts +10 -10
  60. package/src/cli/error.ts +1 -1
  61. package/src/cli/ui.ts +1 -1
  62. package/src/cli/upgrade.ts +4 -4
  63. package/src/command/index.ts +1 -1
  64. package/src/config/config.ts +10 -10
  65. package/src/config/markdown.ts +1 -1
  66. package/src/config/migrate-tui-config.ts +5 -5
  67. package/src/config/paths.ts +4 -4
  68. package/src/config/tui.ts +4 -4
  69. package/src/control/control.sql.ts +1 -1
  70. package/src/control/index.ts +1 -1
  71. package/src/control-plane/adaptors/worktree.ts +1 -1
  72. package/src/control-plane/session-proxy-middleware.ts +1 -1
  73. package/src/control-plane/workspace.sql.ts +1 -1
  74. package/src/control-plane/workspace.ts +7 -7
  75. package/src/file/index.ts +1 -1
  76. package/src/file/ripgrep.ts +2 -2
  77. package/src/file/watcher.ts +5 -5
  78. package/src/format/formatter.ts +1 -1
  79. package/src/ide/index.ts +3 -3
  80. package/src/index.ts +1 -1
  81. package/src/installation/index.ts +3 -3
  82. package/src/lsp/client.ts +3 -3
  83. package/src/lsp/index.ts +3 -3
  84. package/src/mcp/index.ts +4 -4
  85. package/src/permission/index.ts +2 -2
  86. package/src/permission/next.ts +10 -10
  87. package/src/plugin/codex.ts +1 -1
  88. package/src/plugin/copilot.ts +2 -2
  89. package/src/plugin/index.ts +1 -1
  90. package/src/project/bootstrap.ts +2 -2
  91. package/src/project/instance.ts +4 -4
  92. package/src/project/project.sql.ts +1 -1
  93. package/src/project/project.ts +5 -5
  94. package/src/project/state.ts +1 -1
  95. package/src/project/vcs.ts +4 -4
  96. package/src/provider/auth.ts +4 -4
  97. package/src/provider/error.ts +1 -1
  98. package/src/provider/models-snapshot.ts +2 -0
  99. package/src/provider/models.ts +1 -1
  100. package/src/provider/provider.ts +2 -2
  101. package/src/provider/transform.ts +2 -2
  102. package/src/pty/index.ts +5 -5
  103. package/src/question/index.ts +5 -5
  104. package/src/server/event.ts +1 -1
  105. package/src/server/mdns.ts +1 -1
  106. package/src/server/routes/global.ts +3 -3
  107. package/src/server/routes/permission.ts +1 -1
  108. package/src/server/routes/pty.ts +1 -1
  109. package/src/server/routes/session.ts +4 -4
  110. package/src/server/routes/tui.ts +1 -1
  111. package/src/server/server.ts +3 -3
  112. package/src/session/compaction.ts +7 -7
  113. package/src/session/index.ts +10 -10
  114. package/src/session/instruction.ts +1 -1
  115. package/src/session/llm.ts +11 -11
  116. package/src/session/message-v2.ts +10 -10
  117. package/src/session/message.ts +1 -1
  118. package/src/session/processor.ts +10 -10
  119. package/src/session/prompt.ts +8 -8
  120. package/src/session/retry.ts +2 -2
  121. package/src/session/revert.ts +1 -1
  122. package/src/session/session.sql.ts +3 -3
  123. package/src/session/status.ts +3 -3
  124. package/src/session/summary.ts +5 -5
  125. package/src/session/system.ts +1 -1
  126. package/src/session/todo.ts +2 -2
  127. package/src/share/share-next.ts +7 -7
  128. package/src/share/share.sql.ts +1 -1
  129. package/src/shell/shell.ts +3 -3
  130. package/src/skill/skill.ts +6 -6
  131. package/src/storage/db.ts +1 -1
  132. package/src/storage/storage.ts +1 -1
  133. package/src/tool/bash.ts +6 -6
  134. package/src/tool/edit.ts +1 -1
  135. package/src/tool/registry.ts +2 -2
  136. package/src/tool/skill.ts +1 -1
  137. package/src/tool/task.ts +3 -3
  138. package/src/util/array.ts +10 -0
  139. package/src/util/binary.ts +41 -0
  140. package/src/util/encode.ts +51 -0
  141. package/src/util/error.ts +54 -0
  142. package/src/util/identifier.ts +48 -0
  143. package/src/util/lazy.ts +4 -16
  144. package/src/util/path.ts +37 -0
  145. package/src/util/retry.ts +41 -0
  146. package/src/util/slug.ts +74 -0
  147. package/src/worktree/index.ts +3 -3
  148. package/AGENTS.md +0 -10
  149. package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
  150. package/Dockerfile +0 -18
  151. package/README.md +0 -15
  152. package/bunfig.toml +0 -7
  153. package/drizzle.config.ts +0 -10
  154. package/script/build.ts +0 -224
  155. package/script/check-migrations.ts +0 -16
  156. package/script/postinstall.mjs +0 -131
  157. package/script/publish.ts +0 -181
  158. package/script/schema.ts +0 -63
  159. package/script/seed-e2e.ts +0 -50
  160. package/sst-env.d.ts +0 -10
  161. package/test/AGENTS.md +0 -81
  162. package/test/acp/agent-interface.test.ts +0 -51
  163. package/test/acp/event-subscription.test.ts +0 -683
  164. package/test/agent/agent.test.ts +0 -689
  165. package/test/bun.test.ts +0 -53
  166. package/test/cli/github-action.test.ts +0 -197
  167. package/test/cli/github-remote.test.ts +0 -80
  168. package/test/cli/import.test.ts +0 -38
  169. package/test/cli/plugin-auth-picker.test.ts +0 -120
  170. package/test/cli/tui/transcript.test.ts +0 -322
  171. package/test/config/agent-color.test.ts +0 -71
  172. package/test/config/config.test.ts +0 -1886
  173. package/test/config/fixtures/empty-frontmatter.md +0 -4
  174. package/test/config/fixtures/frontmatter.md +0 -28
  175. package/test/config/fixtures/markdown-header.md +0 -11
  176. package/test/config/fixtures/no-frontmatter.md +0 -1
  177. package/test/config/fixtures/weird-model-id.md +0 -13
  178. package/test/config/markdown.test.ts +0 -228
  179. package/test/config/tui.test.ts +0 -510
  180. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  181. package/test/control-plane/sse.test.ts +0 -56
  182. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  183. package/test/control-plane/workspace-sync.test.ts +0 -97
  184. package/test/file/ignore.test.ts +0 -10
  185. package/test/file/index.test.ts +0 -394
  186. package/test/file/path-traversal.test.ts +0 -198
  187. package/test/file/ripgrep.test.ts +0 -39
  188. package/test/file/time.test.ts +0 -361
  189. package/test/fixture/db.ts +0 -11
  190. package/test/fixture/fixture.ts +0 -45
  191. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  192. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  193. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  194. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  195. package/test/fixture/skills/index.json +0 -6
  196. package/test/ide/ide.test.ts +0 -82
  197. package/test/keybind.test.ts +0 -421
  198. package/test/lsp/client.test.ts +0 -95
  199. package/test/mcp/headers.test.ts +0 -153
  200. package/test/mcp/oauth-browser.test.ts +0 -249
  201. package/test/memory/abort-leak.test.ts +0 -136
  202. package/test/patch/patch.test.ts +0 -348
  203. package/test/permission/arity.test.ts +0 -33
  204. package/test/permission/next.test.ts +0 -689
  205. package/test/permission-task.test.ts +0 -319
  206. package/test/plugin/auth-override.test.ts +0 -44
  207. package/test/plugin/codex.test.ts +0 -123
  208. package/test/preload.ts +0 -80
  209. package/test/project/project.test.ts +0 -348
  210. package/test/project/worktree-remove.test.ts +0 -65
  211. package/test/provider/amazon-bedrock.test.ts +0 -446
  212. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  213. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  214. package/test/provider/gitlab-duo.test.ts +0 -262
  215. package/test/provider/provider.test.ts +0 -2220
  216. package/test/provider/transform.test.ts +0 -2353
  217. package/test/pty/pty-output-isolation.test.ts +0 -140
  218. package/test/question/question.test.ts +0 -300
  219. package/test/scheduler.test.ts +0 -73
  220. package/test/server/global-session-list.test.ts +0 -89
  221. package/test/server/session-list.test.ts +0 -90
  222. package/test/server/session-select.test.ts +0 -78
  223. package/test/session/compaction.test.ts +0 -423
  224. package/test/session/instruction.test.ts +0 -170
  225. package/test/session/llm.test.ts +0 -667
  226. package/test/session/message-v2.test.ts +0 -924
  227. package/test/session/prompt.test.ts +0 -211
  228. package/test/session/retry.test.ts +0 -188
  229. package/test/session/revert-compact.test.ts +0 -285
  230. package/test/session/session.test.ts +0 -71
  231. package/test/session/structured-output-integration.test.ts +0 -233
  232. package/test/session/structured-output.test.ts +0 -385
  233. package/test/skill/discovery.test.ts +0 -110
  234. package/test/skill/skill.test.ts +0 -388
  235. package/test/snapshot/snapshot.test.ts +0 -1180
  236. package/test/storage/json-migration.test.ts +0 -846
  237. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  238. package/test/tool/apply_patch.test.ts +0 -566
  239. package/test/tool/bash.test.ts +0 -402
  240. package/test/tool/edit.test.ts +0 -496
  241. package/test/tool/external-directory.test.ts +0 -127
  242. package/test/tool/fixtures/large-image.png +0 -0
  243. package/test/tool/fixtures/models-api.json +0 -38413
  244. package/test/tool/grep.test.ts +0 -110
  245. package/test/tool/question.test.ts +0 -107
  246. package/test/tool/read.test.ts +0 -504
  247. package/test/tool/registry.test.ts +0 -122
  248. package/test/tool/skill.test.ts +0 -112
  249. package/test/tool/truncation.test.ts +0 -160
  250. package/test/tool/webfetch.test.ts +0 -100
  251. package/test/tool/write.test.ts +0 -348
  252. package/test/util/filesystem.test.ts +0 -443
  253. package/test/util/format.test.ts +0 -59
  254. package/test/util/glob.test.ts +0 -164
  255. package/test/util/iife.test.ts +0 -36
  256. package/test/util/lazy.test.ts +0 -50
  257. package/test/util/lock.test.ts +0 -72
  258. package/test/util/process.test.ts +0 -59
  259. package/test/util/timeout.test.ts +0 -21
  260. package/test/util/wildcard.test.ts +0 -90
  261. package/tsconfig.json +0 -16
@@ -1,504 +0,0 @@
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
- })
@@ -1,122 +0,0 @@
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
- })