agentquad 0.4.1 → 0.4.3
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/dist-web/assets/{index-Wl5vjZ8T.css → index-CEiuiF0m.css} +1 -1
- package/dist-web/assets/{index-HC0Fs5po.js → index-nkG0O5n8.js} +244 -229
- package/dist-web/index.html +2 -2
- package/package.json +1 -1
- package/src/agent-installer-dispatcher.js +87 -0
- package/src/agent-installer-shared.js +101 -0
- package/src/claude-agent-installer.js +135 -0
- package/src/cli.js +208 -0
- package/src/codex-agent-installer.js +165 -0
- package/src/config.js +6 -0
- package/src/cursor-agent-installer.js +128 -0
- package/src/lark-api-client.js +59 -4
- package/src/lark-bot.js +7 -7
- package/src/lark-post.js +285 -0
- package/src/mcp/server.js +34 -21
- package/src/mcp/tools/openclaw/index.js +6 -0
- package/src/openclaw-hook.js +1 -1
- package/src/permission-prompt.js +188 -0
- package/src/pty.js +35 -6
- package/src/routes/ai-terminal.js +147 -15
- package/src/server.js +4 -3
- package/src/templates/agent-skills/agentquad-child.cursor.mdc +26 -0
- package/src/templates/agent-skills/agentquad-child.skill.md +29 -0
package/dist-web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>AgentQuad</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-nkG0O5n8.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CEiuiF0m.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三家 agent installer 统一分发:
|
|
3
|
+
* - install / uninstall / inspect / preview
|
|
4
|
+
* - 单个 target 失败不阻断其它
|
|
5
|
+
* - overrides 注入测试用路径
|
|
6
|
+
*/
|
|
7
|
+
import * as claudeInst from './claude-agent-installer.js'
|
|
8
|
+
import * as codexInst from './codex-agent-installer.js'
|
|
9
|
+
import * as cursorInst from './cursor-agent-installer.js'
|
|
10
|
+
|
|
11
|
+
const TARGETS = ['claude', 'codex', 'cursor']
|
|
12
|
+
|
|
13
|
+
function targetMod(name) {
|
|
14
|
+
if (name === 'claude') return claudeInst
|
|
15
|
+
if (name === 'codex') return codexInst
|
|
16
|
+
if (name === 'cursor') return cursorInst
|
|
17
|
+
throw new Error('unknown_target:' + name)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pickTargets(only) {
|
|
21
|
+
if (!only) return TARGETS
|
|
22
|
+
return TARGETS.filter(t => only.includes(t))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function installAllAgents({ port, version, only = null, overrides = {} } = {}) {
|
|
26
|
+
const results = {}
|
|
27
|
+
const failed = []
|
|
28
|
+
for (const t of pickTargets(only)) {
|
|
29
|
+
const args = { port, version, ...(overrides[t] || {}) }
|
|
30
|
+
try {
|
|
31
|
+
results[t] = targetMod(t).installAgent(args)
|
|
32
|
+
} catch (e) {
|
|
33
|
+
results[t] = { ok: false, error: e?.message || String(e) }
|
|
34
|
+
failed.push(t)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { results, summary: { failed } }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function uninstallAllAgents({ only = null, overrides = {} } = {}) {
|
|
41
|
+
const results = {}
|
|
42
|
+
const failed = []
|
|
43
|
+
for (const t of pickTargets(only)) {
|
|
44
|
+
try {
|
|
45
|
+
results[t] = targetMod(t).uninstallAgent(overrides[t] || {})
|
|
46
|
+
} catch (e) {
|
|
47
|
+
results[t] = { ok: false, error: e?.message || String(e) }
|
|
48
|
+
failed.push(t)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { results, summary: { failed } }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function inspectAllAgents({ expectedPort = null, only = null, overrides = {} } = {}) {
|
|
55
|
+
const results = {}
|
|
56
|
+
const failed = []
|
|
57
|
+
for (const t of pickTargets(only)) {
|
|
58
|
+
try {
|
|
59
|
+
results[t] = targetMod(t).inspectAgent({ expectedPort, ...(overrides[t] || {}) })
|
|
60
|
+
} catch (e) {
|
|
61
|
+
results[t] = { target: t, mcpRegistered: false, skillPresent: false, drift: false, error: e?.message || String(e) }
|
|
62
|
+
failed.push(t)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { results, summary: { failed } }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function previewAllAgents({ port, version, only = null, overrides = {} } = {}) {
|
|
69
|
+
// 干跑:先 inspect 看现状,再算出"如果 install 会做什么"
|
|
70
|
+
const results = {}
|
|
71
|
+
const failed = []
|
|
72
|
+
for (const t of pickTargets(only)) {
|
|
73
|
+
try {
|
|
74
|
+
const ins = targetMod(t).inspectAgent({ expectedPort: port, ...(overrides[t] || {}) })
|
|
75
|
+
const changes = []
|
|
76
|
+
if (!ins.mcpRegistered) changes.push('mcp_registered')
|
|
77
|
+
else if (ins.drift) changes.push('mcp_port_update')
|
|
78
|
+
else if (ins.version !== version) changes.push('mcp_version_update')
|
|
79
|
+
if (!ins.skillPresent) changes.push(t === 'cursor' ? 'rule_installed' : 'skill_installed')
|
|
80
|
+
results[t] = { changes }
|
|
81
|
+
} catch (e) {
|
|
82
|
+
results[t] = { changes: [], error: e?.message || String(e) }
|
|
83
|
+
failed.push(t)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { results, summary: { failed } }
|
|
87
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三家 agent installer 共享工具:
|
|
3
|
+
* - marker 元数据(_agentquadManaged 旁路键)
|
|
4
|
+
* - atomic JSON 写入(O_EXCL + rename)
|
|
5
|
+
* - 运行时 MCP 配置文件读写(spec C 方案)
|
|
6
|
+
*
|
|
7
|
+
* Marker 约定:JSON 文件里和 mcpServers.agentquad 同级放:
|
|
8
|
+
* { _agentquadManaged: { version, port, generatedAt } }
|
|
9
|
+
* 不引入独立 lockfile,跟现有 hook installer 风格保持一致。
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, renameSync, unlinkSync, readdirSync, statSync } from 'node:fs'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { randomBytes } from 'node:crypto'
|
|
14
|
+
|
|
15
|
+
export function buildMarker({ version, port }) {
|
|
16
|
+
return {
|
|
17
|
+
version: String(version || ''),
|
|
18
|
+
port: Number(port) || 0,
|
|
19
|
+
generatedAt: new Date().toISOString(),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isAgentquadManaged(entry) {
|
|
24
|
+
if (!entry || typeof entry !== 'object') return false
|
|
25
|
+
const m = entry._agentquadManaged
|
|
26
|
+
return !!(m && typeof m === 'object' && typeof m.version === 'string')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Atomic generic file write via tmp + rename. Used by writeJsonAtomic and TOML callers.
|
|
31
|
+
*/
|
|
32
|
+
export function writeFileAtomic(targetPath, content) {
|
|
33
|
+
mkdirSync(dirname(targetPath), { recursive: true })
|
|
34
|
+
const tmp = `${targetPath}.tmp.${randomBytes(4).toString('hex')}`
|
|
35
|
+
writeFileSync(tmp, content, { encoding: 'utf8' })
|
|
36
|
+
renameSync(tmp, targetPath)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Atomic JSON 写入。
|
|
41
|
+
* 通过 `<target>.tmp.<rand>` 中转 + rename,保证不出现部分写入。
|
|
42
|
+
*/
|
|
43
|
+
export function writeJsonAtomic(targetPath, value) {
|
|
44
|
+
writeFileAtomic(targetPath, JSON.stringify(value, null, 2) + '\n')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 运行时 MCP 配置文件(C 方案)。
|
|
49
|
+
* - tool=claude → JSON 格式(claude --mcp-config 接受 JSON)
|
|
50
|
+
* - tool=codex → TOML 格式
|
|
51
|
+
* 路径:<runtimeDir>/mcp-<sessionId>.{json|toml}
|
|
52
|
+
*/
|
|
53
|
+
export function writeRuntimeMcpConfig({ runtimeDir, sessionId, port, tool }) {
|
|
54
|
+
mkdirSync(runtimeDir, { recursive: true })
|
|
55
|
+
const url = `http://127.0.0.1:${port}/mcp`
|
|
56
|
+
if (tool === 'codex') {
|
|
57
|
+
const path = join(runtimeDir, `mcp-${sessionId}.toml`)
|
|
58
|
+
const toml = `# agentquad runtime mcp config — auto generated, do not edit\n` +
|
|
59
|
+
`[mcp_servers.agentquad]\n` +
|
|
60
|
+
`url = "${url}"\n` +
|
|
61
|
+
`transport = "http"\n`
|
|
62
|
+
writeFileAtomic(path, toml)
|
|
63
|
+
return { path, format: 'toml' }
|
|
64
|
+
}
|
|
65
|
+
// default: claude json format
|
|
66
|
+
const path = join(runtimeDir, `mcp-${sessionId}.json`)
|
|
67
|
+
writeJsonAtomic(path, {
|
|
68
|
+
mcpServers: {
|
|
69
|
+
agentquad: {
|
|
70
|
+
type: 'http',
|
|
71
|
+
url,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
return { path, format: 'json' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function cleanupRuntimeMcpConfig({ runtimeDir, sessionId }) {
|
|
79
|
+
for (const ext of ['json', 'toml']) {
|
|
80
|
+
const p = join(runtimeDir, `mcp-${sessionId}.${ext}`)
|
|
81
|
+
try { if (existsSync(p)) unlinkSync(p) } catch { /* swallow */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 给 doctor / dispatcher 用:扫 runtimeDir,列出过去 maxAgeMs 没刷新的孤儿。
|
|
87
|
+
*/
|
|
88
|
+
export function listStaleRuntimeConfigs({ runtimeDir, maxAgeMs = 24 * 3600 * 1000 } = {}) {
|
|
89
|
+
if (!existsSync(runtimeDir)) return []
|
|
90
|
+
const now = Date.now()
|
|
91
|
+
return readdirSync(runtimeDir)
|
|
92
|
+
.filter(n => /^mcp-.*\.(json|toml)$/.test(n))
|
|
93
|
+
.map(n => {
|
|
94
|
+
try {
|
|
95
|
+
return { name: n, path: join(runtimeDir, n), age: now - statSync(join(runtimeDir, n)).mtimeMs }
|
|
96
|
+
} catch {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.filter(x => x && x.age > maxAgeMs)
|
|
101
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code agent installer:
|
|
3
|
+
* - 写 ~/.claude.json 的 mcpServers.agentquad(带 _agentquadManaged 旁路 marker)
|
|
4
|
+
* - 装 ~/.claude/skills/agentquad-child/SKILL.md
|
|
5
|
+
*
|
|
6
|
+
* 跟 src/openclaw-hook-installer.js 风格保持一致 —— 都是改 ~/.claude.json。
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, copyFileSync, rmSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { buildMarker, isAgentquadManaged, writeJsonAtomic } from './agent-installer-shared.js'
|
|
13
|
+
|
|
14
|
+
const SKILL_NAME = 'agentquad-child'
|
|
15
|
+
|
|
16
|
+
function defaultClaudeJsonPath() {
|
|
17
|
+
return join(homedir(), '.claude.json')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function defaultSkillsDir() {
|
|
21
|
+
return join(homedir(), '.claude', 'skills')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function defaultSkillTemplatePath() {
|
|
25
|
+
return fileURLToPath(new URL('./templates/agent-skills/agentquad-child.skill.md', import.meta.url))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readClaudeJson(path) {
|
|
29
|
+
if (!existsSync(path)) return {}
|
|
30
|
+
const raw = readFileSync(path, 'utf8')
|
|
31
|
+
if (!raw.trim()) return {}
|
|
32
|
+
try { return JSON.parse(raw) } catch (e) { throw new Error(`malformed_claude_json: ${e.message}`) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function installAgent({
|
|
36
|
+
claudeJsonPath = defaultClaudeJsonPath(),
|
|
37
|
+
skillsDir = defaultSkillsDir(),
|
|
38
|
+
skillTemplatePath = defaultSkillTemplatePath(),
|
|
39
|
+
port,
|
|
40
|
+
version,
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (!port) throw new Error('port_required')
|
|
43
|
+
if (!version) throw new Error('version_required')
|
|
44
|
+
|
|
45
|
+
const changes = []
|
|
46
|
+
const cur = readClaudeJson(claudeJsonPath)
|
|
47
|
+
cur.mcpServers = cur.mcpServers || {}
|
|
48
|
+
|
|
49
|
+
const desired = {
|
|
50
|
+
type: 'http',
|
|
51
|
+
url: `http://127.0.0.1:${port}/mcp`,
|
|
52
|
+
}
|
|
53
|
+
const prev = cur.mcpServers.agentquad
|
|
54
|
+
const prevMarker = cur._agentquadManaged
|
|
55
|
+
const samePort = prev && prev.url === desired.url
|
|
56
|
+
const sameVersion = prevMarker && prevMarker.version === version
|
|
57
|
+
|
|
58
|
+
cur.mcpServers.agentquad = desired
|
|
59
|
+
if (samePort && sameVersion && isAgentquadManaged(cur)) {
|
|
60
|
+
// 保留旧 generatedAt,让 idempotent 写回不改 hash
|
|
61
|
+
cur._agentquadManaged = prevMarker
|
|
62
|
+
} else {
|
|
63
|
+
cur._agentquadManaged = buildMarker({ version, port })
|
|
64
|
+
changes.push('mcp_registered')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
writeJsonAtomic(claudeJsonPath, cur)
|
|
68
|
+
|
|
69
|
+
// skill
|
|
70
|
+
const skillDir = join(skillsDir, SKILL_NAME)
|
|
71
|
+
const skillFile = join(skillDir, 'SKILL.md')
|
|
72
|
+
if (!existsSync(skillFile) || readFileSync(skillFile, 'utf8') !== readFileSync(skillTemplatePath, 'utf8')) {
|
|
73
|
+
mkdirSync(skillDir, { recursive: true })
|
|
74
|
+
copyFileSync(skillTemplatePath, skillFile)
|
|
75
|
+
changes.push('skill_installed')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ok: true, changes, configPath: claudeJsonPath, skillPath: skillFile }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function uninstallAgent({
|
|
82
|
+
claudeJsonPath = defaultClaudeJsonPath(),
|
|
83
|
+
skillsDir = defaultSkillsDir(),
|
|
84
|
+
} = {}) {
|
|
85
|
+
const removed = []
|
|
86
|
+
if (existsSync(claudeJsonPath)) {
|
|
87
|
+
const cur = readClaudeJson(claudeJsonPath)
|
|
88
|
+
if (cur.mcpServers?.agentquad) {
|
|
89
|
+
delete cur.mcpServers.agentquad
|
|
90
|
+
removed.push('mcp_entry')
|
|
91
|
+
}
|
|
92
|
+
if (cur._agentquadManaged) {
|
|
93
|
+
delete cur._agentquadManaged
|
|
94
|
+
removed.push('marker')
|
|
95
|
+
}
|
|
96
|
+
if (removed.length > 0) writeJsonAtomic(claudeJsonPath, cur)
|
|
97
|
+
}
|
|
98
|
+
const skillDir = join(skillsDir, SKILL_NAME)
|
|
99
|
+
if (existsSync(skillDir)) {
|
|
100
|
+
rmSync(skillDir, { recursive: true, force: true })
|
|
101
|
+
removed.push('skill')
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, removed }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function inspectAgent({
|
|
107
|
+
claudeJsonPath = defaultClaudeJsonPath(),
|
|
108
|
+
skillsDir = defaultSkillsDir(),
|
|
109
|
+
expectedPort = null,
|
|
110
|
+
} = {}) {
|
|
111
|
+
const out = {
|
|
112
|
+
target: 'claude',
|
|
113
|
+
mcpRegistered: false,
|
|
114
|
+
skillPresent: false,
|
|
115
|
+
drift: false,
|
|
116
|
+
configPath: claudeJsonPath,
|
|
117
|
+
expectedPort,
|
|
118
|
+
actualPort: null,
|
|
119
|
+
version: null,
|
|
120
|
+
}
|
|
121
|
+
if (existsSync(claudeJsonPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const cur = readClaudeJson(claudeJsonPath)
|
|
124
|
+
if (cur.mcpServers?.agentquad?.url) {
|
|
125
|
+
out.mcpRegistered = true
|
|
126
|
+
const m = cur.mcpServers.agentquad.url.match(/:(\d+)\//)
|
|
127
|
+
if (m) out.actualPort = Number(m[1])
|
|
128
|
+
out.version = cur._agentquadManaged?.version || null
|
|
129
|
+
}
|
|
130
|
+
} catch { /* malformed, treat as not registered */ }
|
|
131
|
+
}
|
|
132
|
+
if (existsSync(join(skillsDir, SKILL_NAME, 'SKILL.md'))) out.skillPresent = true
|
|
133
|
+
if (out.mcpRegistered && expectedPort && out.actualPort !== expectedPort) out.drift = true
|
|
134
|
+
return out
|
|
135
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -361,9 +361,127 @@ export async function doctorReport({ rootDir = DEFAULT_ROOT_DIR } = {}) {
|
|
|
361
361
|
// 注:hook check 已经在 openclaw 段做过;不重复
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
// ─── agents(claude/codex/cursor 的 AgentQuad MCP + skill 装机状态)──────
|
|
365
|
+
try {
|
|
366
|
+
const { inspectAllAgents } = await import('./agent-installer-dispatcher.js')
|
|
367
|
+
const { listStaleRuntimeConfigs } = await import('./agent-installer-shared.js')
|
|
368
|
+
const expectedPort = cfg?.port || 5677
|
|
369
|
+
const r = inspectAllAgents({ expectedPort })
|
|
370
|
+
|
|
371
|
+
for (const [t, ins] of Object.entries(r.results)) {
|
|
372
|
+
if (ins.error) {
|
|
373
|
+
checks.push({ name: `agents.${t}`, ok: false, detail: `error reading config: ${ins.error}` })
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
const mcpOk = ins.mcpRegistered
|
|
377
|
+
const skillOk = ins.skillPresent
|
|
378
|
+
const both = mcpOk && skillOk
|
|
379
|
+
const fixHint = `跑 \`agentquad agents install --target ${t}\``
|
|
380
|
+
const parts = []
|
|
381
|
+
parts.push(mcpOk ? 'MCP ✓' : 'MCP ✗')
|
|
382
|
+
parts.push(skillOk ? 'skill ✓' : 'skill ✗')
|
|
383
|
+
if (ins.drift) parts.push(`⚠ drift (actual:${ins.actualPort} expected:${ins.expectedPort})`)
|
|
384
|
+
checks.push({
|
|
385
|
+
name: `agents.${t}`,
|
|
386
|
+
ok: both && !ins.drift,
|
|
387
|
+
detail: both && !ins.drift
|
|
388
|
+
? parts.join(' ')
|
|
389
|
+
: `${parts.join(' ')} — 修复: ${fixHint}`,
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// orphan runtime configs
|
|
394
|
+
const runtimeDir = cfg?.agents?.runtimeDir
|
|
395
|
+
? cfg.agents.runtimeDir.replace(/^~/, homedir())
|
|
396
|
+
: join(homedir(), '.agentquad', 'run')
|
|
397
|
+
const stale = listStaleRuntimeConfigs({ runtimeDir })
|
|
398
|
+
if (stale.length > 0) {
|
|
399
|
+
checks.push({
|
|
400
|
+
name: 'agents runtime orphans',
|
|
401
|
+
ok: true, // 仅 warning,不影响整体 ok
|
|
402
|
+
detail: `${stale.length} 个 24h+ 未刷新的运行时 MCP 配置在 ${runtimeDir}(可手动删除)`,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// active PTY soft warning — 查 running server's /api/status
|
|
407
|
+
try {
|
|
408
|
+
const pidInfo = readPidFile(DEFAULT_ROOT_DIR)
|
|
409
|
+
if (pidInfo?.pid && isAlive(pidInfo.pid)) {
|
|
410
|
+
const port = pidInfo.port ?? expectedPort
|
|
411
|
+
const resp = await fetch(`http://127.0.0.1:${port}/api/status`, { signal: AbortSignal.timeout(1500) })
|
|
412
|
+
const body = await resp.json()
|
|
413
|
+
const active = body.activeSessions ?? 0
|
|
414
|
+
const warnAt = cfg?.agents?.warnPtyCount || 8
|
|
415
|
+
if (active >= warnAt) {
|
|
416
|
+
checks.push({
|
|
417
|
+
name: 'agents pty count',
|
|
418
|
+
ok: true, // 仅 warning
|
|
419
|
+
detail: `⚠ 活跃 AgentQuad PTY 数 = ${active}(≥ 阈值 ${warnAt},请留意是否失控)`,
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} catch { /* server 没在跑 / 网络抖动,忽略 */ }
|
|
424
|
+
|
|
425
|
+
} catch (e) {
|
|
426
|
+
checks.push({ name: 'agents check', ok: false, detail: `agents inspect failed: ${e?.message || e}` })
|
|
427
|
+
}
|
|
428
|
+
|
|
364
429
|
return { ok: checks.every(c => c.ok), checks }
|
|
365
430
|
}
|
|
366
431
|
|
|
432
|
+
async function promptYesNo(question) {
|
|
433
|
+
const readline = await import('node:readline')
|
|
434
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
435
|
+
return new Promise((resolve) => {
|
|
436
|
+
rl.question(question, (a) => {
|
|
437
|
+
rl.close()
|
|
438
|
+
const text = (a || '').trim().toLowerCase()
|
|
439
|
+
// empty input → default Yes
|
|
440
|
+
resolve(text === '' || text === 'y' || text === 'yes')
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function bootstrapAgentsIfNeeded({ cfg, version, port, isTTY }) {
|
|
446
|
+
const mode = cfg?.agents?.autoBootstrap || 'prompt'
|
|
447
|
+
if (mode === 'never') return
|
|
448
|
+
if (cfg?.agents?.bootstrapDismissed) return
|
|
449
|
+
|
|
450
|
+
const { previewAllAgents, installAllAgents } = await import('./agent-installer-dispatcher.js')
|
|
451
|
+
const p = previewAllAgents({ port, version })
|
|
452
|
+
const needed = Object.entries(p.results)
|
|
453
|
+
.filter(([, v]) => (v.changes || []).length > 0)
|
|
454
|
+
.map(([k]) => k)
|
|
455
|
+
if (needed.length === 0) return
|
|
456
|
+
|
|
457
|
+
if (mode === 'silent') {
|
|
458
|
+
const r = installAllAgents({ port, version, only: needed })
|
|
459
|
+
console.log(`[agents] auto bootstrap: ${Object.keys(r.results).join(', ')}`)
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!isTTY) {
|
|
464
|
+
console.warn(`[agents] 检测到未配置的 agent 工具: ${needed.join(', ')}(运行 \`agentquad agents install\` 启用)`)
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// prompt mode (interactive TTY)
|
|
469
|
+
const ok = await promptYesNo(`[agents] 检测到 ${needed.join(', ')} 未配置 AgentQuad MCP / skill,现在安装吗?[Y/n] `)
|
|
470
|
+
if (ok) {
|
|
471
|
+
const r = installAllAgents({ port, version, only: needed })
|
|
472
|
+
for (const [t, res] of Object.entries(r.results)) {
|
|
473
|
+
console.log(` ${t}: ${res.ok ? (res.changes?.join(', ') || 'ok') : `error: ${res.error}`}`)
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
try {
|
|
477
|
+
setConfigValue('agents.bootstrapDismissed', 'true')
|
|
478
|
+
} catch (e) {
|
|
479
|
+
console.warn(`[agents] 持久化 dismissed 失败: ${e.message}`)
|
|
480
|
+
}
|
|
481
|
+
console.log('[agents] 已记住你的选择;运行 `agentquad agents install` 可手动启用')
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
367
485
|
// runStart:start 子命令的核心实现,导出给默认 action / 首跑向导复用
|
|
368
486
|
export async function runStart(cmdOpts = {}) {
|
|
369
487
|
// dry-run 短路(仅用于测试,让默认 action 测试不真起服务 / 不跑向导)
|
|
@@ -505,6 +623,19 @@ export async function runStart(cmdOpts = {}) {
|
|
|
505
623
|
}
|
|
506
624
|
}
|
|
507
625
|
|
|
626
|
+
// ─── 自动 bootstrap 三家 AI CLI 的 AgentQuad MCP + skill(agents)───
|
|
627
|
+
try {
|
|
628
|
+
const pkg = JSON.parse(readFileSync(resolvePath(__dirname, '../package.json'), 'utf8'))
|
|
629
|
+
await bootstrapAgentsIfNeeded({
|
|
630
|
+
cfg,
|
|
631
|
+
version: pkg.version,
|
|
632
|
+
port: actualPort,
|
|
633
|
+
isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
634
|
+
})
|
|
635
|
+
} catch (e) {
|
|
636
|
+
console.warn(`[agents] bootstrap failed: ${e?.message || e}`)
|
|
637
|
+
}
|
|
638
|
+
|
|
508
639
|
// listen 完成后异步发"重启完成 + Resume N 个会话"通知到 telegram。
|
|
509
640
|
// 不 await,发不发都不阻塞 boot;postText 走 telegram HTTPS 直发,不依赖 long-poll
|
|
510
641
|
if (typeof srv.notifyStartupRecovery === 'function') {
|
|
@@ -1061,6 +1192,83 @@ addToolFlags(hookCmd.command('bootstrap'))
|
|
|
1061
1192
|
.description('一键部署 hook script + 安装 hooks(强制忽略 .uninstalled marker,用于"删过又想恢复"场景)')
|
|
1062
1193
|
.action(actBootstrapHookMulti)
|
|
1063
1194
|
|
|
1195
|
+
// ─── agents 子命令组:装/卸 AgentQuad MCP + skill 到 Claude Code / Codex / Cursor ───
|
|
1196
|
+
const agentsCmd = program.command('agents').description('为 Claude Code / Codex / Cursor 装 AgentQuad MCP + skill(嵌套子 agent 能力)')
|
|
1197
|
+
|
|
1198
|
+
const VALID_AGENT_TARGETS = ['claude', 'codex', 'cursor']
|
|
1199
|
+
|
|
1200
|
+
function addAgentTargetFlag(cmd) {
|
|
1201
|
+
return cmd.option('--target <name>', '指定 claude / codex / cursor,多次传入累加', (v, acc = []) => {
|
|
1202
|
+
if (!VALID_AGENT_TARGETS.includes(v)) throw new Error(`unknown agents target: ${v}`)
|
|
1203
|
+
acc.push(v)
|
|
1204
|
+
return acc
|
|
1205
|
+
}, undefined)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function readAgentsCtx() {
|
|
1209
|
+
const pkg = JSON.parse(readFileSync(resolvePath(__dirname, '../package.json'), 'utf8'))
|
|
1210
|
+
const cfg = loadConfig()
|
|
1211
|
+
return { port: cfg.port || 5677, version: pkg.version }
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
addAgentTargetFlag(agentsCmd.command('install'))
|
|
1215
|
+
.description('合并写入 MCP 配置 + skill 文件;默认装三家')
|
|
1216
|
+
.option('--dry-run', '只 preview 不写盘')
|
|
1217
|
+
.action(async (opts) => {
|
|
1218
|
+
const { installAllAgents, previewAllAgents } = await import('./agent-installer-dispatcher.js')
|
|
1219
|
+
const { port, version } = readAgentsCtx()
|
|
1220
|
+
const only = opts.target || null
|
|
1221
|
+
if (opts.dryRun) {
|
|
1222
|
+
const p = previewAllAgents({ port, version, only })
|
|
1223
|
+
console.log('dry-run preview:')
|
|
1224
|
+
for (const [t, r] of Object.entries(p.results)) {
|
|
1225
|
+
const text = r.changes && r.changes.length ? r.changes.join(', ') : 'no changes'
|
|
1226
|
+
console.log(` ${t}: ${text}`)
|
|
1227
|
+
}
|
|
1228
|
+
if (p.summary?.failed?.length) {
|
|
1229
|
+
for (const t of p.summary.failed) console.error(` ${t}: error — ${p.results[t]?.error || 'unknown'}`)
|
|
1230
|
+
}
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
const r = installAllAgents({ port, version, only })
|
|
1234
|
+
for (const [t, res] of Object.entries(r.results)) {
|
|
1235
|
+
if (res.ok) console.log(`✓ ${t}:`, res.changes?.length ? res.changes.join(', ') : 'already up to date')
|
|
1236
|
+
else console.error(`✗ ${t}:`, res.error)
|
|
1237
|
+
}
|
|
1238
|
+
if (r.summary?.failed?.length) process.exitCode = 1
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
addAgentTargetFlag(agentsCmd.command('uninstall'))
|
|
1242
|
+
.description('删除 marker 段 + skill 文件;用户其他 mcpServers / skill 不动')
|
|
1243
|
+
.action(async (opts) => {
|
|
1244
|
+
const { uninstallAllAgents } = await import('./agent-installer-dispatcher.js')
|
|
1245
|
+
const only = opts.target || null
|
|
1246
|
+
const r = uninstallAllAgents({ only })
|
|
1247
|
+
for (const [t, res] of Object.entries(r.results)) {
|
|
1248
|
+
if (res.ok) console.log(`✓ ${t}: removed`, (res.removed || []).join(', ') || 'nothing')
|
|
1249
|
+
else console.error(`✗ ${t}:`, res.error)
|
|
1250
|
+
}
|
|
1251
|
+
if (r.summary?.failed?.length) process.exitCode = 1
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
agentsCmd.command('status')
|
|
1255
|
+
.description('查看三家 agent 工具的 AgentQuad MCP / skill 安装状态 + drift')
|
|
1256
|
+
.action(async () => {
|
|
1257
|
+
const { inspectAllAgents } = await import('./agent-installer-dispatcher.js')
|
|
1258
|
+
const { port } = readAgentsCtx()
|
|
1259
|
+
const r = inspectAllAgents({ expectedPort: port })
|
|
1260
|
+
for (const [t, ins] of Object.entries(r.results)) {
|
|
1261
|
+
if (ins.error) {
|
|
1262
|
+
console.log(` ${t.padEnd(8)} ✗ error — ${ins.error}`)
|
|
1263
|
+
continue
|
|
1264
|
+
}
|
|
1265
|
+
const mcp = ins.mcpRegistered ? '✓ MCP' : '✗ MCP'
|
|
1266
|
+
const sk = ins.skillPresent ? '✓ skill' : '✗ skill'
|
|
1267
|
+
const drift = ins.drift ? ` ⚠ drift (actual:${ins.actualPort} expected:${ins.expectedPort})` : ''
|
|
1268
|
+
console.log(` ${t.padEnd(8)} ${mcp} ${sk} ${ins.configPath}${drift}`)
|
|
1269
|
+
}
|
|
1270
|
+
})
|
|
1271
|
+
|
|
1064
1272
|
// ─── openclaw 子命令组:保留旧路径以向后兼容;hook 操作建议改用 `agentquad hook *` ───
|
|
1065
1273
|
const openclawCmd = program.command('openclaw').description('OpenClaw bridge: install/uninstall Claude Code hooks for proactive WeChat push')
|
|
1066
1274
|
|