agenthalo 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.
- package/README.md +39 -0
- package/bin/agenthalo.js +183 -0
- package/bin/checksums.json +4 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# agenthalo
|
|
2
|
+
|
|
3
|
+
一条命令装好 [AgentHalo](https://github.com/addsumtech/AgentHalo):跟着 AI 任务状态动的 macOS 桌面伙伴。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx agenthalo
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
装完会自动打开,桌宠在菜单栏。接着到「设置 → 连接应用」给你在用的 AI 工具装上 hook。
|
|
10
|
+
|
|
11
|
+
## 它做了什么
|
|
12
|
+
|
|
13
|
+
1. 识别芯片(Apple Silicon / Intel),选对应的包。
|
|
14
|
+
2. 从对应版本的 GitHub release 下载 zip。
|
|
15
|
+
3. 核对 SHA-256,和本包内固定的校验和比对。
|
|
16
|
+
4. 用 `ditto` 解压安装到 `/Applications`,并启动。
|
|
17
|
+
|
|
18
|
+
安装到别处:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx agenthalo --dir ~/Applications
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`/Applications` 不可写时会自动退到 `~/Applications`,全程不需要 sudo。
|
|
25
|
+
|
|
26
|
+
## 为什么用 npx 而不是下 dmg
|
|
27
|
+
|
|
28
|
+
macOS 的 Gatekeeper 只拦带 `com.apple.quarantine` 属性的文件,而这个属性是**下载文件的那个程序**打上去的:浏览器会打,`curl` 和 Node 不打。
|
|
29
|
+
|
|
30
|
+
所以浏览器下载的 dmg 会触发「无法验证开发者」,需要手动去「系统设置 → 隐私与安全性」放行;从这里装则不会,装完直接能开。两种方式拿到的是同一个应用。
|
|
31
|
+
|
|
32
|
+
## 要求
|
|
33
|
+
|
|
34
|
+
- macOS,Node 18 以上。
|
|
35
|
+
- 只有 macOS 包。其它平台见 [Releases](https://github.com/addsumtech/AgentHalo/releases)。
|
|
36
|
+
|
|
37
|
+
## 许可证
|
|
38
|
+
|
|
39
|
+
AGPL-3.0-only,与 AgentHalo 本体一致。AgentHalo 基于 [Clawd on Desk](https://github.com/rullerzhou-afk/clawd-on-desk) 开发,上游版权与许可证均已保留。
|
package/bin/agenthalo.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// npx agenthalo — 下载并安装 AgentHalo 桌面端。
|
|
5
|
+
//
|
|
6
|
+
// 为什么走这条路而不是让用户下 dmg:macOS 的 Gatekeeper 只拦带
|
|
7
|
+
// com.apple.quarantine 属性的文件,而这个属性是**下载的那个程序**打上去的。
|
|
8
|
+
// 浏览器会打,curl 和 Node 不打。所以从这里装的 app 不会触发「无法验证开发者」
|
|
9
|
+
// 的拦截,用户装完直接能开。
|
|
10
|
+
//
|
|
11
|
+
// 零依赖,只用 Node 内建模块:npx 要先把这个包拉下来才能跑,依赖越少启动越快。
|
|
12
|
+
|
|
13
|
+
const fs = require("node:fs");
|
|
14
|
+
const os = require("node:os");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
const crypto = require("node:crypto");
|
|
17
|
+
const { execFileSync, spawnSync } = require("node:child_process");
|
|
18
|
+
const { Readable } = require("node:stream");
|
|
19
|
+
const { pipeline } = require("node:stream/promises");
|
|
20
|
+
|
|
21
|
+
const VERSION = require("../package.json").version;
|
|
22
|
+
const REPO = "addsumtech/AgentHalo";
|
|
23
|
+
const APP_NAME = "AgentHalo.app";
|
|
24
|
+
// 覆盖下载来源,只为发版前拿真实产物端到端跑一遍(见 scripts/verify-install.sh)。
|
|
25
|
+
const BASE = process.env.AGENTHALO_DOWNLOAD_BASE
|
|
26
|
+
|| `https://github.com/${REPO}/releases/download/v${VERSION}`;
|
|
27
|
+
|
|
28
|
+
// 按版本固定校验和:既能挡住下到一半的坏包,也能发现 release 资产被换过。
|
|
29
|
+
// 每次发版由 scripts/sync-checksums.js 从 dist/ 重新写入。
|
|
30
|
+
const CHECKSUMS = require("./checksums.json");
|
|
31
|
+
|
|
32
|
+
function log(msg) {
|
|
33
|
+
process.stdout.write(`${msg}\n`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fail(msg) {
|
|
37
|
+
process.stderr.write(`\nAgentHalo 安装失败:${msg}\n`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// process.arch 在 Rosetta 下的 Node 里会报 x64,哪怕机器是 Apple Silicon。
|
|
42
|
+
// 直接问硬件,免得给 M 系列芯片装上 Intel 包。
|
|
43
|
+
function detectArch() {
|
|
44
|
+
if (process.arch === "arm64") return "arm64";
|
|
45
|
+
try {
|
|
46
|
+
const out = execFileSync("/usr/sbin/sysctl", ["-n", "hw.optional.arm64"], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
49
|
+
}).trim();
|
|
50
|
+
if (out === "1") return "arm64";
|
|
51
|
+
} catch {}
|
|
52
|
+
return "x64";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatMB(bytes) {
|
|
56
|
+
return (bytes / 1024 / 1024).toFixed(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function download(url, dest) {
|
|
60
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
fail(`下载返回 HTTP ${res.status}。\n ${url}\n 如果是 404,说明这个版本的 release 还没发布。`);
|
|
63
|
+
}
|
|
64
|
+
const total = Number(res.headers.get("content-length")) || 0;
|
|
65
|
+
let seen = 0;
|
|
66
|
+
let lastShown = 0;
|
|
67
|
+
|
|
68
|
+
const source = Readable.fromWeb(res.body);
|
|
69
|
+
// \r 只在终端里能原地覆盖。被重定向或管道接走时每一行都会留下,
|
|
70
|
+
// 120MB 就是几百行噪声,所以非终端环境只在结束时报一次。
|
|
71
|
+
const interactive = process.stdout.isTTY;
|
|
72
|
+
if (interactive) {
|
|
73
|
+
source.on("data", (chunk) => {
|
|
74
|
+
seen += chunk.length;
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
if (now - lastShown < 200 && seen !== total) return;
|
|
77
|
+
lastShown = now;
|
|
78
|
+
const pct = total ? ` ${Math.floor((seen / total) * 100)}%` : "";
|
|
79
|
+
process.stdout.write(`\r 已下载 ${formatMB(seen)}MB${total ? ` / ${formatMB(total)}MB` : ""}${pct} `);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await pipeline(source, fs.createWriteStream(dest));
|
|
84
|
+
if (interactive) process.stdout.write("\n");
|
|
85
|
+
else log(` 完成 ${formatMB(fs.statSync(dest).size)}MB`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sha256(file) {
|
|
89
|
+
const hash = crypto.createHash("sha256");
|
|
90
|
+
hash.update(fs.readFileSync(file));
|
|
91
|
+
return hash.digest("hex");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function quitRunningApp(appPath) {
|
|
95
|
+
const running = spawnSync("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
|
|
96
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
97
|
+
});
|
|
98
|
+
if (running.status !== 0) return;
|
|
99
|
+
log(" 检测到正在运行,先退出旧版本");
|
|
100
|
+
spawnSync("/usr/bin/osascript", ["-e", 'quit app "AgentHalo"'], { stdio: "ignore" });
|
|
101
|
+
for (let i = 0; i < 20; i += 1) {
|
|
102
|
+
const still = spawnSync("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
|
|
103
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
104
|
+
});
|
|
105
|
+
if (still.status !== 0) return;
|
|
106
|
+
spawnSync("/bin/sleep", ["0.5"], { stdio: "ignore" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveInstallDir() {
|
|
111
|
+
// --dir 给受管机器和想装到别处的人留的出口,也让安装流程可以在临时目录里验证。
|
|
112
|
+
const flag = process.argv.indexOf("--dir");
|
|
113
|
+
if (flag !== -1) {
|
|
114
|
+
const dir = process.argv[flag + 1];
|
|
115
|
+
if (!dir) fail("--dir 后面要跟一个目录路径。");
|
|
116
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
117
|
+
return path.resolve(dir);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
fs.accessSync("/Applications", fs.constants.W_OK);
|
|
121
|
+
return "/Applications";
|
|
122
|
+
} catch {
|
|
123
|
+
// 受管机器上 /Applications 可能不可写,退到用户自己的应用目录,不提权。
|
|
124
|
+
const userApps = path.join(os.homedir(), "Applications");
|
|
125
|
+
fs.mkdirSync(userApps, { recursive: true });
|
|
126
|
+
return userApps;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function main() {
|
|
131
|
+
if (process.platform !== "darwin") {
|
|
132
|
+
fail(
|
|
133
|
+
`当前只提供 macOS 版本(检测到 ${process.platform})。\n`
|
|
134
|
+
+ ` 其它平台请到 https://github.com/${REPO}/releases 查看。`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const arch = detectArch();
|
|
139
|
+
const asset = `AgentHalo-${VERSION}-${arch}.zip`;
|
|
140
|
+
const expected = CHECKSUMS[asset];
|
|
141
|
+
if (!expected) fail(`这个包里没有 ${asset} 的校验和,安装脚本和版本对不上。`);
|
|
142
|
+
|
|
143
|
+
log(`AgentHalo ${VERSION}(${arch === "arm64" ? "Apple Silicon" : "Intel"})`);
|
|
144
|
+
|
|
145
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), "agenthalo-"));
|
|
146
|
+
const zipPath = path.join(work, asset);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
log("下载中");
|
|
150
|
+
await download(`${BASE}/${asset}`, zipPath);
|
|
151
|
+
|
|
152
|
+
log("校验");
|
|
153
|
+
const actual = sha256(zipPath);
|
|
154
|
+
if (actual !== expected) {
|
|
155
|
+
fail(`校验和不匹配,文件可能没下完或已被改动。\n 期望 ${expected}\n 实际 ${actual}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
log("解压");
|
|
159
|
+
// 用 ditto 而不是 unzip:.app 里的 Frameworks 有符号链接和特殊权限,
|
|
160
|
+
// unzip 会弄坏它们,ditto 是 macOS 原生、electron-builder 的 zip 就是按它打的。
|
|
161
|
+
execFileSync("/usr/bin/ditto", ["-x", "-k", zipPath, work], { stdio: "inherit" });
|
|
162
|
+
const staged = path.join(work, APP_NAME);
|
|
163
|
+
if (!fs.existsSync(staged)) fail("压缩包里没有找到 AgentHalo.app。");
|
|
164
|
+
|
|
165
|
+
const installDir = resolveInstallDir();
|
|
166
|
+
const target = path.join(installDir, APP_NAME);
|
|
167
|
+
log(`安装到 ${target}`);
|
|
168
|
+
if (fs.existsSync(target)) {
|
|
169
|
+
quitRunningApp(target);
|
|
170
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
execFileSync("/usr/bin/ditto", [staged, target], { stdio: "inherit" });
|
|
173
|
+
|
|
174
|
+
spawnSync("/usr/bin/open", ["-a", target], { stdio: "ignore" });
|
|
175
|
+
log("");
|
|
176
|
+
log("装好了,桌宠在菜单栏。");
|
|
177
|
+
log("下一步:设置 → 连接应用,给你在用的 AI 工具装上 hook。");
|
|
178
|
+
} finally {
|
|
179
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
main().catch((err) => fail(err && err.message ? err.message : String(err)));
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agenthalo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "一条命令装好 AgentHalo:跟着 AI 任务状态动的桌面伙伴。Install AgentHalo, a desktop companion that follows your AI agents' state.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agenthalo": "bin/agenthalo.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"os": [
|
|
16
|
+
"darwin"
|
|
17
|
+
],
|
|
18
|
+
"license": "AGPL-3.0-only",
|
|
19
|
+
"keywords": [
|
|
20
|
+
"agenthalo",
|
|
21
|
+
"desktop-pet",
|
|
22
|
+
"ai-agent",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"codex",
|
|
25
|
+
"macos"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/addsumtech/AgentHalo.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/addsumtech/AgentHalo",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/addsumtech/AgentHalo/issues"
|
|
34
|
+
}
|
|
35
|
+
}
|