libero3 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 (192) hide show
  1. package/COLD_START.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +43 -0
  4. package/data/migrations/001_initial_schema.sql +43 -0
  5. package/data/migrations/002_seed_categories.sql +18 -0
  6. package/data/migrations/003_add_rating.sql +4 -0
  7. package/data/migrations/004_timer_sessions.sql +21 -0
  8. package/data/migrations/005_profile_settings.sql +5 -0
  9. package/data/migrations/006_add_timeblock_status.sql +1 -0
  10. package/data/migrations/007_reminders.sql +17 -0
  11. package/data/migrations/008_timer_target_ms.sql +5 -0
  12. package/data/migrations/009_tasks.sql +22 -0
  13. package/data/migrations/010_timer_task_id.sql +4 -0
  14. package/data/migrations/011_profile_default_confirmed.sql +4 -0
  15. package/data/migrations/012_identity_tables.sql +68 -0
  16. package/data/migrations/013_identity_managed_by.sql +3 -0
  17. package/data/migrations/014_task_difficulty_priority.sql +6 -0
  18. package/data/migrations/015_timeblock_asked_at.sql +5 -0
  19. package/data/migrations/016_intent_columns.sql +8 -0
  20. package/data/migrations/017_ap_wallet.sql +25 -0
  21. package/data/migrations/018_ap_rewards.sql +21 -0
  22. package/data/migrations/019_ap_bounty.sql +10 -0
  23. package/data/migrations/020_chat_turns.sql +10 -0
  24. package/data/migrations/021_portrait.sql +4 -0
  25. package/data/migrations/022_family_emoji_single.sql +3 -0
  26. package/data/migrations/023_ap_balance_view_log.sql +9 -0
  27. package/data/schema.sql +75 -0
  28. package/dist/data/db.js +83 -0
  29. package/dist/data/migrations/001_initial_schema.sql +43 -0
  30. package/dist/data/migrations/002_seed_categories.sql +18 -0
  31. package/dist/data/migrations/003_add_rating.sql +4 -0
  32. package/dist/data/migrations/004_timer_sessions.sql +21 -0
  33. package/dist/data/migrations/005_profile_settings.sql +5 -0
  34. package/dist/data/migrations/006_add_timeblock_status.sql +1 -0
  35. package/dist/data/migrations/007_reminders.sql +17 -0
  36. package/dist/data/migrations/008_timer_target_ms.sql +5 -0
  37. package/dist/data/migrations/009_tasks.sql +22 -0
  38. package/dist/data/migrations/010_timer_task_id.sql +4 -0
  39. package/dist/data/migrations/011_profile_default_confirmed.sql +4 -0
  40. package/dist/data/migrations/012_identity_tables.sql +68 -0
  41. package/dist/data/migrations/013_identity_managed_by.sql +3 -0
  42. package/dist/data/migrations/014_task_difficulty_priority.sql +6 -0
  43. package/dist/data/migrations/015_timeblock_asked_at.sql +5 -0
  44. package/dist/data/migrations/016_intent_columns.sql +8 -0
  45. package/dist/data/migrations/017_ap_wallet.sql +25 -0
  46. package/dist/data/migrations/018_ap_rewards.sql +21 -0
  47. package/dist/data/migrations/019_ap_bounty.sql +10 -0
  48. package/dist/data/migrations/020_chat_turns.sql +10 -0
  49. package/dist/data/migrations/021_portrait.sql +4 -0
  50. package/dist/data/migrations/022_family_emoji_single.sql +3 -0
  51. package/dist/data/migrations/023_ap_balance_view_log.sql +9 -0
  52. package/dist/data/schema.sql +75 -0
  53. package/dist/knowledge/_registry/dimensions.yaml +10 -0
  54. package/dist/knowledge/_registry/personas.yaml +7 -0
  55. package/dist/knowledge/_schema/analysis-model.schema.js +32 -0
  56. package/dist/knowledge/_schema/entry.schema.js +45 -0
  57. package/dist/knowledge/_schema/persona.schema.js +23 -0
  58. package/dist/knowledge/_schema/scenario-pack.schema.js +29 -0
  59. package/dist/knowledge/analysis-models/review-default.yaml +16 -0
  60. package/dist/knowledge/analysis-models/review-summer-plan.yaml +15 -0
  61. package/dist/knowledge/brain-science/brain-deep-warmup.yaml +22 -0
  62. package/dist/knowledge/brain-science/brain-emotion-granularity.yaml +22 -0
  63. package/dist/knowledge/brain-science/brain-env-engineering.yaml +22 -0
  64. package/dist/knowledge/brain-science/brain-extended-mind.yaml +22 -0
  65. package/dist/knowledge/brain-science/brain-friction-barrier.yaml +22 -0
  66. package/dist/knowledge/brain-science/brain-peak-end.yaml +29 -0
  67. package/dist/knowledge/brain-science/brain-pomodoro.yaml +31 -0
  68. package/dist/knowledge/brain-science/brain-revenge-bedtime.yaml +22 -0
  69. package/dist/knowledge/brain-science/brain-single-variable.yaml +15 -0
  70. package/dist/knowledge/brain-science/brain-tiny-habits.yaml +22 -0
  71. package/dist/knowledge/energy/energy-active-recovery.yaml +22 -0
  72. package/dist/knowledge/energy/energy-deliberate-rest.yaml +22 -0
  73. package/dist/knowledge/energy/energy-exercise-embedded.yaml +22 -0
  74. package/dist/knowledge/energy/energy-fixed-wake-time.yaml +22 -0
  75. package/dist/knowledge/energy/energy-four-energy-radar.yaml +22 -0
  76. package/dist/knowledge/energy/energy-intensity-recovery.yaml +22 -0
  77. package/dist/knowledge/scenario-packs/pack-summer-kid.yaml +12 -0
  78. package/dist/knowledge/src/repo/fs-yaml.js +252 -0
  79. package/dist/knowledge/src/repo/types.js +1 -0
  80. package/dist/knowledge/time/time-batch-windows.yaml +22 -0
  81. package/dist/knowledge/time/time-conscious-abandon.yaml +22 -0
  82. package/dist/knowledge/time/time-deep-work-block.yaml +22 -0
  83. package/dist/knowledge/time/time-finite-commitment.yaml +22 -0
  84. package/dist/knowledge/time/time-forced-whitespace.yaml +22 -0
  85. package/dist/knowledge/time/time-gtd-next-action.yaml +22 -0
  86. package/dist/knowledge/time/time-three-tier-list.yaml +22 -0
  87. package/dist/mcp/cold-start.js +43 -0
  88. package/dist/mcp/contract/v1/rest.js +56 -0
  89. package/dist/mcp/rest-server.js +52 -0
  90. package/dist/mcp/server-core.js +1334 -0
  91. package/dist/mcp/server.js +34 -0
  92. package/dist/mcp/src/ap/index.js +14 -0
  93. package/dist/mcp/src/ap/rewards.js +44 -0
  94. package/dist/mcp/src/categories/index.js +12 -0
  95. package/dist/mcp/src/chat/from-env.js +111 -0
  96. package/dist/mcp/src/chat/history.js +35 -0
  97. package/dist/mcp/src/chat/loop.js +131 -0
  98. package/dist/mcp/src/chat/portrait.js +133 -0
  99. package/dist/mcp/src/chat/system-prompt.js +23 -0
  100. package/dist/mcp/src/chat/tool-names.js +27 -0
  101. package/dist/mcp/src/chat/tool-schema.js +20 -0
  102. package/dist/mcp/src/chat/types.js +1 -0
  103. package/dist/mcp/src/clock/index.js +78 -0
  104. package/dist/mcp/src/coachmod/index.js +135 -0
  105. package/dist/mcp/src/host-adapters/hermes-cron.js +185 -0
  106. package/dist/mcp/src/host-adapters/index.js +61 -0
  107. package/dist/mcp/src/host-adapters/types.js +7 -0
  108. package/dist/mcp/src/identity/equivalent-ids.js +41 -0
  109. package/dist/mcp/src/identity/in-memory.js +100 -0
  110. package/dist/mcp/src/identity/resolve.js +60 -0
  111. package/dist/mcp/src/identity/types.js +4 -0
  112. package/dist/mcp/src/matrix/index.js +15 -0
  113. package/dist/mcp/src/profile/confirm-default.js +48 -0
  114. package/dist/mcp/src/profile/index.js +298 -0
  115. package/dist/mcp/src/profile-confirm.js +28 -0
  116. package/dist/mcp/src/profile-helpers.js +109 -0
  117. package/dist/mcp/src/reminder/hermes-channel.js +40 -0
  118. package/dist/mcp/src/reminder/index.js +70 -0
  119. package/dist/mcp/src/reminder/push-channel.js +10 -0
  120. package/dist/mcp/src/repo/factory.js +34 -0
  121. package/dist/mcp/src/repo/in-memory.js +469 -0
  122. package/dist/mcp/src/repo/sqlite-ap.js +188 -0
  123. package/dist/mcp/src/repo/sqlite-helpers.js +39 -0
  124. package/dist/mcp/src/repo/sqlite-identity.js +129 -0
  125. package/dist/mcp/src/repo/sqlite.js +754 -0
  126. package/dist/mcp/src/repo/types.js +4 -0
  127. package/dist/mcp/src/rest/app.js +121 -0
  128. package/dist/mcp/src/rest/invoke-tool.js +98 -0
  129. package/dist/mcp/src/rest/short-token.js +38 -0
  130. package/dist/mcp/src/rest/tokens.js +55 -0
  131. package/dist/mcp/src/scheduler/index.js +319 -0
  132. package/dist/mcp/src/stats/index.js +29 -0
  133. package/dist/mcp/src/stickiness/report.js +174 -0
  134. package/dist/mcp/src/stickiness/types.js +1 -0
  135. package/dist/mcp/src/task/index.js +412 -0
  136. package/dist/mcp/src/timeblock/index.js +206 -0
  137. package/dist/mcp/src/timeline/index.js +705 -0
  138. package/dist/mcp/src/timer/index.js +234 -0
  139. package/dist/mcp/src/user-id-resolver.js +30 -0
  140. package/dist/mcp/src/weixin/app.js +97 -0
  141. package/dist/mcp/src/weixin/custom-send.js +82 -0
  142. package/dist/mcp/src/weixin/handle-text.js +49 -0
  143. package/dist/mcp/src/weixin/reminder-loop.js +39 -0
  144. package/dist/mcp/src/weixin/signature.js +13 -0
  145. package/dist/mcp/src/weixin/tencent-asr.js +101 -0
  146. package/dist/mcp/src/weixin/xml.js +37 -0
  147. package/dist/mcp/weixin-server.js +158 -0
  148. package/knowledge/_registry/dimensions.yaml +10 -0
  149. package/knowledge/_registry/personas.yaml +7 -0
  150. package/knowledge/_schema/analysis-model.schema.ts +36 -0
  151. package/knowledge/_schema/entry.schema.ts +50 -0
  152. package/knowledge/_schema/persona.schema.ts +30 -0
  153. package/knowledge/_schema/scenario-pack.schema.ts +33 -0
  154. package/knowledge/analysis-models/review-default.yaml +16 -0
  155. package/knowledge/analysis-models/review-summer-plan.yaml +15 -0
  156. package/knowledge/brain-science/brain-deep-warmup.yaml +22 -0
  157. package/knowledge/brain-science/brain-emotion-granularity.yaml +22 -0
  158. package/knowledge/brain-science/brain-env-engineering.yaml +22 -0
  159. package/knowledge/brain-science/brain-extended-mind.yaml +22 -0
  160. package/knowledge/brain-science/brain-friction-barrier.yaml +22 -0
  161. package/knowledge/brain-science/brain-peak-end.yaml +29 -0
  162. package/knowledge/brain-science/brain-pomodoro.yaml +31 -0
  163. package/knowledge/brain-science/brain-revenge-bedtime.yaml +22 -0
  164. package/knowledge/brain-science/brain-single-variable.yaml +15 -0
  165. package/knowledge/brain-science/brain-tiny-habits.yaml +22 -0
  166. package/knowledge/energy/energy-active-recovery.yaml +22 -0
  167. package/knowledge/energy/energy-deliberate-rest.yaml +22 -0
  168. package/knowledge/energy/energy-exercise-embedded.yaml +22 -0
  169. package/knowledge/energy/energy-fixed-wake-time.yaml +22 -0
  170. package/knowledge/energy/energy-four-energy-radar.yaml +22 -0
  171. package/knowledge/energy/energy-intensity-recovery.yaml +22 -0
  172. package/knowledge/scenario-packs/pack-summer-kid.yaml +12 -0
  173. package/knowledge/src/repo/fs-yaml.ts +282 -0
  174. package/knowledge/src/repo/types.ts +24 -0
  175. package/knowledge/tests/fixtures/base/_registry/dimensions.yaml +4 -0
  176. package/knowledge/tests/fixtures/base/time/valid-entry.yaml +24 -0
  177. package/knowledge/tests/fixtures/dup/_registry/dimensions.yaml +4 -0
  178. package/knowledge/tests/fixtures/dup/time/entry-a.yaml +13 -0
  179. package/knowledge/tests/fixtures/dup/time/entry-b.yaml +13 -0
  180. package/knowledge/tests/fixtures/unregistered/_registry/dimensions.yaml +7 -0
  181. package/knowledge/tests/fixtures/unregistered/time/anxiety-entry.yaml +13 -0
  182. package/knowledge/tests/repo/fs-yaml-knowledge-repo.test.ts +333 -0
  183. package/knowledge/time/time-batch-windows.yaml +22 -0
  184. package/knowledge/time/time-conscious-abandon.yaml +22 -0
  185. package/knowledge/time/time-deep-work-block.yaml +22 -0
  186. package/knowledge/time/time-finite-commitment.yaml +22 -0
  187. package/knowledge/time/time-forced-whitespace.yaml +22 -0
  188. package/knowledge/time/time-gtd-next-action.yaml +22 -0
  189. package/knowledge/time/time-three-tier-list.yaml +22 -0
  190. package/knowledge/validate.ts +50 -0
  191. package/package.json +76 -0
  192. package/scripts/postinstall-cold-start.js +45 -0
@@ -0,0 +1,1334 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { createRepos } from "./src/repo/factory.js";
4
+ import { createCategoryTools } from "./src/categories/index.js";
5
+ import { createTimeBlockTools } from "./src/timeblock/index.js";
6
+ import { createStatsTools } from "./src/stats/index.js";
7
+ import { createMatrixTools } from "./src/matrix/index.js";
8
+ import { createTimelineTools } from "./src/timeline/index.js";
9
+ import { createTimerTools } from "./src/timer/index.js";
10
+ import { createProfileTools } from "./src/profile/index.js";
11
+ import { createProfileConfirmTools } from "./src/profile/confirm-default.js";
12
+ import { createReminderTools } from "./src/reminder/index.js";
13
+ import { createSchedulerTools } from "./src/scheduler/index.js";
14
+ import { createAdaptersWithDb, selectAdapter } from "./src/host-adapters/index.js";
15
+ import { createTaskTools } from "./src/task/index.js";
16
+ import { createCoachModTools } from "./src/coachmod/index.js";
17
+ import { createApTools } from "./src/ap/index.js";
18
+ import { timerMinMinutes } from "./src/ap/rewards.js";
19
+ import { FsYamlKnowledgeRepo } from "../knowledge/src/repo/fs-yaml.js";
20
+ import { nowLocal, formatTime, parseTime } from "./src/clock/index.js";
21
+ import { buildServerInstructions } from "./cold-start.js";
22
+ import { resolveUserId } from "./src/identity/resolve.js";
23
+ import { profileIdForWrite } from "./src/identity/equivalent-ids.js";
24
+ import { resolveDefaultUserId, logResolvedDefaultUserId, } from "./src/user-id-resolver.js";
25
+ /** Wrap any tool result as MCP text content (JSON). Void results become {ok:true}. */
26
+ function asContent(result) {
27
+ return {
28
+ content: [
29
+ {
30
+ type: "text",
31
+ text: JSON.stringify(result !== undefined ? result : { ok: true }),
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ /** Inject default user_id; map alias → existing user_profile.id for FK writes (地基②). */
37
+ export function makeUserIdInjector(defaultUserId, db) {
38
+ return function inject(input) {
39
+ const raw = input.user_id || defaultUserId;
40
+ if (!raw) {
41
+ return {
42
+ error: "user_id is required (no default configured: set LIBERO_DEFAULT_USER_ID or have at least one non-system user_profile)",
43
+ };
44
+ }
45
+ const user_id = profileIdForWrite(db, raw) ?? raw;
46
+ return { ...input, user_id };
47
+ };
48
+ }
49
+ export function errorContent(msg, code) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text",
54
+ text: JSON.stringify(code ? { error: msg, code } : { error: msg }),
55
+ },
56
+ ],
57
+ isError: true,
58
+ };
59
+ }
60
+ /**
61
+ * Build the full tool registry (48 tools). Each entry is a data-fied version of
62
+ * the pre-refactor inline registration calls — handlers are byte-for-byte copies
63
+ * preserving declaration order for diagnostic readability.
64
+ */
65
+ function buildToolRegistry(ctx) {
66
+ // INT-ap-rewards:实绩事件 → 换算引擎入账。声明折叠(AGENTS §4.5):
67
+ // 奖励失败不阻断业务动作本身,stderr 告警;事件未消费,重放同 event_id 可补。
68
+ // 返回 {nth, amount, bonus?}(null=重放)并入工具响应,让脑层能播报 AP 变动。
69
+ const rewardHook = async (userId, action_type, eventId) => {
70
+ if (!userId)
71
+ return null;
72
+ try {
73
+ return await ctx.repos.apRepo.recordRewardEvent({ user_id: userId, action_type, event_id: eventId });
74
+ }
75
+ catch (e) {
76
+ console.error(`[rewards] ${action_type} hook failed for ${userId}:`, e);
77
+ return null;
78
+ }
79
+ };
80
+ const { categories, tb, stats, matrix, timeline, timer, profile, profileConfirm, reminder, scheduler, task, coachmod, ap, injector, } = ctx;
81
+ // identityRepo is referenced directly inside handlers below (closure scope),
82
+ // matching the pre-refactor behavior.
83
+ const identityRepo = ctx.repos.identityRepo;
84
+ const tools = [];
85
+ // ============================================================
86
+ // categories (2 tools)
87
+ // ============================================================
88
+ tools.push({
89
+ name: "categories.getAll",
90
+ description: "获取全部时间分类(15 类)",
91
+ inputSchema: {},
92
+ handler: async () => asContent(await categories.getAllCategories()),
93
+ });
94
+ tools.push({
95
+ name: "categories.getById",
96
+ description: "按 id 获取单个分类",
97
+ inputSchema: { id: z.string().min(1) },
98
+ handler: async ({ id }) => asContent(await categories.getCategoryById(id)),
99
+ });
100
+ // ============================================================
101
+ // timer (9 tools — positional userId args, R4 pattern)
102
+ // ============================================================
103
+ tools.push({
104
+ name: "timer.start",
105
+ description: "开始一个计时会话",
106
+ inputSchema: {
107
+ user_id: z.string().optional(),
108
+ target_user_id: z.string().min(1).optional(),
109
+ event: z.string().min(1),
110
+ categoryId: z.string().optional(),
111
+ task_id: z.string().optional(),
112
+ },
113
+ needsUserIdInjection: true,
114
+ handler: async (rawInput) => {
115
+ const r = await injector(rawInput);
116
+ if ("error" in r)
117
+ return errorContent(r.error);
118
+ return asContent(await timer.start(r.user_id, r.event, r.categoryId, r.task_id, r.target_user_id));
119
+ },
120
+ });
121
+ tools.push({
122
+ name: "timer.pause",
123
+ description: "暂停当前计时",
124
+ inputSchema: { user_id: z.string().optional() },
125
+ needsUserIdInjection: true,
126
+ handler: async (rawInput) => {
127
+ const r = await injector(rawInput);
128
+ if ("error" in r)
129
+ return errorContent(r.error);
130
+ return asContent(await timer.pause(r.user_id));
131
+ },
132
+ });
133
+ tools.push({
134
+ name: "timer.resume",
135
+ description: "恢复已暂停的计时",
136
+ inputSchema: { user_id: z.string().optional() },
137
+ needsUserIdInjection: true,
138
+ handler: async (rawInput) => {
139
+ const r = await injector(rawInput);
140
+ if ("error" in r)
141
+ return errorContent(r.error);
142
+ return asContent(await timer.resume(r.user_id));
143
+ },
144
+ });
145
+ tools.push({
146
+ name: "timer.stop",
147
+ description: "停止计时并返回记录(传 rating 时自动落盘 timeblock)",
148
+ inputSchema: {
149
+ user_id: z.string().optional(),
150
+ rating: z.number().min(1).max(5).optional(),
151
+ rating_reason: z.string().optional(),
152
+ },
153
+ needsUserIdInjection: true,
154
+ handler: async (rawInput) => {
155
+ const r = await injector(rawInput);
156
+ if ("error" in r)
157
+ return errorContent(r.error);
158
+ const rec = await timer.stop(r.user_id, {
159
+ rating: r.rating,
160
+ rating_reason: r.rating_reason,
161
+ });
162
+ // INT-ap-rewards:有效时长 ≥ 门槛才算计时实绩(spec §2)
163
+ if (rec.durationMin >= timerMinMinutes()) {
164
+ const ap = await rewardHook(r.user_id, "timer", `timer:${r.user_id}:${rec.startedAt}`);
165
+ if (ap)
166
+ return asContent({ ...rec, ap });
167
+ }
168
+ return asContent(rec);
169
+ },
170
+ });
171
+ tools.push({
172
+ name: "timer.status",
173
+ description: "查询某用户当前计时状态",
174
+ inputSchema: { user_id: z.string().optional() },
175
+ needsUserIdInjection: true,
176
+ handler: async (rawInput) => {
177
+ const r = await injector(rawInput);
178
+ if ("error" in r)
179
+ return errorContent(r.error);
180
+ return asContent(await timer.status(r.user_id));
181
+ },
182
+ });
183
+ tools.push({
184
+ name: "timer.elapsed",
185
+ description: "查询当前计时已过毫秒数",
186
+ inputSchema: { user_id: z.string().optional() },
187
+ needsUserIdInjection: true,
188
+ handler: async (rawInput) => {
189
+ const r = await injector(rawInput);
190
+ if ("error" in r)
191
+ return errorContent(r.error);
192
+ return asContent(await timer.elapsed(r.user_id));
193
+ },
194
+ });
195
+ tools.push({
196
+ name: "timer.statusAll",
197
+ description: "查询所有用户的当前计时状态(调试/兜底用)",
198
+ inputSchema: {},
199
+ handler: async () => asContent(await timer.statusAll()),
200
+ });
201
+ // ── S26: pomodoro v1 tools ──
202
+ tools.push({
203
+ name: "timer.startFocus",
204
+ description: "开始一个专注计时(番茄钟),带目标时长",
205
+ inputSchema: {
206
+ user_id: z.string().optional(),
207
+ event: z.string().min(1),
208
+ focusMin: z.number().positive().optional(),
209
+ categoryId: z.string().optional(),
210
+ task_id: z.string().optional(),
211
+ },
212
+ needsUserIdInjection: true,
213
+ handler: async (rawInput) => {
214
+ const r = await injector(rawInput);
215
+ if ("error" in r)
216
+ return errorContent(r.error);
217
+ return asContent(await timer.startFocus(r.user_id, r.event, r.focusMin, r.categoryId, r.task_id));
218
+ },
219
+ });
220
+ tools.push({
221
+ name: "timer.remaining",
222
+ description: "查询专注计时剩余时间(负数=超时)",
223
+ inputSchema: {
224
+ user_id: z.string().optional(),
225
+ },
226
+ needsUserIdInjection: true,
227
+ handler: async (rawInput) => {
228
+ const r = await injector(rawInput);
229
+ if ("error" in r)
230
+ return errorContent(r.error);
231
+ return asContent(await timer.remaining(r.user_id));
232
+ },
233
+ });
234
+ // ============================================================
235
+ // timeblock (6 tools — single-object args)
236
+ // ============================================================
237
+ tools.push({
238
+ name: "timeblock.create",
239
+ description: "创建一条时间记录",
240
+ inputSchema: {
241
+ user_id: z.string().optional(),
242
+ target_user_id: z.string().min(1).optional(),
243
+ title: z.string().min(1),
244
+ category_id: z.string().optional(),
245
+ start_time: z.string(),
246
+ end_time: z.string(),
247
+ duration_minutes: z.number().optional(),
248
+ rating: z.number().min(1).max(5).optional(),
249
+ rating_reason: z.string().optional(),
250
+ notes: z.string().optional(),
251
+ },
252
+ needsUserIdInjection: true,
253
+ handler: async (rawInput) => {
254
+ const r = await injector(rawInput);
255
+ if ("error" in r)
256
+ return errorContent(r.error);
257
+ const created = await tb.create(r);
258
+ // INT-ap-rewards:repo 层 create 默认 status='completed'(补录语义),
259
+ // 创建即完成也要计实绩(2026-09-02 事故:补录全漏挂钩)。
260
+ if ("user_id" in created && created.status === "completed") {
261
+ const ap = await rewardHook(created.user_id, "schedule", `tb:${created.id}`);
262
+ if (ap)
263
+ return asContent({ ...created, ap });
264
+ }
265
+ return asContent(created);
266
+ },
267
+ });
268
+ tools.push({
269
+ name: "timeblock.getById",
270
+ description: "按 id 获取单条时间记录。" +
271
+ "S41 多用户:默认按当前 user_id 过滤,跨用户查不到返回 null。",
272
+ inputSchema: {
273
+ id: z.string().min(1),
274
+ user_id: z.string().optional(),
275
+ },
276
+ needsUserIdInjection: true,
277
+ handler: async (rawInput) => {
278
+ const r = await injector(rawInput);
279
+ if ("error" in r)
280
+ return errorContent(r.error);
281
+ return asContent(await tb.getById(rawInput.id, r.user_id));
282
+ },
283
+ });
284
+ tools.push({
285
+ name: "timeblock.update",
286
+ description: "更新一条时间记录(标题/时间/评分/status 等)。" +
287
+ "MV-COACH-3 Slice 4 用户主权状态机:" +
288
+ "① 开工 cron 触发到点 → status='unverified'(等用户核实,**不计入实绩**);" +
289
+ "② 用户确认执行(主动开始计时 / 回报「执行了」/ 追问后答执行了)→ status='completed'(**唯一计入实绩**);" +
290
+ "③ 用户答「没执行」→ 不改 status,直接 timeblock.remove 软删(留 deleted_at 可复盘,不进实绩统计);" +
291
+ "④ 用户一直不答 → 保持 unverified,永远等(不自动判死)。" +
292
+ "⚠️ LLM 不能擅自把 planned/unverified 升级到 completed——只有用户确认才能升。",
293
+ inputSchema: {
294
+ id: z.string().min(1),
295
+ target_user_id: z.string().min(1).optional(),
296
+ title: z.string().optional(),
297
+ category_id: z.string().optional(),
298
+ start_time: z.string().optional(),
299
+ end_time: z.string().optional(),
300
+ duration_minutes: z.number().optional(),
301
+ rating: z.number().min(1).max(5).optional(),
302
+ rating_reason: z.string().optional(),
303
+ notes: z.string().optional(),
304
+ status: z.enum(["planned", "unverified", "completed"]).optional(),
305
+ },
306
+ handler: async ({ id, ...rest }) => {
307
+ const updated = await tb.update(id, rest);
308
+ // INT-ap-rewards:用户确认执行(status=completed)= 唯一计入实绩的日程信号。
309
+ // NeedsConfirm 返回 = 块未落 completed,不挂钩;timer.stop 内部直改 repo
310
+ // 不走本工具 → 计时/日程曲线天然去重(spec §2)。
311
+ if (rest.status === "completed" && "user_id" in updated) {
312
+ const ap = await rewardHook(updated.user_id, "schedule", `tb:${id}`);
313
+ if (ap)
314
+ return asContent({ ...updated, ap });
315
+ }
316
+ return asContent(updated);
317
+ },
318
+ });
319
+ tools.push({
320
+ name: "timeblock.remove",
321
+ description: "软删除一条时间记录",
322
+ inputSchema: {
323
+ id: z.string().min(1),
324
+ target_user_id: z.string().min(1).optional(),
325
+ },
326
+ handler: async ({ id, target_user_id }) => asContent(await tb.remove(id, { target_user_id })),
327
+ });
328
+ tools.push({
329
+ name: "timeblock.restore",
330
+ description: "恢复一条已软删除的时间记录",
331
+ inputSchema: { id: z.string().min(1) },
332
+ handler: async ({ id }) => asContent(await tb.restore(id)),
333
+ });
334
+ tools.push({
335
+ name: "timeblock.list",
336
+ description: "按条件查询时间记录列表",
337
+ inputSchema: {
338
+ user_id: z.string().optional(),
339
+ target_user_id: z.string().min(1).optional(),
340
+ date: z.string().optional(),
341
+ from: z.string().optional(),
342
+ to: z.string().optional(),
343
+ category_id: z.string().optional(),
344
+ include_deleted: z.boolean().optional(),
345
+ limit: z.number().optional(),
346
+ offset: z.number().optional(),
347
+ },
348
+ needsUserIdInjection: true,
349
+ handler: async (rawInput) => {
350
+ const r = await injector(rawInput);
351
+ if ("error" in r)
352
+ return errorContent(r.error);
353
+ return asContent(await tb.list(r));
354
+ },
355
+ });
356
+ tools.push({
357
+ name: "timeblock.sweepStale",
358
+ description: "S43 过期扫描:列出某用户当天过期未确认的 timeblock,分两组返回。" +
359
+ "to_unverified:end_time 已过 30min 仍 planned(该升级 unverified)。" +
360
+ "to_ask:end_time 已过 60min 仍 unverified/planned,**且未被问过**(S48:每条只问一次,asked_at IS NULL)。" +
361
+ "⚠️ 纯查询,不改 db——caller(cron-session LLM)需对 to_unverified 显式调 timeblock.update(status=unverified),对 to_ask 调 cronjob create 发追问,**发完后必须调 timeblock.markAsked(ids) 幂等标记**(否则下次扫描还会再问)。" +
362
+ "⚠️ 仅 cron-session 调用;user_id 必填。",
363
+ inputSchema: {
364
+ user_id: z.string().min(1),
365
+ now: z.string().optional(),
366
+ overdue_min_to_unverified: z.number().optional(),
367
+ overdue_min_to_ask: z.number().optional(),
368
+ },
369
+ handler: async (rawInput) => {
370
+ try {
371
+ const result = await tb.sweepStale({
372
+ user_id: rawInput.user_id,
373
+ now: rawInput.now,
374
+ overdue_min_to_unverified: rawInput.overdue_min_to_unverified,
375
+ overdue_min_to_ask: rawInput.overdue_min_to_ask,
376
+ });
377
+ return asContent(result);
378
+ }
379
+ catch (e) {
380
+ return errorContent(e.message);
381
+ }
382
+ },
383
+ });
384
+ tools.push({
385
+ name: "timeblock.markAsked",
386
+ description: "S48 幂等标记:把给定 timeblock 的 asked_at 设为当前时间。" +
387
+ "libero-cron-scan 发完追问后必须调用,否则同一条 unverified 会被下次扫描重复追问。" +
388
+ "asked_at 永久保留作审计;用户回报(completed / 软删)后这条不再被扫,无需复位。" +
389
+ "返回 { marked: N }(受影响行数)。⚠️ 仅 cron-session 调用。",
390
+ inputSchema: {
391
+ ids: z.array(z.string().min(1)).min(1),
392
+ },
393
+ handler: async (rawInput) => {
394
+ try {
395
+ const result = await tb.markAsked(rawInput.ids);
396
+ return asContent(result);
397
+ }
398
+ catch (e) {
399
+ return errorContent(e.message);
400
+ }
401
+ },
402
+ });
403
+ // ============================================================
404
+ // coachmod (2 tools — COACH-MOD §4,情境化复盘模块装载)
405
+ // ============================================================
406
+ tools.push({
407
+ name: "coachmod.suggest",
408
+ description: "COACH-MOD §4.1 情境化复盘模块装载——粗筛候选。" +
409
+ "按 context_hint + 用户 profile.focus_areas 确定性匹配 scenario-packs,始终附默认基础分析模型。" +
410
+ "脑(coach skill)拿到候选后**语义精选**并提议给用户(必经同意门,不静默装)。" +
411
+ "无候选 → candidates:[] + note(不静默空当正常)。user_id / context_hint 都可选。",
412
+ inputSchema: {
413
+ user_id: z.string().optional(),
414
+ context_hint: z.string().optional(),
415
+ },
416
+ handler: async (rawInput) => {
417
+ try {
418
+ const result = await coachmod.suggest({
419
+ user_id: rawInput.user_id,
420
+ context_hint: rawInput.context_hint,
421
+ });
422
+ return asContent(result);
423
+ }
424
+ catch (e) {
425
+ return errorContent(e.message);
426
+ }
427
+ },
428
+ });
429
+ tools.push({
430
+ name: "coachmod.load",
431
+ description: "COACH-MOD §4.2 情境化复盘模块装载——渐进披露取内容。" +
432
+ "只返回请求的 ids(脑精选后传);scenario-pack id 展开其 recommends(画像+模型+subskill_prompt)。" +
433
+ "不存在的 id 报错(不静默返回空 loaded)。ids 必填。",
434
+ inputSchema: {
435
+ ids: z.array(z.string().min(1)).min(1),
436
+ },
437
+ handler: async (rawInput) => {
438
+ try {
439
+ const result = await coachmod.load({ ids: rawInput.ids });
440
+ return asContent(result);
441
+ }
442
+ catch (e) {
443
+ return errorContent(e.message);
444
+ }
445
+ },
446
+ });
447
+ // ============================================================
448
+ // stats (2 tools — single-object filters)
449
+ // ============================================================
450
+ tools.push({
451
+ name: "stats.summarize",
452
+ description: "按分类汇总时间统计(占比/记录数)",
453
+ inputSchema: {
454
+ user_id: z.string().optional(),
455
+ target_user_id: z.string().min(1).optional(),
456
+ from: z.string().optional(),
457
+ to: z.string().optional(),
458
+ category_id: z.string().optional(),
459
+ },
460
+ needsUserIdInjection: true,
461
+ handler: async (rawInput) => {
462
+ const r = await injector(rawInput);
463
+ if ("error" in r)
464
+ return errorContent(r.error);
465
+ return asContent(await stats.summarize(r));
466
+ },
467
+ });
468
+ tools.push({
469
+ name: "stats.dailyBreakdown",
470
+ description: "按天×分类拆分时间明细",
471
+ inputSchema: {
472
+ user_id: z.string().optional(),
473
+ target_user_id: z.string().min(1).optional(),
474
+ from: z.string().optional(),
475
+ to: z.string().optional(),
476
+ category_id: z.string().optional(),
477
+ },
478
+ needsUserIdInjection: true,
479
+ handler: async (rawInput) => {
480
+ const r = await injector(rawInput);
481
+ if ("error" in r)
482
+ return errorContent(r.error);
483
+ return asContent(await stats.dailyBreakdown(r));
484
+ },
485
+ });
486
+ // ============================================================
487
+ // matrix (1 tool — single-object filters, reuses StatsRepo)
488
+ // ============================================================
489
+ tools.push({
490
+ name: "matrix.renderMatrix",
491
+ description: "时间×评分四象限矩阵(能耗黑洞/保持区/亮点区/可放弃)。" +
492
+ "⚠️ 用户说「时间矩阵 / 时间表 / 看日程 / 今天时间表」时**不要**调本工具——那些是指 emoji 时间块视图,请改调 schedule.timeline。" +
493
+ "本工具仅用于:阶段复盘(这周/最近 N 天)+ 已评分记录的象限分析。",
494
+ inputSchema: {
495
+ user_id: z.string().optional(),
496
+ from: z.string().optional(),
497
+ to: z.string().optional(),
498
+ },
499
+ needsUserIdInjection: true,
500
+ handler: async (rawInput) => {
501
+ const r = await injector(rawInput);
502
+ if ("error" in r)
503
+ return errorContent(r.error);
504
+ return asContent(await matrix.renderMatrix(r));
505
+ },
506
+ });
507
+ // ============================================================
508
+ // schedule.timeline (emoji matrix + detail table)
509
+ // ============================================================
510
+ tools.push({
511
+ name: "schedule.timeline",
512
+ description: "看日程/复盘的首选视图:emoji 矩阵+细项表格,含计划/计时中/未回应/实绩全状态。" +
513
+ "用户说「看今天/看日程/在干啥/复盘/接下来 N 小时/时间矩阵/时间表/今天时间表」时调本工具,并直接 echo rendered_text(不重写、不改格式)。" +
514
+ "不要调 scheduler.listSchedules(那是内部排程查询,不是用户视图)。",
515
+ inputSchema: {
516
+ user_id: z.string().optional(),
517
+ target_user_id: z.string().min(1).optional(),
518
+ from: z.string().optional(),
519
+ to: z.string().optional(),
520
+ format: z.enum(["emoji", "ascii"]).optional(),
521
+ },
522
+ needsUserIdInjection: true,
523
+ handler: async (rawInput) => {
524
+ const r = await injector(rawInput);
525
+ if ("error" in r)
526
+ return errorContent(r.error);
527
+ return asContent(await timeline.renderTimeline(r));
528
+ },
529
+ });
530
+ // ============================================================
531
+ // clock (3 tools — pure functions, no repo)
532
+ // ============================================================
533
+ tools.push({
534
+ name: "clock.now",
535
+ description: "返回当前 ISO 时间字符串(可选时区偏移)",
536
+ inputSchema: {
537
+ offsetMinutes: z.number().optional(),
538
+ },
539
+ handler: async ({ offsetMinutes }) => asContent(offsetMinutes !== undefined ? nowLocal(offsetMinutes) : nowLocal()),
540
+ });
541
+ tools.push({
542
+ name: "clock.formatTime",
543
+ description: "按指定格式格式化 ISO 时间",
544
+ inputSchema: {
545
+ iso: z.string().min(1),
546
+ format: z.string().min(1),
547
+ },
548
+ handler: async ({ iso, format }) => asContent(formatTime(iso, format)),
549
+ });
550
+ tools.push({
551
+ name: "clock.parseTime",
552
+ description: "解析 ISO 时间字符串为结构化字段",
553
+ inputSchema: { iso: z.string().min(1) },
554
+ handler: async ({ iso }) => asContent(parseTime(iso)),
555
+ });
556
+ // ============================================================
557
+ // profile (8 tools — get / upsert / markInactivityNudge / claimNudge /
558
+ // confirm_default / bindAlias / listMyChannels / createUser)
559
+ // ============================================================
560
+ tools.push({
561
+ name: "profile.get",
562
+ description: "获取用户 profile(含结构化约定)",
563
+ inputSchema: { user_id: z.string().optional() },
564
+ needsUserIdInjection: true,
565
+ handler: async (rawInput) => {
566
+ const r = await injector(rawInput);
567
+ if ("error" in r)
568
+ return errorContent(r.error);
569
+ return asContent(await profile.get(r.user_id));
570
+ },
571
+ });
572
+ tools.push({
573
+ name: "profile.upsert",
574
+ description: "创建或更新用户 profile(display_name / expectations / settings)。首次确认请用 profile.confirm_default。",
575
+ inputSchema: {
576
+ user_id: z.string().optional(),
577
+ display_name: z.string().optional(),
578
+ expectations: z.string().optional(),
579
+ settings: z
580
+ .object({})
581
+ .passthrough()
582
+ .optional(),
583
+ },
584
+ needsUserIdInjection: true,
585
+ handler: async (rawInput) => {
586
+ const r = await injector(rawInput);
587
+ if ("error" in r)
588
+ return errorContent(r.error);
589
+ return asContent(await profile.upsert(r));
590
+ },
591
+ });
592
+ tools.push({
593
+ name: "profile.markInactivityNudge",
594
+ description: "MV-COACH-3 slice 2(防重复失效专用):触发「5 天没记录轻触」问候后必调。" +
595
+ "专门写 settings.last_inactivity_nudge_at = now。" +
596
+ "⚠️ 不要用 profile.upsert 写这个字段——upsert 是 whole-replace 会清掉其它 settings key。" +
597
+ "本工具 deep-merge,只更新 last_inactivity_nudge_at 一个 key。",
598
+ inputSchema: {
599
+ user_id: z.string().optional(),
600
+ },
601
+ needsUserIdInjection: true,
602
+ handler: async (rawInput) => {
603
+ const r = await injector(rawInput);
604
+ if ("error" in r)
605
+ return errorContent(r.error);
606
+ return asContent(await profile.markInactivityNudge({ user_id: r.user_id }));
607
+ },
608
+ });
609
+ tools.push({
610
+ name: "profile.claimNudge",
611
+ description: "S40 slice 2(多 cron 并发 atomic 抢占):nag fan-out 多渠道时用。" +
612
+ "window_minutes 内只有第一个调用赢(won=true),其它返回 won=false。" +
613
+ "**调用方必须看返回的 won 字段**:" +
614
+ "won=true → 输出问候;won=false → 输出 [SILENT](已被其它 cron 抢先)。" +
615
+ "默认 window=5 分钟,覆盖 cron 启动间隔差。",
616
+ inputSchema: {
617
+ user_id: z.string().optional(),
618
+ window_minutes: z.number().optional(),
619
+ },
620
+ needsUserIdInjection: true,
621
+ handler: async (rawInput) => {
622
+ const r = await injector(rawInput);
623
+ if ("error" in r)
624
+ return errorContent(r.error);
625
+ return asContent(await profile.claimNudge({
626
+ user_id: r.user_id,
627
+ window_minutes: typeof rawInput.window_minutes === "number"
628
+ ? rawInput.window_minutes
629
+ : undefined,
630
+ }));
631
+ },
632
+ });
633
+ tools.push({
634
+ name: "profile.confirm_default",
635
+ description: "首次写入前确认默认用户:写入 user_profile,设置 display_name 与 default_confirmed_at。display_name 重名则拒绝。",
636
+ inputSchema: {
637
+ platform_user_id: z.string().min(1),
638
+ display_name: z.string().min(1),
639
+ },
640
+ handler: async (input) => asContent(await profileConfirm.confirmDefault(input)),
641
+ });
642
+ // ============================================================
643
+ // S40: cross-platform identity (bindAlias / listMyChannels)
644
+ // ============================================================
645
+ tools.push({
646
+ name: "profile.bindAlias",
647
+ description: "S40 跨平台身份层:把一个平台 chat_id 绑定到已有 libero 用户身份。" +
648
+ "场景:用户在新设备(如飞书)首次开口,主会话问『要和已有账号(如企微)合并吗』,得到肯定后调这个。" +
649
+ "libero_user_id 为空 = 首次绑定,自动建一个 libero_user_id。" +
650
+ "平台 chat_id 跟 libero_user_id 一对多关系(1 个真人 ↔ N 平台)。",
651
+ inputSchema: {
652
+ libero_user_id: z.string().optional(),
653
+ platform: z.enum(["wecom", "feishu", "weixin", "telegram", "discord", "signal", "unknown"]),
654
+ platform_user_id: z.string().min(1),
655
+ chat_id: z.string().optional(),
656
+ display_name: z.string().optional(),
657
+ },
658
+ handler: async (input) => {
659
+ const crypto = await import("node:crypto");
660
+ let lid = input.libero_user_id;
661
+ if (!lid) {
662
+ lid = `u_libero_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
663
+ await identityRepo.createIdentity({
664
+ libero_user_id: lid,
665
+ display_name: input.display_name ?? input.platform_user_id.slice(0, 16),
666
+ });
667
+ }
668
+ else {
669
+ const existing = await identityRepo.getIdentity(lid);
670
+ if (!existing) {
671
+ return errorContent(`libero_user_id not found: ${lid}`);
672
+ }
673
+ }
674
+ try {
675
+ await identityRepo.bindAlias({
676
+ libero_user_id: lid,
677
+ platform: input.platform,
678
+ platform_user_id: input.platform_user_id,
679
+ chat_id: input.chat_id,
680
+ });
681
+ }
682
+ catch (e) {
683
+ return errorContent(e.message);
684
+ }
685
+ const aliases = await identityRepo.listAliases(lid);
686
+ return asContent({ libero_user_id: lid, bound_aliases: aliases });
687
+ },
688
+ });
689
+ tools.push({
690
+ name: "profile.listMyChannels",
691
+ description: "S40 跨平台身份层:列出当前用户绑定的所有平台渠道。" +
692
+ "用途:nag fan-out(按渠道建独立 cron)、跨平台数据查询、身份确认。",
693
+ inputSchema: {
694
+ user_id: z.string().optional(),
695
+ },
696
+ needsUserIdInjection: true,
697
+ handler: async (rawInput) => {
698
+ const r = await injector(rawInput);
699
+ if ("error" in r)
700
+ return errorContent(r.error);
701
+ const lid = await resolveUserId(identityRepo, r.user_id);
702
+ const aliases = await identityRepo.listAliases(lid);
703
+ return asContent({
704
+ libero_user_id: lid,
705
+ channels: aliases.map((a) => ({
706
+ platform: a.platform,
707
+ platform_user_id: a.platform_user_id,
708
+ chat_id: a.chat_id,
709
+ bound_at: a.bound_at,
710
+ })),
711
+ });
712
+ },
713
+ });
714
+ tools.push({
715
+ name: "profile.createUser",
716
+ description: "S41 多用户建档:建立新 libero 用户(父母代管模式)。" +
717
+ "用户体感:「帮我建一个新用户」「我要给我儿子建档」时调用。" +
718
+ "返回 libero_user_id,建档后该 id 立即可用于 target_user_id。" +
719
+ "同一家长下同名孩子会复用已有档案并自动 setManager。" +
720
+ "可选绑定平台 alias(如果建档时已知 chat_id)。" +
721
+ "display_name 必填;platform 和 platform_user_id 必须同时给或同时不给。",
722
+ inputSchema: {
723
+ user_id: z.string().optional(),
724
+ display_name: z.string().min(1),
725
+ focus_areas: z.array(z.string()).optional(),
726
+ goals: z.string().optional(),
727
+ platform: z
728
+ .enum(["wecom", "feishu", "weixin", "telegram", "discord", "signal", "unknown"])
729
+ .optional(),
730
+ platform_user_id: z.string().optional(),
731
+ chat_id: z.string().optional(),
732
+ },
733
+ needsUserIdInjection: true,
734
+ handler: async (rawInput) => {
735
+ try {
736
+ const r = await injector(rawInput);
737
+ if ("error" in r)
738
+ return errorContent(r.error);
739
+ const result = await profile.createUser({
740
+ user_id: r.user_id,
741
+ display_name: r.display_name,
742
+ focus_areas: r.focus_areas,
743
+ goals: r.goals,
744
+ platform: r.platform,
745
+ platform_user_id: r.platform_user_id,
746
+ chat_id: r.chat_id,
747
+ });
748
+ return asContent(result);
749
+ }
750
+ catch (e) {
751
+ return errorContent(e.message);
752
+ }
753
+ },
754
+ });
755
+ tools.push({
756
+ name: "profile.listAllUsers",
757
+ description: "S43 多用户扫描:列出所有 libero 用户(libero_user_id + display_name)。" +
758
+ "用途:cron 扫描(每 30min)跑所有用户的状态升级和主动询问,不靠 LLM 从上下文猜。" +
759
+ "⚠️ 仅 cron-session 调用,普通对话不要用。",
760
+ inputSchema: {},
761
+ handler: async () => {
762
+ try {
763
+ const result = await profile.listAllUsers();
764
+ return asContent({ users: result });
765
+ }
766
+ catch (e) {
767
+ return errorContent(e.message);
768
+ }
769
+ },
770
+ });
771
+ tools.push({
772
+ name: "profile.getManager",
773
+ description: "S44 代管转发:查某用户的代管人(managed_by + 代管人 display_name)。" +
774
+ "用途:扫描 cron 发现用户没自己渠道时,查 managed_by 把追问发给代管人。" +
775
+ "返回 managed_by=null 表示该用户没代管人(普通独立用户)。",
776
+ inputSchema: {
777
+ user_id: z.string().min(1),
778
+ },
779
+ handler: async (rawInput) => {
780
+ try {
781
+ const result = await profile.getManager({ user_id: rawInput.user_id });
782
+ return asContent(result);
783
+ }
784
+ catch (e) {
785
+ return errorContent(e.message);
786
+ }
787
+ },
788
+ });
789
+ tools.push({
790
+ name: "profile.setManager",
791
+ description: "S44 代管转发:设置或清除某用户的代管人。" +
792
+ "用法:profile.setManager({ user_id: '<被代管人>', managed_by: '<代管人 libero_user_id>' })。" +
793
+ "managed_by=null 清除代管关系;非 null 必须指向存在的 libero_user_id(FK 校验)。" +
794
+ "典型场景:父母给孩子建账户后,profile.setManager({ user_id: '<孩子>', managed_by: '<父母>' })。",
795
+ inputSchema: {
796
+ user_id: z.string().min(1),
797
+ managed_by: z.string().nullable(),
798
+ },
799
+ handler: async (rawInput) => {
800
+ try {
801
+ const result = await profile.setManager({
802
+ user_id: rawInput.user_id,
803
+ managed_by: rawInput.managed_by,
804
+ });
805
+ return asContent(result);
806
+ }
807
+ catch (e) {
808
+ return errorContent(e.message);
809
+ }
810
+ },
811
+ });
812
+ // ============================================================
813
+ // reminder (6 tools — S11)
814
+ // ============================================================
815
+ tools.push({
816
+ name: "reminder.create",
817
+ description: "⚠️ 仅写 DB,不主动推送(libero-mcp 无 background sender)。" +
818
+ "在 hermes 等有 cronjob 工具的平台上,用户要'X 时间提醒我 Y'请调宿主 cronjob(action=create, deliver=origin),不要用本工具。" +
819
+ "本工具仅用于:① 无 cron 能力的平台兜底;② 查询场景配合 getDue/getMissed。",
820
+ inputSchema: {
821
+ user_id: z.string().optional(),
822
+ target_user_id: z.string().min(1).optional(),
823
+ content: z.string().min(1),
824
+ remind_at: z.string().min(1),
825
+ channel: z.string().optional(),
826
+ timeblock_id: z.string().optional(),
827
+ },
828
+ needsUserIdInjection: true,
829
+ handler: async (rawInput) => {
830
+ const r = await injector(rawInput);
831
+ if ("error" in r)
832
+ return errorContent(r.error);
833
+ return asContent(await reminder.create(r));
834
+ },
835
+ });
836
+ tools.push({
837
+ name: "reminder.getDue",
838
+ description: "获取到期且待发送的提醒列表",
839
+ inputSchema: {
840
+ user_id: z.string().optional(),
841
+ until: z.string().optional(),
842
+ },
843
+ handler: async (input) => asContent(await reminder.getDueReminders(input)),
844
+ });
845
+ tools.push({
846
+ name: "reminder.getNextDue",
847
+ description: "获取下一条待发送提醒的时间",
848
+ inputSchema: {
849
+ user_id: z.string().optional(),
850
+ },
851
+ handler: async (input) => asContent(await reminder.getNextDue(input)),
852
+ });
853
+ tools.push({
854
+ name: "reminder.getMissed",
855
+ description: "获取过期但未送达的提醒(被动补发用)",
856
+ inputSchema: {
857
+ user_id: z.string().optional(),
858
+ },
859
+ needsUserIdInjection: true,
860
+ handler: async (rawInput) => {
861
+ const r = await injector(rawInput);
862
+ if ("error" in r)
863
+ return errorContent(r.error);
864
+ return asContent(await reminder.getMissedReminders({ user_id: r.user_id }));
865
+ },
866
+ });
867
+ tools.push({
868
+ name: "reminder.markSent",
869
+ description: "标记提醒为已发送(幂等)",
870
+ inputSchema: { id: z.string().min(1) },
871
+ handler: async ({ id }) => asContent(await reminder.markSent(id)),
872
+ });
873
+ tools.push({
874
+ name: "reminder.cancel",
875
+ description: "取消一条提醒(幂等)",
876
+ inputSchema: { id: z.string().min(1) },
877
+ handler: async ({ id }) => asContent(await reminder.cancelReminder(id)),
878
+ });
879
+ // ============================================================
880
+ // scheduler (3 tools — S27, reuses timeblockRepo)
881
+ // ============================================================
882
+ tools.push({
883
+ name: "scheduler.createSchedule",
884
+ description: "创建一个日程(planned 时间块)。" +
885
+ "⚠️ MV-COACH-3 Slice 4 v2 教练闭环(用户主权模型):建完 schedule 后,**必须**在同一回复里完成:" +
886
+ "① 调 cronjob(action=create, deliver=origin) 建开工 cron(schedule=日程开始时间),cron prompt 必须用 libero-coach skill 的'开工 cron 标准 prompt 模板'——**不允许自创结构**。" +
887
+ "② 那条标准 prompt 里写死了 3 步:推提醒 + 调 timeblock.update(status=**unverified**,不是 in_progress) + 建追问 cron(时间动态算:有耗时→过半;无耗时→+30min)。" +
888
+ "③ 顺带问用户'要不要每天 22:00 做个简短复盘?'(B 类推送必须先问再建)。" +
889
+ "关键:状态机用户主权——LLM 只能到点标 unverified 等用户核实,**不能擅自升级到 completed**。" +
890
+ "详见 libero-coach skill 的'推送抓手 + 状态机一条龙'段。",
891
+ inputSchema: {
892
+ user_id: z.string().optional(),
893
+ target_user_id: z.string().min(1).optional(),
894
+ title: z.string().min(1),
895
+ category_id: z.string().optional(),
896
+ start_time: z.string(),
897
+ end_time: z.string(),
898
+ duration_minutes: z.number().optional(),
899
+ rating: z.number().min(1).max(5).optional(),
900
+ rating_reason: z.string().optional(),
901
+ notes: z.string().optional(),
902
+ },
903
+ needsUserIdInjection: true,
904
+ handler: async (rawInput) => {
905
+ const r = await injector(rawInput);
906
+ if ("error" in r)
907
+ return errorContent(r.error);
908
+ return asContent(await scheduler.createSchedule(r));
909
+ },
910
+ });
911
+ tools.push({
912
+ name: "scheduler.listSchedules",
913
+ description: "内部工具:查询 planned 日程块用于排程冲突检查。" +
914
+ "⚠️ 用户「看今天/看日程/复盘/在干啥」请调 schedule.timeline(含矩阵+表格+全状态视图,不要调本工具)。" +
915
+ "本工具仅用于排程编排时的内部查询。",
916
+ inputSchema: {
917
+ user_id: z.string().optional(),
918
+ from: z.string().optional(),
919
+ to: z.string().optional(),
920
+ },
921
+ needsUserIdInjection: true,
922
+ handler: async (rawInput) => {
923
+ const r = await injector(rawInput);
924
+ if ("error" in r)
925
+ return errorContent(r.error);
926
+ return asContent(await scheduler.listSchedules(r));
927
+ },
928
+ });
929
+ tools.push({
930
+ name: "scheduler.checkConflict",
931
+ description: "检查时间段是否与已有日程冲突",
932
+ inputSchema: {
933
+ user_id: z.string().optional(),
934
+ start_time: z.string(),
935
+ end_time: z.string(),
936
+ },
937
+ needsUserIdInjection: true,
938
+ handler: async (rawInput) => {
939
+ const r = await injector(rawInput);
940
+ if ("error" in r)
941
+ return errorContent(r.error);
942
+ return asContent(await scheduler.checkConflict(r));
943
+ },
944
+ });
945
+ // ============================================================
946
+ // scheduler MV-COACH-5 (2 tools — 3F 架构意图 API)
947
+ // ============================================================
948
+ tools.push({
949
+ name: "scheduler.setIntent",
950
+ description: "MV-COACH-5 3F 架构 §4.1:注册一个意图(提前提醒/追问/复盘/一次性提醒)。" +
951
+ "脑层(skill)调这个,不直接调宿主的 cronjob——通用核心记意图到 DB,调宿主适配层翻译成该平台机制。" +
952
+ "hermes 上 → 翻译成 cron job;无推送能力的平台 → 标 pending 等用户开口补推(slice 4 实装)。" +
953
+ "⚠️ slice 1 纯加法:现有 skill 继续直调 cronjob(v4/v5 路径不破坏),slice 3 才迁移 skill 调本工具。" +
954
+ "intent_type ∈ {pre_start_reminder, missed_check, daily_review, one_off_reminder};" +
955
+ "pre_start/missed_check 必填 schedule_id。trigger.kind ∈ {relative, absolute, recurring}。",
956
+ inputSchema: {
957
+ user_id: z.string().optional(),
958
+ target_user_id: z.string().min(1).optional(),
959
+ intent_type: z.enum([
960
+ "pre_start_reminder",
961
+ "missed_check",
962
+ "daily_review",
963
+ "one_off_reminder",
964
+ ]),
965
+ schedule_id: z.string().optional(),
966
+ trigger: z.object({
967
+ kind: z.enum(["relative", "absolute", "recurring"]),
968
+ offset: z.string().optional(),
969
+ relative_to: z.enum(["schedule.start", "now"]).optional(),
970
+ at: z.string().optional(),
971
+ cron_expr: z.string().optional(),
972
+ }),
973
+ action_prompt: z.string().min(1),
974
+ },
975
+ needsUserIdInjection: true,
976
+ handler: async (rawInput) => {
977
+ const r = await injector(rawInput);
978
+ if ("error" in r)
979
+ return errorContent(r.error);
980
+ try {
981
+ const result = await scheduler.setIntent({
982
+ user_id: r.user_id,
983
+ intent_type: rawInput.intent_type,
984
+ schedule_id: rawInput.schedule_id,
985
+ trigger: rawInput.trigger,
986
+ action_prompt: rawInput.action_prompt,
987
+ });
988
+ return asContent(result);
989
+ }
990
+ catch (e) {
991
+ return errorContent(e.message);
992
+ }
993
+ },
994
+ });
995
+ tools.push({
996
+ name: "scheduler.cancelIntent",
997
+ description: "MV-COACH-5 3F 架构 §4.2:撤销一个意图(用户删日程时连带撤提醒/追问)。" +
998
+ "intent_id 必填(从 setIntent 返回值拿)。",
999
+ inputSchema: {
1000
+ intent_id: z.string().min(1),
1001
+ },
1002
+ handler: async (rawInput) => {
1003
+ try {
1004
+ const result = await scheduler.cancelIntent({
1005
+ intent_id: rawInput.intent_id,
1006
+ });
1007
+ return asContent(result);
1008
+ }
1009
+ catch (e) {
1010
+ return errorContent(e.message);
1011
+ }
1012
+ },
1013
+ });
1014
+ // ============================================================
1015
+ // task (6 tools — S29 P2-3 + 事一 draft/commit, takes raw db)
1016
+ // ============================================================
1017
+ tools.push({
1018
+ name: "task.create",
1019
+ description: "建单个独立任务(不挂任何 timeblock)。⚠️ 拆解请用 decompose.split,本工具一次只能建一个任务、不能建子任务。S46 支持 difficulty(easy/medium/hard,默认 medium)+ priority(high/medium/low,默认 medium),用于'快速启动+峰终'智能排程。",
1020
+ // S33 v3: 用 z.object().strict() 显式拒 parent_id(zod 默认 strip 不报错,
1021
+ // 严格模式让多余键触发 safeParse 失败 → MCP 返回 isError)。
1022
+ // 底层 createTask 仍保留 parent_id 能力(task.test.ts 单测不破)。
1023
+ inputSchema: z
1024
+ .object({
1025
+ user_id: z.string().optional(),
1026
+ target_user_id: z.string().min(1).optional(),
1027
+ title: z.string().min(1),
1028
+ est_minutes: z.number().optional(),
1029
+ difficulty: z.enum(["easy", "medium", "hard"]).optional(),
1030
+ priority: z.enum(["high", "medium", "low"]).optional(),
1031
+ })
1032
+ .strict(),
1033
+ needsUserIdInjection: true,
1034
+ handler: async (rawInput) => {
1035
+ const r = await injector(rawInput);
1036
+ if ("error" in r)
1037
+ return errorContent(r.error);
1038
+ return asContent(await task.createTask(r));
1039
+ },
1040
+ });
1041
+ tools.push({
1042
+ name: "task.get",
1043
+ description: "按 id 获取任务(含递归子树)",
1044
+ inputSchema: { id: z.string().min(1) },
1045
+ handler: async ({ id }) => asContent(await task.getTask(id)),
1046
+ });
1047
+ tools.push({
1048
+ name: "task.list",
1049
+ description: "列出任务(parent_id=null 仅根;给定则该父的子;省略则全部)",
1050
+ inputSchema: {
1051
+ user_id: z.string().optional(),
1052
+ target_user_id: z.string().min(1).optional(),
1053
+ parent_id: z.string().nullable().optional(),
1054
+ },
1055
+ needsUserIdInjection: true,
1056
+ handler: async (rawInput) => {
1057
+ const r = await injector(rawInput);
1058
+ if ("error" in r)
1059
+ return errorContent(r.error);
1060
+ return asContent(await task.listTasks(r));
1061
+ },
1062
+ });
1063
+ tools.push({
1064
+ name: "task.update",
1065
+ description: "更新任务(title/est_minutes/status/difficulty/priority;状态机校验非法流转)",
1066
+ inputSchema: {
1067
+ id: z.string().min(1),
1068
+ title: z.string().optional(),
1069
+ est_minutes: z.number().optional(),
1070
+ status: z.string().optional(),
1071
+ difficulty: z.enum(["easy", "medium", "hard"]).optional(),
1072
+ priority: z.enum(["high", "medium", "low"]).optional(),
1073
+ },
1074
+ handler: async ({ id, ...rest }) => {
1075
+ const updated = await task.updateTask(id, rest);
1076
+ // INT-ap-rewards:任务状态机流转到 done = 任务实绩
1077
+ if (rest.status === "done") {
1078
+ await rewardHook(updated.user_id, "task", `task:${id}`);
1079
+ }
1080
+ return asContent(updated);
1081
+ },
1082
+ });
1083
+ tools.push({
1084
+ name: "task.linkTimeblock",
1085
+ description: "把任务关联到一个时间块(子任务开工执行时)",
1086
+ inputSchema: {
1087
+ task_id: z.string().min(1),
1088
+ block_id: z.string().min(1),
1089
+ },
1090
+ handler: async ({ task_id, block_id }) => asContent(await task.linkTimeblock(task_id, block_id)),
1091
+ });
1092
+ tools.push({
1093
+ name: "task.updateDifficulty",
1094
+ description: "S46: 仅更新 task 的 difficulty(easy/medium/hard)或 priority(high/medium/low)。用户主动调整时用,默认 medium。用于'快速启动+峰终'智能排程。",
1095
+ inputSchema: {
1096
+ task_id: z.string().min(1),
1097
+ difficulty: z.enum(["easy", "medium", "hard"]).optional(),
1098
+ priority: z.enum(["high", "medium", "low"]).optional(),
1099
+ },
1100
+ handler: async ({ task_id, difficulty, priority }) => asContent(await task.updateDifficulty(task_id, { difficulty, priority })),
1101
+ });
1102
+ tools.push({
1103
+ name: "task.findPlan",
1104
+ description: "按事件名 exact-match 查拆解计划:返回根任务 + 第一块未完成子任务(record 开工时链 task_id,驱动闭环正反馈)",
1105
+ inputSchema: {
1106
+ user_id: z.string().optional(),
1107
+ title: z.string().min(1),
1108
+ },
1109
+ needsUserIdInjection: true,
1110
+ handler: async (rawInput) => {
1111
+ const r = await injector(rawInput);
1112
+ if ("error" in r)
1113
+ return errorContent(r.error);
1114
+ return asContent(await task.findPlan(r));
1115
+ },
1116
+ });
1117
+ // S33 v3: task.draftPlan / task.commitPlan 从 MCP 工具列表移除(不再暴露给 mimo)。
1118
+ // 拆解唯一入口 = decompose.split(见下方 decompose 段)。
1119
+ // 底层 draftPlan/commitPlan 方法保留在 mcp/src/task/index.ts(被 decompose.split 复用 + task.test.ts 单测覆盖)。
1120
+ // ============================================================
1121
+ // decompose (1 tool — S33 v3:动作分离重构)
1122
+ // decompose.split = 拆解唯一入口。状态机强制 draft→confirm→commit。
1123
+ // 错误返回 isError content(不 throw),避 hermes 熔断器(v2 实证坑)。
1124
+ // ============================================================
1125
+ tools.push({
1126
+ name: "decompose.split",
1127
+ description: "拆解 timeblock 为结构化子任务树(拆解唯一入口)。第一次调用出方案(不写库),第二次带 confirm_token 落地。状态机强制 draft→confirm→commit。⚠️ 拆子任务请用本工具,不要用 task.create。",
1128
+ inputSchema: {
1129
+ user_id: z.string().optional(),
1130
+ timeblock_id: z.string().min(1),
1131
+ children: z
1132
+ .array(z.object({
1133
+ title: z.string().min(1),
1134
+ est_minutes: z.number().optional(),
1135
+ }))
1136
+ .min(1),
1137
+ confirm_token: z.string().optional(),
1138
+ },
1139
+ needsUserIdInjection: true,
1140
+ handler: async (rawInput) => {
1141
+ const r = await injector(rawInput);
1142
+ if ("error" in r)
1143
+ return errorContent(r.error);
1144
+ const result = await task.splitDecompose(r);
1145
+ // DecomposeErrorResult → isError content(不 throw,避 hermes 熔断器)
1146
+ if ("ok" in result && result.ok === false) {
1147
+ return {
1148
+ content: [
1149
+ {
1150
+ type: "text",
1151
+ text: JSON.stringify(result),
1152
+ },
1153
+ ],
1154
+ isError: true,
1155
+ };
1156
+ }
1157
+ return asContent(result);
1158
+ },
1159
+ });
1160
+ // ============================================================
1161
+ // ap (3 tools — INT-AP-tools R2: credit / balance / spend)
1162
+ // ============================================================
1163
+ tools.push({
1164
+ name: "ap.credit",
1165
+ description: "给用户 AP 账入账(时间管理动作换算后的结果,或手动投喂)",
1166
+ inputSchema: {
1167
+ user_id: z.string().optional(),
1168
+ source: z.string().min(1),
1169
+ amount: z.number().int().positive(),
1170
+ idempotency_key: z.string().min(1).optional(),
1171
+ },
1172
+ needsUserIdInjection: true,
1173
+ handler: async (rawInput) => {
1174
+ const r = await injector(rawInput);
1175
+ if ("error" in r)
1176
+ return errorContent(r.error);
1177
+ return asContent(await ap.credit(r));
1178
+ },
1179
+ });
1180
+ tools.push({
1181
+ name: "ap.balance",
1182
+ description: "查询用户当前 AP 余额(只读)",
1183
+ inputSchema: {
1184
+ user_id: z.string().optional(),
1185
+ },
1186
+ needsUserIdInjection: true,
1187
+ handler: async (rawInput) => {
1188
+ const r = await injector(rawInput);
1189
+ if ("error" in r)
1190
+ return errorContent(r.error);
1191
+ return asContent(await ap.balance(r));
1192
+ },
1193
+ });
1194
+ tools.push({
1195
+ name: "ap.spend",
1196
+ description: "出击时带出 AP:缺省 amount = 带出当前全部余额(B2',一次往返);显式 amount 时余额不足返回 forbidden",
1197
+ inputSchema: {
1198
+ user_id: z.string().optional(),
1199
+ amount: z.number().int().positive().optional(),
1200
+ idempotency_key: z.string().min(1).optional(),
1201
+ },
1202
+ needsUserIdInjection: true,
1203
+ handler: async (rawInput) => {
1204
+ const r = await injector(rawInput);
1205
+ if ("error" in r)
1206
+ return errorContent(r.error);
1207
+ // B2' 带出全部:缺省 amount = 当前余额(省一次客户端 refresh 往返)
1208
+ if (r.amount === undefined) {
1209
+ const bal = (await ap.balance(r)).balance;
1210
+ if (bal <= 0)
1211
+ return asContent({ balance: 0, spent: 0 }); // 空钱包:无操作,幂等键不消耗
1212
+ const all = await ap.spend({ ...r, amount: bal });
1213
+ if (!all.ok) {
1214
+ return errorContent(`insufficient AP (balance=${all.balance})`, "forbidden");
1215
+ }
1216
+ return asContent({ balance: all.balance, spent: all.spent });
1217
+ }
1218
+ const result = await ap.spend(r);
1219
+ if (!result.ok) {
1220
+ return errorContent(`insufficient AP (balance=${result.balance})`, "forbidden");
1221
+ }
1222
+ return asContent({ balance: result.balance, spent: result.spent });
1223
+ },
1224
+ });
1225
+ tools.push({
1226
+ name: "rewards.status",
1227
+ description: "INT-ap-rewards:本期各实绩动作(计时/日程/任务/复盘)已计次数与下次收益,只读(悬赏呈现层用)",
1228
+ inputSchema: {
1229
+ user_id: z.string().optional(),
1230
+ },
1231
+ needsUserIdInjection: true,
1232
+ handler: async (rawInput) => {
1233
+ const r = await injector(rawInput);
1234
+ if ("error" in r)
1235
+ return errorContent(r.error);
1236
+ return asContent(await ctx.repos.apRepo.rewardStatus(r.user_id));
1237
+ },
1238
+ });
1239
+ // ── rewards 悬赏(方案 A)+ 复盘完成信号 ──
1240
+ tools.push({
1241
+ name: "bounties.status",
1242
+ description: "INT-ap-rewards 悬赏:今日 3 个日常任务(计时×1/时间块×3/复盘×1)进度与完成态,只读(营地任务板/脑层发布用)",
1243
+ inputSchema: {
1244
+ user_id: z.string().optional(),
1245
+ },
1246
+ needsUserIdInjection: true,
1247
+ handler: async (rawInput) => {
1248
+ const r = await injector(rawInput);
1249
+ if ("error" in r)
1250
+ return errorContent(r.error);
1251
+ return asContent(await ctx.repos.apRepo.bountiesStatus(r.user_id));
1252
+ },
1253
+ });
1254
+ tools.push({
1255
+ name: "review.complete",
1256
+ description: "INT-ap-rewards:复盘完成信号——脑层在一次复盘对话收尾时调用,记一次复盘实绩(周曲线 5,3,1→保底1;当日悬赏 复盘×1)",
1257
+ inputSchema: {
1258
+ user_id: z.string().optional(),
1259
+ notes: z.string().optional(),
1260
+ },
1261
+ needsUserIdInjection: true,
1262
+ handler: async (rawInput) => {
1263
+ const r = await injector(rawInput);
1264
+ if ("error" in r)
1265
+ return errorContent(r.error);
1266
+ void r.notes; // 预留:复盘摘要留档(不进奖励逻辑)
1267
+ const result = await ctx.repos.apRepo.recordRewardEvent({
1268
+ user_id: r.user_id,
1269
+ action_type: "review",
1270
+ event_id: `review:${Date.now().toString(36)}-${Math.floor(Math.random() * 1e6).toString(36)}`,
1271
+ });
1272
+ return asContent(result ?? { nth: 0, amount: 0 });
1273
+ },
1274
+ });
1275
+ return tools;
1276
+ }
1277
+ /**
1278
+ * Shared runtime for MCP + REST: same repos, same registry, same sqlite.
1279
+ * MC-0b REST adapter calls handlers on this registry; must not duplicate business logic.
1280
+ */
1281
+ export function createToolRuntime(db) {
1282
+ // MV-COACH-3 Slice 2: resolve default user_id once at server build
1283
+ const defaultUser = resolveDefaultUserId({ db, env: process.env });
1284
+ logResolvedDefaultUserId(defaultUser);
1285
+ const injector = makeUserIdInjector(defaultUser.userId, db);
1286
+ const repos = createRepos(db);
1287
+ const categories = createCategoryTools(repos.categoryRepo);
1288
+ const tb = createTimeBlockTools(repos.timeblockRepo, db);
1289
+ const stats = createStatsTools(repos.statsRepo);
1290
+ const matrix = createMatrixTools(repos.statsRepo);
1291
+ const timeline = createTimelineTools(repos.timelineRepo);
1292
+ const timer = createTimerTools(repos.timerRepo, repos.timeblockRepo, db);
1293
+ const profile = createProfileTools(repos.profileRepo, repos.identityRepo);
1294
+ const profileConfirm = createProfileConfirmTools(db);
1295
+ const reminder = createReminderTools(repos.reminderRepo, undefined, db);
1296
+ const scheduler = createSchedulerTools(repos.timeblockRepo, repos.reminderRepo, repos.categoryRepo, selectAdapter(createAdaptersWithDb(db)), db);
1297
+ const task = createTaskTools(db);
1298
+ const knowledgeRepo = new FsYamlKnowledgeRepo();
1299
+ const coachmod = createCoachModTools(knowledgeRepo, db);
1300
+ const ap = createApTools(repos.apRepo);
1301
+ const ctx = {
1302
+ repos,
1303
+ categories,
1304
+ tb,
1305
+ stats,
1306
+ matrix,
1307
+ timeline,
1308
+ timer,
1309
+ profile,
1310
+ profileConfirm,
1311
+ reminder,
1312
+ scheduler,
1313
+ task,
1314
+ coachmod,
1315
+ ap,
1316
+ injector,
1317
+ db,
1318
+ };
1319
+ return { registry: buildToolRegistry(ctx) };
1320
+ }
1321
+ /**
1322
+ * Build the libero MCP server on a given DB connection.
1323
+ * Tools throw Error on business violations; handlers let them propagate —
1324
+ * the SDK turns thrown errors into MCP tool errors (isError) preserving the message.
1325
+ */
1326
+ export function buildServer(db) {
1327
+ const { registry } = createToolRuntime(db);
1328
+ // instructions 必须放在第 2 参 ServerOptions(不是 serverInfo)—— SDK 只从 options 读
1329
+ const server = new McpServer({ name: "libero-mcp", version: "0.2.3" }, { instructions: buildServerInstructions() });
1330
+ for (const tool of registry) {
1331
+ server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, tool.handler);
1332
+ }
1333
+ return server;
1334
+ }