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
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
const ScopeEnum = z.enum(['todos', 'comments', 'wiki', 'ai_sessions'])
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 把一个 JS 对象序列化成 MCP 工具响应。
|
|
9
|
+
* MCP 工具返回的 content 数组里每项必须有 type + 相应字段;
|
|
10
|
+
* 对 LLM 最友好的是 type:"text" + pretty JSON。
|
|
11
|
+
*/
|
|
12
|
+
function asText(value) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asError(message) {
|
|
19
|
+
return {
|
|
20
|
+
isError: true,
|
|
21
|
+
content: [{ type: 'text', text: `error: ${message}` }],
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerReadTools(server, { db, searchService, wikiDir, transcriptScanner }) {
|
|
26
|
+
// ─── 1. search ────────────────────────────────────────────────
|
|
27
|
+
server.registerTool(
|
|
28
|
+
'search',
|
|
29
|
+
{
|
|
30
|
+
description:
|
|
31
|
+
'全局搜索 AgentQuad 的所有语料:todo 标题/描述、评论、wiki 记忆、AI 会话元信息。基于 SQLite FTS5 + BM25 排序。返回每条命中的 scope/snippet/todoId/todoTitle 等。',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
query: z.string().min(1).describe('搜索关键词;可以是自然语言词组,会自动转成 FTS5 前缀匹配'),
|
|
34
|
+
scopes: z.array(ScopeEnum).optional().describe('限定搜索范围,默认 4 个全开'),
|
|
35
|
+
includeArchived: z.boolean().optional().describe('是否包含已归档的 todo。默认 false'),
|
|
36
|
+
limit: z.number().int().positive().max(100).optional().describe('返回条数上限,1-100,默认 20'),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
async (args) => {
|
|
40
|
+
try {
|
|
41
|
+
const out = searchService.search(args)
|
|
42
|
+
return asText(out)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return asError(e?.message || 'search_failed')
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// ─── 2. list_todos ────────────────────────────────────────────
|
|
50
|
+
server.registerTool(
|
|
51
|
+
'list_todos',
|
|
52
|
+
{
|
|
53
|
+
description:
|
|
54
|
+
'按过滤条件列出 todos。不做全文搜索——用 search 做模糊搜索。返回元数据数组,不含 AI 会话详情(用 get_todo 取)。',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
quadrant: z.number().int().min(1).max(4).optional().describe('1=重要且紧急 2=重要不紧急 3=紧急不重要 4=不重要不紧急'),
|
|
57
|
+
status: z.enum(['todo', 'done', 'all']).optional().describe('默认 all'),
|
|
58
|
+
archived: z.union([z.boolean(), z.literal('all')]).optional().describe('默认 false 只看未归档;"all" 两者都要'),
|
|
59
|
+
parentId: z.string().optional().describe('仅列某 parent 下的子任务'),
|
|
60
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
async (args = {}) => {
|
|
64
|
+
try {
|
|
65
|
+
const rawStatus = args.status === 'all' ? '' : args.status
|
|
66
|
+
const list = db.listTodos({
|
|
67
|
+
quadrant: args.quadrant,
|
|
68
|
+
status: rawStatus,
|
|
69
|
+
archived: args.archived,
|
|
70
|
+
})
|
|
71
|
+
let filtered = list
|
|
72
|
+
if (args.parentId) {
|
|
73
|
+
filtered = filtered.filter((t) => t.parentId === args.parentId)
|
|
74
|
+
}
|
|
75
|
+
const limit = args.limit || 100
|
|
76
|
+
const truncated = filtered.length > limit
|
|
77
|
+
const slim = filtered.slice(0, limit).map((t) => ({
|
|
78
|
+
id: t.id,
|
|
79
|
+
parentId: t.parentId,
|
|
80
|
+
title: t.title,
|
|
81
|
+
quadrant: t.quadrant,
|
|
82
|
+
status: t.status,
|
|
83
|
+
dueDate: t.dueDate,
|
|
84
|
+
workDir: t.workDir,
|
|
85
|
+
archivedAt: t.archivedAt,
|
|
86
|
+
completedAt: t.completedAt,
|
|
87
|
+
updatedAt: t.updatedAt,
|
|
88
|
+
aiSessionCount: Array.isArray(t.aiSessions) ? t.aiSessions.length : 0,
|
|
89
|
+
}))
|
|
90
|
+
return asText({ total: filtered.length, returned: slim.length, truncated, todos: slim })
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return asError(e?.message || 'list_failed')
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// ─── 3. get_todo ──────────────────────────────────────────────
|
|
98
|
+
server.registerTool(
|
|
99
|
+
'get_todo',
|
|
100
|
+
{
|
|
101
|
+
description:
|
|
102
|
+
'获取单个 todo 的完整信息:本体字段 + 子任务 id 列表 + 评论数组 + AI 会话数组 + wiki 是否存在。不直接返回 wiki 正文(用 read_wiki)也不返回 transcript(用 read_transcript)。',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
id: z.string().min(1),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
async ({ id }) => {
|
|
108
|
+
try {
|
|
109
|
+
const todo = db.getTodo(id)
|
|
110
|
+
if (!todo) return asError('todo_not_found')
|
|
111
|
+
const comments = db.listComments(id)
|
|
112
|
+
const children = db.raw.prepare(`SELECT id, title FROM todos WHERE parent_id = ?`).all(id)
|
|
113
|
+
const wikiFile = wikiDir ? join(wikiDir, `${id}.md`) : null
|
|
114
|
+
const hasWiki = wikiFile ? existsSync(wikiFile) : false
|
|
115
|
+
return asText({
|
|
116
|
+
todo,
|
|
117
|
+
children,
|
|
118
|
+
comments,
|
|
119
|
+
hasWiki,
|
|
120
|
+
})
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return asError(e?.message || 'get_failed')
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// ─── 4. read_wiki ─────────────────────────────────────────────
|
|
128
|
+
server.registerTool(
|
|
129
|
+
'read_wiki',
|
|
130
|
+
{
|
|
131
|
+
description: '读取指定 todo 的 wiki 记忆文件(markdown)。若该 todo 还没沉淀 wiki,返回 exists:false。',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
todoId: z.string().min(1),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
async ({ todoId }) => {
|
|
137
|
+
try {
|
|
138
|
+
if (!wikiDir) return asText({ exists: false, body: null, reason: 'wikiDir_not_configured' })
|
|
139
|
+
const file = join(wikiDir, `${todoId}.md`)
|
|
140
|
+
if (!existsSync(file)) return asText({ exists: false, body: null })
|
|
141
|
+
const body = readFileSync(file, 'utf8')
|
|
142
|
+
return asText({ exists: true, body, path: file })
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return asError(e?.message || 'read_failed')
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// ─── 5. get_stats ─────────────────────────────────────────────
|
|
150
|
+
server.registerTool(
|
|
151
|
+
'get_stats',
|
|
152
|
+
{
|
|
153
|
+
description:
|
|
154
|
+
'一个当前快照:按象限/状态的分布、今日截止、本周完成、最近 7 天活跃度、归档数量。轻量实时计算。',
|
|
155
|
+
inputSchema: {},
|
|
156
|
+
},
|
|
157
|
+
async () => {
|
|
158
|
+
try {
|
|
159
|
+
const now = Date.now()
|
|
160
|
+
const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0)
|
|
161
|
+
const startOfWeek = new Date(startOfDay); startOfWeek.setDate(startOfWeek.getDate() - 6)
|
|
162
|
+
const all = db.listTodos({ archived: 'all' })
|
|
163
|
+
const open = all.filter((t) => t.status !== 'done' && t.status !== 'missed' && t.archivedAt == null)
|
|
164
|
+
const archivedCount = all.filter((t) => t.archivedAt != null).length
|
|
165
|
+
const byQuadrant = {}
|
|
166
|
+
for (const t of open) byQuadrant[t.quadrant] = (byQuadrant[t.quadrant] || 0) + 1
|
|
167
|
+
const overdue = open.filter((t) => t.dueDate && t.dueDate < now)
|
|
168
|
+
const dueToday = open.filter((t) => t.dueDate && t.dueDate >= startOfDay.getTime() && t.dueDate < startOfDay.getTime() + 86_400_000)
|
|
169
|
+
const weekDone = db.listCompletedTodos({ since: startOfWeek.getTime(), until: now + 1 })
|
|
170
|
+
return asText({
|
|
171
|
+
openCount: open.length,
|
|
172
|
+
archivedCount,
|
|
173
|
+
byQuadrant,
|
|
174
|
+
overdue: { count: overdue.length, ids: overdue.slice(0, 10).map((t) => t.id) },
|
|
175
|
+
dueToday: { count: dueToday.length, ids: dueToday.map((t) => t.id) },
|
|
176
|
+
completedThisWeek: weekDone.length,
|
|
177
|
+
generatedAt: now,
|
|
178
|
+
})
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return asError(e?.message || 'stats_failed')
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// ─── 7. search_transcripts ────────────────────────────────────
|
|
186
|
+
if (transcriptScanner) {
|
|
187
|
+
server.registerTool(
|
|
188
|
+
'search_transcripts',
|
|
189
|
+
{
|
|
190
|
+
description:
|
|
191
|
+
'在 AI 会话日志(~/.agentquad/logs/*.log)里做纯文本逐行扫描。不使用 FTS,适合查 "当时 Claude 说的那句话"。结果带前后文;单次返回上限默认 30 条、单文件最多 5 条。',
|
|
192
|
+
inputSchema: {
|
|
193
|
+
query: z.string().min(1),
|
|
194
|
+
todoId: z.string().optional().describe('只搜这个 todo 下的会话;不传就全局'),
|
|
195
|
+
afterTs: z.number().int().optional(),
|
|
196
|
+
beforeTs: z.number().int().optional(),
|
|
197
|
+
maxMatches: z.number().int().positive().max(100).optional(),
|
|
198
|
+
perFileLimit: z.number().int().positive().max(20).optional(),
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
async (args) => {
|
|
202
|
+
try {
|
|
203
|
+
const out = transcriptScanner.search(args)
|
|
204
|
+
return asText(out)
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return asError(e?.message || 'scan_failed')
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── 8. read_transcript ───────────────────────────────────────
|
|
213
|
+
if (transcriptScanner) {
|
|
214
|
+
server.registerTool(
|
|
215
|
+
'read_transcript',
|
|
216
|
+
{
|
|
217
|
+
description:
|
|
218
|
+
'读某个 sessionId 的完整会话日志。maxChars 控制 token 预算(字符数 ≈ 4×tokens,默认 32000 ≈ 8k tokens);超出时尾部优先保留,前端标记 [truncated]。',
|
|
219
|
+
inputSchema: {
|
|
220
|
+
sessionId: z.string().min(1),
|
|
221
|
+
maxChars: z.number().int().positive().max(200_000).optional(),
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
async (args) => {
|
|
225
|
+
try {
|
|
226
|
+
const out = transcriptScanner.readSession(args)
|
|
227
|
+
return asText(out)
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return asError(e?.message || 'read_failed')
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── 6. get_recent_sessions ───────────────────────────────────
|
|
236
|
+
server.registerTool(
|
|
237
|
+
'get_recent_sessions',
|
|
238
|
+
{
|
|
239
|
+
description:
|
|
240
|
+
'跨 todo 的最近 AI 会话列表。每项含 sessionId / tool / todoId / todoTitle / startedAt / completedAt / durationMs / status。',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
limit: z.number().int().positive().max(100).optional(),
|
|
243
|
+
tool: z.enum(['claude', 'codex']).optional(),
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
async ({ limit, tool } = {}) => {
|
|
247
|
+
try {
|
|
248
|
+
const cap = Math.max(1, Math.min(limit || 20, 100))
|
|
249
|
+
const whereExtras = tool ? 'AND s.tool = ?' : ''
|
|
250
|
+
const rows = db.raw
|
|
251
|
+
.prepare(
|
|
252
|
+
`SELECT s.id AS sessionId, s.todo_id AS todoId, s.tool, s.status,
|
|
253
|
+
s.exit_code AS exitCode, s.started_at AS startedAt,
|
|
254
|
+
s.completed_at AS completedAt, s.duration_ms AS durationMs,
|
|
255
|
+
t.title AS todoTitle
|
|
256
|
+
FROM ai_session_log s
|
|
257
|
+
LEFT JOIN todos t ON t.id = s.todo_id
|
|
258
|
+
WHERE 1=1 ${whereExtras}
|
|
259
|
+
ORDER BY s.completed_at DESC
|
|
260
|
+
LIMIT ?`,
|
|
261
|
+
)
|
|
262
|
+
.all(...(tool ? [tool, cap] : [cap]))
|
|
263
|
+
return asText({ count: rows.length, sessions: rows })
|
|
264
|
+
} catch (e) {
|
|
265
|
+
return asError(e?.message || 'recent_failed')
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
function asText(value) {
|
|
4
|
+
return {
|
|
5
|
+
content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function asError(message) {
|
|
10
|
+
return {
|
|
11
|
+
isError: true,
|
|
12
|
+
content: [{ type: 'text', text: `error: ${message}` }],
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 注册 5 个安全写工具:create_todo / update_todo / add_comment / complete_todo / unarchive_todo。
|
|
18
|
+
* 这些操作都是可逆或低风险的(不会破坏关联),不需要 preview/confirm。
|
|
19
|
+
*/
|
|
20
|
+
export function registerWriteTools(server, { db }) {
|
|
21
|
+
// ─── 1. create_todo ───────────────────────────────────────────
|
|
22
|
+
server.registerTool(
|
|
23
|
+
'create_todo',
|
|
24
|
+
{
|
|
25
|
+
description:
|
|
26
|
+
'新建一条 todo。至少需要 title + quadrant。象限语义:1=重要且紧急,2=重要不紧急,3=紧急不重要,4=不重要不紧急。',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
title: z.string().min(1).describe('标题,必填'),
|
|
29
|
+
quadrant: z.number().int().min(1).max(4).describe('1 / 2 / 3 / 4'),
|
|
30
|
+
description: z.string().optional(),
|
|
31
|
+
parentId: z.string().optional().describe('如果是子任务,指定父 todo id(子任务会继承父的象限)'),
|
|
32
|
+
dueDate: z.number().int().optional().describe('截止时间戳(毫秒 epoch)'),
|
|
33
|
+
workDir: z.string().optional().describe('关联的代码仓路径'),
|
|
34
|
+
brainstorm: z.boolean().optional(),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async (args) => {
|
|
38
|
+
try {
|
|
39
|
+
if (!args.title?.trim()) return asError('title_required')
|
|
40
|
+
const created = db.createTodo({
|
|
41
|
+
title: args.title.trim(),
|
|
42
|
+
quadrant: args.quadrant,
|
|
43
|
+
description: args.description || '',
|
|
44
|
+
parentId: args.parentId,
|
|
45
|
+
dueDate: args.dueDate,
|
|
46
|
+
workDir: args.workDir,
|
|
47
|
+
brainstorm: !!args.brainstorm,
|
|
48
|
+
})
|
|
49
|
+
return asText({ todo: created, ok: true })
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return asError(e?.message || 'create_failed')
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// ─── 2. update_todo ───────────────────────────────────────────
|
|
57
|
+
server.registerTool(
|
|
58
|
+
'update_todo',
|
|
59
|
+
{
|
|
60
|
+
description:
|
|
61
|
+
'修改已有 todo 的字段(patch 语义,只改传入的字段)。不能通过此工具 complete / archive / delete —— 用专用工具。',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
id: z.string().min(1),
|
|
64
|
+
title: z.string().optional(),
|
|
65
|
+
description: z.string().optional(),
|
|
66
|
+
quadrant: z.number().int().min(1).max(4).optional(),
|
|
67
|
+
dueDate: z.number().int().nullable().optional().describe('传 null 显式清除'),
|
|
68
|
+
workDir: z.string().nullable().optional(),
|
|
69
|
+
parentId: z.string().nullable().optional().describe('传 null 升为顶层 todo'),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
async (args) => {
|
|
73
|
+
try {
|
|
74
|
+
const { id, ...patch } = args
|
|
75
|
+
if (!id) return asError('id_required')
|
|
76
|
+
// 禁止通过此工具改 status,status 变更有专用工具(complete/archive)
|
|
77
|
+
const cleanPatch = {}
|
|
78
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
79
|
+
if (v === undefined) continue
|
|
80
|
+
cleanPatch[k] = v
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(cleanPatch).length === 0) return asError('patch_empty')
|
|
83
|
+
const updated = db.updateTodo(id, cleanPatch)
|
|
84
|
+
if (!updated) return asError('todo_not_found')
|
|
85
|
+
return asText({ todo: updated, ok: true })
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return asError(e?.message || 'update_failed')
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// ─── 3. add_comment ───────────────────────────────────────────
|
|
93
|
+
server.registerTool(
|
|
94
|
+
'add_comment',
|
|
95
|
+
{
|
|
96
|
+
description: '给某个 todo 加一条评论(纯文本)。',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
todoId: z.string().min(1),
|
|
99
|
+
content: z.string().min(1),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
async ({ todoId, content }) => {
|
|
103
|
+
try {
|
|
104
|
+
const todo = db.getTodo(todoId)
|
|
105
|
+
if (!todo) return asError('todo_not_found')
|
|
106
|
+
const comment = db.addComment(todoId, content)
|
|
107
|
+
return asText({ comment, ok: true })
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return asError(e?.message || 'comment_failed')
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// ─── 4. complete_todo ─────────────────────────────────────────
|
|
115
|
+
server.registerTool(
|
|
116
|
+
'complete_todo',
|
|
117
|
+
{
|
|
118
|
+
description:
|
|
119
|
+
'把 todo 标记为已完成(status=done)。可逆:通过 update_todo 把 status 改回 todo 可恢复(但该字段不允许 update 工具直接改,因此请改用 reopen_todo —— 暂未实现,需要时再加)。',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
id: z.string().min(1),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
async ({ id }) => {
|
|
125
|
+
try {
|
|
126
|
+
const todo = db.getTodo(id)
|
|
127
|
+
if (!todo) return asError('todo_not_found')
|
|
128
|
+
if (todo.status === 'done') return asText({ todo, ok: true, alreadyDone: true })
|
|
129
|
+
const updated = db.updateTodo(id, { status: 'done' })
|
|
130
|
+
return asText({ todo: updated, ok: true })
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return asError(e?.message || 'complete_failed')
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
// ─── 5. unarchive_todo ────────────────────────────────────────
|
|
138
|
+
server.registerTool(
|
|
139
|
+
'unarchive_todo',
|
|
140
|
+
{
|
|
141
|
+
description: '取消某 todo 的归档状态,让它重新出现在默认列表里。',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
id: z.string().min(1),
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
async ({ id }) => {
|
|
147
|
+
try {
|
|
148
|
+
const todo = db.getTodo(id)
|
|
149
|
+
if (!todo) return asError('todo_not_found')
|
|
150
|
+
const updated = db.unarchiveTodo(id)
|
|
151
|
+
return asText({ todo: updated, ok: true })
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return asError(e?.message || 'unarchive_failed')
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
}
|