magic-agent 1.0.2 → 1.0.46
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/CLIENT-CONTRACT.md +254 -8
- package/README.md +48 -5
- package/client-ui/index.html +32 -0
- package/client-ui/pc/app.css +89 -1
- package/client-ui/pc/index.html +18 -8
- package/client-ui/pc/js/00-base.js +8 -30
- package/client-ui/pc/js/05-task-list-render.js +4 -1
- package/client-ui/pc/js/07-chat-stream-render.js +2 -85
- package/client-ui/pc/js/10-markdown.js +3 -171
- package/client-ui/pc/js/18-askbox.js +2 -176
- package/client-ui/pc/js/90-glue.js +1 -1
- package/client-ui/pc/js/91-workbench-stubs.js +4 -10
- package/client-ui/pc/js/93-rail.js +10 -3
- package/client-ui/shared/conv/01-dom.js +52 -0
- package/client-ui/shared/conv/02-media-kinds.js +34 -0
- package/client-ui/shared/conv/03-chat-message.js +1561 -0
- package/client-ui/shared/conv/06-ask-answer.js +377 -0
- package/client-ui/shared/conv/07-stream-delegation.js +158 -0
- package/client-ui/{pc/js → shared/conv}/09-stream-apply.js +121 -6
- package/client-ui/shared/conv/10-markdown-render.js +252 -0
- package/client-ui/shared/conv/18-askbox-markup.js +185 -0
- package/client-ui/{pc/js → shared/conv}/19-attachments.js +365 -21
- package/client-ui/shared/conv/27-conv-ledger.js +381 -0
- package/client-ui/{pc/js → shared/conv}/29-msg-variants.js +112 -3
- package/client-ui/shared/conv/README.md +257 -0
- package/client-ui/shared/conv/conv.css +1638 -0
- package/client-ui/shared/conv/dom-map.md +170 -0
- package/client-ui/shared/conv/manifest.json +44 -0
- package/client-ui/shared/conversation.css +368 -190
- package/client-ui/shared/conversation.js +1722 -788
- package/client-ui/shared/host-glue.js +143 -0
- package/client-ui/shared/transport-http.js +130 -6
- package/client-ui-mobile/index.html +2417 -103
- package/cmd/magic-client/clientctl.go +273 -0
- package/cmd/magic-client/main.go +71 -5
- package/go.mod +6 -3
- package/go.sum +4 -1
- package/internal/agent/arkclaw.go +3 -2
- package/internal/agent/arkclaw_stream.go +126 -8
- package/internal/agent/arkclaw_stream_test.go +97 -15
- package/internal/agent/arkclaw_test.go +3 -3
- package/internal/agent/attachments.go +18 -5
- package/internal/agent/attachments_test.go +7 -1
- package/internal/agent/capability_test.go +11 -1
- package/internal/agent/claude.go +96 -158
- package/internal/agent/codebuddy.go +108 -133
- package/internal/agent/codebuddy_gateway.go +8 -4
- package/internal/agent/codebuddy_gateway_stream.go +82 -2
- package/internal/agent/codebuddy_gateway_test.go +236 -1
- package/internal/agent/codebuddy_stream_test.go +262 -0
- package/internal/agent/codex.go +50 -9
- package/internal/agent/codex_stream_test.go +30 -0
- package/internal/agent/codex_test.go +24 -0
- package/internal/agent/custom_models.go +260 -8
- package/internal/agent/custom_models_test.go +204 -7
- package/internal/agent/dsh.go +464 -130
- package/internal/agent/dsh_resume.go +258 -0
- package/internal/agent/dsh_sdk.go +68 -17
- package/internal/agent/dsh_sdk_test.go +102 -2
- package/internal/agent/dsh_test.go +409 -36
- package/internal/agent/engine.go +26 -6
- package/internal/agent/engine_base.go +46 -2
- package/internal/agent/engine_base_test.go +29 -0
- package/internal/agent/engine_test.go +101 -0
- package/internal/agent/env.go +78 -2
- package/internal/agent/env_test.go +37 -0
- package/internal/agent/itemtracker.go +271 -0
- package/internal/agent/launch.go +192 -0
- package/internal/agent/launch_test.go +206 -0
- package/internal/agent/launch_unix.go +15 -0
- package/internal/agent/launch_windows.go +24 -0
- package/internal/agent/limitnotice.go +103 -0
- package/internal/agent/limitnotice_test.go +211 -0
- package/internal/agent/llmengine.go +2 -2
- package/internal/agent/models.go +3 -2
- package/internal/agent/openclaw.go +4 -3
- package/internal/agent/openclaw_acp.go +7 -0
- package/internal/agent/openclaw_test.go +2 -2
- package/internal/agent/permission.go +100 -11
- package/internal/agent/permission_test.go +100 -36
- package/internal/agent/repair_models_test.go +194 -0
- package/internal/agent/runcmd.go +13 -3
- package/internal/agent/runner.go +7 -1
- package/internal/agent/runner_test.go +1 -1
- package/internal/agent/stream.go +777 -43
- package/internal/agent/stream_test.go +3 -2
- package/internal/agent/stream_timeline_test.go +905 -0
- package/internal/agent/streamjson.go +36 -2
- package/internal/agent/toolkind.go +20 -1
- package/internal/agent/toolkind_test.go +7 -1
- package/internal/agent/tools_suffix_test.go +10 -3
- package/internal/agent/trae.go +5 -3
- package/internal/agent/workspace_test.go +52 -0
- package/internal/cli/ask.go +282 -51
- package/internal/cli/ask_test.go +15 -11
- package/internal/cli/attach_test.go +32 -20
- package/internal/cli/clientcmd.go +156 -0
- package/internal/cli/events.go +53 -8
- package/internal/cli/events_test.go +230 -0
- package/internal/cli/keepalive_test.go +101 -6
- package/internal/cli/login.go +61 -4
- package/internal/cli/login_test.go +99 -0
- package/internal/cli/permission_test.go +5 -1
- package/internal/cli/repair.go +60 -0
- package/internal/cli/repair_test.go +51 -0
- package/internal/cli/root.go +195 -32
- package/internal/cli/session.go +183 -29
- package/internal/cli/session_test.go +144 -5
- package/internal/cli/sessionlog.go +385 -38
- package/internal/cli/sessionlog_test.go +708 -0
- package/internal/cli/sessionlog_user_test.go +142 -1
- package/internal/cli/sessiontitle.go +141 -0
- package/internal/client/browse.go +456 -0
- package/internal/client/browse_test.go +199 -0
- package/internal/client/cors_test.go +35 -0
- package/internal/client/daemon.go +525 -0
- package/internal/client/daemon_test.go +212 -0
- package/internal/client/detach_unix.go +19 -0
- package/internal/client/detach_windows.go +27 -0
- package/internal/client/pair.go +3 -3
- package/internal/client/plugins.go +376 -0
- package/internal/client/plugins_test.go +223 -0
- package/internal/client/proxy.go +374 -0
- package/internal/client/proxy_test.go +212 -0
- package/internal/client/service.go +1464 -48
- package/internal/client/service_test.go +649 -206
- package/internal/client/session_events_test.go +130 -0
- package/internal/client/wsproxy.go +113 -0
- package/internal/client/wsproxy_test.go +102 -0
- package/internal/session/delete.go +151 -0
- package/internal/session/delete_test.go +103 -0
- package/internal/session/session.go +155 -20
- package/internal/session/session_append.go +79 -21
- package/internal/session/session_test.go +124 -10
- package/internal/session/sessionlog.go +572 -6
- package/internal/session/sessionlog_read_test.go +410 -0
- package/internal/session/title.go +486 -0
- package/internal/session/title_test.go +298 -0
- package/npm/bin/magic-agent.js +78 -10
- package/npm/bin/magic-client.js +100 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/npm/install.js +36 -13
- package/npm/lib/platform.js +27 -5
- package/package.json +8 -3
- package/client-ui/pc/js/03-chat-message.js +0 -880
- package/client-ui/pc/js/06-ask-answer.js +0 -204
- /package/client-ui/{pc/js → shared/conv}/08-ask-event-map.js +0 -0
- /package/client-ui/{pc → shared}/vendor/highlight.min.js +0 -0
- /package/client-ui/{pc → shared}/vendor/marked.min.js +0 -0
- /package/client-ui/{pc → shared}/vendor/purify.min.js +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
package session
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"testing"
|
|
8
|
+
"time"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// withTitleHome 把标题表指到临时目录(测试期间不碰用户真实的 ~/.magic-agent)。
|
|
12
|
+
func withTitleHome(t *testing.T) string {
|
|
13
|
+
t.Helper()
|
|
14
|
+
dir := t.TempDir()
|
|
15
|
+
t.Setenv(EnvDir, filepath.Join(dir, "sessions"))
|
|
16
|
+
// 进程内缓存是包级变量:换目录后必须失效,否则会读到上一个用例的表。
|
|
17
|
+
titleMu.Lock()
|
|
18
|
+
titleCache, titleLoaded = nil, false
|
|
19
|
+
titleMu.Unlock()
|
|
20
|
+
return dir
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func TestSetTitleRoundTrip(t *testing.T) {
|
|
24
|
+
withTitleHome(t)
|
|
25
|
+
if err := SetTitle("sess-1", "设置界面版本号"); err != nil {
|
|
26
|
+
t.Fatalf("SetTitle: %v", err)
|
|
27
|
+
}
|
|
28
|
+
if got := TitleFor("sess-1"); got != "设置界面版本号" {
|
|
29
|
+
t.Fatalf("TitleFor = %q", got)
|
|
30
|
+
}
|
|
31
|
+
if got := Titles()["sess-1"]; got != "设置界面版本号" {
|
|
32
|
+
t.Fatalf("Titles()[sess-1] = %q", got)
|
|
33
|
+
}
|
|
34
|
+
// 落盘形状:路径不变(插件时代就是它),所以已有的重命名结果不丢。
|
|
35
|
+
p, err := TitleStorePath()
|
|
36
|
+
if err != nil {
|
|
37
|
+
t.Fatal(err)
|
|
38
|
+
}
|
|
39
|
+
if filepath.Base(p) != "session-titles.json" {
|
|
40
|
+
t.Fatalf("标题表文件名变了:%s", p)
|
|
41
|
+
}
|
|
42
|
+
if b, err := os.ReadFile(p); err != nil || len(b) == 0 {
|
|
43
|
+
t.Fatalf("标题表没落盘:%v", err)
|
|
44
|
+
}
|
|
45
|
+
// 空标题 = 清除覆盖(界面「恢复默认标题」用这条)。
|
|
46
|
+
if err := SetTitle("sess-1", " "); err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
if got := TitleFor("sess-1"); got != "" {
|
|
50
|
+
t.Fatalf("空标题应清除覆盖,实际 %q", got)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func TestSetTitleEmptyIDRejected(t *testing.T) {
|
|
55
|
+
withTitleHome(t)
|
|
56
|
+
if err := SetTitle(" ", "x"); err == nil {
|
|
57
|
+
t.Fatal("空 id 应被拒(否则会写出一条认不出来的覆盖)")
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// TestSetTitleKeepsOtherEntries 回归:整表覆盖曾把别的条目抹掉。
|
|
62
|
+
// 两条覆盖先后写入,先写的那条必须还在。
|
|
63
|
+
func TestSetTitleKeepsOtherEntries(t *testing.T) {
|
|
64
|
+
withTitleHome(t)
|
|
65
|
+
if err := SetTitle("a", "标题甲"); err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
if err := SetTitle("b", "标题乙"); err != nil {
|
|
69
|
+
t.Fatal(err)
|
|
70
|
+
}
|
|
71
|
+
m := Titles()
|
|
72
|
+
if m["a"] != "标题甲" || m["b"] != "标题乙" {
|
|
73
|
+
t.Fatalf("后写的一条把先写的抹了:%+v", m)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func TestNormalizeTitle(t *testing.T) {
|
|
78
|
+
cases := []struct{ in, want string }{
|
|
79
|
+
{"设置界面版本号", "设置界面版本号"},
|
|
80
|
+
{"标题:设置界面版本号", "设置界面版本号"},
|
|
81
|
+
{"标题: 设置界面版本号", "设置界面版本号"},
|
|
82
|
+
{"【标题】设置界面版本号", "设置界面版本号"},
|
|
83
|
+
{"1. 设置界面版本号", "设置界面版本号"},
|
|
84
|
+
{"1、设置界面版本号", "设置界面版本号"},
|
|
85
|
+
{"- 设置界面版本号", "设置界面版本号"},
|
|
86
|
+
{"## 设置界面版本号", "设置界面版本号"},
|
|
87
|
+
{"“设置界面版本号”", "设置界面版本号"},
|
|
88
|
+
{"「设置界面版本号」", "设置界面版本号"},
|
|
89
|
+
{"(设置界面版本号)", "设置界面版本号"},
|
|
90
|
+
{"设置界面版本号。", "设置界面版本号"},
|
|
91
|
+
{"设置界面版本号。\n", "设置界面版本号"},
|
|
92
|
+
{"```\n设置界面版本号\n```", "设置界面版本号"},
|
|
93
|
+
{"设置界面版本号\n这是解释,不该进标题", "设置界面版本号"},
|
|
94
|
+
{"", ""},
|
|
95
|
+
{"\n\n", ""},
|
|
96
|
+
}
|
|
97
|
+
for _, c := range cases {
|
|
98
|
+
if got := normalizeTitle(c.in, TitleMaxChars); got != c.want {
|
|
99
|
+
t.Errorf("normalizeTitle(%q) = %q,期望 %q", c.in, got, c.want)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// TestNormalizeTitleKeepsTitlePrefix 回归:剥前缀不能误伤**本身就以「标题」开头**的正常标题。
|
|
105
|
+
func TestNormalizeTitleKeepsTitlePrefix(t *testing.T) {
|
|
106
|
+
// 「标题设计评审」里「标题」后面不是冒号/破折号 → 不该被当成前缀剥掉。
|
|
107
|
+
if got := normalizeTitle("标题设计评审", TitleMaxChars); got != "标题设计评审" {
|
|
108
|
+
t.Fatalf("正常标题被误剥前缀:%q", got)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func TestNormalizeTitleTruncates(t *testing.T) {
|
|
113
|
+
// 超长按 rune 截断(不是字节):中文不能被切坏。
|
|
114
|
+
long := "一二三四五六七八九十一二三四五六七八九十"
|
|
115
|
+
got := normalizeTitle(long, TitleMaxChars)
|
|
116
|
+
if n := len([]rune(got)); n != TitleMaxChars {
|
|
117
|
+
t.Fatalf("截断后 %d 字,期望 %d(%q)", n, TitleMaxChars, got)
|
|
118
|
+
}
|
|
119
|
+
for _, r := range got {
|
|
120
|
+
if r == '�' {
|
|
121
|
+
t.Fatalf("按字节截了中文:%q", got)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func TestFirstUserTextPrefersLog(t *testing.T) {
|
|
127
|
+
withTitleHome(t)
|
|
128
|
+
dir := t.TempDir()
|
|
129
|
+
log := `{"seq":0,"kind":"user","text":"要在设置界面显示版本号"}
|
|
130
|
+
{"seq":1,"kind":"text","text":"好的"}`
|
|
131
|
+
if err := os.WriteFile(filepath.Join(dir, "sess-1.jsonl"), []byte(log), 0o600); err != nil {
|
|
132
|
+
t.Fatal(err)
|
|
133
|
+
}
|
|
134
|
+
// 日志优先:prompt_head 只有前 80 字,长首问靠它才归纳得出要点。
|
|
135
|
+
if got := FirstUserText(dir, "sess-1", "截断的首问"); got != "要在设置界面显示版本号" {
|
|
136
|
+
t.Fatalf("FirstUserText = %q", got)
|
|
137
|
+
}
|
|
138
|
+
// 没日志 → 回落 prompt_head(归纳仍能工作)。
|
|
139
|
+
if got := FirstUserText(dir, "sess-none", "回落用的首问"); got != "回落用的首问" {
|
|
140
|
+
t.Fatalf("无日志时未回落 prompt_head:%q", got)
|
|
141
|
+
}
|
|
142
|
+
// 空 sid + 空 head → 空(调用方据此报错,不硬编一个标题)。
|
|
143
|
+
if got := FirstUserText(dir, "", ""); got != "" {
|
|
144
|
+
t.Fatalf("应为空,实际 %q", got)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func TestSummarizeRequiresText(t *testing.T) {
|
|
149
|
+
withTitleHome(t)
|
|
150
|
+
if _, err := Summarize(context.Background(), SummarizeOptions{SessionID: "s"}); err == nil {
|
|
151
|
+
t.Fatal("没有可归纳的原文时应报错,而不是返回一个空标题")
|
|
152
|
+
}
|
|
153
|
+
if _, err := Summarize(context.Background(), SummarizeOptions{}); err == nil {
|
|
154
|
+
t.Fatal("空 id 应被拒")
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// TestSummarizeUsesCache 幂等:已有覆盖就直接回,不该再调一次 LLM。
|
|
159
|
+
func TestSummarizeUsesCache(t *testing.T) {
|
|
160
|
+
withTitleHome(t)
|
|
161
|
+
if err := SetTitle("sess-1", "设置界面版本号"); err != nil {
|
|
162
|
+
t.Fatal(err)
|
|
163
|
+
}
|
|
164
|
+
res, err := Summarize(context.Background(), SummarizeOptions{SessionID: "sess-1", Text: "任意"})
|
|
165
|
+
if err != nil {
|
|
166
|
+
t.Fatalf("命中缓存不该报错:%v", err)
|
|
167
|
+
}
|
|
168
|
+
if !res.Cached || res.Title != "设置界面版本号" {
|
|
169
|
+
t.Fatalf("应命中缓存并回现有标题,实际 %+v", res)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ── 标题的键必须稳定(2026-10-15,用户:「标题只在第一次创建时生成」)──
|
|
174
|
+
首轮归纳时引擎可能还没吐 session_id,键就落在 run_id 上;而 run_id **每轮追问都换一个**
|
|
175
|
+
→ 下一轮按 session_id 查不到 → 客户端以为「还没归纳」→ 再归纳一次(输入还会回落成
|
|
176
|
+
最新那条追问)→ 标题每轮都在变。两个键都认 + 迁移到 session_id 就是为了堵这个洞。 */
|
|
177
|
+
|
|
178
|
+
// TestTitleForAnyAdoptsRunIDKey 存量「键在 run_id 上」的标题,按 session_id 也要读得到,
|
|
179
|
+
// 且能被迁到 session_id 上。
|
|
180
|
+
func TestTitleForAnyAdoptsRunIDKey(t *testing.T) {
|
|
181
|
+
withTitleHome(t)
|
|
182
|
+
if err := SetTitle("run-old", "补全学校场景功能"); err != nil {
|
|
183
|
+
t.Fatal(err)
|
|
184
|
+
}
|
|
185
|
+
if got := TitleFor("sess-1"); got != "" {
|
|
186
|
+
t.Fatalf("前置条件:session_id 键上本该没有标题,实得 %q", got)
|
|
187
|
+
}
|
|
188
|
+
// ① 两键都认。
|
|
189
|
+
if got := TitleForAny("sess-1", "run-old"); got != "补全学校场景功能" {
|
|
190
|
+
t.Fatalf("TitleForAny = %q,want 补全学校场景功能", got)
|
|
191
|
+
}
|
|
192
|
+
// ② 迁移之后按 session_id 直接命中(此后 run_id 再换也不影响);
|
|
193
|
+
// 旧键要**删掉** —— 留着就成了「同一会话两个键、两个值」。
|
|
194
|
+
AdoptTitle("sess-1", "run-old")
|
|
195
|
+
if got := TitleFor("sess-1"); got != "补全学校场景功能" {
|
|
196
|
+
t.Fatalf("迁移后 TitleFor(sess-1) = %q", got)
|
|
197
|
+
}
|
|
198
|
+
if got := TitleFor("run-old"); got != "" {
|
|
199
|
+
t.Fatalf("迁移后旧键该被清掉,实得 %q", got)
|
|
200
|
+
}
|
|
201
|
+
// ③ 幂等;且**不覆盖**人工重命名(新键有值就不搬)。
|
|
202
|
+
AdoptTitle("sess-1", "run-old")
|
|
203
|
+
if err := SetTitle("sess-1", "我自己改的名字"); err != nil {
|
|
204
|
+
t.Fatal(err)
|
|
205
|
+
}
|
|
206
|
+
if err := SetTitle("run-old", "旧键上的名字"); err != nil {
|
|
207
|
+
t.Fatal(err)
|
|
208
|
+
}
|
|
209
|
+
AdoptTitle("sess-1", "run-old")
|
|
210
|
+
if got := TitleFor("sess-1"); got != "我自己改的名字" {
|
|
211
|
+
t.Fatalf("人工重命名不能被旧键覆盖,实得 %q", got)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// TestConsolidateTitleDropsRunKeys 归纳出新标题时,该会话**各轮 run_id** 上的旧键都要清掉,
|
|
216
|
+
// 只留 session_id 一个 —— 否则按不同 id 读会看到不同标题。
|
|
217
|
+
func TestConsolidateTitleDropsRunKeys(t *testing.T) {
|
|
218
|
+
dir := withTitleHome(t)
|
|
219
|
+
if err := SetTitle("run-1", "旧标题一"); err != nil {
|
|
220
|
+
t.Fatal(err)
|
|
221
|
+
}
|
|
222
|
+
if err := SetTitle("run-2", "旧标题二"); err != nil {
|
|
223
|
+
t.Fatal(err)
|
|
224
|
+
}
|
|
225
|
+
// 登记表里这两轮都属于同一条会话(免得依赖真实 home 目录)。
|
|
226
|
+
regDir := filepath.Join(dir, "sessions")
|
|
227
|
+
if err := os.MkdirAll(regDir, 0o700); err != nil {
|
|
228
|
+
t.Fatal(err)
|
|
229
|
+
}
|
|
230
|
+
for _, rid := range []string{"run-1", "run-2"} {
|
|
231
|
+
writeRecordForTest(t, regDir,
|
|
232
|
+
Record{RunID: rid, SessionID: "sess-c", State: StateDone,
|
|
233
|
+
StartedAt: time.Now(), UpdatedAt: time.Now()})
|
|
234
|
+
}
|
|
235
|
+
if err := ConsolidateTitle("sess-c", "run-2", "归纳出来的标题"); err != nil {
|
|
236
|
+
t.Fatal(err)
|
|
237
|
+
}
|
|
238
|
+
if got := TitleFor("sess-c"); got != "归纳出来的标题" {
|
|
239
|
+
t.Fatalf("TitleFor(sess-c) = %q", got)
|
|
240
|
+
}
|
|
241
|
+
for _, rid := range []string{"run-1", "run-2"} {
|
|
242
|
+
if got := TitleFor(rid); got != "" {
|
|
243
|
+
t.Fatalf("该会话的 run 键 %s 应被清掉,实得 %q", rid, got)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// 按 run_id 读也要得到同一个标题(先查登记表翻到 session_id)。
|
|
247
|
+
if got := TitleForAny("sess-c", "run-2"); got != "归纳出来的标题" {
|
|
248
|
+
t.Fatalf("TitleForAny = %q", got)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// TestTitleForSessionRuns 标题挂在**其他轮**的 run_id 上时(首轮归纳时还没有 session_id),
|
|
253
|
+
// 按 session_id 也要认到;取最早那一轮的值。
|
|
254
|
+
func TestTitleForSessionRuns(t *testing.T) {
|
|
255
|
+
dir := withTitleHome(t)
|
|
256
|
+
regDir := filepath.Join(dir, "sessions")
|
|
257
|
+
if err := os.MkdirAll(regDir, 0o700); err != nil {
|
|
258
|
+
t.Fatal(err)
|
|
259
|
+
}
|
|
260
|
+
now := time.Now()
|
|
261
|
+
writeRecordForTest(t, regDir, Record{RunID: "run-new", SessionID: "sess-r", State: StateDone,
|
|
262
|
+
StartedAt: now, UpdatedAt: now})
|
|
263
|
+
writeRecordForTest(t, regDir, Record{RunID: "run-old", SessionID: "sess-r", State: StateDone,
|
|
264
|
+
StartedAt: now.Add(-time.Hour), UpdatedAt: now.Add(-time.Hour)})
|
|
265
|
+
writeRecordForTest(t, regDir, Record{RunID: "run-other", SessionID: "sess-other", State: StateDone,
|
|
266
|
+
StartedAt: now.Add(-2 * time.Hour), UpdatedAt: now.Add(-2 * time.Hour)})
|
|
267
|
+
if err := SetTitle("run-old", "第一次生成的标题"); err != nil {
|
|
268
|
+
t.Fatal(err)
|
|
269
|
+
}
|
|
270
|
+
if err := SetTitle("run-other", "别的会话的标题"); err != nil {
|
|
271
|
+
t.Fatal(err)
|
|
272
|
+
}
|
|
273
|
+
if got := TitleForSessionRuns("sess-r"); got != "第一次生成的标题" {
|
|
274
|
+
t.Fatalf("TitleForSessionRuns(sess-r) = %q", got)
|
|
275
|
+
}
|
|
276
|
+
if got := TitleForSessionRuns("nope"); got != "" {
|
|
277
|
+
t.Fatalf("没有记录的会话应为空,实得 %q", got)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// TestSummarizeCachedAcrossKeys 按 run_id 建的标题,在按 session_id 归纳时也算命中
|
|
282
|
+
// (不调 LLM),并把键迁到 session_id。
|
|
283
|
+
func TestSummarizeCachedAcrossKeys(t *testing.T) {
|
|
284
|
+
withTitleHome(t)
|
|
285
|
+
if err := SetTitle("run-old", "设置页展示版本号"); err != nil {
|
|
286
|
+
t.Fatal(err)
|
|
287
|
+
}
|
|
288
|
+
res, err := Summarize(context.Background(), SummarizeOptions{SessionID: "sess-1", RunID: "run-old", Text: "任意"})
|
|
289
|
+
if err != nil {
|
|
290
|
+
t.Fatalf("跨键命中不该报错:%v", err)
|
|
291
|
+
}
|
|
292
|
+
if !res.Cached || res.Title != "设置页展示版本号" {
|
|
293
|
+
t.Fatalf("应按 run_id 键命中缓存,实际 %+v", res)
|
|
294
|
+
}
|
|
295
|
+
if TitleFor("sess-1") == "" {
|
|
296
|
+
t.Fatal("命中时应把标题迁到 session_id(否则下一轮又要重算)")
|
|
297
|
+
}
|
|
298
|
+
}
|
package/npm/bin/magic-agent.js
CHANGED
|
@@ -7,24 +7,89 @@
|
|
|
7
7
|
// 之所以用 spawnSync + stdio:inherit 而不是直接 exec:
|
|
8
8
|
// - 保持真实 fd 0/1/2,Go 侧 process.stdin.isTTY 判定不受影响(管道读 prompt 依赖它)
|
|
9
9
|
// - 退出码、Ctrl-C 信号行为与直接运行二进制一致
|
|
10
|
+
//
|
|
11
|
+
// 定位顺序(逐级兜底,尽量不让用户卡在「装上了但跑不了」):
|
|
12
|
+
// 1) MAGIC_AGENT_BIN 环境变量显式指定
|
|
13
|
+
// 2) npm/dist/<plat>/ 预编译产物(npm 包正常路径)
|
|
14
|
+
// 3) bin/ 本地开发产物(go build -o bin/...)
|
|
15
|
+
// 4) 本机 go 现场编译 与 npm/install.js 的兜底策略保持一致
|
|
16
|
+
// 全都落空才报错退出,错误信息里带上「已打包了哪些平台」便于自查。
|
|
10
17
|
|
|
11
|
-
const { spawnSync } = require("child_process");
|
|
18
|
+
const { execFileSync, spawnSync } = require("child_process");
|
|
12
19
|
const fs = require("fs");
|
|
13
20
|
const path = require("path");
|
|
14
21
|
|
|
15
|
-
const { binaryPath, distDir, currentTarget, exeName } = require("../lib/platform");
|
|
22
|
+
const { binaryPath, distDir, currentTarget, exeName, findGo, pkgRoot } = require("../lib/platform");
|
|
16
23
|
|
|
17
24
|
const target = currentTarget();
|
|
18
|
-
const
|
|
25
|
+
const exe = exeName(target.goos);
|
|
26
|
+
const isWin = process.platform === "win32";
|
|
27
|
+
|
|
28
|
+
function fromDist() {
|
|
29
|
+
const p = binaryPath(target);
|
|
30
|
+
return fs.existsSync(p) ? p : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fromLocalBin() {
|
|
34
|
+
const p = path.join(pkgRoot, "bin", exe);
|
|
35
|
+
return fs.existsSync(p) ? p : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 最后兜底:本机装了 Go 就现场编译一份。 */
|
|
39
|
+
function buildFromSource() {
|
|
40
|
+
const go = findGo();
|
|
41
|
+
if (!go) return null;
|
|
42
|
+
const out = fromLocalBin() || path.join(pkgRoot, "bin", exe);
|
|
43
|
+
process.stderr.write(`magic-agent: 未找到 ${target.dir} 预编译产物,用 go 现场编译 ...\n`);
|
|
44
|
+
try {
|
|
45
|
+
// 版本号从 package.json 注入,与 npm/build.js 保持一致 ——
|
|
46
|
+
// 否则兜底编译出来的产物会报 0.1.0(Go 侧默认值),和正式包对不上。
|
|
47
|
+
let version = "";
|
|
48
|
+
try {
|
|
49
|
+
version = require(path.join(pkgRoot, "package.json")).version || "";
|
|
50
|
+
} catch (_) {
|
|
51
|
+
/* 读不到就不注入,Go 侧会用默认值 */
|
|
52
|
+
}
|
|
53
|
+
const ldflags = version
|
|
54
|
+
? `-s -w -X github.com/darren/magic-agent/internal/cli.Version=${version}`
|
|
55
|
+
: "-s -w";
|
|
56
|
+
|
|
57
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
58
|
+
execFileSync(go, ["build", "-trimpath", "-ldflags", ldflags, "-o", out, "./cmd/magic-agent"], {
|
|
59
|
+
cwd: pkgRoot,
|
|
60
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
61
|
+
env: { ...process.env, CGO_ENABLED: "0", GOOS: target.goos, GOARCH: target.goarch },
|
|
62
|
+
});
|
|
63
|
+
return fs.existsSync(out) ? out : null;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
process.stderr.write(`magic-agent: 现场编译失败: ${String(err.message).split("\n")[0]}\n`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolve() {
|
|
71
|
+
if (process.env.MAGIC_AGENT_BIN && fs.existsSync(process.env.MAGIC_AGENT_BIN)) {
|
|
72
|
+
return process.env.MAGIC_AGENT_BIN;
|
|
73
|
+
}
|
|
74
|
+
return fromDist() || fromLocalBin() || buildFromSource();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const bin = resolve();
|
|
19
78
|
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
|
|
79
|
+
if (!bin) {
|
|
80
|
+
// 列出包里实际带了哪些平台,方便判断是「平台没打」还是「包不完整」。
|
|
81
|
+
const packed = fs.existsSync(distDir)
|
|
82
|
+
? fs
|
|
83
|
+
.readdirSync(distDir)
|
|
84
|
+
.filter((d) => fs.existsSync(path.join(distDir, d, exeName(d.startsWith("win32") ? "windows" : "linux"))))
|
|
23
85
|
: [];
|
|
24
86
|
process.stderr.write(
|
|
25
|
-
`magic-agent: 当前平台 ${target.dir} 没有可用二进制(期望 ${
|
|
26
|
-
`已打包的平台:${
|
|
27
|
-
|
|
87
|
+
`magic-agent: 当前平台 ${target.dir} 没有可用二进制(期望 ${binaryPath(target)})\n` +
|
|
88
|
+
`已打包的平台:${packed.length ? packed.join(", ") : "(无)"}\n` +
|
|
89
|
+
`解决办法:装 Go 后重跑本命令(会自动现场编译),或手工执行\n` +
|
|
90
|
+
` npm run build\n` +
|
|
91
|
+
` go build -o bin/${exe} ./cmd/magic-agent\n` +
|
|
92
|
+
`或用 MAGIC_AGENT_BIN 指向已有的可执行文件。\n`
|
|
28
93
|
);
|
|
29
94
|
process.exit(1);
|
|
30
95
|
}
|
|
@@ -36,7 +101,10 @@ if (res.error) {
|
|
|
36
101
|
process.exit(1);
|
|
37
102
|
}
|
|
38
103
|
if (res.signal) {
|
|
39
|
-
// 让父进程以相同信号退出,保持 shell
|
|
104
|
+
// 让父进程以相同信号退出,保持 shell 语义(如超时/中断)。
|
|
105
|
+
// Windows 上没有真正的信号投递,process.kill 自杀会丢掉退出码语义,
|
|
106
|
+
// 这种情况退回 1(通用失败),不要假装成功。
|
|
107
|
+
if (isWin) process.exit(1);
|
|
40
108
|
process.kill(process.pid, res.signal);
|
|
41
109
|
process.exit(1);
|
|
42
110
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// magic-client 的 npm bin 转发层:与 magic-agent.js 同一套定位顺序,
|
|
5
|
+
// 只是把目标二进制换成同目录的 magic-client。
|
|
6
|
+
//
|
|
7
|
+
// 为什么需要独立的 bin:`magic-agent client start` 已经能管它,但直接跑
|
|
8
|
+
// `magic-client serve --lan`(前台、看日志)仍然是最常用的排障姿势 —— 没有这个转发,
|
|
9
|
+
// 用户得自己去 node_modules/magic-agent/npm/dist/<plat>/ 里翻路径。
|
|
10
|
+
//
|
|
11
|
+
// 定位顺序:MAGIC_CLIENT_BIN > npm/dist/<plat>/magic-client > bin/ > 本机 go 现场编译。
|
|
12
|
+
|
|
13
|
+
const { execFileSync, spawnSync } = require("child_process");
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
const { clientBinaryPath, distDir, currentTarget, exeName, findGo, pkgRoot } = require("../lib/platform");
|
|
18
|
+
|
|
19
|
+
const target = currentTarget();
|
|
20
|
+
const exe = exeName(target.goos, "magic-client");
|
|
21
|
+
const isWin = process.platform === "win32";
|
|
22
|
+
|
|
23
|
+
function fromDist() {
|
|
24
|
+
const p = clientBinaryPath(target);
|
|
25
|
+
return fs.existsSync(p) ? p : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fromLocalBin() {
|
|
29
|
+
const p = path.join(pkgRoot, "bin", exe);
|
|
30
|
+
return fs.existsSync(p) ? p : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildFromSource() {
|
|
34
|
+
const go = findGo();
|
|
35
|
+
if (!go) return null;
|
|
36
|
+
const out = fromLocalBin() || path.join(pkgRoot, "bin", exe);
|
|
37
|
+
process.stderr.write(`magic-client: 未找到 ${target.dir} 预编译产物,用 go 现场编译 ...\n`);
|
|
38
|
+
try {
|
|
39
|
+
let version = "";
|
|
40
|
+
try {
|
|
41
|
+
version = require(path.join(pkgRoot, "package.json")).version || "";
|
|
42
|
+
} catch (_) {
|
|
43
|
+
/* 读不到就不注入 */
|
|
44
|
+
}
|
|
45
|
+
const ldflags = version
|
|
46
|
+
? `-s -w -X github.com/darren/magic-agent/internal/cli.Version=${version}`
|
|
47
|
+
: "-s -w";
|
|
48
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
49
|
+
execFileSync(go, ["build", "-trimpath", "-ldflags", ldflags, "-o", out, "./cmd/magic-client"], {
|
|
50
|
+
cwd: pkgRoot,
|
|
51
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
52
|
+
env: { ...process.env, CGO_ENABLED: "0", GOOS: target.goos, GOARCH: target.goarch },
|
|
53
|
+
});
|
|
54
|
+
return fs.existsSync(out) ? out : null;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
process.stderr.write(`magic-client: 现场编译失败: ${String(err.message).split("\n")[0]}\n`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const bin =
|
|
62
|
+
process.env.MAGIC_CLIENT_BIN && fs.existsSync(process.env.MAGIC_CLIENT_BIN)
|
|
63
|
+
? process.env.MAGIC_CLIENT_BIN
|
|
64
|
+
: fromDist() || fromLocalBin() || buildFromSource();
|
|
65
|
+
|
|
66
|
+
if (!bin) {
|
|
67
|
+
const packed = fs.existsSync(distDir) ? fs.readdirSync(distDir) : [];
|
|
68
|
+
process.stderr.write(
|
|
69
|
+
`magic-client: 当前平台 ${target.dir} 没有可用二进制(期望 ${clientBinaryPath(target)})\n` +
|
|
70
|
+
`已打包的平台:${packed.length ? packed.join(", ") : "(无)"}\n` +
|
|
71
|
+
`解决办法:装 Go 后重跑本命令(会自动现场编译),或 npm run build 重新打包。\n`
|
|
72
|
+
);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const res = spawnSync(bin, process.argv.slice(2), { stdio: "inherit", env: clientEnv(bin) });
|
|
77
|
+
|
|
78
|
+
// clientEnv 让插件用**同目录的** core 校验契约。
|
|
79
|
+
//
|
|
80
|
+
// 为什么不能靠 PATH:npm 包里两个二进制并排(npm/dist/<plat>/),而 PATH 里那份
|
|
81
|
+
// 可能是别处装的旧版本 —— 契约版本一比对不过,插件就直接拒绝启动,
|
|
82
|
+
// 用户看到的却是「刚装的这个坏了」。已经显式设了 MAGIC_AGENT_BIN 就尊重它。
|
|
83
|
+
function clientEnv(self) {
|
|
84
|
+
const env = { ...process.env };
|
|
85
|
+
if (env.MAGIC_AGENT_BIN) return env;
|
|
86
|
+
const sibling = path.join(path.dirname(self), exeName(currentTarget().goos));
|
|
87
|
+
if (fs.existsSync(sibling)) env.MAGIC_AGENT_BIN = sibling;
|
|
88
|
+
return env;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (res.error) {
|
|
92
|
+
process.stderr.write(`magic-client: 启动失败: ${res.error.message}\n`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
if (res.signal) {
|
|
96
|
+
if (isWin) process.exit(1);
|
|
97
|
+
process.kill(process.pid, res.signal);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
process.exit(res.status === null ? 1 : res.status);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/npm/install.js
CHANGED
|
@@ -20,46 +20,69 @@ const fs = require("fs");
|
|
|
20
20
|
const os = require("os");
|
|
21
21
|
const path = require("path");
|
|
22
22
|
|
|
23
|
-
const { currentTarget, binaryPath, pkgRoot, findGo } = require("./lib/platform");
|
|
23
|
+
const { currentTarget, binaryPath, clientBinaryPath, pkgRoot, findGo } = require("./lib/platform");
|
|
24
24
|
|
|
25
|
-
// ── 1) magic-agent 二进制
|
|
25
|
+
// ── 1) magic-agent / magic-client 二进制 ───────────────────────
|
|
26
|
+
//
|
|
27
|
+
// 两个入口一起兜底:client 缺了,`magic-agent client start` 会报「找不到 magic-client」,
|
|
28
|
+
// 而手机连接完全依赖它 —— 装上了却连不上,比装不上更难查。
|
|
26
29
|
|
|
27
30
|
const target = currentTarget();
|
|
28
|
-
const
|
|
31
|
+
const BINARIES = [
|
|
32
|
+
{ name: "magic-agent", out: binaryPath(target), entry: "./cmd/magic-agent" },
|
|
33
|
+
{ name: "magic-client", out: clientBinaryPath(target), entry: "./cmd/magic-client" },
|
|
34
|
+
];
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
for (const b of BINARIES) {
|
|
37
|
+
if (fs.existsSync(b.out)) continue;
|
|
31
38
|
const go = findGo();
|
|
32
39
|
if (go) {
|
|
33
40
|
const pkg = require(path.join(pkgRoot, "package.json"));
|
|
34
|
-
process.stdout.write(`magic-agent: 当前平台 ${target.dir}
|
|
41
|
+
process.stdout.write(`magic-agent: 当前平台 ${target.dir} 无 ${b.name} 预编译产物,尝试用 go 现场编译 ... `);
|
|
35
42
|
try {
|
|
36
43
|
execFileSync(
|
|
37
44
|
go,
|
|
38
|
-
["build", "-trimpath", "-ldflags", `-s -w -X github.com/darren/magic-agent/internal/cli.Version=${pkg.version}`, "-o", out,
|
|
45
|
+
["build", "-trimpath", "-ldflags", `-s -w -X github.com/darren/magic-agent/internal/cli.Version=${pkg.version}`, "-o", b.out, b.entry],
|
|
39
46
|
{
|
|
40
47
|
cwd: pkgRoot,
|
|
41
48
|
stdio: ["ignore", "ignore", "pipe"],
|
|
42
49
|
env: { ...process.env, CGO_ENABLED: "0", GOOS: target.goos, GOARCH: target.goarch },
|
|
43
50
|
}
|
|
44
51
|
);
|
|
45
|
-
|
|
52
|
+
// Windows 上没有 POSIX 权限位,chmodSync 会抛 EPERM/EINVAL,别碰。
|
|
53
|
+
if (target.goos !== "windows") fs.chmodSync(b.out, 0o755);
|
|
46
54
|
console.log("ok");
|
|
47
55
|
} catch (err) {
|
|
48
56
|
console.log(`失败: ${err.message}`);
|
|
49
57
|
}
|
|
50
58
|
} else {
|
|
51
59
|
process.stderr.write(
|
|
52
|
-
`magic-agent: 警告 - 当前平台 ${target.dir}
|
|
60
|
+
`magic-agent: 警告 - 当前平台 ${target.dir} 缺少 ${b.name} 预编译二进制,且本机未找到 go,命令可能无法运行。\n`
|
|
53
61
|
);
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
// ── 2) llm CLI(simonw/LLM)────────────────────────────────────
|
|
58
66
|
|
|
67
|
+
// venv 的可执行目录:POSIX 是 <venv>/bin,Windows 是 <venv>/Scripts。
|
|
68
|
+
// (Windows 的 venv 从来就是 Scripts;bin 只出现在 Unix。)
|
|
69
|
+
function venvDir(home) {
|
|
70
|
+
return path.join(home, ".llm-venv", process.platform === "win32" ? "Scripts" : "bin");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** llm 在 venv 里的可能文件名(Windows 上是 llm.exe)。 */
|
|
74
|
+
function venvExeCandidates(home) {
|
|
75
|
+
const dir = venvDir(home);
|
|
76
|
+
return process.platform === "win32"
|
|
77
|
+
? [path.join(dir, "llm.exe"), path.join(dir, "llm")]
|
|
78
|
+
: [path.join(dir, "llm")];
|
|
79
|
+
}
|
|
80
|
+
|
|
59
81
|
function haveLlm() {
|
|
60
82
|
if (process.env.MAGIC_AGENT_LLM_BIN) return process.env.MAGIC_AGENT_LLM_BIN;
|
|
61
|
-
const
|
|
62
|
-
|
|
83
|
+
for (const c of venvExeCandidates(os.homedir())) {
|
|
84
|
+
if (fs.existsSync(c)) return c;
|
|
85
|
+
}
|
|
63
86
|
try {
|
|
64
87
|
return execFileSync("llm", ["--version"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim() && "llm";
|
|
65
88
|
} catch (_) {
|
|
@@ -70,7 +93,6 @@ function haveLlm() {
|
|
|
70
93
|
|
|
71
94
|
function installLlm() {
|
|
72
95
|
const home = os.homedir();
|
|
73
|
-
const venvBin = path.join(home, ".llm-venv", "bin");
|
|
74
96
|
|
|
75
97
|
// 找一个能用的 python3(python3 > python;都不行时放弃)。
|
|
76
98
|
let py = null;
|
|
@@ -91,10 +113,11 @@ function installLlm() {
|
|
|
91
113
|
// Homebrew Python pip 可能因 PIP_BREAK_SYSTEM_PACKAGES / truststore 报错,
|
|
92
114
|
// venv 内不需要这些;静音升级提示。
|
|
93
115
|
delete env.PIP_BREAK_SYSTEM_PACKAGES;
|
|
116
|
+
// Windows 的 venv 自带 pip 一般已装好,但 -m pip 走解释器更稳。
|
|
94
117
|
execFileSync(py, ["-m", "venv", path.join(home, ".llm-venv")], { stdio: ["ignore", "ignore", "pipe"] });
|
|
95
118
|
execFileSync(
|
|
96
|
-
|
|
97
|
-
["install", "--quiet", "--disable-pip-version-check", "llm"],
|
|
119
|
+
py,
|
|
120
|
+
["-m", "pip", "install", "--quiet", "--disable-pip-version-check", "llm"],
|
|
98
121
|
{ stdio: ["ignore", "ignore", "pipe"], env }
|
|
99
122
|
);
|
|
100
123
|
console.log("ok");
|