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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/dist-web/assets/index-CMaXwixo.js +1234 -0
  4. package/dist-web/assets/index-DBHApzV1.css +32 -0
  5. package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  6. package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  7. package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  8. package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  9. package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  10. package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  11. package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  12. package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  13. package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  14. package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  15. package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  16. package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  17. package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  18. package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  19. package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  20. package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  21. package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  22. package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  23. package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  24. package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  25. package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  26. package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  27. package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  28. package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  29. package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  30. package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  31. package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  32. package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  33. package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  34. package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  35. package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  36. package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  37. package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  38. package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  39. package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  40. package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  41. package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  42. package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  43. package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  44. package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  45. package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  46. package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  47. package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  48. package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  49. package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  50. package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  51. package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  52. package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  53. package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  54. package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  55. package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  56. package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  57. package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  58. package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  59. package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  60. package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  61. package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  62. package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  63. package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  64. package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  65. package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  66. package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  67. package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  68. package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  69. package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  70. package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  71. package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  72. package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  73. package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  74. package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  75. package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  76. package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  77. package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  78. package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  79. package/dist-web/assets/logo-D4DDtU-r.png +0 -0
  80. package/dist-web/favicon.png +0 -0
  81. package/dist-web/index.html +14 -0
  82. package/package.json +88 -0
  83. package/src/ask-user-buttons.js +142 -0
  84. package/src/claude-transcript.js +203 -0
  85. package/src/cli.js +1040 -0
  86. package/src/codex-event-emitter.js +111 -0
  87. package/src/codex-prompt-detector.js +53 -0
  88. package/src/codex-sidecar.js +52 -0
  89. package/src/codex-transcript.js +74 -0
  90. package/src/config.js +692 -0
  91. package/src/data/claude-code-commands.json +52 -0
  92. package/src/db.js +1503 -0
  93. package/src/dispatch.js +13 -0
  94. package/src/export/todoMarkdown.js +246 -0
  95. package/src/first-run-wizard.js +82 -0
  96. package/src/git/gitStatus.js +139 -0
  97. package/src/lark-api-client.js +205 -0
  98. package/src/lark-bot.js +510 -0
  99. package/src/lark-card.js +88 -0
  100. package/src/lark-config-service.js +16 -0
  101. package/src/lark-event-client.js +107 -0
  102. package/src/lark-image.js +99 -0
  103. package/src/lark-markdown.js +51 -0
  104. package/src/lark-video.js +163 -0
  105. package/src/mcp/audit.js +34 -0
  106. package/src/mcp/server.js +83 -0
  107. package/src/mcp/tools/destructive/index.js +252 -0
  108. package/src/mcp/tools/openclaw/index.js +405 -0
  109. package/src/mcp/tools/read/index.js +269 -0
  110. package/src/mcp/tools/write/index.js +157 -0
  111. package/src/openclaw-bridge.js +566 -0
  112. package/src/openclaw-hook-installer.js +338 -0
  113. package/src/openclaw-hook.js +908 -0
  114. package/src/openclaw-wizard.js +2442 -0
  115. package/src/pending-questions.js +297 -0
  116. package/src/pricing.js +45 -0
  117. package/src/prompt-render.js +36 -0
  118. package/src/pty.js +992 -0
  119. package/src/routes/ai-terminal.js +1228 -0
  120. package/src/routes/git.js +89 -0
  121. package/src/routes/openclaw-hook.js +67 -0
  122. package/src/routes/openclaw-inbound.js +36 -0
  123. package/src/routes/recurringRules.js +80 -0
  124. package/src/routes/reports.js +50 -0
  125. package/src/routes/search.js +46 -0
  126. package/src/routes/stats.js +31 -0
  127. package/src/routes/telegram-config.js +152 -0
  128. package/src/routes/telegram-sync.js +221 -0
  129. package/src/routes/templates.js +63 -0
  130. package/src/routes/todos.js +649 -0
  131. package/src/routes/transcripts.js +75 -0
  132. package/src/routes/uploads.js +107 -0
  133. package/src/routes/wiki.js +142 -0
  134. package/src/search/fts.js +209 -0
  135. package/src/search/index.js +199 -0
  136. package/src/search/transcripts.js +148 -0
  137. package/src/server.js +1791 -0
  138. package/src/session-input-dispatcher.js +256 -0
  139. package/src/stats/markdown.js +42 -0
  140. package/src/stats/report.js +207 -0
  141. package/src/summarize.js +84 -0
  142. package/src/system-rules.js +52 -0
  143. package/src/telegram-bot.js +875 -0
  144. package/src/telegram-commands.js +149 -0
  145. package/src/telegram-config-service.js +84 -0
  146. package/src/telegram-image.js +95 -0
  147. package/src/telegram-loading-status.js +112 -0
  148. package/src/telegram-markdown.js +82 -0
  149. package/src/telegram-reaction-tracker.js +69 -0
  150. package/src/telegram-video.js +75 -0
  151. package/src/templates/claude-hooks/notify.js +103 -0
  152. package/src/transcript.js +305 -0
  153. package/src/transcripts/blocks.js +56 -0
  154. package/src/transcripts/index.js +222 -0
  155. package/src/transcripts/indexer.js +34 -0
  156. package/src/transcripts/matcher.js +70 -0
  157. package/src/transcripts/scanner.js +259 -0
  158. package/src/usage-footer.js +170 -0
  159. package/src/usage-parser.js +132 -0
  160. package/src/wiki/guide.js +44 -0
  161. package/src/wiki/index.js +232 -0
  162. package/src/wiki/redact.js +34 -0
  163. package/src/wiki/sources.js +122 -0
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Claude Code hooks 安装器:把 AgentQuad 的 3 个 hook entry 合并写入
3
+ * `~/.claude/settings.json`,不破坏用户现有 hooks 配置。
4
+ *
5
+ * 合并策略:
6
+ * - 已有 hooks.<event> 数组 → append;不删除已有 entry
7
+ * - AgentQuad 加的 entry 用 `_quadtodoManaged: true` 字段标记,方便 uninstall
8
+ * - 卸载时仅删带这个标记的 entry,其他保留不动
9
+ * - settings.json 不存在 → 创建
10
+ * - settings.json 损坏 → 抛错让用户修,绝不擅自覆盖
11
+ *
12
+ * 启动期 bootstrap(bootstrapHooks):
13
+ * - 部署/升级 ~/.agentquad/claude-hooks/notify.js(带版本号比对)
14
+ * - 合并 hooks 到 settings.json(已装则 noop,避免 .bak 刷屏)
15
+ * - 用户跑过 uninstall-hook → 留 .uninstalled marker,bootstrap 默认尊重;
16
+ * `agentquad openclaw bootstrap` 显式忽略 marker 强制装回
17
+ * - settings.json 损坏 → warn-skip,不让 agentquad start 挂掉
18
+ */
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from 'node:fs'
20
+ import { dirname, join } from 'node:path'
21
+ import { homedir } from 'node:os'
22
+ import { fileURLToPath } from 'node:url'
23
+ import { DEFAULT_ROOT_DIR } from './config.js'
24
+
25
+ const QUADTODO_MANAGED_KEY = '_quadtodoManaged'
26
+ const HOOK_EVENTS = ['Stop', 'Notification', 'SessionEnd']
27
+ const HOOK_VERSION_RE = /quadtodo-hook-version:\s*(\d+)/
28
+
29
+ function defaultHookScriptPath() {
30
+ return join(DEFAULT_ROOT_DIR, 'claude-hooks', 'notify.js')
31
+ }
32
+
33
+ function defaultSettingsPath() {
34
+ return join(homedir(), '.claude', 'settings.json')
35
+ }
36
+
37
+ function defaultTemplatePath() {
38
+ return fileURLToPath(new URL('./templates/claude-hooks/notify.js', import.meta.url))
39
+ }
40
+
41
+ function defaultUninstallMarkerPath() {
42
+ return join(DEFAULT_ROOT_DIR, 'claude-hooks', '.uninstalled')
43
+ }
44
+
45
+ function parseHookVersion(content) {
46
+ if (!content) return null
47
+ const m = content.match(HOOK_VERSION_RE)
48
+ return m ? Number(m[1]) : 0 // 0 = unversioned legacy script
49
+ }
50
+
51
+ function buildHookEntry(event, hookScriptPath) {
52
+ // Claude Code hook 格式(参考其文档):matchers 数组里每项有 type+command
53
+ const eventLower = event === 'SessionEnd' ? 'session-end'
54
+ : event === 'Notification' ? 'notification'
55
+ : 'stop'
56
+ return {
57
+ matcher: '*',
58
+ hooks: [
59
+ {
60
+ type: 'command',
61
+ command: `node ${hookScriptPath} ${eventLower}`,
62
+ [QUADTODO_MANAGED_KEY]: true,
63
+ },
64
+ ],
65
+ [QUADTODO_MANAGED_KEY]: true,
66
+ }
67
+ }
68
+
69
+ function loadSettings(path) {
70
+ if (!existsSync(path)) return {}
71
+ const raw = readFileSync(path, 'utf8')
72
+ try {
73
+ return JSON.parse(raw)
74
+ } catch (e) {
75
+ const err = new Error(`settings.json malformed: ${e.message}`)
76
+ err.code = 'malformed_settings'
77
+ err.path = path
78
+ throw err
79
+ }
80
+ }
81
+
82
+ function saveSettings(path, data) {
83
+ const dir = dirname(path)
84
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
85
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n')
86
+ }
87
+
88
+ function backupSettings(path) {
89
+ if (!existsSync(path)) return null
90
+ const bak = `${path}.bak.${Date.now()}`
91
+ copyFileSync(path, bak)
92
+ return bak
93
+ }
94
+
95
+ /**
96
+ * 把 AgentQuad 的 3 个 hook entry 合并到 settings.json。
97
+ * 重复安装是幂等的:如果已经有 quadtodo-managed 同名 entry,先清掉再加,避免重复 fire。
98
+ *
99
+ * 返回 { settingsPath, backup, added: [event...], skipped: [event...] }
100
+ */
101
+ export function installHooks({
102
+ settingsPath = defaultSettingsPath(),
103
+ hookScriptPath = defaultHookScriptPath(),
104
+ events = HOOK_EVENTS,
105
+ uninstallMarkerPath = defaultUninstallMarkerPath(),
106
+ clearUninstallMarker = true,
107
+ } = {}) {
108
+ if (!existsSync(hookScriptPath)) {
109
+ const err = new Error(`hook script not found: ${hookScriptPath}`)
110
+ err.code = 'hook_script_missing'
111
+ throw err
112
+ }
113
+
114
+ const data = loadSettings(settingsPath)
115
+ const backup = backupSettings(settingsPath)
116
+ if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {}
117
+
118
+ const added = []
119
+ for (const event of events) {
120
+ if (!Array.isArray(data.hooks[event])) data.hooks[event] = []
121
+ // 移除旧的 _quadtodoManaged entry(避免重复 install 累加)
122
+ data.hooks[event] = data.hooks[event].filter((entry) => !entry?.[QUADTODO_MANAGED_KEY])
123
+ data.hooks[event].push(buildHookEntry(event, hookScriptPath))
124
+ added.push(event)
125
+ }
126
+
127
+ saveSettings(settingsPath, data)
128
+ // 用户重新装 = 收回之前的 uninstall 拒绝;下次 start 不再被 marker 拦
129
+ let markerCleared = false
130
+ if (clearUninstallMarker && existsSync(uninstallMarkerPath)) {
131
+ try { unlinkSync(uninstallMarkerPath); markerCleared = true } catch { /* 不阻塞 */ }
132
+ }
133
+ return { settingsPath, backup, added, skipped: [], markerCleared }
134
+ }
135
+
136
+ /**
137
+ * 移除 AgentQuad 加的所有 hook entry(按 _quadtodoManaged 标记)。
138
+ * 不动其他 entry。默认会写一个 .uninstalled marker,让后续 `agentquad start` 自检不再回写。
139
+ */
140
+ export function uninstallHooks({
141
+ settingsPath = defaultSettingsPath(),
142
+ uninstallMarkerPath = defaultUninstallMarkerPath(),
143
+ writeUninstallMarker = true,
144
+ } = {}) {
145
+ let markerWritten = false
146
+ const writeMarker = () => {
147
+ if (!writeUninstallMarker) return
148
+ try {
149
+ const dir = dirname(uninstallMarkerPath)
150
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
151
+ writeFileSync(uninstallMarkerPath, `${new Date().toISOString()}\n`)
152
+ markerWritten = true
153
+ } catch { /* 不阻塞 */ }
154
+ }
155
+
156
+ if (!existsSync(settingsPath)) {
157
+ writeMarker()
158
+ return { settingsPath, removed: [], backup: null, markerWritten }
159
+ }
160
+
161
+ const data = loadSettings(settingsPath)
162
+ const backup = backupSettings(settingsPath)
163
+ const removed = []
164
+
165
+ if (data.hooks && typeof data.hooks === 'object') {
166
+ for (const event of Object.keys(data.hooks)) {
167
+ if (!Array.isArray(data.hooks[event])) continue
168
+ const before = data.hooks[event].length
169
+ data.hooks[event] = data.hooks[event].filter((entry) => !entry?.[QUADTODO_MANAGED_KEY])
170
+ if (data.hooks[event].length !== before) {
171
+ removed.push({ event, removedCount: before - data.hooks[event].length })
172
+ }
173
+ // 空数组干净掉
174
+ if (data.hooks[event].length === 0) delete data.hooks[event]
175
+ }
176
+ if (Object.keys(data.hooks).length === 0) delete data.hooks
177
+ }
178
+
179
+ saveSettings(settingsPath, data)
180
+ writeMarker()
181
+ return { settingsPath, removed, backup, markerWritten }
182
+ }
183
+
184
+ /**
185
+ * 查询当前 AgentQuad hook 安装状态。返回 {installed: bool, eventsInstalled: [], settingsPath, scriptExists}
186
+ */
187
+ export function inspectHooks({
188
+ settingsPath = defaultSettingsPath(),
189
+ hookScriptPath = defaultHookScriptPath(),
190
+ } = {}) {
191
+ const scriptExists = existsSync(hookScriptPath)
192
+ if (!existsSync(settingsPath)) {
193
+ return { installed: false, eventsInstalled: [], settingsPath, hookScriptPath, scriptExists }
194
+ }
195
+ let data
196
+ try {
197
+ data = loadSettings(settingsPath)
198
+ } catch (e) {
199
+ return { installed: false, eventsInstalled: [], settingsPath, hookScriptPath, scriptExists, error: e.code }
200
+ }
201
+ const eventsInstalled = []
202
+ for (const event of HOOK_EVENTS) {
203
+ const arr = data?.hooks?.[event]
204
+ if (!Array.isArray(arr)) continue
205
+ if (arr.some((entry) => entry?.[QUADTODO_MANAGED_KEY])) eventsInstalled.push(event)
206
+ }
207
+ return {
208
+ installed: eventsInstalled.length === HOOK_EVENTS.length,
209
+ eventsInstalled,
210
+ settingsPath,
211
+ hookScriptPath,
212
+ scriptExists,
213
+ }
214
+ }
215
+
216
+ /**
217
+ * 把仓库内置的 notify.js 模板部署到 ~/.agentquad/claude-hooks/notify.js。
218
+ *
219
+ * 行为:
220
+ * - 目标不存在 → 直接写(action: 'installed')
221
+ * - 目标版本 < 模板版本 → 备份旧脚本,覆盖(action: 'upgraded')
222
+ * - 目标版本 = 模板版本 → 不动(action: 'unchanged')
223
+ * - 目标存在但解析不出版本号 → 视为 v0,按升级路径处理(保护用户改动)
224
+ *
225
+ * 返回 { action, version, previousVersion, scriptPath, backup }
226
+ */
227
+ export function deployHookScript({
228
+ scriptPath = defaultHookScriptPath(),
229
+ templatePath = defaultTemplatePath(),
230
+ } = {}) {
231
+ if (!existsSync(templatePath)) {
232
+ const err = new Error(`hook template not found: ${templatePath}`)
233
+ err.code = 'hook_template_missing'
234
+ throw err
235
+ }
236
+ const templateContent = readFileSync(templatePath, 'utf8')
237
+ const templateVersion = parseHookVersion(templateContent)
238
+
239
+ const dir = dirname(scriptPath)
240
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
241
+
242
+ const previousVersion = existsSync(scriptPath)
243
+ ? parseHookVersion(readFileSync(scriptPath, 'utf8'))
244
+ : null
245
+
246
+ if (previousVersion !== null && previousVersion === templateVersion) {
247
+ return { action: 'unchanged', version: templateVersion, previousVersion, scriptPath, backup: null }
248
+ }
249
+
250
+ let backup = null
251
+ if (previousVersion !== null) {
252
+ backup = `${scriptPath}.bak.${Date.now()}`
253
+ copyFileSync(scriptPath, backup)
254
+ }
255
+ writeFileSync(scriptPath, templateContent)
256
+ return {
257
+ action: previousVersion === null ? 'installed' : 'upgraded',
258
+ version: templateVersion,
259
+ previousVersion,
260
+ scriptPath,
261
+ backup,
262
+ }
263
+ }
264
+
265
+ /**
266
+ * `agentquad start` 启动时调用:部署 notify.js + 合入 settings.json hook entry。
267
+ *
268
+ * 设计要点:
269
+ * - 已经装好(inspectHooks.installed === true)→ 不重写 settings.json,避免每次 start 都生成 .bak
270
+ * - settings.json 损坏 → skip + 返回 reason,让调用方 warn 而不是让启动挂掉
271
+ * - 用户跑过 uninstall-hook 留下 marker → respectUninstallMarker=true 时 skip
272
+ * `agentquad openclaw bootstrap` 子命令传 false 强装回(同时清掉 marker)
273
+ *
274
+ * 返回 { skipped, reason?, scriptResult, hookResult, alreadyInstalled }
275
+ */
276
+ export function bootstrapHooks({
277
+ settingsPath = defaultSettingsPath(),
278
+ scriptPath = defaultHookScriptPath(),
279
+ templatePath = defaultTemplatePath(),
280
+ uninstallMarkerPath = defaultUninstallMarkerPath(),
281
+ respectUninstallMarker = true,
282
+ } = {}) {
283
+ if (respectUninstallMarker && existsSync(uninstallMarkerPath)) {
284
+ return { skipped: true, reason: 'uninstall_marker', uninstallMarkerPath }
285
+ }
286
+
287
+ // 显式 bootstrap 时清掉 marker(即便文件不存在也安全)
288
+ let markerCleared = false
289
+ if (!respectUninstallMarker && existsSync(uninstallMarkerPath)) {
290
+ try { unlinkSync(uninstallMarkerPath); markerCleared = true } catch { /* 不阻塞 */ }
291
+ }
292
+
293
+ const scriptResult = deployHookScript({ scriptPath, templatePath })
294
+
295
+ const inspect = inspectHooks({ settingsPath, hookScriptPath: scriptPath })
296
+ if (inspect.error === 'malformed_settings') {
297
+ return {
298
+ skipped: true,
299
+ reason: 'malformed_settings',
300
+ settingsPath,
301
+ scriptResult,
302
+ markerCleared,
303
+ }
304
+ }
305
+
306
+ if (inspect.installed) {
307
+ return {
308
+ skipped: false,
309
+ alreadyInstalled: true,
310
+ scriptResult,
311
+ hookResult: null,
312
+ markerCleared,
313
+ }
314
+ }
315
+
316
+ const hookResult = installHooks({
317
+ settingsPath,
318
+ hookScriptPath: scriptPath,
319
+ uninstallMarkerPath,
320
+ clearUninstallMarker: false, // 已在上面处理
321
+ })
322
+ return {
323
+ skipped: false,
324
+ alreadyInstalled: false,
325
+ scriptResult,
326
+ hookResult,
327
+ markerCleared,
328
+ }
329
+ }
330
+
331
+ export const __test__ = {
332
+ buildHookEntry,
333
+ QUADTODO_MANAGED_KEY,
334
+ HOOK_EVENTS,
335
+ parseHookVersion,
336
+ defaultTemplatePath,
337
+ defaultUninstallMarkerPath,
338
+ }