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,13 @@
1
+ import { SUPPORTED_TOOLS } from './config.js'
2
+
3
+ export function resolveTool({ channel, userId, chatId, override } = {}, config = {}) {
4
+ if (override && SUPPORTED_TOOLS.includes(override)) return override
5
+ const ch = channel ? config?.dispatch?.[channel] : null
6
+ if (ch) {
7
+ if (userId && ch.perUser && SUPPORTED_TOOLS.includes(ch.perUser[userId])) return ch.perUser[userId]
8
+ if (chatId && ch.perChat && SUPPORTED_TOOLS.includes(ch.perChat[chatId])) return ch.perChat[chatId]
9
+ if (SUPPORTED_TOOLS.includes(ch.default)) return ch.default
10
+ }
11
+ if (SUPPORTED_TOOLS.includes(config?.defaultTool)) return config.defaultTool
12
+ return 'claude'
13
+ }
@@ -0,0 +1,246 @@
1
+ import { parseTranscriptFile } from '../transcripts/scanner.js'
2
+ import { estimateCost, DEFAULT_PRICING } from '../pricing.js'
3
+
4
+ const QUADRANT_LABEL = {
5
+ 1: 'Q1 紧急且重要',
6
+ 2: 'Q2 重要不紧急',
7
+ 3: 'Q3 紧急不重要',
8
+ 4: 'Q4 不紧急不重要',
9
+ }
10
+
11
+ const STATUS_LABEL = {
12
+ todo: '待办',
13
+ ai_pending: 'AI 待确认',
14
+ ai_running: 'AI 进行中',
15
+ ai_done: 'AI 已完成',
16
+ done: '已完成',
17
+ }
18
+
19
+ const ROLE_LABEL = {
20
+ user: '用户',
21
+ assistant: 'AI',
22
+ thinking: '思考',
23
+ tool_use: '工具调用',
24
+ tool_result: '工具输出',
25
+ system: '系统',
26
+ }
27
+
28
+ export async function buildTodoExport(db, todoId, { turns = 'summary', turnLimit = 80, pricing = DEFAULT_PRICING } = {}) {
29
+ const todo = db.getTodo(todoId)
30
+ if (!todo) return null
31
+ const comments = db.listComments(todoId)
32
+ const aiSessions = Array.isArray(todo.aiSessions) ? todo.aiSessions : []
33
+ const subtodos = todo.parentId ? [] : db.listTodos({ quadrant: todo.quadrant }).filter(item => item.parentId === todo.id)
34
+
35
+ const sessionRows = []
36
+ for (const s of aiSessions) {
37
+ const tool = s?.tool || 'claude'
38
+ const nativeId = s?.nativeSessionId
39
+ let file = null
40
+ if (nativeId) {
41
+ file = db.findTranscriptByNative(nativeId, tool)
42
+ }
43
+ const tokens = file ? {
44
+ input: file.input_tokens || 0,
45
+ output: file.output_tokens || 0,
46
+ cacheRead: file.cache_read_tokens || 0,
47
+ cacheCreation: file.cache_creation_tokens || 0,
48
+ } : { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }
49
+ const cost = file?.primary_model
50
+ ? estimateCost(tokens, file.primary_model, pricing)
51
+ : { usd: 0, cny: 0 }
52
+
53
+ let loadedTurns = null
54
+ if (file && turns !== 'none') {
55
+ try {
56
+ const parsed = await parseTranscriptFile(tool, file.jsonl_path)
57
+ loadedTurns = parsed.turns
58
+ } catch (e) {
59
+ loadedTurns = []
60
+ }
61
+ }
62
+
63
+ sessionRows.push({
64
+ session: s,
65
+ file,
66
+ tokens,
67
+ cost,
68
+ turns: loadedTurns,
69
+ })
70
+ }
71
+
72
+ return { todo, comments, sessions: sessionRows, subtodos, generatedAt: Date.now(), turnsMode: turns, turnLimit }
73
+ }
74
+
75
+ function fmtTokens(n) {
76
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + 'M'
77
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'
78
+ return String(n)
79
+ }
80
+
81
+ function fmtCost(c) {
82
+ if (!c || (!c.usd && !c.cny)) return '—'
83
+ return `$${c.usd.toFixed(2)} / ¥${c.cny.toFixed(1)}`
84
+ }
85
+
86
+ function fmtDateTime(ms) {
87
+ if (!ms) return '—'
88
+ const d = new Date(ms)
89
+ const pad = n => String(n).padStart(2, '0')
90
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
91
+ }
92
+
93
+ function fmtDuration(ms) {
94
+ if (!ms || ms < 0) return '—'
95
+ const s = Math.round(ms / 1000)
96
+ if (s < 60) return `${s}s`
97
+ const m = Math.floor(s / 60)
98
+ const sec = s % 60
99
+ if (m < 60) return `${m}m${sec ? ` ${sec}s` : ''}`
100
+ const h = Math.floor(m / 60)
101
+ return `${h}h ${m % 60}m`
102
+ }
103
+
104
+ function summarizeTurnContent(content, max = 600) {
105
+ const text = String(content || '').trim()
106
+ if (text.length <= max) return text
107
+ return text.slice(0, max) + '…'
108
+ }
109
+
110
+ function pickHighlightTurns(turns, limit) {
111
+ const useful = turns.filter(t => t.role === 'user' || t.role === 'assistant')
112
+ if (useful.length <= limit) return useful
113
+ const head = Math.ceil(limit / 2)
114
+ const tail = limit - head
115
+ return [...useful.slice(0, head), { role: 'separator', content: `… (省略 ${useful.length - limit} 段) …` }, ...useful.slice(-tail)]
116
+ }
117
+
118
+ export function renderTodoMarkdown(report) {
119
+ if (!report) return ''
120
+ const { todo, comments, sessions, subtodos = [], turnsMode, turnLimit } = report
121
+ const lines = []
122
+ lines.push(`# ${todo.title}`)
123
+ lines.push('')
124
+ const meta = [
125
+ `**象限**:${QUADRANT_LABEL[todo.quadrant] || todo.quadrant}`,
126
+ `**状态**:${STATUS_LABEL[todo.status] || todo.status}`,
127
+ ]
128
+ if (todo.dueDate) meta.push(`**截止**:${fmtDateTime(todo.dueDate)}`)
129
+ if (todo.workDir) meta.push(`**目录**:\`${todo.workDir}\``)
130
+ meta.push(`**创建**:${fmtDateTime(todo.createdAt)}`)
131
+ meta.push(`**更新**:${fmtDateTime(todo.updatedAt)}`)
132
+ lines.push(meta.join(' · '))
133
+ lines.push('')
134
+
135
+ if (todo.description?.trim()) {
136
+ lines.push('## 描述')
137
+ lines.push('')
138
+ lines.push(todo.description.trim())
139
+ lines.push('')
140
+ }
141
+
142
+ if (comments.length) {
143
+ lines.push('## 评论时间线')
144
+ lines.push('')
145
+ for (const c of comments) {
146
+ lines.push(`- **${fmtDateTime(c.createdAt)}** ${c.content.replace(/\n/g, ' ')}`)
147
+ }
148
+ lines.push('')
149
+ }
150
+
151
+ if (subtodos.length) {
152
+ lines.push('## 子待办')
153
+ lines.push('')
154
+ for (const subtodo of subtodos) {
155
+ lines.push(`- [${subtodo.status === 'done' ? 'x' : ' '}] ${subtodo.title}`)
156
+ }
157
+ lines.push('')
158
+ }
159
+
160
+ if (sessions.length) {
161
+ const totals = sessions.reduce((acc, r) => {
162
+ acc.input += r.tokens.input
163
+ acc.output += r.tokens.output
164
+ acc.cacheRead += r.tokens.cacheRead
165
+ acc.cacheCreation += r.tokens.cacheCreation
166
+ acc.usd += r.cost.usd
167
+ acc.cny += r.cost.cny
168
+ return acc
169
+ }, { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, usd: 0, cny: 0 })
170
+ const tokenTotal = totals.input + totals.output + totals.cacheRead + totals.cacheCreation
171
+
172
+ lines.push('## AI 会话')
173
+ lines.push('')
174
+ lines.push(`共 ${sessions.length} 段 · Token ${fmtTokens(tokenTotal)}(cache 命中 ${fmtTokens(totals.cacheRead)})· 成本 ${fmtCost(totals)}`)
175
+ lines.push('')
176
+
177
+ sessions.forEach((row, idx) => {
178
+ const { session, file, tokens, cost, turns } = row
179
+ const tool = (session?.tool || 'claude').toUpperCase()
180
+ const startedAt = file?.started_at || session?.startedAt
181
+ const endedAt = file?.ended_at || session?.completedAt
182
+ const duration = startedAt && endedAt ? endedAt - startedAt : null
183
+ const activeMs = file?.active_ms
184
+
185
+ lines.push(`### 会话 ${idx + 1} · ${tool}`)
186
+ const metaLine = [
187
+ `起始 ${fmtDateTime(startedAt)}`,
188
+ `时长 ${fmtDuration(duration)}`,
189
+ ]
190
+ if (activeMs != null) metaLine.push(`活跃 ${fmtDuration(activeMs)}`)
191
+ if (file?.primary_model) metaLine.push(`模型 \`${file.primary_model}\``)
192
+ metaLine.push(`Token ${fmtTokens(tokens.input + tokens.output + tokens.cacheRead + tokens.cacheCreation)}`)
193
+ metaLine.push(`成本 ${fmtCost(cost)}`)
194
+ lines.push(metaLine.join(' · '))
195
+ lines.push('')
196
+
197
+ if (session?.prompt) {
198
+ lines.push('**触发 Prompt:**')
199
+ lines.push('')
200
+ lines.push('> ' + session.prompt.replace(/\n/g, '\n> '))
201
+ lines.push('')
202
+ }
203
+
204
+ if (!file) {
205
+ lines.push('_(未关联到 transcript 文件)_')
206
+ lines.push('')
207
+ return
208
+ }
209
+
210
+ if (turnsMode === 'none' || !turns) {
211
+ if (file.first_user_prompt) {
212
+ lines.push(`**首条用户消息:** ${file.first_user_prompt}`)
213
+ lines.push('')
214
+ }
215
+ return
216
+ }
217
+
218
+ const picked = pickHighlightTurns(turns, turnLimit)
219
+ if (turnsMode === 'summary') {
220
+ lines.push('<details><summary>展开对话节选</summary>')
221
+ lines.push('')
222
+ }
223
+ for (const t of picked) {
224
+ if (t.role === 'separator') {
225
+ lines.push(`*${t.content}*`)
226
+ lines.push('')
227
+ continue
228
+ }
229
+ const label = ROLE_LABEL[t.role] || t.role
230
+ lines.push(`**【${label}】**`)
231
+ lines.push('')
232
+ const body = turnsMode === 'full' ? String(t.content || '') : summarizeTurnContent(t.content)
233
+ lines.push(body)
234
+ lines.push('')
235
+ }
236
+ if (turnsMode === 'summary') {
237
+ lines.push('</details>')
238
+ lines.push('')
239
+ }
240
+ })
241
+ }
242
+
243
+ lines.push('---')
244
+ lines.push(`_生成于 ${fmtDateTime(report.generatedAt)} · AgentQuad_`)
245
+ return lines.join('\n')
246
+ }
@@ -0,0 +1,82 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { spawnSync } from 'node:child_process'
4
+ import readline from 'node:readline'
5
+
6
+ export function shouldRunWizard({ rootDir, isTTY, env, flags }) {
7
+ if (env.AGENTQUAD_SKIP_WIZARD === '1' || env.AGENTQUAD_SKIP_WIZARD === 'true') return false
8
+ if (flags?.wizard === false) return false
9
+ if (!isTTY) return false
10
+ if (existsSync(join(rootDir, 'config.json'))) return false
11
+ if (existsSync(join(rootDir, 'data.db'))) return false
12
+ return true
13
+ }
14
+
15
+ export function defaultAsk(stdin = process.stdin, stdout = process.stdout) {
16
+ return (question) => new Promise((resolve) => {
17
+ const rl = readline.createInterface({ input: stdin, output: stdout })
18
+ rl.question(question, (ans) => { rl.close(); resolve(ans) })
19
+ })
20
+ }
21
+
22
+ export function defaultChecks() {
23
+ const has = (bin) => spawnSync('command', ['-v', bin], { encoding: 'utf8', shell: '/bin/sh' }).status === 0
24
+ return { claude: () => has('claude'), codex: () => has('codex') }
25
+ }
26
+
27
+ export async function defaultInstallTools(tools) {
28
+ const r = spawnSync(process.execPath, [
29
+ new URL('./cli.js', import.meta.url).pathname,
30
+ 'install-tools',
31
+ ...tools.map((t) => `--${t}`),
32
+ '-y',
33
+ ], { stdio: 'inherit' })
34
+ return r.status ?? 1
35
+ }
36
+
37
+ export async function runFirstRunWizard({
38
+ checks = defaultChecks(),
39
+ installTools = defaultInstallTools,
40
+ ask = defaultAsk(),
41
+ log = console.log,
42
+ } = {}) {
43
+ log('\n👋 第一次启动 AgentQuad。\n')
44
+
45
+ const claudeOK = checks.claude()
46
+ const codexOK = checks.codex()
47
+ const missing = []
48
+ if (!claudeOK) missing.push('claude')
49
+ if (!codexOK) missing.push('codex')
50
+
51
+ let installedTools = []
52
+ let skippedInstall = false
53
+
54
+ if (missing.length > 0) {
55
+ log(`[1/2] 检测到未安装:${missing.join(', ')}(AI 终端必需)`)
56
+ const ans = (await ask(` 运行 'agentquad install-tools --all' 自动安装?(Y/n) `)).trim().toLowerCase()
57
+ if (ans === '' || ans === 'y' || ans === 'yes') {
58
+ try {
59
+ const code = await installTools(missing)
60
+ if (code === 0) installedTools = [...missing]
61
+ else log('\n⚠ 工具安装失败,AI 终端将不可用。修复后跑 agentquad install-tools --all\n')
62
+ } catch (e) {
63
+ log(`\n⚠ 工具安装异常(${e?.message || e}),AI 终端将不可用。\n`)
64
+ }
65
+ } else {
66
+ skippedInstall = true
67
+ }
68
+ }
69
+
70
+ const available = []
71
+ if (claudeOK || installedTools.includes('claude')) available.push('claude')
72
+ if (codexOK || installedTools.includes('codex')) available.push('codex')
73
+
74
+ let defaultTool = 'claude'
75
+ if (available.length > 0) {
76
+ const optsStr = available.join(' / ')
77
+ const ans = (await ask(`[2/2] 选择默认 AI 工具 (${optsStr}) [默认: ${available[0]}]: `)).trim().toLowerCase()
78
+ defaultTool = available.includes(ans) ? ans : available[0]
79
+ }
80
+
81
+ return { skipped: false, installedTools, defaultTool, skippedInstall }
82
+ }
@@ -0,0 +1,139 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, statSync } from 'node:fs'
3
+
4
+ const DEFAULT_TIMEOUT_MS = 5000
5
+
6
+ function runGit(args, { cwd, timeoutMs = DEFAULT_TIMEOUT_MS, env, maxBytes } = {}) {
7
+ return new Promise((resolve) => {
8
+ let proc
9
+ try {
10
+ proc = spawn('git', args, { cwd, env: env || process.env })
11
+ } catch (e) {
12
+ resolve({ error: 'spawn_failed', code: e?.code || '', stderr: e?.message || '' })
13
+ return
14
+ }
15
+ let stdout = Buffer.alloc(0)
16
+ let stderr = ''
17
+ let truncated = false
18
+ let settled = false
19
+ const timer = setTimeout(() => {
20
+ if (settled) return
21
+ settled = true
22
+ try { proc.kill('SIGTERM') } catch {}
23
+ resolve({ error: 'timeout' })
24
+ }, timeoutMs)
25
+ proc.on('error', (err) => {
26
+ if (settled) return
27
+ settled = true
28
+ clearTimeout(timer)
29
+ resolve({
30
+ error: err?.code === 'ENOENT' ? 'git_missing' : 'spawn_failed',
31
+ code: err?.code || '',
32
+ stderr: err?.message || '',
33
+ })
34
+ })
35
+ proc.stdout?.on('data', (chunk) => {
36
+ if (maxBytes && stdout.length + chunk.length > maxBytes) {
37
+ const remaining = Math.max(0, maxBytes - stdout.length)
38
+ if (remaining > 0) stdout = Buffer.concat([stdout, chunk.slice(0, remaining)])
39
+ truncated = true
40
+ try { proc.kill('SIGTERM') } catch {}
41
+ } else {
42
+ stdout = Buffer.concat([stdout, chunk])
43
+ }
44
+ })
45
+ proc.stderr?.on('data', (chunk) => { stderr += chunk.toString() })
46
+ proc.on('close', (code) => {
47
+ if (settled) return
48
+ settled = true
49
+ clearTimeout(timer)
50
+ resolve({ code, stdout: stdout.toString('utf8'), stderr, truncated })
51
+ })
52
+ })
53
+ }
54
+
55
+ function checkDir(workDir) {
56
+ if (!workDir || !existsSync(workDir)) return { state: 'not_found' }
57
+ try {
58
+ if (!statSync(workDir).isDirectory()) return { state: 'not_found' }
59
+ } catch {
60
+ return { state: 'not_found' }
61
+ }
62
+ return null
63
+ }
64
+
65
+ export async function readGitStatus(workDir, opts = {}) {
66
+ const pre = checkDir(workDir)
67
+ if (pre) return pre
68
+
69
+ const isRepo = await runGit(['rev-parse', '--is-inside-work-tree'], { cwd: workDir, ...opts })
70
+ if (isRepo.error === 'git_missing') return { state: 'git_missing' }
71
+ if (isRepo.error === 'timeout') return { state: 'timeout' }
72
+ if (isRepo.error) return { state: 'error', message: (isRepo.stderr || '').slice(0, 500) }
73
+ if (isRepo.code !== 0 || (isRepo.stdout || '').trim() !== 'true') return { state: 'not_a_repo' }
74
+
75
+ const branchRes = await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: workDir, ...opts })
76
+ if (branchRes.error === 'timeout') return { state: 'timeout' }
77
+ if (branchRes.error) return { state: 'error', message: (branchRes.stderr || '').slice(0, 500) }
78
+ const branch = (branchRes.stdout || '').trim() || 'HEAD'
79
+
80
+ let headShort
81
+ if (branch === 'HEAD') {
82
+ const shaRes = await runGit(['rev-parse', '--short=7', 'HEAD'], { cwd: workDir, ...opts })
83
+ if (shaRes.code === 0) headShort = (shaRes.stdout || '').trim()
84
+ }
85
+
86
+ const statusRes = await runGit(['status', '--porcelain'], { cwd: workDir, ...opts })
87
+ if (statusRes.error === 'timeout') return { state: 'timeout' }
88
+ if (statusRes.error) return { state: 'error', message: (statusRes.stderr || '').slice(0, 500) }
89
+ const dirty = (statusRes.stdout || '').split('\n').filter((l) => l.trim().length > 0).length
90
+
91
+ let hasUpstream = false
92
+ let ahead = 0
93
+ let behind = 0
94
+ const revListRes = await runGit(
95
+ ['rev-list', '--count', '--left-right', '@{upstream}...HEAD'],
96
+ { cwd: workDir, ...opts }
97
+ )
98
+ if (revListRes.error === 'timeout') return { state: 'timeout' }
99
+ if (!revListRes.error && revListRes.code === 0) {
100
+ const parts = (revListRes.stdout || '').trim().split(/\s+/)
101
+ if (parts.length === 2) {
102
+ hasUpstream = true
103
+ behind = Number(parts[0]) || 0
104
+ ahead = Number(parts[1]) || 0
105
+ }
106
+ }
107
+
108
+ const out = { state: 'ok', branch, dirty, ahead, behind, hasUpstream }
109
+ if (headShort) out.headShort = headShort
110
+ return out
111
+ }
112
+
113
+ export async function readGitDiff(workDir, opts = {}) {
114
+ const pre = checkDir(workDir)
115
+ if (pre) return pre
116
+ const maxBytes = Number.isFinite(opts.maxBytes) ? opts.maxBytes : 200 * 1024
117
+
118
+ const isRepo = await runGit(['rev-parse', '--is-inside-work-tree'], { cwd: workDir })
119
+ if (isRepo.error === 'git_missing') return { state: 'git_missing' }
120
+ if (isRepo.error === 'timeout') return { state: 'timeout' }
121
+ if (isRepo.error) return { state: 'error', message: (isRepo.stderr || '').slice(0, 500) }
122
+ if (isRepo.code !== 0 || (isRepo.stdout || '').trim() !== 'true') return { state: 'not_a_repo' }
123
+
124
+ const diffRes = await runGit(['diff', 'HEAD'], { cwd: workDir, maxBytes })
125
+ if (diffRes.error === 'timeout') return { state: 'timeout' }
126
+ if (diffRes.error) return { state: 'error', message: (diffRes.stderr || '').slice(0, 500) }
127
+
128
+ const untrackedRes = await runGit(['ls-files', '--others', '--exclude-standard'], { cwd: workDir })
129
+ const untracked = untrackedRes.code === 0
130
+ ? (untrackedRes.stdout || '').split('\n').map((l) => l.trim()).filter(Boolean)
131
+ : []
132
+
133
+ return {
134
+ state: 'ok',
135
+ diff: diffRes.stdout || '',
136
+ untracked,
137
+ truncated: !!diffRes.truncated,
138
+ }
139
+ }
@@ -0,0 +1,205 @@
1
+ import * as Lark from '@larksuiteoapi/node-sdk'
2
+ import { toLarkText } from './lark-markdown.js'
3
+
4
+ function isBlank(value) {
5
+ return value == null || String(value) === ''
6
+ }
7
+
8
+ function normalizePayload(response) {
9
+ return response?.data || response || null
10
+ }
11
+
12
+ function normalizeError(error) {
13
+ // 飞书 SDK 把 axios 错误抛出来,response.data 里有 {code, msg} 是真正的业务错误。
14
+ // 优先把它捞出来 —— "code 231001: reaction type is invalid" 比 "Request failed with
15
+ // status code 400" 有用得多。
16
+ const data = error?.response?.data
17
+ if (data && typeof data === 'object') {
18
+ const parts = []
19
+ if (data.code != null) parts.push(`code ${data.code}`)
20
+ if (data.msg) parts.push(data.msg)
21
+ if (parts.length) return parts.join(': ')
22
+ }
23
+ return error?.message || error?.description || String(error)
24
+ }
25
+
26
+ function defaultClientFactory({ appId, appSecret }) {
27
+ return new Lark.Client({
28
+ appId,
29
+ appSecret,
30
+ appType: Lark.AppType.SelfBuild,
31
+ })
32
+ }
33
+
34
+ export function createLarkApiClient({ appId, appSecret, clientFactory = defaultClientFactory, logger = console } = {}) {
35
+ let client = null
36
+
37
+ function hasCredentials() {
38
+ return !isBlank(appId) && !isBlank(appSecret)
39
+ }
40
+
41
+ function getClient() {
42
+ if (!hasCredentials()) return null
43
+ if (!client) client = clientFactory({ appId, appSecret })
44
+ return client
45
+ }
46
+
47
+ async function sendMessage({ chatId, text } = {}) {
48
+ if (!hasCredentials()) return { ok: false, reason: 'lark_credentials_missing' }
49
+ if (isBlank(chatId)) return { ok: false, reason: 'chatId_required' }
50
+ if (isBlank(text)) return { ok: false, reason: 'text_required' }
51
+ try {
52
+ const response = await getClient().im.message.create({
53
+ params: { receive_id_type: 'chat_id' },
54
+ data: {
55
+ receive_id: String(chatId),
56
+ msg_type: 'text',
57
+ content: JSON.stringify({ text: toLarkText(String(text)) }),
58
+ },
59
+ })
60
+ return { ok: true, payload: normalizePayload(response) }
61
+ } catch (e) {
62
+ const detail = normalizeError(e)
63
+ logger.warn?.(`[lark-api] send failed: ${detail}`)
64
+ return { ok: false, reason: 'lark_send_failed', detail }
65
+ }
66
+ }
67
+
68
+ async function replyInThread({ rootMessageId, text } = {}) {
69
+ if (!hasCredentials()) return { ok: false, reason: 'lark_credentials_missing' }
70
+ if (isBlank(rootMessageId)) return { ok: false, reason: 'rootMessageId_required' }
71
+ if (isBlank(text)) return { ok: false, reason: 'text_required' }
72
+ try {
73
+ const response = await getClient().im.message.reply({
74
+ path: { message_id: String(rootMessageId) },
75
+ data: {
76
+ msg_type: 'text',
77
+ content: JSON.stringify({ text: toLarkText(String(text)) }),
78
+ reply_in_thread: true,
79
+ },
80
+ })
81
+ return { ok: true, payload: normalizePayload(response) }
82
+ } catch (e) {
83
+ const detail = normalizeError(e)
84
+ logger.warn?.(`[lark-api] reply failed: ${detail}`)
85
+ return { ok: false, reason: 'lark_reply_failed', detail }
86
+ }
87
+ }
88
+
89
+ async function sendCard({ chatId, card } = {}) {
90
+ if (!hasCredentials()) return { ok: false, reason: 'lark_credentials_missing' }
91
+ if (isBlank(chatId)) return { ok: false, reason: 'chatId_required' }
92
+ if (!card || typeof card !== 'object') return { ok: false, reason: 'card_required' }
93
+ try {
94
+ const response = await getClient().im.message.create({
95
+ params: { receive_id_type: 'chat_id' },
96
+ data: {
97
+ receive_id: String(chatId),
98
+ msg_type: 'interactive',
99
+ content: JSON.stringify(card),
100
+ },
101
+ })
102
+ return { ok: true, payload: normalizePayload(response) }
103
+ } catch (e) {
104
+ const detail = normalizeError(e)
105
+ logger.warn?.(`[lark-api] sendCard failed: ${detail}`)
106
+ return { ok: false, reason: 'lark_send_card_failed', detail }
107
+ }
108
+ }
109
+
110
+ async function replyWithCard({ rootMessageId, card } = {}) {
111
+ if (!hasCredentials()) return { ok: false, reason: 'lark_credentials_missing' }
112
+ if (isBlank(rootMessageId)) return { ok: false, reason: 'rootMessageId_required' }
113
+ if (!card || typeof card !== 'object') return { ok: false, reason: 'card_required' }
114
+ try {
115
+ const response = await getClient().im.message.reply({
116
+ path: { message_id: String(rootMessageId) },
117
+ data: {
118
+ msg_type: 'interactive',
119
+ content: JSON.stringify(card),
120
+ reply_in_thread: true,
121
+ },
122
+ })
123
+ return { ok: true, payload: normalizePayload(response) }
124
+ } catch (e) {
125
+ const detail = normalizeError(e)
126
+ logger.warn?.(`[lark-api] replyCard failed: ${detail}`)
127
+ return { ok: false, reason: 'lark_reply_card_failed', detail }
128
+ }
129
+ }
130
+
131
+ async function addReaction({ messageId, emojiType } = {}) {
132
+ if (!hasCredentials()) return { ok: false, reason: 'lark_credentials_missing' }
133
+ if (isBlank(messageId)) return { ok: false, reason: 'messageId_required' }
134
+ if (isBlank(emojiType)) return { ok: false, reason: 'emojiType_required' }
135
+ try {
136
+ const response = await getClient().im.messageReaction.create({
137
+ path: { message_id: String(messageId) },
138
+ data: { reaction_type: { emoji_type: String(emojiType) } },
139
+ })
140
+ return { ok: true, payload: normalizePayload(response) }
141
+ } catch (e) {
142
+ const detail = normalizeError(e)
143
+ logger.warn?.(`[lark-api] reaction failed: ${detail}`)
144
+ return { ok: false, reason: 'lark_reaction_failed', detail }
145
+ }
146
+ }
147
+
148
+ async function getMessageResource({ messageId, fileKey, type = 'image' } = {}) {
149
+ if (!hasCredentials()) return { ok: false, reason: 'lark_credentials_missing' }
150
+ if (isBlank(messageId)) return { ok: false, reason: 'messageId_required' }
151
+ if (isBlank(fileKey)) return { ok: false, reason: 'fileKey_required' }
152
+ try {
153
+ const result = await getClient().im.messageResource.get({
154
+ path: { message_id: String(messageId), file_key: String(fileKey) },
155
+ params: { type: String(type) },
156
+ })
157
+ // SDK 返回 { writeFile(path), getReadableStream(), headers } —— 直接透传
158
+ return {
159
+ ok: true,
160
+ writeFile: result.writeFile,
161
+ getReadableStream: result.getReadableStream,
162
+ headers: result.headers,
163
+ }
164
+ } catch (e) {
165
+ const detail = normalizeError(e)
166
+ logger.warn?.(`[lark-api] getMessageResource failed: ${detail}`)
167
+ return { ok: false, reason: 'lark_resource_failed', detail }
168
+ }
169
+ }
170
+
171
+ async function deleteReaction({ messageId, reactionId } = {}) {
172
+ if (!hasCredentials()) return { ok: false, reason: 'lark_credentials_missing' }
173
+ if (isBlank(messageId)) return { ok: false, reason: 'messageId_required' }
174
+ if (isBlank(reactionId)) return { ok: false, reason: 'reactionId_required' }
175
+ try {
176
+ const response = await getClient().im.messageReaction.delete({
177
+ path: { message_id: String(messageId), reaction_id: String(reactionId) },
178
+ })
179
+ return { ok: true, payload: normalizePayload(response) }
180
+ } catch (e) {
181
+ const detail = normalizeError(e)
182
+ logger.warn?.(`[lark-api] reaction delete failed: ${detail}`)
183
+ return { ok: false, reason: 'lark_reaction_delete_failed', detail }
184
+ }
185
+ }
186
+
187
+ async function testConnection() {
188
+ if (!hasCredentials()) return { ok: false, reason: 'lark_credentials_missing' }
189
+ try {
190
+ const sdkClient = getClient()
191
+ if (sdkClient.auth?.tenantAccessToken?.internal) {
192
+ await sdkClient.auth.tenantAccessToken.internal({
193
+ data: { app_id: String(appId), app_secret: String(appSecret) },
194
+ })
195
+ } else {
196
+ await sendMessage({ chatId: '', text: '' })
197
+ }
198
+ return { ok: true }
199
+ } catch (e) {
200
+ return { ok: false, reason: 'lark_client_init_failed', detail: normalizeError(e) }
201
+ }
202
+ }
203
+
204
+ return { sendMessage, replyInThread, sendCard, replyWithCard, addReaction, deleteReaction, getMessageResource, testConnection }
205
+ }