agentquad 0.4.0 → 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.
@@ -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-oovKASxm.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Wl5vjZ8T.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentquad",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "AgentQuad — local four-quadrant AI task scheduler with embedded Claude Code / Codex terminals",
5
5
  "license": "MIT",
6
6
  "author": "LIUZHENHUA521",
@@ -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