magic-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +392 -0
  3. package/cmd/magic-agent/main.go +12 -0
  4. package/cmd/magic-client/main.go +144 -0
  5. package/go.mod +11 -0
  6. package/go.sum +12 -0
  7. package/internal/agent/arkclaw.go +587 -0
  8. package/internal/agent/arkclaw_stream.go +312 -0
  9. package/internal/agent/arkclaw_stream_test.go +428 -0
  10. package/internal/agent/arkclaw_test.go +827 -0
  11. package/internal/agent/ask.go +694 -0
  12. package/internal/agent/ask_test.go +587 -0
  13. package/internal/agent/attachments.go +200 -0
  14. package/internal/agent/attachments_test.go +481 -0
  15. package/internal/agent/capability.go +154 -0
  16. package/internal/agent/capability_test.go +190 -0
  17. package/internal/agent/claude.go +406 -0
  18. package/internal/agent/codebuddy.go +1044 -0
  19. package/internal/agent/codebuddy_account_test.go +128 -0
  20. package/internal/agent/codebuddy_gateway.go +479 -0
  21. package/internal/agent/codebuddy_gateway_stream.go +358 -0
  22. package/internal/agent/codebuddy_gateway_test.go +673 -0
  23. package/internal/agent/codex.go +599 -0
  24. package/internal/agent/codex_home.go +206 -0
  25. package/internal/agent/codex_home_test.go +130 -0
  26. package/internal/agent/codex_stream_test.go +97 -0
  27. package/internal/agent/codex_test.go +280 -0
  28. package/internal/agent/custom_models.go +197 -0
  29. package/internal/agent/custom_models_test.go +283 -0
  30. package/internal/agent/dsh.go +754 -0
  31. package/internal/agent/dsh_sdk.go +679 -0
  32. package/internal/agent/dsh_sdk_test.go +726 -0
  33. package/internal/agent/dsh_test.go +759 -0
  34. package/internal/agent/engine.go +469 -0
  35. package/internal/agent/engine_base.go +497 -0
  36. package/internal/agent/engine_base_test.go +308 -0
  37. package/internal/agent/engine_test.go +858 -0
  38. package/internal/agent/env.go +88 -0
  39. package/internal/agent/env_test.go +106 -0
  40. package/internal/agent/jsonextract.go +128 -0
  41. package/internal/agent/jsonextract_test.go +110 -0
  42. package/internal/agent/keepalive_test.go +216 -0
  43. package/internal/agent/llmengine.go +470 -0
  44. package/internal/agent/llmengine_test.go +551 -0
  45. package/internal/agent/llmhttp.go +437 -0
  46. package/internal/agent/llmhttp_test.go +304 -0
  47. package/internal/agent/llmmodels.go +205 -0
  48. package/internal/agent/login.go +27 -0
  49. package/internal/agent/main_test.go +23 -0
  50. package/internal/agent/models.go +686 -0
  51. package/internal/agent/models_test.go +877 -0
  52. package/internal/agent/openclaw.go +543 -0
  53. package/internal/agent/openclaw_acp.go +949 -0
  54. package/internal/agent/openclaw_acp_test.go +406 -0
  55. package/internal/agent/openclaw_test.go +451 -0
  56. package/internal/agent/output.go +191 -0
  57. package/internal/agent/permission.go +362 -0
  58. package/internal/agent/permission_test.go +526 -0
  59. package/internal/agent/prompt.go +65 -0
  60. package/internal/agent/runcmd.go +259 -0
  61. package/internal/agent/runcmd_test.go +81 -0
  62. package/internal/agent/runcmd_unix.go +56 -0
  63. package/internal/agent/runcmd_windows.go +66 -0
  64. package/internal/agent/runner.go +180 -0
  65. package/internal/agent/runner_test.go +263 -0
  66. package/internal/agent/spawnhook_test.go +96 -0
  67. package/internal/agent/stream.go +882 -0
  68. package/internal/agent/stream_test.go +565 -0
  69. package/internal/agent/streamjson.go +185 -0
  70. package/internal/agent/tags.go +52 -0
  71. package/internal/agent/toolkind.go +96 -0
  72. package/internal/agent/toolkind_test.go +70 -0
  73. package/internal/agent/tools_suffix_test.go +261 -0
  74. package/internal/agent/trae.go +342 -0
  75. package/internal/agent/workspace_test.go +166 -0
  76. package/internal/boundary/boundary_test.go +165 -0
  77. package/internal/cli/ask.go +1113 -0
  78. package/internal/cli/ask_test.go +1088 -0
  79. package/internal/cli/attach_test.go +239 -0
  80. package/internal/cli/contract_test.go +157 -0
  81. package/internal/cli/control.go +149 -0
  82. package/internal/cli/control_test.go +192 -0
  83. package/internal/cli/engines_test.go +205 -0
  84. package/internal/cli/events.go +198 -0
  85. package/internal/cli/events_test.go +166 -0
  86. package/internal/cli/keepalive_test.go +391 -0
  87. package/internal/cli/login.go +78 -0
  88. package/internal/cli/permission_test.go +295 -0
  89. package/internal/cli/prompt_source_test.go +222 -0
  90. package/internal/cli/root.go +752 -0
  91. package/internal/cli/session.go +486 -0
  92. package/internal/cli/session_test.go +202 -0
  93. package/internal/cli/sessionlog.go +325 -0
  94. package/internal/cli/sessionlog_command_test.go +204 -0
  95. package/internal/cli/sessionlog_test.go +262 -0
  96. package/internal/cli/sessionlog_user_test.go +129 -0
  97. package/internal/cli/toolkind_wire_test.go +84 -0
  98. package/internal/cli/workspace_test.go +188 -0
  99. package/internal/client/cors_test.go +136 -0
  100. package/internal/client/lan.go +68 -0
  101. package/internal/client/lan_test.go +53 -0
  102. package/internal/client/pair.go +163 -0
  103. package/internal/client/pair_test.go +109 -0
  104. package/internal/client/profile.go +184 -0
  105. package/internal/client/service.go +888 -0
  106. package/internal/client/service_test.go +654 -0
  107. package/internal/client/session_events_test.go +186 -0
  108. package/internal/client/session_messages_test.go +150 -0
  109. package/internal/config/config.go +439 -0
  110. package/internal/config/config_test.go +448 -0
  111. package/internal/session/session.go +447 -0
  112. package/internal/session/session_append.go +241 -0
  113. package/internal/session/session_append_test.go +161 -0
  114. package/internal/session/session_kill_unix_test.go +90 -0
  115. package/internal/session/session_mark_test.go +95 -0
  116. package/internal/session/session_test.go +237 -0
  117. package/internal/session/sessionlog.go +128 -0
  118. package/internal/session/sessionlog_read_test.go +157 -0
  119. package/npm/dist/darwin-arm64/magic-agent +0 -0
  120. package/npm/dist/darwin-x64/magic-agent +0 -0
  121. package/npm/dist/linux-arm64/magic-agent +0 -0
  122. package/npm/dist/linux-x64/magic-agent +0 -0
  123. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  124. package/npm/install.js +121 -0
  125. package/npm/lib/platform.js +78 -0
  126. package/package.json +36 -0
@@ -0,0 +1,308 @@
1
+ package agent
2
+
3
+ import (
4
+ "context"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ // TestMaxTokensSettingsHelper maxTokensSettings 的载荷与边界:
12
+ // n<=0 视为未设置(ok=false,不注入 --settings)。
13
+ func TestMaxTokensSettingsHelper(t *testing.T) {
14
+ if payload, ok := maxTokensSettings(0); ok {
15
+ t.Errorf("0 应视为未设置,got payload=%q", payload)
16
+ }
17
+ if payload, ok := maxTokensSettings(-5); ok {
18
+ t.Errorf("负数应视为未设置,got payload=%q", payload)
19
+ }
20
+ payload, ok := maxTokensSettings(8000)
21
+ if !ok {
22
+ t.Fatal("8000 应产生载荷")
23
+ }
24
+ want := `{"env":{"CLAUDE_CODE_MAX_OUTPUT_TOKENS":"8000"}}`
25
+ if payload != want {
26
+ t.Errorf("payload = %q, want %q", payload, want)
27
+ }
28
+ }
29
+
30
+ // TestExpandHome 基类路径展开:仅处理 ~ 与 ~/ 前缀。
31
+ func TestExpandHome(t *testing.T) {
32
+ home, err := os.UserHomeDir()
33
+ if err != nil {
34
+ t.Skipf("无法获取主目录: %v", err)
35
+ }
36
+ cases := []struct {
37
+ in string
38
+ want string
39
+ }{
40
+ {"~/bin/x", filepath.Join(home, "bin/x")},
41
+ {"~", home},
42
+ {"~user/x", "~user/x"}, // 不支持 ~user 形式
43
+ {"/opt/homebrew/bin", "/opt/homebrew/bin"},
44
+ {"", ""},
45
+ }
46
+ for _, tc := range cases {
47
+ if got := expandHome(tc.in); got != tc.want {
48
+ t.Errorf("expandHome(%q) = %q, want %q", tc.in, got, tc.want)
49
+ }
50
+ }
51
+ }
52
+
53
+ // TestNpmGlobalBinDirs npm 全局前缀解析:env(NPM_CONFIG_PREFIX / npm_config_prefix)
54
+ // 给出的前缀排在前,其后是**标准全局前缀兜底**(GUI 进程 PATH 窄时要靠它,
55
+ // 见 engine_base.go 的 npmGlobalBinDirs 注释;2026-09-28 为 codebuddy 独立 CLI 加的)。
56
+ func TestNpmGlobalBinDirs(t *testing.T) {
57
+ t.Setenv("NPM_CONFIG_PREFIX", "")
58
+ t.Setenv("npm_config_prefix", "")
59
+
60
+ // 未设 env:只剩标准前缀兜底 —— 不能为空(否则 GUI 侧探测不到 /opt/homebrew/bin)
61
+ base := npmGlobalBinDirs()
62
+ if len(base) == 0 {
63
+ t.Fatal("标准全局前缀兜底不该为空")
64
+ }
65
+ if base[0] != "/opt/homebrew/bin" {
66
+ t.Errorf("标准前缀应排在首位(无 env 前缀时),got %v", base)
67
+ }
68
+
69
+ // 设了 env:env 前缀排在标准前缀**之前**(优先级不因新增兜底而变)
70
+ t.Setenv("NPM_CONFIG_PREFIX", "/p/upper")
71
+ got := npmGlobalBinDirs()
72
+ if len(got) == 0 || got[0] != "/p/upper/bin" {
73
+ t.Errorf("env 前缀应排首位, got %v", got)
74
+ }
75
+ var hasStd bool
76
+ var upperCount int
77
+ for _, d := range got {
78
+ switch d {
79
+ case "/opt/homebrew/bin":
80
+ hasStd = true
81
+ case "/p/upper/bin":
82
+ upperCount++
83
+ }
84
+ }
85
+ if !hasStd {
86
+ t.Errorf("标准前缀应始终包含(env 存在也不能丢兜底), got %v", got)
87
+ }
88
+ if upperCount != 1 {
89
+ t.Errorf("同前缀应去重(出现 %d 次), got %v", upperCount, got)
90
+ }
91
+
92
+ // 大写为空时小写单独生效
93
+ t.Setenv("NPM_CONFIG_PREFIX", "")
94
+ t.Setenv("npm_config_prefix", "/p/lower")
95
+ if got := npmGlobalBinDirs(); len(got) == 0 || got[0] != "/p/lower/bin" {
96
+ t.Errorf("got %v, want 首位 /p/lower/bin", got)
97
+ }
98
+ }
99
+
100
+ // TestResolveNpmGlobalPrefixFallback 探测链最后一跳:候选与 PATH 都落空时用 npm 全局
101
+ // 前缀兜底。回归背景:沙箱把 `npm i -g` 的落点设成不在 PATH 里的私有目录,dsh 装好了
102
+ // 却报 not found(2026-09-22 真机复现)。
103
+ func TestResolveNpmGlobalPrefixFallback(t *testing.T) {
104
+ prefix := t.TempDir()
105
+ binDir := filepath.Join(prefix, "bin")
106
+ if err := os.MkdirAll(binDir, 0o755); err != nil {
107
+ t.Fatal(err)
108
+ }
109
+ // 用唯一名,避免撞上真实二进制或各引擎的候选路径
110
+ const name = "magic-agent-test-cli-xyz"
111
+ fake := filepath.Join(binDir, name)
112
+ if err := os.WriteFile(fake, []byte("#!/bin/sh\n"), 0o755); err != nil {
113
+ t.Fatal(err)
114
+ }
115
+
116
+ base := cliBase{binName: name, envVar: "MAGIC_AGENT_TEST_XYZ_BIN"}
117
+ t.Setenv("MAGIC_AGENT_TEST_XYZ_BIN", "")
118
+ t.Setenv("NPM_CONFIG_PREFIX", prefix)
119
+ t.Setenv("npm_config_prefix", "")
120
+ t.Setenv("PATH", "") // 排除 LookPath,只留 npm 前缀这一跳
121
+
122
+ if got := base.resolve(""); got != fake {
123
+ t.Errorf("resolve() = %q, want %q(应靠 npm 全局前缀兜底)", got, fake)
124
+ }
125
+
126
+ // 前缀也关掉 → 如实找不到(证明上面的命中确实来自该跳,而非误报)
127
+ t.Setenv("NPM_CONFIG_PREFIX", "")
128
+ if got := base.resolve(""); got != "" {
129
+ t.Errorf("无任何来源时 resolve() = %q, want 空", got)
130
+ }
131
+ }
132
+
133
+ // TestResolveOrder npm 兜底不得改变既有优先级:显式参数 > 环境变量 > 候选 > PATH > npm 前缀。
134
+ func TestResolveOrder(t *testing.T) {
135
+ prefix := t.TempDir()
136
+ binDir := filepath.Join(prefix, "bin")
137
+ if err := os.MkdirAll(binDir, 0o755); err != nil {
138
+ t.Fatal(err)
139
+ }
140
+ const name = "magic-agent-test-cli-abc"
141
+ npmPath := filepath.Join(binDir, name)
142
+ if err := os.WriteFile(npmPath, []byte("#!/bin/sh\n"), 0o755); err != nil {
143
+ t.Fatal(err)
144
+ }
145
+
146
+ candDir := t.TempDir()
147
+ candPath := filepath.Join(candDir, name)
148
+ if err := os.WriteFile(candPath, []byte("#!/bin/sh\n"), 0o755); err != nil {
149
+ t.Fatal(err)
150
+ }
151
+
152
+ base := cliBase{
153
+ binName: name,
154
+ envVar: "MAGIC_AGENT_TEST_ABC_BIN",
155
+ candidates: []string{candPath},
156
+ }
157
+ t.Setenv("NPM_CONFIG_PREFIX", prefix)
158
+ t.Setenv("npm_config_prefix", "")
159
+ t.Setenv("PATH", "")
160
+
161
+ t.Setenv("MAGIC_AGENT_TEST_ABC_BIN", "/explicit/env/path")
162
+ if got := base.resolve(""); got != "/explicit/env/path" {
163
+ t.Errorf("环境变量应优先于候选/npm, got %q", got)
164
+ }
165
+ if got := base.resolve("/explicit/arg"); got != "/explicit/arg" {
166
+ t.Errorf("显式参数应最优先, got %q", got)
167
+ }
168
+
169
+ t.Setenv("MAGIC_AGENT_TEST_ABC_BIN", "")
170
+ if got := base.resolve(""); got != candPath {
171
+ t.Errorf("候选应优先于 npm 前缀, got %q want %q", got, candPath)
172
+ }
173
+
174
+ base.candidates = []string{filepath.Join(candDir, "nope")}
175
+ if got := base.resolve(""); got != npmPath {
176
+ t.Errorf("候选落空应落 npm 前缀, got %q want %q", got, npmPath)
177
+ }
178
+ }
179
+
180
+ // TestInstallCommandOf 一键安装命令能力表:有官方安装命令的引擎必须给出可执行的一行,
181
+ // 没有安装路径的(GUI 应用 / 配置文件类)如实留空 —— 不编跑不通的命令。
182
+ func TestInstallCommandOf(t *testing.T) {
183
+ // 有安装命令:非空 + 形如一行 shell(不含换行)。
184
+ for _, engine := range []string{"claude", "codebuddy", "codebuddy-ai", "codex", "openclaw", "dsh", "trae", "llm"} {
185
+ got := InstallCommandOf(engine)
186
+ if got == "" {
187
+ t.Errorf("InstallCommandOf(%q) 为空,应给出一键安装命令", engine)
188
+ continue
189
+ }
190
+ if strings.ContainsAny(got, "\n\r") {
191
+ t.Errorf("InstallCommandOf(%q) = %q,必须是一行", engine, got)
192
+ }
193
+ }
194
+ // 具体命令取自各 CLI 官方安装方式(回归保护:防止手滑改坏包名)。
195
+ cases := map[string]string{
196
+ "claude": "npm install -g @anthropic-ai/claude-code",
197
+ "codebuddy": "npm install -g @tencent-ai/codebuddy-code",
198
+ "codebuddy-ai": "npm install -g @tencent-ai/codebuddy-code", // 同一个独立 CLI、两个账号
199
+ "codex": "npm install -g @openai/codex",
200
+ "openclaw": "npm install -g openclaw@latest",
201
+ "dsh": "npm i -g @deepseek-ai/dsh",
202
+ "trae": `sh -c "$(curl -L https://trae.cn/trae-cli/install.sh)"`,
203
+ "llm": "python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm",
204
+ }
205
+ for engine, want := range cases {
206
+ if got := InstallCommandOf(engine); got != want {
207
+ t.Errorf("InstallCommandOf(%q) = %q want %q", engine, got, want)
208
+ }
209
+ }
210
+ // 没有可执行安装路径的引擎:留空(原因在 note 里)。
211
+ for _, engine := range []string{"arkclaw", "codebuddy-gateway", "unknown", ""} {
212
+ if got := InstallCommandOf(engine); got != "" {
213
+ t.Errorf("InstallCommandOf(%q) = %q,应为空(无安装命令)", engine, got)
214
+ }
215
+ }
216
+ }
217
+
218
+ // TestToolsSwitchableOf `--tools` 的落地通道能力表:只有真有 CLI 参数的引擎为 true
219
+ // (dsh / openclaw / arkclaw 自带工具循环,llm 无工具概念)。
220
+ func TestToolsSwitchableOf(t *testing.T) {
221
+ for engine, want := range map[string]bool{
222
+ "claude": true,
223
+ "codebuddy": true,
224
+ "codebuddy-ai": true,
225
+ "trae": true,
226
+ "codex": true,
227
+ "llm": false,
228
+ "openclaw": false,
229
+ "dsh": false,
230
+ "arkclaw": false,
231
+ "unknown": false,
232
+ } {
233
+ if got := ToolsSwitchableOf(engine); got != want {
234
+ t.Errorf("ToolsSwitchableOf(%q) = %v want %v", engine, got, want)
235
+ }
236
+ }
237
+ }
238
+
239
+ // TestCLIResolveEnvVar 探测链优先级:显式 BinPath > 环境变量 > 候选路径。
240
+ func TestCLIResolveEnvVar(t *testing.T) {
241
+ t.Setenv("MAGIC_AGENT_LLM_BIN", "/custom/llm-path")
242
+ if got := llmBase.resolve(""); got != "/custom/llm-path" {
243
+ t.Errorf("env 覆盖未生效: %q", got)
244
+ }
245
+ // 显式路径优先于环境变量。
246
+ if got := llmBase.resolve("/explicit/bin"); got != "/explicit/bin" {
247
+ t.Errorf("显式路径应最优先: %q", got)
248
+ }
249
+ }
250
+
251
+ /* ── 引擎版本读取(2026-09-23)──
252
+ * 用户:「引擎检测除了 a2a 的 其他也要支持有升级」—— 版本号是「升级」可验证的前提
253
+ * (重探一次比对旧新,才能如实说「已升级」或「版本未变」)。 */
254
+
255
+ func TestFirstSemver(t *testing.T) {
256
+ cases := []struct{ in, want string }{
257
+ // 实测样例(各 CLI 的真实输出形态)
258
+ {"2.1.146 (Claude Code)", "2.1.146"},
259
+ {"codex-cli 0.154.0", "0.154.0"},
260
+ {"OpenClaw 2026.6.11 (e085fa1)", "2026.6.11"},
261
+ {"0.1.5-rc.2", "0.1.5-rc.2"},
262
+ {"trae-cli 0.120.52\n", "0.120.52"},
263
+ {"1.2.3+build.7", "1.2.3+build.7"},
264
+ // 认不出来就空串(**不编**)
265
+ {"no version here", ""},
266
+ {"", ""},
267
+ // ⚠️ 两段的**不算**:CLI 输出里的 `1.0` / `3.12` 常是协议版本、窗口尺寸这类东西
268
+ {"protocol 1.0", ""},
269
+ // ⚠️ **只看第一行**:后面的行可能是路径里的别的版本号
270
+ //(实测 llm 的 venv python 坏掉时,输出里满是 /…/python3.12.13/… 这种路径)
271
+ {"Python path configuration:\n /…/python3.12.13/…", ""},
272
+ {"llm, version 0.27.1\n /…/python3.12.13/…", "0.27.1"},
273
+ }
274
+ for _, c := range cases {
275
+ if got := firstSemver(c.in); got != c.want {
276
+ t.Errorf("firstSemver(%q) = %q want %q", c.in, got, c.want)
277
+ }
278
+ }
279
+ }
280
+
281
+ func TestEngineVersionOfSkipsNonExecutables(t *testing.T) {
282
+ // A2A 网关的 bin 是 URL(本机没二进制)→ 不该去 spawn 它;空 bin 同理。
283
+ if got := EngineVersionOf(context.Background(), "https://h/a2a/jsonrpc"); got != "" {
284
+ t.Errorf("HTTP 端点应返回空串, got %q", got)
285
+ }
286
+ if got := EngineVersionOf(context.Background(), " "); got != "" {
287
+ t.Errorf("空 bin 应返回空串, got %q", got)
288
+ }
289
+ }
290
+
291
+ func TestEngineVersionOfReadsCLIOutput(t *testing.T) {
292
+ dir := t.TempDir()
293
+ bin := filepath.Join(dir, "fake-cli")
294
+ if err := os.WriteFile(bin, []byte("#!/bin/sh\necho 'fake-cli 9.8.7 (build abc)'\n"), 0o755); err != nil {
295
+ t.Fatal(err)
296
+ }
297
+ if got := EngineVersionOf(context.Background(), bin); got != "9.8.7" {
298
+ t.Errorf("got %q want 9.8.7", got)
299
+ }
300
+ // 认不出 --version 的 CLI(退出码非 0、输出没有版本号)→ 空串,不编
301
+ bad := filepath.Join(dir, "bad-cli")
302
+ if err := os.WriteFile(bad, []byte("#!/bin/sh\necho 'unknown flag: --version' >&2\nexit 1\n"), 0o755); err != nil {
303
+ t.Fatal(err)
304
+ }
305
+ if got := EngineVersionOf(context.Background(), bad); got != "" {
306
+ t.Errorf("认不出时应返回空串, got %q", got)
307
+ }
308
+ }