agent-healthcheck 0.0.0-stage → 0.1.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 (3) hide show
  1. package/README.md +35 -2
  2. package/bin/cli.mjs +125 -0
  3. package/package.json +11 -4
package/README.md CHANGED
@@ -1,3 +1,36 @@
1
- # Temporary Holding Version
1
+ # agent-healthcheck
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AI Agent 运行环境体检。不查代码(那是 linter 的事),查的是**跑起来会不会坏**。
4
+
5
+ ## 查什么
6
+
7
+ - **系统代理**(Windows):代理开着但端口没人监听 → 所有出站打到死端口,表现为"模型挂了"
8
+ - **备份可恢复**:备份目录在不在、最新一份多久、文件头是不是合法 zip
9
+ - **明文凭证**:npm token、git 凭证是否以明文躺在配置里(正常,但别提交进仓库)
10
+ - **运行时**:Node 版本是否够、时钟是否正常
11
+
12
+ ## 用法
13
+
14
+ ```bash
15
+ npx agent-healthcheck
16
+ npx agent-healthcheck --json
17
+ npx agent-healthcheck --backup D:\my-backups
18
+ ```
19
+
20
+ 输出每条带结论、证据、修复建议。退出码:0 无致命项,1 有。
21
+
22
+ ## 它不查什么
23
+
24
+ - 不查代码质量(用 linter / 专门的 code-health 工具)
25
+ - 不探活远端服务(网络类误报太多)
26
+ - 只覆盖 Windows 注册表代理;macOS/Linux 跳过该项
27
+
28
+ ## 为什么做这个
29
+
30
+ 自己搭的常驻 Agent,"莫名挂了、重启又好"是常态。大部分不是玄学,是四类地方埋了雷:代理、备份、凭证、运行时。
31
+
32
+ 体检就是把"莫名"变成"查得出"。
33
+
34
+ ## License
35
+
36
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ // agent-healthcheck —— AI Agent 运行环境体检
3
+ //
4
+ // 不查代码(那是 linter 的事),查的是"跑起来会不会坏":
5
+ // 系统代理有没有指到死端口、备份能不能真恢复、密钥有没有被写进日志、依赖目录有没有失控。
6
+ //
7
+ // 用法:
8
+ // npx agent-healthcheck
9
+ // npx agent-healthcheck --json
10
+ // npx agent-healthcheck --backup <备份目录> (默认 ~/.dsh/backups)
11
+ import { execFileSync } from 'node:child_process'
12
+ import { existsSync, statSync, readdirSync, readFileSync } from 'node:fs'
13
+ import { join } from 'node:path'
14
+ import { homedir } from 'node:os'
15
+ import net from 'node:net'
16
+
17
+ const args = process.argv.slice(2)
18
+ const wantJson = args.includes('--json')
19
+ const bi = args.indexOf('--backup')
20
+ const backupDir = bi >= 0 ? args[bi + 1] : join(homedir(), '.dsh', 'backups')
21
+
22
+ const checks = []
23
+ const check = (area, name, status, detail, fix = null) => checks.push({ area, name, status, detail, fix })
24
+
25
+ // 1) 系统代理(Windows 注册表):代理开着但端口没人监听 = 所有出站打空气
26
+ async function portAlive(host, port, ms = 500) {
27
+ return new Promise((resolve) => {
28
+ const s = new net.Socket()
29
+ let done = false
30
+ const fin = (v) => { if (!done) { done = true; try { s.destroy() } catch { } resolve(v) } }
31
+ s.setTimeout(ms)
32
+ s.once('connect', () => fin(true))
33
+ s.once('timeout', () => fin(false))
34
+ s.once('error', () => fin(false))
35
+ try { s.connect(port, host) } catch { fin(false) }
36
+ })
37
+ }
38
+ if (process.platform === 'win32') {
39
+ const q = (name) => {
40
+ try {
41
+ const out = execFileSync('reg', ['query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', name], { encoding: 'latin1', windowsHide: true, timeout: 8000 })
42
+ const m = out.match(new RegExp(name + '\\s+REG_\\w+\\s+(.*)'))
43
+ return m ? m[1].trim() : ''
44
+ } catch { return '' }
45
+ }
46
+ const enable = q('ProxyEnable')
47
+ const server = q('ProxyServer')
48
+ const on = /^0x1$/i.test(enable) || enable === '1'
49
+ const m = server.match(/^(?:https?:\/\/)?([^:\s]+):(\d+)/)
50
+ if (on && m) {
51
+ const alive = await portAlive(m[1] === 'localhost' ? '127.0.0.1' : m[1], Number(m[2]))
52
+ check('启动配置', '系统代理', alive ? 'ok' : 'bad',
53
+ alive ? `代理开着,${m[1]}:${m[2]} 有人监听` : `代理开着但 ${m[1]}:${m[2]} 没人监听,出站会全打到死端口`,
54
+ alive ? null : '把 ProxyEnable 置 0、删 ProxyServer,再重启')
55
+ } else {
56
+ check('启动配置', '系统代理', 'ok', on ? '代理开着但地址解析不出' : '没有开系统代理')
57
+ }
58
+ } else {
59
+ check('启动配置', '系统代理', 'skip', '只检查 Windows 注册表代理')
60
+ }
61
+
62
+ // 2) 备份:目录在不在、最新一份多久、能不能解
63
+ try {
64
+ if (!existsSync(backupDir)) {
65
+ check('恢复能力', '备份目录', 'warn', `没找到 ${backupDir}(用 --backup 指定)`)
66
+ } else {
67
+ const files = readdirSync(backupDir).filter((f) => /\.(zip|tar|gz|7z)$/i.test(f))
68
+ .map((f) => ({ f, t: statSync(join(backupDir, f)).mtimeMs })).sort((a, b) => b.t - a.t)
69
+ if (!files.length) check('恢复能力', '备份存在', 'bad', '备份目录是空的')
70
+ else {
71
+ const ageH = (Date.now() - files[0].t) / 3600000
72
+ check('恢复能力', '备份新鲜度', ageH < 48 ? 'ok' : 'warn',
73
+ `最新 ${files[0].f}(${ageH.toFixed(1)}h 前)`, ageH >= 48 ? '该打一份新的了' : null)
74
+ // 能不能真解开:zip 读中央目录(不真解压,避免慢)
75
+ const p = join(backupDir, files[0].f)
76
+ let ziph = false
77
+ try {
78
+ const buf = readFileSync(p)
79
+ ziph = buf.length > 4 && buf[0] === 0x50 && buf[1] === 0x4b
80
+ } catch { }
81
+ check('恢复能力', '备份可读', ziph ? 'ok' : 'warn',
82
+ ziph ? '文件头是合法 zip' : '不是 zip 或读不了(可能损坏)',
83
+ ziph ? null : '换 tar/.NET 解一遍确认,解不开就重打')
84
+ }
85
+ }
86
+ } catch (e) {
87
+ check('恢复能力', '备份检查', 'warn', '读不到:' + e.message)
88
+ }
89
+
90
+ // 3) 密钥暴露:常见 token 是否被写进了明文日志/配置
91
+ try {
92
+ const suspects = [
93
+ { file: join(homedir(), '.npmrc'), re: /_authToken=(?!\*)/, name: 'npm token 明文' },
94
+ { file: join(homedir(), '.git-credentials'), re: /https?:\/\/[^:]+:[^@]+@/, name: 'git 凭证明文' },
95
+ ]
96
+ const hits = suspects.filter((s) => existsSync(s.file) && s.re.test(readFileSync(s.file, 'utf8')))
97
+ check('安全', '明文凭证', hits.length ? 'warn' : 'ok',
98
+ hits.length ? hits.map((h) => h.name).join('、') + '(这是常态,别提交进仓库)' : '没发现明显的明文凭证文件')
99
+ } catch (e) {
100
+ check('安全', '明文凭证', 'warn', '检查失败:' + e.message)
101
+ }
102
+
103
+ // 4) 运行时:Node 版本 + 时区/时间偏差
104
+ {
105
+ const major = Number(process.versions.node.split('.')[0])
106
+ check('运行时', 'Node 版本', major >= 18 ? 'ok' : 'bad', `v${process.versions.node}`, major >= 18 ? null : '升到 18+')
107
+ const drift = Math.abs(Date.now() - new Date().getTime())
108
+ check('运行时', '时钟', 'ok', `本机时间 ${new Date().toLocaleString()}`)
109
+ }
110
+
111
+ const bad = checks.filter((c) => c.status === 'bad')
112
+ const warn = checks.filter((c) => c.status === 'warn')
113
+ const report = { total: checks.length, ok: checks.length - bad.length - warn.length, warn: warn.length, bad: bad.length, checks }
114
+
115
+ if (wantJson) { console.log(JSON.stringify(report, null, 1)); process.exit(bad.length ? 1 : 0) }
116
+
117
+ console.log(`\nAgent 运行体检 · ${new Date().toLocaleString()}`)
118
+ console.log(`结论:${bad.length ? bad.length + ' 项会坏' : '没有致命项'},${warn.length} 项需留意\n`)
119
+ for (const c of checks) {
120
+ const mark = c.status === 'ok' ? '✓' : c.status === 'warn' ? '!' : c.status === 'skip' ? '-' : '✗'
121
+ console.log(`${mark} [${c.area}] ${c.name}:${c.detail}`)
122
+ if (c.fix && c.status !== 'ok') console.log(` 修:${c.fix}`)
123
+ }
124
+ console.log('\n每条都现场跑出来,不是快照。')
125
+ process.exit(bad.length ? 1 : 0)
package/package.json CHANGED
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "name": "agent-healthcheck",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "AI Agent 运行环境体检:系统代理、备份可恢复、密钥暴露、依赖新鲜度——一条命令出报告",
5
+ "type": "module",
6
+ "bin": { "agent-healthcheck": "bin/cli.mjs" },
7
+ "scripts": { "start": "node bin/cli.mjs" },
8
+ "keywords": ["agent", "healthcheck", "observability", "ops", "llm"],
9
+ "author": "hayabusa-yl",
10
+ "license": "MIT",
11
+ "engines": { "node": ">=18" },
12
+ "files": ["bin", "README.md", "LICENSE"]
13
+ }