agentquad 0.3.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/LICENSE +21 -0
- package/README.md +318 -0
- package/dist-web/assets/index-CMaXwixo.js +1234 -0
- package/dist-web/assets/index-DBHApzV1.css +32 -0
- package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
- package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
- package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist-web/assets/logo-D4DDtU-r.png +0 -0
- package/dist-web/favicon.png +0 -0
- package/dist-web/index.html +14 -0
- package/package.json +88 -0
- package/src/ask-user-buttons.js +142 -0
- package/src/claude-transcript.js +203 -0
- package/src/cli.js +1040 -0
- package/src/codex-event-emitter.js +111 -0
- package/src/codex-prompt-detector.js +53 -0
- package/src/codex-sidecar.js +52 -0
- package/src/codex-transcript.js +74 -0
- package/src/config.js +692 -0
- package/src/data/claude-code-commands.json +52 -0
- package/src/db.js +1503 -0
- package/src/dispatch.js +13 -0
- package/src/export/todoMarkdown.js +246 -0
- package/src/first-run-wizard.js +82 -0
- package/src/git/gitStatus.js +139 -0
- package/src/lark-api-client.js +205 -0
- package/src/lark-bot.js +510 -0
- package/src/lark-card.js +88 -0
- package/src/lark-config-service.js +16 -0
- package/src/lark-event-client.js +107 -0
- package/src/lark-image.js +99 -0
- package/src/lark-markdown.js +51 -0
- package/src/lark-video.js +163 -0
- package/src/mcp/audit.js +34 -0
- package/src/mcp/server.js +83 -0
- package/src/mcp/tools/destructive/index.js +252 -0
- package/src/mcp/tools/openclaw/index.js +405 -0
- package/src/mcp/tools/read/index.js +269 -0
- package/src/mcp/tools/write/index.js +157 -0
- package/src/openclaw-bridge.js +566 -0
- package/src/openclaw-hook-installer.js +338 -0
- package/src/openclaw-hook.js +908 -0
- package/src/openclaw-wizard.js +2442 -0
- package/src/pending-questions.js +297 -0
- package/src/pricing.js +45 -0
- package/src/prompt-render.js +36 -0
- package/src/pty.js +992 -0
- package/src/routes/ai-terminal.js +1228 -0
- package/src/routes/git.js +89 -0
- package/src/routes/openclaw-hook.js +67 -0
- package/src/routes/openclaw-inbound.js +36 -0
- package/src/routes/recurringRules.js +80 -0
- package/src/routes/reports.js +50 -0
- package/src/routes/search.js +46 -0
- package/src/routes/stats.js +31 -0
- package/src/routes/telegram-config.js +152 -0
- package/src/routes/telegram-sync.js +221 -0
- package/src/routes/templates.js +63 -0
- package/src/routes/todos.js +649 -0
- package/src/routes/transcripts.js +75 -0
- package/src/routes/uploads.js +107 -0
- package/src/routes/wiki.js +142 -0
- package/src/search/fts.js +209 -0
- package/src/search/index.js +199 -0
- package/src/search/transcripts.js +148 -0
- package/src/server.js +1791 -0
- package/src/session-input-dispatcher.js +256 -0
- package/src/stats/markdown.js +42 -0
- package/src/stats/report.js +207 -0
- package/src/summarize.js +84 -0
- package/src/system-rules.js +52 -0
- package/src/telegram-bot.js +875 -0
- package/src/telegram-commands.js +149 -0
- package/src/telegram-config-service.js +84 -0
- package/src/telegram-image.js +95 -0
- package/src/telegram-loading-status.js +112 -0
- package/src/telegram-markdown.js +82 -0
- package/src/telegram-reaction-tracker.js +69 -0
- package/src/telegram-video.js +75 -0
- package/src/templates/claude-hooks/notify.js +103 -0
- package/src/transcript.js +305 -0
- package/src/transcripts/blocks.js +56 -0
- package/src/transcripts/index.js +222 -0
- package/src/transcripts/indexer.js +34 -0
- package/src/transcripts/matcher.js +70 -0
- package/src/transcripts/scanner.js +259 -0
- package/src/usage-footer.js +170 -0
- package/src/usage-parser.js +132 -0
- package/src/wiki/guide.js +44 -0
- package/src/wiki/index.js +232 -0
- package/src/wiki/redact.js +34 -0
- package/src/wiki/sources.js +122 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, realpathSync } from 'node:fs'
|
|
4
|
+
import { homedir, networkInterfaces } from 'node:os'
|
|
5
|
+
import { join, dirname, resolve as resolvePath } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_ROOT_DIR,
|
|
10
|
+
loadConfig,
|
|
11
|
+
saveConfig,
|
|
12
|
+
getConfigValue,
|
|
13
|
+
setConfigValue,
|
|
14
|
+
resolveToolsConfig,
|
|
15
|
+
} from './config.js'
|
|
16
|
+
import { shouldRunWizard, runFirstRunWizard } from './first-run-wizard.js'
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
19
|
+
const __dirname = dirname(__filename)
|
|
20
|
+
|
|
21
|
+
// Bin names verified via `npm view <pkg> bin`.
|
|
22
|
+
// kind:
|
|
23
|
+
// - 'npm' → installed via `npm install -g <pkg>` (claude / codex)
|
|
24
|
+
// - 'shell' → installed via piping `<script>` to a shell (cursor; upstream installer)
|
|
25
|
+
export const TOOL_PACKAGES = {
|
|
26
|
+
claude: { kind: 'npm', pkg: '@anthropic-ai/claude-code', bin: 'claude' },
|
|
27
|
+
codex: { kind: 'npm', pkg: '@openai/codex', bin: 'codex' },
|
|
28
|
+
cursor: { kind: 'shell', script: 'curl https://cursor.com/install -fsSL | bash', bin: 'cursor-agent' },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function planInstallTools(opts) {
|
|
32
|
+
const flags = opts || {}
|
|
33
|
+
const explicit = []
|
|
34
|
+
if (flags.claude) explicit.push('claude')
|
|
35
|
+
if (flags.codex) explicit.push('codex')
|
|
36
|
+
if (flags.cursor) explicit.push('cursor')
|
|
37
|
+
if (flags.all || explicit.length === 0) return ['claude', 'codex', 'cursor']
|
|
38
|
+
return explicit
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function loadPkgVersion() {
|
|
42
|
+
try {
|
|
43
|
+
const pkg = JSON.parse(readFileSync(resolvePath(__dirname, '../package.json'), 'utf8'))
|
|
44
|
+
return pkg.version || '0.0.0'
|
|
45
|
+
} catch { return '0.0.0' }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pidFile(rootDir = DEFAULT_ROOT_DIR) {
|
|
49
|
+
return join(rootDir, 'agentquad.pid')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function writePidFile(rootDir, { pid, port, host }) {
|
|
53
|
+
const payload = { pid, port, host, startedAt: new Date().toISOString() }
|
|
54
|
+
writeFileSync(pidFile(rootDir), JSON.stringify(payload))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readPidFile(rootDir) {
|
|
58
|
+
const pf = pidFile(rootDir)
|
|
59
|
+
if (!existsSync(pf)) return null
|
|
60
|
+
let raw
|
|
61
|
+
try {
|
|
62
|
+
raw = readFileSync(pf, 'utf8').trim()
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const obj = JSON.parse(raw)
|
|
68
|
+
if (obj && typeof obj.pid === 'number' && obj.pid > 0) return obj
|
|
69
|
+
} catch { /* legacy plain-number */ }
|
|
70
|
+
const n = Number(raw)
|
|
71
|
+
if (Number.isFinite(n) && n > 0) return { pid: n }
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isAlive(pid) {
|
|
76
|
+
try { process.kill(pid, 0); return true } catch { return false }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Tailscale 私网段:100.64.0.0 / 10 (RFC 6598 CGNAT)
|
|
80
|
+
function isTailscaleIPv4(addr) {
|
|
81
|
+
if (!addr || typeof addr !== 'string') return false
|
|
82
|
+
const parts = addr.split('.').map(Number)
|
|
83
|
+
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return false
|
|
84
|
+
return parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 枚举本机可用于访问的地址:区分 Tailscale / LAN / loopback。
|
|
88
|
+
// 返回 { tailscale: [...], lan: [...], loopback: [...] },每项带 name + address。
|
|
89
|
+
export function collectReachableAddresses() {
|
|
90
|
+
const out = { tailscale: [], lan: [], loopback: [] }
|
|
91
|
+
const ifs = networkInterfaces()
|
|
92
|
+
for (const [name, entries] of Object.entries(ifs)) {
|
|
93
|
+
for (const entry of entries || []) {
|
|
94
|
+
if (entry.family !== 'IPv4') continue
|
|
95
|
+
if (entry.internal) {
|
|
96
|
+
out.loopback.push({ name, address: entry.address })
|
|
97
|
+
} else if (isTailscaleIPv4(entry.address) || /tailscale|utun/i.test(name)) {
|
|
98
|
+
// 兜底:macOS 下 Tailscale 通常是 utunN 接口,配合 100.x 判定更稳
|
|
99
|
+
if (isTailscaleIPv4(entry.address)) {
|
|
100
|
+
out.tailscale.push({ name, address: entry.address })
|
|
101
|
+
} else {
|
|
102
|
+
out.lan.push({ name, address: entry.address })
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
out.lan.push({ name, address: entry.address })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildStartupBanner({ port, host, addresses = collectReachableAddresses() }) {
|
|
113
|
+
const lines = []
|
|
114
|
+
const url = (addr) => `http://${addr}:${port}`
|
|
115
|
+
const isLoopbackOnly = host === '127.0.0.1' || host === 'localhost'
|
|
116
|
+
|
|
117
|
+
if (isLoopbackOnly) {
|
|
118
|
+
lines.push(`AgentQuad listening on ${url('127.0.0.1')} (loopback only)`)
|
|
119
|
+
lines.push('')
|
|
120
|
+
lines.push('⚠️ To access from phone via Tailscale, run:')
|
|
121
|
+
lines.push(' agentquad config set host 0.0.0.0')
|
|
122
|
+
lines.push(' or start with:')
|
|
123
|
+
lines.push(' agentquad start --expose')
|
|
124
|
+
} else {
|
|
125
|
+
lines.push(`AgentQuad listening on ${url(host === '0.0.0.0' || host === '::' ? 'all-interfaces' : host)} (port ${port})`)
|
|
126
|
+
lines.push('')
|
|
127
|
+
lines.push('⚠️ SECURITY: AgentQuad exposes a shell + AI terminal. Reachable URLs:')
|
|
128
|
+
if (addresses.tailscale.length) {
|
|
129
|
+
lines.push(' Tailscale (recommended — private mesh VPN):')
|
|
130
|
+
for (const item of addresses.tailscale) {
|
|
131
|
+
lines.push(` ${url(item.address)} [${item.name}]`)
|
|
132
|
+
}
|
|
133
|
+
lines.push(' Tip: with MagicDNS you can also use http://<your-mac-name>:' + port)
|
|
134
|
+
} else {
|
|
135
|
+
lines.push(' ❌ No Tailscale interface detected.')
|
|
136
|
+
lines.push(' Install Tailscale on this Mac + your phone, sign into the same account.')
|
|
137
|
+
lines.push(' Guide: docs/MOBILE.md')
|
|
138
|
+
}
|
|
139
|
+
if (addresses.lan.length) {
|
|
140
|
+
lines.push(' LAN (same-WiFi only — anyone on the same network can reach these):')
|
|
141
|
+
for (const item of addresses.lan) {
|
|
142
|
+
lines.push(` ${url(item.address)} [${item.name}]`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push('')
|
|
146
|
+
lines.push(' Do NOT put this URL on the public internet without an auth layer.')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return lines.join('\n')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── exported helpers (for tests) ───
|
|
153
|
+
|
|
154
|
+
/** Fixed list of check names — lets tests assert the structure is complete. */
|
|
155
|
+
export function buildDoctorChecks() {
|
|
156
|
+
return [
|
|
157
|
+
'rootDir exists',
|
|
158
|
+
'config.json parseable',
|
|
159
|
+
'better-sqlite3 loadable',
|
|
160
|
+
'node-pty loadable',
|
|
161
|
+
'claude binary',
|
|
162
|
+
'codex binary',
|
|
163
|
+
'cursor binary',
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Runs every check and returns a structured report.
|
|
169
|
+
* @param {object} opts
|
|
170
|
+
* @param {string} [opts.rootDir] override root dir (tests use tmpdir)
|
|
171
|
+
*/
|
|
172
|
+
export async function doctorReport({ rootDir = DEFAULT_ROOT_DIR } = {}) {
|
|
173
|
+
const checks = []
|
|
174
|
+
|
|
175
|
+
checks.push({
|
|
176
|
+
name: 'rootDir exists',
|
|
177
|
+
ok: existsSync(rootDir) || (loadConfig({ rootDir }), true),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
const major = Number(process.version.slice(1).split('.')[0])
|
|
182
|
+
checks.push({
|
|
183
|
+
name: 'Node version',
|
|
184
|
+
ok: major >= 20,
|
|
185
|
+
detail: process.version + (major >= 20 ? '' : ' (please upgrade to Node 20+; e.g. `nvm install 20`)'),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
const distIndex = resolvePath(__dirname, '../dist-web/index.html')
|
|
191
|
+
const ok = existsSync(distIndex)
|
|
192
|
+
checks.push({
|
|
193
|
+
name: 'frontend assets',
|
|
194
|
+
ok,
|
|
195
|
+
detail: ok
|
|
196
|
+
? distIndex
|
|
197
|
+
: `missing ${distIndex} — run \`npm run build\` (from source) or \`npm i -g agentquad\` (reinstall)`,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let cfg = null
|
|
202
|
+
try {
|
|
203
|
+
cfg = loadConfig({ rootDir })
|
|
204
|
+
checks.push({ name: 'config.json parseable', ok: true })
|
|
205
|
+
} catch (e) {
|
|
206
|
+
checks.push({ name: 'config.json parseable', ok: false, detail: e.message })
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const { default: Database } = await import('better-sqlite3')
|
|
211
|
+
const test = new Database(':memory:')
|
|
212
|
+
test.prepare('SELECT 1').get()
|
|
213
|
+
test.close()
|
|
214
|
+
checks.push({ name: 'better-sqlite3 loadable', ok: true })
|
|
215
|
+
} catch (e) {
|
|
216
|
+
checks.push({ name: 'better-sqlite3 loadable', ok: false, detail: e.message })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await import('node-pty')
|
|
221
|
+
checks.push({ name: 'node-pty loadable', ok: true })
|
|
222
|
+
} catch (e) {
|
|
223
|
+
checks.push({ name: 'node-pty loadable', ok: false, detail: e.message })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const tool of ['claude', 'codex', 'cursor']) {
|
|
227
|
+
const bin = cfg?.tools?.[tool]?.bin || cfg?.tools?.[tool]?.command || tool
|
|
228
|
+
const which = spawnSync('command', ['-v', bin], {
|
|
229
|
+
encoding: 'utf8',
|
|
230
|
+
shell: '/bin/sh',
|
|
231
|
+
})
|
|
232
|
+
const ok = which.status === 0 && which.stdout.trim().length > 0
|
|
233
|
+
checks.push({
|
|
234
|
+
name: `${tool} binary`,
|
|
235
|
+
ok,
|
|
236
|
+
detail: ok ? which.stdout.trim() : `${bin} not found in PATH`,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── OpenClaw 桥接(仅当启用时检查)─────────────────────
|
|
241
|
+
const oc = cfg?.openclaw || {}
|
|
242
|
+
if (oc.enabled) {
|
|
243
|
+
// 1. openclaw CLI 可用?
|
|
244
|
+
const ocCli = spawnSync('command', ['-v', 'openclaw'], {
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
shell: '/bin/sh',
|
|
247
|
+
})
|
|
248
|
+
const ocCliOk = ocCli.status === 0 && ocCli.stdout.trim().length > 0
|
|
249
|
+
checks.push({
|
|
250
|
+
name: 'openclaw CLI',
|
|
251
|
+
ok: ocCliOk,
|
|
252
|
+
detail: ocCliOk ? ocCli.stdout.trim() : 'openclaw not in PATH (install via `npm i -g openclaw`)',
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// 2. targetUserId 配置(fallback)
|
|
256
|
+
// 主路径下,每个 ai-session 启动时由 OpenClaw skill 显式传 routeUserId(per-session)。
|
|
257
|
+
// 这里的 targetUserId 只是 ad-hoc ask_user / 没绑 session 时的兜底。
|
|
258
|
+
// 因此空值仅警告,不算 fail。
|
|
259
|
+
if (oc.targetUserId) {
|
|
260
|
+
checks.push({
|
|
261
|
+
name: 'openclaw.targetUserId (fallback)',
|
|
262
|
+
ok: true,
|
|
263
|
+
detail: oc.targetUserId,
|
|
264
|
+
})
|
|
265
|
+
} else {
|
|
266
|
+
checks.push({
|
|
267
|
+
name: 'openclaw.targetUserId (fallback)',
|
|
268
|
+
ok: true,
|
|
269
|
+
detail: '空(per-session 路由仍可工作;如要 ad-hoc 推送,set via `agentquad config set openclaw.targetUserId <peer-id>`)',
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 3. AgentQuad skill 装好了吗(OpenClaw 端配置)
|
|
274
|
+
const skillFile = join(homedir(), '.openclaw', 'skills', 'agentquad-claw', 'SKILL.md')
|
|
275
|
+
checks.push({
|
|
276
|
+
name: 'agentquad-claw skill installed',
|
|
277
|
+
ok: existsSync(skillFile),
|
|
278
|
+
detail: existsSync(skillFile)
|
|
279
|
+
? skillFile
|
|
280
|
+
: '缺失:参考 docs/OPENCLAW.md',
|
|
281
|
+
})
|
|
282
|
+
// 3b. legacy skill 目录还在?软警告(非 failing)
|
|
283
|
+
const legacySkillDir = join(homedir(), '.openclaw', 'skills', 'quadtodo-claw')
|
|
284
|
+
if (existsSync(legacySkillDir)) {
|
|
285
|
+
checks.push({
|
|
286
|
+
name: 'legacy openclaw skill folder',
|
|
287
|
+
ok: true,
|
|
288
|
+
detail: 'legacy ~/.openclaw/skills/quadtodo-claw/ still exists — safe to delete',
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 4. Claude Code hook 安装状态(主动推送)
|
|
293
|
+
try {
|
|
294
|
+
const { inspectHooks } = await import('./openclaw-hook-installer.js')
|
|
295
|
+
const hk = inspectHooks()
|
|
296
|
+
checks.push({
|
|
297
|
+
name: 'claude-code hook script',
|
|
298
|
+
ok: hk.scriptExists,
|
|
299
|
+
detail: hk.hookScriptPath + (hk.scriptExists ? '' : ' (missing — should be auto-installed)'),
|
|
300
|
+
})
|
|
301
|
+
checks.push({
|
|
302
|
+
name: 'claude-code hooks installed',
|
|
303
|
+
ok: hk.installed,
|
|
304
|
+
detail: hk.installed
|
|
305
|
+
? `events: ${hk.eventsInstalled.join(', ')}`
|
|
306
|
+
: '缺失:跑 `agentquad openclaw install-hook` 一次',
|
|
307
|
+
})
|
|
308
|
+
} catch (e) {
|
|
309
|
+
checks.push({
|
|
310
|
+
name: 'claude-code hooks',
|
|
311
|
+
ok: false,
|
|
312
|
+
detail: `inspect failed: ${e.message}`,
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── Telegram 直连(仅当启用时检查)────────────────────────
|
|
318
|
+
const tg = cfg?.telegram || {}
|
|
319
|
+
if (tg.enabled) {
|
|
320
|
+
// 5. supergroupId
|
|
321
|
+
checks.push({
|
|
322
|
+
name: 'telegram.supergroupId',
|
|
323
|
+
ok: Boolean(tg.supergroupId),
|
|
324
|
+
detail: tg.supergroupId || '未配置:第一次跑 AgentQuad 时让 bot 拿 chat.id(log 里),再 `agentquad config set telegram.supergroupId <id>`',
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// 6. allowedChatIds(白名单)
|
|
328
|
+
const allowList = Array.isArray(tg.allowedChatIds) ? tg.allowedChatIds : []
|
|
329
|
+
checks.push({
|
|
330
|
+
name: 'telegram.allowedChatIds',
|
|
331
|
+
ok: allowList.length > 0,
|
|
332
|
+
detail: allowList.length > 0
|
|
333
|
+
? allowList.join(', ')
|
|
334
|
+
: '空 = 拒所有:跑 `agentquad config set telegram.allowedChatIds.0 <supergroup-id>`',
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// 7. token(从 ~/.agentquad/config.json 读)
|
|
338
|
+
try {
|
|
339
|
+
const { readBotToken } = await import('./telegram-bot.js')
|
|
340
|
+
const tok = readBotToken(() => cfg)
|
|
341
|
+
checks.push({
|
|
342
|
+
name: 'telegram bot token',
|
|
343
|
+
ok: Boolean(tok),
|
|
344
|
+
detail: tok ? '✓ token in ~/.agentquad/config.json' : '缺失:在 Web Settings → Telegram 里填 Bot Token,或编辑 ~/.agentquad/config.json 的 telegram.botToken',
|
|
345
|
+
})
|
|
346
|
+
} catch (e) {
|
|
347
|
+
checks.push({ name: 'telegram bot token', ok: false, detail: e.message })
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 注:hook check 已经在 openclaw 段做过;不重复
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { ok: checks.every(c => c.ok), checks }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// runStart:start 子命令的核心实现,导出给默认 action / 首跑向导复用
|
|
357
|
+
export async function runStart(cmdOpts = {}) {
|
|
358
|
+
// dry-run 短路(仅用于测试,让默认 action 测试不真起服务 / 不跑向导)
|
|
359
|
+
if (process.env.AGENTQUAD_DRY_RUN === '1') return
|
|
360
|
+
|
|
361
|
+
const rootDir = DEFAULT_ROOT_DIR
|
|
362
|
+
const cfg = loadConfig({ rootDir })
|
|
363
|
+
const defaultCwd = cmdOpts.cwd || cfg.defaultCwd || process.env.HOME || process.cwd()
|
|
364
|
+
const host = cmdOpts.expose
|
|
365
|
+
? '0.0.0.0'
|
|
366
|
+
: (cmdOpts.host || cfg.host || '127.0.0.1')
|
|
367
|
+
|
|
368
|
+
// 首跑向导(命中条件才进;任何异常都不阻塞后续 start)
|
|
369
|
+
try {
|
|
370
|
+
const need = shouldRunWizard({
|
|
371
|
+
rootDir,
|
|
372
|
+
isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
373
|
+
env: process.env,
|
|
374
|
+
flags: { wizard: cmdOpts.wizard !== false },
|
|
375
|
+
})
|
|
376
|
+
if (need) {
|
|
377
|
+
const r = await runFirstRunWizard()
|
|
378
|
+
if (r.defaultTool) {
|
|
379
|
+
setConfigValue('defaultTool', r.defaultTool, { rootDir })
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch (e) {
|
|
383
|
+
console.warn(`⚠ first-run wizard skipped: ${e?.message || e}`)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ─── stdout/stderr 复制到 ~/.agentquad/logs/agentquad.log ───
|
|
387
|
+
// 保留正常 console 输出 + 同步追加到日志文件,方便诊断
|
|
388
|
+
try {
|
|
389
|
+
const logsDir = join(rootDir, 'logs')
|
|
390
|
+
if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true })
|
|
391
|
+
const logFile = join(logsDir, 'agentquad.log')
|
|
392
|
+
// 启动时如果 log > 5MB 就截断到尾部 1MB
|
|
393
|
+
try {
|
|
394
|
+
const { statSync } = await import('node:fs')
|
|
395
|
+
const st = statSync(logFile)
|
|
396
|
+
if (st.size > 5 * 1024 * 1024) {
|
|
397
|
+
const buf = readFileSync(logFile)
|
|
398
|
+
const tail = buf.subarray(buf.length - 1024 * 1024)
|
|
399
|
+
writeFileSync(logFile, tail)
|
|
400
|
+
}
|
|
401
|
+
} catch { /* file 不存在或读不了,忽略 */ }
|
|
402
|
+
const { createWriteStream } = await import('node:fs')
|
|
403
|
+
const logStream = createWriteStream(logFile, { flags: 'a' })
|
|
404
|
+
logStream.write(`\n=== agentquad start ${new Date().toISOString()} pid=${process.pid} ===\n`)
|
|
405
|
+
const wrap = (orig) => (...args) => {
|
|
406
|
+
try {
|
|
407
|
+
const line = args.map((a) => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')
|
|
408
|
+
logStream.write(`${new Date().toISOString()} ${line}\n`)
|
|
409
|
+
} catch { /* 写 log 失败不阻塞 */ }
|
|
410
|
+
orig.apply(console, args)
|
|
411
|
+
}
|
|
412
|
+
console.log = wrap(console.log)
|
|
413
|
+
console.info = wrap(console.info)
|
|
414
|
+
console.warn = wrap(console.warn)
|
|
415
|
+
console.error = wrap(console.error)
|
|
416
|
+
console.debug = wrap(console.debug)
|
|
417
|
+
} catch (e) {
|
|
418
|
+
console.warn(`[startup] log file setup failed: ${e.message}; continuing without file log`)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const pf = pidFile(rootDir)
|
|
422
|
+
const existing = readPidFile(rootDir)
|
|
423
|
+
if (existing && isAlive(existing.pid)) {
|
|
424
|
+
const where = existing.port ? `http://${existing.host || '127.0.0.1'}:${existing.port}` : '(unknown port)'
|
|
425
|
+
console.error(`AgentQuad already running (pid ${existing.pid}) at ${where}. Run 'agentquad stop' first.`)
|
|
426
|
+
process.exit(1)
|
|
427
|
+
}
|
|
428
|
+
if (existing) { try { unlinkSync(pf) } catch { /* ignore */ } }
|
|
429
|
+
|
|
430
|
+
const port = cmdOpts.port || cfg.port
|
|
431
|
+
const { createServer } = await import('./server.js')
|
|
432
|
+
const srv = createServer({
|
|
433
|
+
dbFile: join(rootDir, 'data.db'),
|
|
434
|
+
logDir: join(rootDir, 'logs'),
|
|
435
|
+
tools: resolveToolsConfig(cfg.tools),
|
|
436
|
+
defaultCwd,
|
|
437
|
+
configRootDir: rootDir,
|
|
438
|
+
webDist: resolvePath(__dirname, '../dist-web'),
|
|
439
|
+
strictWebDist: true,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
let actualPort
|
|
443
|
+
try {
|
|
444
|
+
actualPort = await srv.listen(port, host)
|
|
445
|
+
} catch (e) {
|
|
446
|
+
if (e.code === 'EADDRINUSE') {
|
|
447
|
+
console.error(`ports ${port} and ${port + 1} both in use — run 'agentquad config set port <newPort>' or stop whoever holds them`)
|
|
448
|
+
} else if (e.code === 'EADDRNOTAVAIL') {
|
|
449
|
+
console.error(`host ${host} not available on this machine — try --host 0.0.0.0`)
|
|
450
|
+
} else {
|
|
451
|
+
console.error(`listen failed: ${e.message}`)
|
|
452
|
+
}
|
|
453
|
+
process.exit(1)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
writePidFile(rootDir, { pid: process.pid, port: actualPort, host })
|
|
457
|
+
console.log(buildStartupBanner({ port: actualPort, host }))
|
|
458
|
+
console.log(`AI terminal default cwd: ${defaultCwd}`)
|
|
459
|
+
|
|
460
|
+
// ─── 自动 bootstrap Claude Code hook(部署 notify.js + 合入 settings.json)───
|
|
461
|
+
// 设计:缺啥补啥 / 已装则 noop / 用户跑过 uninstall-hook 留下的 marker 会被尊重
|
|
462
|
+
// 任何错误一律 warn-skip,绝不让 hook bootstrap 把 agentquad start 挂掉
|
|
463
|
+
try {
|
|
464
|
+
const { bootstrapHooks } = await import('./openclaw-hook-installer.js')
|
|
465
|
+
const r = bootstrapHooks()
|
|
466
|
+
if (r.skipped) {
|
|
467
|
+
if (r.reason === 'uninstall_marker') {
|
|
468
|
+
console.log(`ℹ claude-code hook: 已被你 uninstall-hook 拒绝;想恢复跑 'agentquad openclaw bootstrap'`)
|
|
469
|
+
} else if (r.reason === 'malformed_settings') {
|
|
470
|
+
console.warn(`⚠ claude-code hook: ~/.claude/settings.json JSON 损坏,跳过自动安装;修好后跑 'agentquad openclaw bootstrap'`)
|
|
471
|
+
} else {
|
|
472
|
+
console.log(`ℹ claude-code hook bootstrap skipped: ${r.reason}`)
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
if (r.scriptResult.action === 'installed') {
|
|
476
|
+
console.log(`✓ claude-code hook script installed (v${r.scriptResult.version}) → ${r.scriptResult.scriptPath}`)
|
|
477
|
+
} else if (r.scriptResult.action === 'upgraded') {
|
|
478
|
+
console.log(`✓ claude-code hook script upgraded v${r.scriptResult.previousVersion ?? 0} → v${r.scriptResult.version} (backup: ${r.scriptResult.backup})`)
|
|
479
|
+
}
|
|
480
|
+
if (r.alreadyInstalled) {
|
|
481
|
+
// 静默:避免每次 start 都刷屏。doctor 会显示状态
|
|
482
|
+
} else if (r.hookResult) {
|
|
483
|
+
console.log(`✓ claude-code hooks installed: ${r.hookResult.added.join(', ')}`)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} catch (e) {
|
|
487
|
+
console.warn(`⚠ claude-code hook bootstrap failed: ${e?.message || e}`)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// listen 完成后异步发"重启完成 + Resume N 个会话"通知到 telegram。
|
|
491
|
+
// 不 await,发不发都不阻塞 boot;postText 走 telegram HTTPS 直发,不依赖 long-poll
|
|
492
|
+
if (typeof srv.notifyStartupRecovery === 'function') {
|
|
493
|
+
Promise.resolve().then(() => srv.notifyStartupRecovery()).catch(() => {})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (cmdOpts.open !== false) {
|
|
497
|
+
try {
|
|
498
|
+
const { default: open } = await import('open')
|
|
499
|
+
// 浏览器自动打开仍走 127.0.0.1(避免 0.0.0.0 在浏览器里非法)
|
|
500
|
+
open(`http://127.0.0.1:${actualPort}`)
|
|
501
|
+
} catch (e) {
|
|
502
|
+
console.warn(`could not auto-open browser: ${e.message}`)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const shutdown = async (signal) => {
|
|
507
|
+
console.log(`\nreceived ${signal}, shutting down...`)
|
|
508
|
+
try { unlinkSync(pf) } catch { /* ignore */ }
|
|
509
|
+
await srv.close()
|
|
510
|
+
process.exit(0)
|
|
511
|
+
}
|
|
512
|
+
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
513
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ─── commander ───
|
|
517
|
+
|
|
518
|
+
const program = new Command()
|
|
519
|
+
program
|
|
520
|
+
.name('agentquad')
|
|
521
|
+
.description('Local four-quadrant todo CLI with embedded Claude Code / Codex terminal')
|
|
522
|
+
.version(loadPkgVersion())
|
|
523
|
+
|
|
524
|
+
program.command('start')
|
|
525
|
+
.option('-p, --port <port>', 'override port', (v) => Number(v))
|
|
526
|
+
.option('--no-open', 'do not auto-open browser')
|
|
527
|
+
.option('--cwd <path>', 'default cwd for AI terminal sessions')
|
|
528
|
+
.option('--host <host>', 'bind address (e.g. 0.0.0.0 to allow Tailscale/LAN access)')
|
|
529
|
+
.option('--expose', 'shorthand for --host 0.0.0.0 (bind all interfaces)')
|
|
530
|
+
.option('--no-wizard', 'skip first-run wizard even if config.json is absent')
|
|
531
|
+
.action(async (cmdOpts) => { await runStart(cmdOpts) })
|
|
532
|
+
|
|
533
|
+
// 裸跑 `agentquad`(无子命令)→ 复用 start 逻辑,默认开向导
|
|
534
|
+
// 注意:--no-wizard 同时挂在 start 子命令上,两边需保持一致
|
|
535
|
+
program
|
|
536
|
+
.option('--no-wizard', 'skip first-run wizard')
|
|
537
|
+
.action(async function (cmdOpts) {
|
|
538
|
+
if (this.args.length) {
|
|
539
|
+
console.error(`Unknown command: ${this.args[0]}`)
|
|
540
|
+
console.error(`Run 'agentquad --help' for available commands.`)
|
|
541
|
+
process.exit(1)
|
|
542
|
+
}
|
|
543
|
+
await runStart(cmdOpts)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
program.command('stop')
|
|
547
|
+
.action(async () => {
|
|
548
|
+
const pf = pidFile(DEFAULT_ROOT_DIR)
|
|
549
|
+
const info = readPidFile(DEFAULT_ROOT_DIR)
|
|
550
|
+
if (!info) { console.log('AgentQuad is not running (no pid file)'); return }
|
|
551
|
+
const pid = info.pid
|
|
552
|
+
if (!pid || !isAlive(pid)) {
|
|
553
|
+
console.log('stale pid file, removing')
|
|
554
|
+
try { unlinkSync(pf) } catch { /* ignore */ }
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
process.kill(pid, 'SIGTERM')
|
|
558
|
+
const deadline = Date.now() + 3000
|
|
559
|
+
while (Date.now() < deadline) {
|
|
560
|
+
if (!isAlive(pid)) {
|
|
561
|
+
try { unlinkSync(pf) } catch { /* ignore */ }
|
|
562
|
+
console.log('AgentQuad stopped')
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
await new Promise(r => setTimeout(r, 100))
|
|
566
|
+
}
|
|
567
|
+
console.log('AgentQuad did not exit in 3s, sending SIGKILL')
|
|
568
|
+
try { process.kill(pid, 'SIGKILL') } catch { /* ignore */ }
|
|
569
|
+
try { unlinkSync(pf) } catch { /* ignore */ }
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
program.command('status')
|
|
573
|
+
.action(async () => {
|
|
574
|
+
const info = readPidFile(DEFAULT_ROOT_DIR)
|
|
575
|
+
if (!info) { console.log('not running'); return }
|
|
576
|
+
const pid = info.pid
|
|
577
|
+
if (!isAlive(pid)) {
|
|
578
|
+
console.log(`stale pid file (${pid}), not running`)
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
const port = info.port ?? loadConfig().port
|
|
582
|
+
try {
|
|
583
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/status`)
|
|
584
|
+
const body = await r.json()
|
|
585
|
+
console.log(`running pid=${pid} port=${port} version=${body.version} activeSessions=${body.activeSessions}`)
|
|
586
|
+
} catch {
|
|
587
|
+
console.log(`running pid=${pid} port=${port} (could not reach /api/status)`)
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
program.command('doctor')
|
|
592
|
+
.action(async () => {
|
|
593
|
+
const report = await doctorReport()
|
|
594
|
+
for (const c of report.checks) {
|
|
595
|
+
const icon = c.ok ? '✓' : '✗'
|
|
596
|
+
const tail = c.detail ? ` — ${c.detail}` : ''
|
|
597
|
+
console.log(`${icon} ${c.name}${tail}`)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const missing = report.checks
|
|
601
|
+
.filter(c => !c.ok && /^(claude|codex|cursor) binary$/.test(c.name))
|
|
602
|
+
.map(c => c.name.split(' ')[0])
|
|
603
|
+
|
|
604
|
+
if (missing.length > 0) {
|
|
605
|
+
const flags = missing.map(t => `--${t}`).join(' ')
|
|
606
|
+
console.log(`\nMissing AI CLI(s): ${missing.join(', ')}`)
|
|
607
|
+
console.log(`Suggested fix: agentquad install-tools ${flags}`)
|
|
608
|
+
if (process.stdin.isTTY) {
|
|
609
|
+
const ans = await prompt(`Run it now? [Enter = yes / q = skip] `)
|
|
610
|
+
if (ans.trim().toLowerCase() !== 'q') {
|
|
611
|
+
const r = spawnSync(process.execPath, [
|
|
612
|
+
fileURLToPath(import.meta.url),
|
|
613
|
+
'install-tools',
|
|
614
|
+
...missing.map(t => `--${t}`),
|
|
615
|
+
'-y',
|
|
616
|
+
], { stdio: 'inherit' })
|
|
617
|
+
process.exit(r.status ?? 1)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
process.exit(report.ok ? 0 : 1)
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
program.command('install-tools')
|
|
626
|
+
.description('Install missing AI CLIs (claude / codex / cursor)')
|
|
627
|
+
.option('--claude', 'install only @anthropic-ai/claude-code (npm)')
|
|
628
|
+
.option('--codex', 'install only @openai/codex (npm)')
|
|
629
|
+
.option('--cursor', 'install only cursor-agent (upstream shell installer)')
|
|
630
|
+
.option('--all', 'install all (default if no flag given)')
|
|
631
|
+
.option('-y, --yes', 'skip the y/N confirmation')
|
|
632
|
+
.action(async (opts) => {
|
|
633
|
+
const tools = planInstallTools(opts)
|
|
634
|
+
const items = tools.map((t) => ({ tool: t, ...TOOL_PACKAGES[t] }))
|
|
635
|
+
|
|
636
|
+
console.log('About to install:')
|
|
637
|
+
for (const it of items) {
|
|
638
|
+
if (it.kind === 'shell') console.log(` - ${it.bin} via: ${it.script}`)
|
|
639
|
+
else console.log(` - ${it.pkg} (binary: ${it.bin}) via npm install -g`)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!opts.yes && process.stdin.isTTY) {
|
|
643
|
+
const ok = await prompt('Continue? [y/N] ')
|
|
644
|
+
if (!/^y(es)?$/i.test(ok.trim())) {
|
|
645
|
+
console.log('Aborted.')
|
|
646
|
+
process.exit(0)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let allOk = true
|
|
651
|
+
for (const it of items) {
|
|
652
|
+
if (it.kind === 'shell') {
|
|
653
|
+
console.log(`\n>> ${it.script}`)
|
|
654
|
+
const r = spawnSync('/bin/sh', ['-lc', it.script], { stdio: 'inherit' })
|
|
655
|
+
if (r.status !== 0) {
|
|
656
|
+
console.error(`\n✗ shell installer for ${it.bin} exited ${r.status}`)
|
|
657
|
+
console.error(` Manual fix: re-run "${it.script}" in your shell, then check PATH.`)
|
|
658
|
+
allOk = false
|
|
659
|
+
break
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
console.log(`\n>> npm install -g ${it.pkg}`)
|
|
663
|
+
const r = spawnSync('npm', ['install', '-g', it.pkg], { stdio: 'inherit' })
|
|
664
|
+
if (r.status !== 0) {
|
|
665
|
+
console.error(`\n✗ npm install -g ${it.pkg} exited ${r.status}`)
|
|
666
|
+
printInstallFailureFix(it)
|
|
667
|
+
allOk = false
|
|
668
|
+
break
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const w = spawnSync('command', ['-v', it.bin], { encoding: 'utf8', shell: '/bin/sh' })
|
|
672
|
+
if (w.status !== 0 || !w.stdout.trim()) {
|
|
673
|
+
console.error(`\n✗ installer reported success but \`${it.bin}\` is not in PATH.`)
|
|
674
|
+
if (it.kind === 'shell') {
|
|
675
|
+
console.error(` You may need to restart your shell, or run the installer manually: ${it.script}`)
|
|
676
|
+
} else {
|
|
677
|
+
printInstallFailureFix(it)
|
|
678
|
+
}
|
|
679
|
+
allOk = false
|
|
680
|
+
break
|
|
681
|
+
}
|
|
682
|
+
console.log(`✓ ${it.bin} → ${w.stdout.trim()}`)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
process.exit(allOk ? 0 : 1)
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
// ─── agentquad mcp install / status ─────────────────────────────
|
|
689
|
+
|
|
690
|
+
export function defaultClaudeSettingsPath() {
|
|
691
|
+
return join(homedir(), '.claude', 'settings.json')
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export function buildMcpServerEntry({ host, port } = {}) {
|
|
695
|
+
const h = host && host !== '0.0.0.0' ? host : '127.0.0.1'
|
|
696
|
+
return {
|
|
697
|
+
type: 'http',
|
|
698
|
+
url: `http://${h}:${port}/mcp`,
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Merge `agentquad` 进 settings.json 的 mcpServers 段,不破坏现有条目。
|
|
704
|
+
* - 如果 settings.json 不存在:创建一个只含 mcpServers 的新文件
|
|
705
|
+
* - 如果存在且有效 JSON:merge
|
|
706
|
+
* - 如果存在但不是 JSON:报错(让用户自己先修好)
|
|
707
|
+
* - 如果存在 legacy `quadtodo` entry 且其 url/command 指向 OUR 包 bin → 删除
|
|
708
|
+
*
|
|
709
|
+
* 返回 { path, action: 'created'|'updated'|'unchanged', entry, legacyRemoved }
|
|
710
|
+
*/
|
|
711
|
+
export function installMcpIntoClaudeSettings({
|
|
712
|
+
settingsPath = defaultClaudeSettingsPath(),
|
|
713
|
+
host,
|
|
714
|
+
port,
|
|
715
|
+
name = 'agentquad',
|
|
716
|
+
} = {}) {
|
|
717
|
+
const entry = buildMcpServerEntry({ host, port })
|
|
718
|
+
let settings = {}
|
|
719
|
+
let existed = false
|
|
720
|
+
if (existsSync(settingsPath)) {
|
|
721
|
+
existed = true
|
|
722
|
+
const raw = readFileSync(settingsPath, 'utf8')
|
|
723
|
+
try {
|
|
724
|
+
settings = JSON.parse(raw)
|
|
725
|
+
} catch (e) {
|
|
726
|
+
const err = new Error(`settings.json exists but is not valid JSON: ${e.message}`)
|
|
727
|
+
err.code = 'invalid_settings'
|
|
728
|
+
throw err
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
const dir = dirname(settingsPath)
|
|
732
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
733
|
+
}
|
|
734
|
+
if (!settings.mcpServers || typeof settings.mcpServers !== 'object') {
|
|
735
|
+
settings.mcpServers = {}
|
|
736
|
+
}
|
|
737
|
+
// legacy 清理:如果有 quadtodo entry 且看起来是 OUR 包(http URL 指向 /mcp,或
|
|
738
|
+
// command 字段含 /quadtodo/ | /agentquad/),删掉它。
|
|
739
|
+
let legacyRemoved = false
|
|
740
|
+
const legacy = settings.mcpServers.quadtodo
|
|
741
|
+
if (legacy && name !== 'quadtodo') {
|
|
742
|
+
const isOurs =
|
|
743
|
+
(typeof legacy.url === 'string' && /\/mcp\/?$/.test(legacy.url)) ||
|
|
744
|
+
(typeof legacy.command === 'string' && /\/agentquad\/|\/quadtodo\//.test(legacy.command))
|
|
745
|
+
if (isOurs) {
|
|
746
|
+
delete settings.mcpServers.quadtodo
|
|
747
|
+
legacyRemoved = true
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const existing = settings.mcpServers[name]
|
|
751
|
+
const same = existing && existing.type === entry.type && existing.url === entry.url
|
|
752
|
+
if (same && !legacyRemoved) {
|
|
753
|
+
return { path: settingsPath, action: 'unchanged', entry, legacyRemoved }
|
|
754
|
+
}
|
|
755
|
+
settings.mcpServers[name] = entry
|
|
756
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
|
757
|
+
return { path: settingsPath, action: existed ? 'updated' : 'created', entry, legacyRemoved }
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const mcpCmd = program.command('mcp').description('Claude Code MCP: install / status')
|
|
761
|
+
|
|
762
|
+
mcpCmd.command('install')
|
|
763
|
+
.option('--settings <path>', 'path to claude settings.json', defaultClaudeSettingsPath())
|
|
764
|
+
.option('--host <host>', 'override host in the URL (useful when this Mac is accessed remotely)')
|
|
765
|
+
.action((opts) => {
|
|
766
|
+
const cfg = loadConfig()
|
|
767
|
+
try {
|
|
768
|
+
const out = installMcpIntoClaudeSettings({
|
|
769
|
+
settingsPath: opts.settings,
|
|
770
|
+
host: opts.host || cfg.host,
|
|
771
|
+
port: cfg.port,
|
|
772
|
+
})
|
|
773
|
+
const icon = out.action === 'unchanged' ? '=' : out.action === 'created' ? '+' : '~'
|
|
774
|
+
console.log(`${icon} ${out.action} ${out.path}`)
|
|
775
|
+
if (out.legacyRemoved) {
|
|
776
|
+
console.log(' removed legacy mcpServers["quadtodo"] entry')
|
|
777
|
+
}
|
|
778
|
+
console.log(` mcpServers.agentquad.url = ${out.entry.url}`)
|
|
779
|
+
if (out.action === 'unchanged') {
|
|
780
|
+
console.log(' (already configured)')
|
|
781
|
+
} else {
|
|
782
|
+
console.log(' Claude Code 里输入 /mcp 可验证连接。')
|
|
783
|
+
}
|
|
784
|
+
} catch (e) {
|
|
785
|
+
console.error(`install failed: ${e.message}`)
|
|
786
|
+
process.exit(1)
|
|
787
|
+
}
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
mcpCmd.command('status')
|
|
791
|
+
.action(async () => {
|
|
792
|
+
const cfg = loadConfig()
|
|
793
|
+
const port = cfg.port
|
|
794
|
+
const url = `http://127.0.0.1:${port}/mcp/health`
|
|
795
|
+
try {
|
|
796
|
+
const r = await fetch(url)
|
|
797
|
+
const body = await r.json()
|
|
798
|
+
console.log(`✓ ${url}`)
|
|
799
|
+
console.log(` ${JSON.stringify(body)}`)
|
|
800
|
+
} catch (e) {
|
|
801
|
+
console.error(`✗ ${url} unreachable: ${e.message}`)
|
|
802
|
+
console.error(` AgentQuad 是不是没跑?试 'agentquad start' 或 'npm start'`)
|
|
803
|
+
process.exit(1)
|
|
804
|
+
}
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
// ─── hook 操作共享 action(被顶层 `hook` 命令组和老的 `openclaw` 子命令复用)─────
|
|
808
|
+
async function actInstallHook() {
|
|
809
|
+
const { installHooks } = await import('./openclaw-hook-installer.js')
|
|
810
|
+
try {
|
|
811
|
+
const out = installHooks()
|
|
812
|
+
console.log(`✓ installed ${out.added.join(', ')} hooks`)
|
|
813
|
+
console.log(` settings: ${out.settingsPath}`)
|
|
814
|
+
if (out.backup) console.log(` backup: ${out.backup}`)
|
|
815
|
+
if (out.markerCleared) console.log(` uninstall marker cleared`)
|
|
816
|
+
console.log('')
|
|
817
|
+
console.log('完成。新的 PTY 会话启动后会自动通过 hook 推送状态到微信。')
|
|
818
|
+
console.log('注意:现存的 PTY 会话(重启前已经在跑的)env 已固定,不受影响;')
|
|
819
|
+
console.log(' 新 agentquad.start_ai_session 启动的 PTY 才会带 hook env。')
|
|
820
|
+
} catch (e) {
|
|
821
|
+
console.error(`install-hook failed: ${e.message}`)
|
|
822
|
+
if (e.code === 'malformed_settings') {
|
|
823
|
+
console.error(` 你的 ~/.claude/settings.json JSON 不合法,先修复再试。`)
|
|
824
|
+
}
|
|
825
|
+
if (e.code === 'hook_script_missing') {
|
|
826
|
+
console.error(` hook 脚本缺失。跑 'agentquad hook bootstrap' 一键部署 + 安装。`)
|
|
827
|
+
}
|
|
828
|
+
process.exit(1)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async function actBootstrapHook() {
|
|
833
|
+
const { bootstrapHooks } = await import('./openclaw-hook-installer.js')
|
|
834
|
+
try {
|
|
835
|
+
const r = bootstrapHooks({ respectUninstallMarker: false })
|
|
836
|
+
if (r.skipped) {
|
|
837
|
+
if (r.reason === 'malformed_settings') {
|
|
838
|
+
console.error(`✗ bootstrap skipped: ${r.settingsPath} JSON 不合法,请先修复`)
|
|
839
|
+
process.exit(1)
|
|
840
|
+
}
|
|
841
|
+
console.log(`= bootstrap skipped: ${r.reason}`)
|
|
842
|
+
return
|
|
843
|
+
}
|
|
844
|
+
const sr = r.scriptResult
|
|
845
|
+
if (sr.action === 'installed') {
|
|
846
|
+
console.log(`✓ hook script installed (v${sr.version}) → ${sr.scriptPath}`)
|
|
847
|
+
} else if (sr.action === 'upgraded') {
|
|
848
|
+
console.log(`✓ hook script upgraded v${sr.previousVersion ?? 0} → v${sr.version}`)
|
|
849
|
+
if (sr.backup) console.log(` backup: ${sr.backup}`)
|
|
850
|
+
} else {
|
|
851
|
+
console.log(`= hook script up-to-date (v${sr.version}) → ${sr.scriptPath}`)
|
|
852
|
+
}
|
|
853
|
+
if (r.alreadyInstalled) {
|
|
854
|
+
console.log(`= hooks already installed in ~/.claude/settings.json`)
|
|
855
|
+
} else if (r.hookResult) {
|
|
856
|
+
console.log(`✓ hooks installed: ${r.hookResult.added.join(', ')}`)
|
|
857
|
+
if (r.hookResult.backup) console.log(` settings backup: ${r.hookResult.backup}`)
|
|
858
|
+
}
|
|
859
|
+
if (r.markerCleared) console.log(` uninstall marker cleared`)
|
|
860
|
+
} catch (e) {
|
|
861
|
+
console.error(`bootstrap failed: ${e.message}`)
|
|
862
|
+
process.exit(1)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function actUninstallHook(opts) {
|
|
867
|
+
const { uninstallHooks } = await import('./openclaw-hook-installer.js')
|
|
868
|
+
try {
|
|
869
|
+
const out = uninstallHooks({ writeUninstallMarker: opts.marker !== false })
|
|
870
|
+
if (out.removed.length === 0) {
|
|
871
|
+
console.log('= no AgentQuad hooks installed; nothing to remove')
|
|
872
|
+
} else {
|
|
873
|
+
console.log(`✓ removed AgentQuad hooks from ${out.settingsPath}`)
|
|
874
|
+
for (const r of out.removed) console.log(` ${r.event}: -${r.removedCount}`)
|
|
875
|
+
if (out.backup) console.log(` backup: ${out.backup}`)
|
|
876
|
+
}
|
|
877
|
+
if (out.markerWritten) {
|
|
878
|
+
console.log(` marker written → 下次 'agentquad start' 不会自动装回;想恢复跑 'agentquad hook bootstrap'`)
|
|
879
|
+
}
|
|
880
|
+
} catch (e) {
|
|
881
|
+
console.error(`uninstall-hook failed: ${e.message}`)
|
|
882
|
+
process.exit(1)
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function actHookStatus() {
|
|
887
|
+
const { inspectHooks } = await import('./openclaw-hook-installer.js')
|
|
888
|
+
const r = inspectHooks()
|
|
889
|
+
const icon = r.installed ? '✓' : '✗'
|
|
890
|
+
console.log(`${icon} hooks installed: ${r.installed}`)
|
|
891
|
+
console.log(` events: ${r.eventsInstalled.length ? r.eventsInstalled.join(', ') : '(none)'}`)
|
|
892
|
+
console.log(` settings: ${r.settingsPath}`)
|
|
893
|
+
console.log(` hook script: ${r.hookScriptPath} (${r.scriptExists ? 'exists' : 'MISSING'})`)
|
|
894
|
+
if (r.error) console.log(` ⚠️ ${r.error}`)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ─── 顶层 hook 子命令组(首选入口;发现性比埋在 openclaw 下好)──────
|
|
898
|
+
const hookCmd = program.command('hook').description('管理 AgentQuad 在 ~/.claude/settings.json 里安装的 hook(装/删/查/恢复)')
|
|
899
|
+
|
|
900
|
+
hookCmd.command('install')
|
|
901
|
+
.description('把 AgentQuad 的 3 个 hook(Stop/Notification/SessionEnd)合并写入 ~/.claude/settings.json')
|
|
902
|
+
.action(actInstallHook)
|
|
903
|
+
|
|
904
|
+
hookCmd.command('uninstall')
|
|
905
|
+
.description('从 ~/.claude/settings.json 移除 AgentQuad 加的 hook entry,保留你其他 hook(默认写 .uninstalled marker,下次 start 不再自动装回)')
|
|
906
|
+
.option('--no-marker', '不写 .uninstalled marker(下次 agentquad start 会自动装回)')
|
|
907
|
+
.action(actUninstallHook)
|
|
908
|
+
|
|
909
|
+
hookCmd.command('status')
|
|
910
|
+
.description('查看 AgentQuad hook 是否安装到 ~/.claude/settings.json')
|
|
911
|
+
.action(actHookStatus)
|
|
912
|
+
|
|
913
|
+
hookCmd.command('bootstrap')
|
|
914
|
+
.description('一键部署 hook script + 安装 hooks(强制忽略 .uninstalled marker,用于"删过又想恢复"场景)')
|
|
915
|
+
.action(actBootstrapHook)
|
|
916
|
+
|
|
917
|
+
// ─── openclaw 子命令组:保留旧路径以向后兼容;hook 操作建议改用 `agentquad hook *` ───
|
|
918
|
+
const openclawCmd = program.command('openclaw').description('OpenClaw bridge: install/uninstall Claude Code hooks for proactive WeChat push')
|
|
919
|
+
|
|
920
|
+
openclawCmd.command('install-hook')
|
|
921
|
+
.description('alias of `agentquad hook install`')
|
|
922
|
+
.action(actInstallHook)
|
|
923
|
+
|
|
924
|
+
openclawCmd.command('bootstrap')
|
|
925
|
+
.description('alias of `agentquad hook bootstrap`')
|
|
926
|
+
.action(actBootstrapHook)
|
|
927
|
+
|
|
928
|
+
openclawCmd.command('uninstall-hook')
|
|
929
|
+
.description('alias of `agentquad hook uninstall`')
|
|
930
|
+
.option('--no-marker', '不写 .uninstalled marker(下次 agentquad start 会自动装回)')
|
|
931
|
+
.action(actUninstallHook)
|
|
932
|
+
|
|
933
|
+
openclawCmd.command('hook-status')
|
|
934
|
+
.description('alias of `agentquad hook status`')
|
|
935
|
+
.action(actHookStatus)
|
|
936
|
+
|
|
937
|
+
openclawCmd.command('inbound')
|
|
938
|
+
.description('OpenClaw skill 单入口:转发一条用户消息到 AgentQuad wizard,stdout 是给用户的回复')
|
|
939
|
+
.requiredOption('--from <peer>', '微信对端 user_id(OpenClaw 给的 from_user_id)')
|
|
940
|
+
.requiredOption('--text <text>', '用户原文')
|
|
941
|
+
.option('--port <port>', 'AgentQuad 端口', (v) => Number(v))
|
|
942
|
+
.action(async (opts) => {
|
|
943
|
+
const cfg = loadConfig()
|
|
944
|
+
const port = opts.port || cfg.port || 5677
|
|
945
|
+
const url = `http://127.0.0.1:${port}/api/openclaw/inbound`
|
|
946
|
+
try {
|
|
947
|
+
const res = await fetch(url, {
|
|
948
|
+
method: 'POST',
|
|
949
|
+
headers: { 'Content-Type': 'application/json' },
|
|
950
|
+
body: JSON.stringify({ from: opts.from, text: opts.text }),
|
|
951
|
+
})
|
|
952
|
+
const data = await res.json().catch(() => ({}))
|
|
953
|
+
if (!res.ok || !data.ok) {
|
|
954
|
+
console.error(`✗ ${res.status} ${data.error || 'unknown'}`)
|
|
955
|
+
process.exit(1)
|
|
956
|
+
}
|
|
957
|
+
// 把 reply 直接打到 stdout — OpenClaw skill 会把它转发回微信用户
|
|
958
|
+
process.stdout.write(String(data.reply || ''))
|
|
959
|
+
// exit 0
|
|
960
|
+
} catch (e) {
|
|
961
|
+
console.error(`✗ inbound failed: ${e.message}`)
|
|
962
|
+
console.error(` AgentQuad 是不是没跑?试 'agentquad status'`)
|
|
963
|
+
process.exit(1)
|
|
964
|
+
}
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
openclawCmd.command('inbound-state')
|
|
968
|
+
.description('查看 wizard 当前进行中的 peer 列表(调试用)')
|
|
969
|
+
.action(async () => {
|
|
970
|
+
const cfg = loadConfig()
|
|
971
|
+
const port = cfg.port || 5677
|
|
972
|
+
try {
|
|
973
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/openclaw/inbound/state`)
|
|
974
|
+
const data = await res.json()
|
|
975
|
+
console.log(JSON.stringify(data, null, 2))
|
|
976
|
+
} catch (e) {
|
|
977
|
+
console.error(`✗ ${e.message}`)
|
|
978
|
+
process.exit(1)
|
|
979
|
+
}
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
const cfgCmd = program.command('config').description('read/write ~/.agentquad/config.json')
|
|
983
|
+
cfgCmd.command('get <key>').action((key) => {
|
|
984
|
+
const v = getConfigValue(key)
|
|
985
|
+
if (v === undefined) process.exit(1)
|
|
986
|
+
console.log(typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v))
|
|
987
|
+
})
|
|
988
|
+
cfgCmd.command('set <key> <value>').action((key, value) => {
|
|
989
|
+
const coerced = setConfigValue(key, value)
|
|
990
|
+
console.log(`set ${key} = ${coerced}`)
|
|
991
|
+
})
|
|
992
|
+
cfgCmd.command('list').action(() => {
|
|
993
|
+
console.log(JSON.stringify(loadConfig(), null, 2))
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
// 仅当被作为可执行脚本运行时才 parse(import 进来做测试时跳过)。
|
|
997
|
+
// 用 realpath 比对,避免 npm link symlink 下 process.argv[1] !== import.meta.url 把判断打飞。
|
|
998
|
+
const isMain = (() => {
|
|
999
|
+
if (!process.argv[1]) return false
|
|
1000
|
+
try {
|
|
1001
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
1002
|
+
} catch {
|
|
1003
|
+
// fallback:argv[1] 是 cli.js 或 bin 名 'agentquad' / 'quadtodo'(legacy alias)
|
|
1004
|
+
if (process.argv[1].endsWith('cli.js')) return true
|
|
1005
|
+
if (/\/(agentquad|quadtodo)$/.test(process.argv[1])) return true
|
|
1006
|
+
return false
|
|
1007
|
+
}
|
|
1008
|
+
})()
|
|
1009
|
+
if (isMain) {
|
|
1010
|
+
program.parseAsync(process.argv)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function printInstallFailureFix(it) {
|
|
1014
|
+
console.error(`
|
|
1015
|
+
Common fixes:
|
|
1016
|
+
- Permissions: try \`sudo npm install -g ${it.pkg}\`,
|
|
1017
|
+
or move npm prefix into your home dir:
|
|
1018
|
+
\`npm config set prefix ~/.npm-global\`
|
|
1019
|
+
and add \`~/.npm-global/bin\` to your PATH.
|
|
1020
|
+
- If you use nvm: \`nvm use 20\` first, then retry.
|
|
1021
|
+
- Network/registry: check \`npm config get registry\`.
|
|
1022
|
+
`)
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function prompt(question) {
|
|
1026
|
+
return new Promise((resolve) => {
|
|
1027
|
+
process.stdout.write(question)
|
|
1028
|
+
let buf = ''
|
|
1029
|
+
process.stdin.setEncoding('utf8')
|
|
1030
|
+
const onData = (chunk) => {
|
|
1031
|
+
buf += chunk
|
|
1032
|
+
const nl = buf.indexOf('\n')
|
|
1033
|
+
if (nl >= 0) {
|
|
1034
|
+
process.stdin.removeListener('data', onData)
|
|
1035
|
+
resolve(buf.slice(0, nl))
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
process.stdin.on('data', onData)
|
|
1039
|
+
})
|
|
1040
|
+
}
|