libero-mcp 0.2.3 → 0.2.4

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.
@@ -0,0 +1,4 @@
1
+ -- 011_profile_default_confirmed: MV-COACH-1 首次确认时间戳
2
+ -- 仅 ADD COLUMN,不改既有字段语义
3
+
4
+ ALTER TABLE user_profile ADD COLUMN default_confirmed_at TEXT;
package/data/schema.sql CHANGED
@@ -5,6 +5,10 @@ CREATE TABLE IF NOT EXISTS user_profile (
5
5
  id TEXT PRIMARY KEY,
6
6
  display_name TEXT NOT NULL DEFAULT '',
7
7
  expectations TEXT,
8
+ -- 005_profile_settings: 结构化约定存为 JSON
9
+ settings TEXT,
10
+ -- 011_profile_default_confirmed: MV-COACH-1 首次确认时间戳
11
+ default_confirmed_at TEXT,
8
12
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
9
13
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
10
14
  deleted_at TEXT
@@ -0,0 +1,4 @@
1
+ -- 011_profile_default_confirmed: MV-COACH-1 首次确认时间戳
2
+ -- 仅 ADD COLUMN,不改既有字段语义
3
+
4
+ ALTER TABLE user_profile ADD COLUMN default_confirmed_at TEXT;
@@ -5,6 +5,10 @@ CREATE TABLE IF NOT EXISTS user_profile (
5
5
  id TEXT PRIMARY KEY,
6
6
  display_name TEXT NOT NULL DEFAULT '',
7
7
  expectations TEXT,
8
+ -- 005_profile_settings: 结构化约定存为 JSON
9
+ settings TEXT,
10
+ -- 011_profile_default_confirmed: MV-COACH-1 首次确认时间戳
11
+ default_confirmed_at TEXT,
8
12
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
9
13
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
10
14
  deleted_at TEXT
@@ -1,17 +1,20 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { SqliteCategoryRepo, SqliteTimeBlockRepo, SqliteStatsRepo, SqliteTimerRepo, SqliteProfileRepo, SqliteReminderRepo, } from "./src/repo/sqlite.js";
3
+ import { SqliteCategoryRepo, SqliteTimeBlockRepo, SqliteStatsRepo, SqliteTimerRepo, SqliteProfileRepo, SqliteReminderRepo, SqliteTimelineRepo, } from "./src/repo/sqlite.js";
4
4
  import { createCategoryTools } from "./src/categories/index.js";
5
5
  import { createTimeBlockTools } from "./src/timeblock/index.js";
6
6
  import { createStatsTools } from "./src/stats/index.js";
7
7
  import { createMatrixTools } from "./src/matrix/index.js";
8
+ import { createTimelineTools } from "./src/timeline/index.js";
8
9
  import { createTimerTools } from "./src/timer/index.js";
9
10
  import { createProfileTools } from "./src/profile/index.js";
11
+ import { createProfileConfirmTools } from "./src/profile/confirm-default.js";
10
12
  import { createReminderTools } from "./src/reminder/index.js";
11
13
  import { createSchedulerTools } from "./src/scheduler/index.js";
12
14
  import { createTaskTools } from "./src/task/index.js";
13
15
  import { nowLocal, formatTime, parseTime } from "./src/clock/index.js";
14
16
  import { buildServerInstructions } from "./cold-start.js";
17
+ import { resolveDefaultUserId, logResolvedDefaultUserId, } from "./src/user-id-resolver.js";
15
18
  /** Wrap any tool result as MCP text content (JSON). Void results become {ok:true}. */
16
19
  function asContent(result) {
17
20
  return {
@@ -23,12 +26,34 @@ function asContent(result) {
23
26
  ],
24
27
  };
25
28
  }
29
+ /** Inject default user_id when LLM omits it; otherwise return isError payload. */
30
+ export function makeUserIdInjector(defaultUserId) {
31
+ return function inject(input) {
32
+ if (input.user_id)
33
+ return input;
34
+ if (defaultUserId)
35
+ return { ...input, user_id: defaultUserId };
36
+ return {
37
+ error: "user_id is required (no default configured: set LIBERO_DEFAULT_USER_ID or have at least one non-system user_profile)",
38
+ };
39
+ };
40
+ }
41
+ export function errorContent(msg) {
42
+ return {
43
+ content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
44
+ isError: true,
45
+ };
46
+ }
26
47
  /**
27
48
  * Build the libero MCP server on a given DB connection.
28
49
  * Tools throw Error on business violations; handlers let them propagate —
29
50
  * the SDK turns thrown errors into MCP tool errors (isError) preserving the message.
30
51
  */
31
52
  export function buildServer(db) {
53
+ // MV-COACH-3 Slice 2: resolve default user_id once at server build
54
+ const defaultUser = resolveDefaultUserId({ db, env: process.env });
55
+ logResolvedDefaultUserId(defaultUser);
56
+ const injector = makeUserIdInjector(defaultUser.userId);
32
57
  // ── construct repos (S10 §3.3) ──
33
58
  const categoryRepo = new SqliteCategoryRepo(db);
34
59
  const timeblockRepo = new SqliteTimeBlockRepo(db);
@@ -36,14 +61,17 @@ export function buildServer(db) {
36
61
  const timerRepo = new SqliteTimerRepo(db);
37
62
  const profileRepo = new SqliteProfileRepo(db);
38
63
  const reminderRepo = new SqliteReminderRepo(db);
64
+ const timelineRepo = new SqliteTimelineRepo(db);
39
65
  // ── inject tool factories ──
40
66
  const categories = createCategoryTools(categoryRepo);
41
- const tb = createTimeBlockTools(timeblockRepo);
67
+ const tb = createTimeBlockTools(timeblockRepo, db);
42
68
  const stats = createStatsTools(statsRepo);
43
69
  const matrix = createMatrixTools(statsRepo); // matrix reuses StatsRepo
44
- const timer = createTimerTools(timerRepo, timeblockRepo);
70
+ const timeline = createTimelineTools(timelineRepo);
71
+ const timer = createTimerTools(timerRepo, timeblockRepo, db);
45
72
  const profile = createProfileTools(profileRepo);
46
- const reminder = createReminderTools(reminderRepo);
73
+ const profileConfirm = createProfileConfirmTools(db);
74
+ const reminder = createReminderTools(reminderRepo, undefined, db);
47
75
  const scheduler = createSchedulerTools(timeblockRepo, reminderRepo, categoryRepo);
48
76
  const task = createTaskTools(db);
49
77
  // instructions 必须放在第 2 参 ServerOptions(不是 serverInfo)—— SDK 只从 options 读
@@ -59,49 +87,94 @@ export function buildServer(db) {
59
87
  server.registerTool("timer.start", {
60
88
  description: "开始一个计时会话",
61
89
  inputSchema: {
62
- user_id: z.string().min(1),
90
+ user_id: z.string().optional(),
91
+ target_user_id: z.string().min(1).optional(),
63
92
  event: z.string().min(1),
64
93
  categoryId: z.string().optional(),
65
94
  task_id: z.string().optional(),
66
95
  },
67
- }, async ({ user_id, event, categoryId, task_id }) => asContent(await timer.start(user_id, event, categoryId, task_id)));
68
- server.registerTool("timer.pause", { description: "暂停当前计时", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.pause(user_id)));
69
- server.registerTool("timer.resume", { description: "恢复已暂停的计时", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.resume(user_id)));
96
+ }, async (rawInput) => {
97
+ const r = injector(rawInput);
98
+ if ("error" in r)
99
+ return errorContent(r.error);
100
+ return asContent(await timer.start(r.user_id, r.event, r.categoryId, r.task_id, r.target_user_id));
101
+ });
102
+ server.registerTool("timer.pause", { description: "暂停当前计时", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
103
+ const r = injector(rawInput);
104
+ if ("error" in r)
105
+ return errorContent(r.error);
106
+ return asContent(await timer.pause(r.user_id));
107
+ });
108
+ server.registerTool("timer.resume", { description: "恢复已暂停的计时", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
109
+ const r = injector(rawInput);
110
+ if ("error" in r)
111
+ return errorContent(r.error);
112
+ return asContent(await timer.resume(r.user_id));
113
+ });
70
114
  server.registerTool("timer.stop", {
71
115
  description: "停止计时并返回记录(传 rating 时自动落盘 timeblock)",
72
116
  inputSchema: {
73
- user_id: z.string().min(1),
117
+ user_id: z.string().optional(),
74
118
  rating: z.number().min(1).max(5).optional(),
75
119
  rating_reason: z.string().optional(),
76
120
  },
77
- }, async ({ user_id, rating, rating_reason }) => asContent(await timer.stop(user_id, { rating, rating_reason })));
78
- server.registerTool("timer.status", { description: "查询某用户当前计时状态", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.status(user_id)));
79
- server.registerTool("timer.elapsed", { description: "查询当前计时已过毫秒数", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.elapsed(user_id)));
121
+ }, async (rawInput) => {
122
+ const r = injector(rawInput);
123
+ if ("error" in r)
124
+ return errorContent(r.error);
125
+ return asContent(await timer.stop(r.user_id, {
126
+ rating: r.rating,
127
+ rating_reason: r.rating_reason,
128
+ }));
129
+ });
130
+ server.registerTool("timer.status", { description: "查询某用户当前计时状态", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
131
+ const r = injector(rawInput);
132
+ if ("error" in r)
133
+ return errorContent(r.error);
134
+ return asContent(await timer.status(r.user_id));
135
+ });
136
+ server.registerTool("timer.elapsed", { description: "查询当前计时已过毫秒数", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
137
+ const r = injector(rawInput);
138
+ if ("error" in r)
139
+ return errorContent(r.error);
140
+ return asContent(await timer.elapsed(r.user_id));
141
+ });
80
142
  server.registerTool("timer.statusAll", { description: "查询所有用户的当前计时状态(调试/兜底用)", inputSchema: {} }, async () => asContent(await timer.statusAll()));
81
143
  // ── S26: pomodoro v1 tools ──
82
144
  server.registerTool("timer.startFocus", {
83
145
  description: "开始一个专注计时(番茄钟),带目标时长",
84
146
  inputSchema: {
85
- user_id: z.string().min(1),
147
+ user_id: z.string().optional(),
86
148
  event: z.string().min(1),
87
149
  focusMin: z.number().positive().optional(),
88
150
  categoryId: z.string().optional(),
89
151
  task_id: z.string().optional(),
90
152
  },
91
- }, async ({ user_id, event, focusMin, categoryId, task_id }) => asContent(await timer.startFocus(user_id, event, focusMin, categoryId, task_id)));
153
+ }, async (rawInput) => {
154
+ const r = injector(rawInput);
155
+ if ("error" in r)
156
+ return errorContent(r.error);
157
+ return asContent(await timer.startFocus(r.user_id, r.event, r.focusMin, r.categoryId, r.task_id));
158
+ });
92
159
  server.registerTool("timer.remaining", {
93
160
  description: "查询专注计时剩余时间(负数=超时)",
94
161
  inputSchema: {
95
- user_id: z.string().min(1),
162
+ user_id: z.string().optional(),
96
163
  },
97
- }, async ({ user_id }) => asContent(await timer.remaining(user_id)));
164
+ }, async (rawInput) => {
165
+ const r = injector(rawInput);
166
+ if ("error" in r)
167
+ return errorContent(r.error);
168
+ return asContent(await timer.remaining(r.user_id));
169
+ });
98
170
  // ============================================================
99
171
  // timeblock (5 tools — single-object args)
100
172
  // ============================================================
101
173
  server.registerTool("timeblock.create", {
102
174
  description: "创建一条时间记录",
103
175
  inputSchema: {
104
- user_id: z.string().min(1),
176
+ user_id: z.string().optional(),
177
+ target_user_id: z.string().min(1).optional(),
105
178
  title: z.string().min(1),
106
179
  category_id: z.string().optional(),
107
180
  start_time: z.string(),
@@ -111,12 +184,24 @@ export function buildServer(db) {
111
184
  rating_reason: z.string().optional(),
112
185
  notes: z.string().optional(),
113
186
  },
114
- }, async (input) => asContent(await tb.create(input)));
187
+ }, async (rawInput) => {
188
+ const r = injector(rawInput);
189
+ if ("error" in r)
190
+ return errorContent(r.error);
191
+ return asContent(await tb.create(r));
192
+ });
115
193
  server.registerTool("timeblock.getById", { description: "按 id 获取单条时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.getById(id)));
116
194
  server.registerTool("timeblock.update", {
117
- description: "更新一条时间记录",
195
+ description: "更新一条时间记录(标题/时间/评分/status 等)。" +
196
+ "MV-COACH-3 Slice 4 用户主权状态机:" +
197
+ "① 开工 cron 触发到点 → status='unverified'(等用户核实,**不计入实绩**);" +
198
+ "② 用户确认执行(主动开始计时 / 回报「执行了」/ 追问后答执行了)→ status='completed'(**唯一计入实绩**);" +
199
+ "③ 用户答「没执行」→ 不改 status,直接 timeblock.remove 软删(留 deleted_at 可复盘,不进实绩统计);" +
200
+ "④ 用户一直不答 → 保持 unverified,永远等(不自动判死)。" +
201
+ "⚠️ LLM 不能擅自把 planned/unverified 升级到 completed——只有用户确认才能升。",
118
202
  inputSchema: {
119
203
  id: z.string().min(1),
204
+ target_user_id: z.string().min(1).optional(),
120
205
  title: z.string().optional(),
121
206
  category_id: z.string().optional(),
122
207
  start_time: z.string().optional(),
@@ -125,14 +210,22 @@ export function buildServer(db) {
125
210
  rating: z.number().min(1).max(5).optional(),
126
211
  rating_reason: z.string().optional(),
127
212
  notes: z.string().optional(),
213
+ status: z.enum(["planned", "unverified", "completed"]).optional(),
128
214
  },
129
215
  }, async ({ id, ...rest }) => asContent(await tb.update(id, rest)));
130
- server.registerTool("timeblock.remove", { description: "软删除一条时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.remove(id)));
216
+ server.registerTool("timeblock.remove", {
217
+ description: "软删除一条时间记录",
218
+ inputSchema: {
219
+ id: z.string().min(1),
220
+ target_user_id: z.string().min(1).optional(),
221
+ },
222
+ }, async ({ id, target_user_id }) => asContent(await tb.remove(id, { target_user_id })));
131
223
  server.registerTool("timeblock.restore", { description: "恢复一条已软删除的时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.restore(id)));
132
224
  server.registerTool("timeblock.list", {
133
225
  description: "按条件查询时间记录列表",
134
226
  inputSchema: {
135
- user_id: z.string().min(1),
227
+ user_id: z.string().optional(),
228
+ target_user_id: z.string().min(1).optional(),
136
229
  date: z.string().optional(),
137
230
  from: z.string().optional(),
138
231
  to: z.string().optional(),
@@ -141,39 +234,81 @@ export function buildServer(db) {
141
234
  limit: z.number().optional(),
142
235
  offset: z.number().optional(),
143
236
  },
144
- }, async (input) => asContent(await tb.list(input)));
237
+ }, async (rawInput) => {
238
+ const r = injector(rawInput);
239
+ if ("error" in r)
240
+ return errorContent(r.error);
241
+ return asContent(await tb.list(r));
242
+ });
145
243
  // ============================================================
146
244
  // stats (2 tools — single-object filters)
147
245
  // ============================================================
148
246
  server.registerTool("stats.summarize", {
149
247
  description: "按分类汇总时间统计(占比/记录数)",
150
248
  inputSchema: {
151
- user_id: z.string().min(1),
249
+ user_id: z.string().optional(),
250
+ target_user_id: z.string().min(1).optional(),
152
251
  from: z.string().optional(),
153
252
  to: z.string().optional(),
154
253
  category_id: z.string().optional(),
155
254
  },
156
- }, async (input) => asContent(await stats.summarize(input)));
255
+ }, async (rawInput) => {
256
+ const r = injector(rawInput);
257
+ if ("error" in r)
258
+ return errorContent(r.error);
259
+ return asContent(await stats.summarize(r));
260
+ });
157
261
  server.registerTool("stats.dailyBreakdown", {
158
262
  description: "按天×分类拆分时间明细",
159
263
  inputSchema: {
160
- user_id: z.string().min(1),
264
+ user_id: z.string().optional(),
265
+ target_user_id: z.string().min(1).optional(),
161
266
  from: z.string().optional(),
162
267
  to: z.string().optional(),
163
268
  category_id: z.string().optional(),
164
269
  },
165
- }, async (input) => asContent(await stats.dailyBreakdown(input)));
270
+ }, async (rawInput) => {
271
+ const r = injector(rawInput);
272
+ if ("error" in r)
273
+ return errorContent(r.error);
274
+ return asContent(await stats.dailyBreakdown(r));
275
+ });
166
276
  // ============================================================
167
277
  // matrix (1 tool — single-object filters, reuses StatsRepo)
168
278
  // ============================================================
169
279
  server.registerTool("matrix.renderMatrix", {
170
280
  description: "时间×评分四象限矩阵(能耗黑洞/保持区/亮点区/可放弃)",
171
281
  inputSchema: {
172
- user_id: z.string().min(1),
282
+ user_id: z.string().optional(),
283
+ from: z.string().optional(),
284
+ to: z.string().optional(),
285
+ },
286
+ }, async (rawInput) => {
287
+ const r = injector(rawInput);
288
+ if ("error" in r)
289
+ return errorContent(r.error);
290
+ return asContent(await matrix.renderMatrix(r));
291
+ });
292
+ // ============================================================
293
+ // schedule.timeline (emoji matrix + detail table)
294
+ // ============================================================
295
+ server.registerTool("schedule.timeline", {
296
+ description: "看日程/复盘的首选视图:emoji 矩阵+细项表格,含计划/计时中/未回应/实绩全状态。" +
297
+ "用户说「看今天/看日程/在干啥/复盘/接下来 N 小时」时调本工具,并直接 echo rendered_text(不重写、不改格式)。" +
298
+ "不要调 scheduler.listSchedules(那是内部排程查询,不是用户视图)。",
299
+ inputSchema: {
300
+ user_id: z.string().optional(),
301
+ target_user_id: z.string().min(1).optional(),
173
302
  from: z.string().optional(),
174
303
  to: z.string().optional(),
304
+ format: z.enum(["emoji", "ascii"]).optional(),
175
305
  },
176
- }, async (input) => asContent(await matrix.renderMatrix(input)));
306
+ }, async (rawInput) => {
307
+ const r = injector(rawInput);
308
+ if ("error" in r)
309
+ return errorContent(r.error);
310
+ return asContent(await timeline.renderTimeline(r));
311
+ });
177
312
  // ============================================================
178
313
  // clock (3 tools — pure functions, no repo)
179
314
  // ============================================================
@@ -199,12 +334,17 @@ export function buildServer(db) {
199
334
  // ============================================================
200
335
  server.registerTool("profile.get", {
201
336
  description: "获取用户 profile(含结构化约定)",
202
- inputSchema: { user_id: z.string().min(1) },
203
- }, async ({ user_id }) => asContent(await profile.get(user_id)));
337
+ inputSchema: { user_id: z.string().optional() },
338
+ }, async (rawInput) => {
339
+ const r = injector(rawInput);
340
+ if ("error" in r)
341
+ return errorContent(r.error);
342
+ return asContent(await profile.get(r.user_id));
343
+ });
204
344
  server.registerTool("profile.upsert", {
205
- description: "创建或更新用户 profile。user_id 从会话上下文静默取,禁止向用户索取。",
345
+ description: "创建或更新用户 profile(display_name / expectations / settings)。首次确认请用 profile.confirm_default。",
206
346
  inputSchema: {
207
- user_id: z.string().min(1).describe("从会话上下文静默取;禁止问用户"),
347
+ user_id: z.string().optional(),
208
348
  display_name: z.string().optional(),
209
349
  expectations: z.string().optional(),
210
350
  settings: z
@@ -212,20 +352,40 @@ export function buildServer(db) {
212
352
  .passthrough()
213
353
  .optional(),
214
354
  },
215
- }, async (input) => asContent(await profile.upsert(input)));
355
+ }, async (rawInput) => {
356
+ const r = injector(rawInput);
357
+ if ("error" in r)
358
+ return errorContent(r.error);
359
+ return asContent(await profile.upsert(r));
360
+ });
361
+ server.registerTool("profile.confirm_default", {
362
+ description: "首次写入前确认默认用户:写入 user_profile,设置 display_name 与 default_confirmed_at。display_name 重名则拒绝。",
363
+ inputSchema: {
364
+ platform_user_id: z.string().min(1),
365
+ display_name: z.string().min(1),
366
+ },
367
+ }, async (input) => asContent(await profileConfirm.confirmDefault(input)));
216
368
  // ============================================================
217
369
  // reminder (6 tools — S11)
218
370
  // ============================================================
219
371
  server.registerTool("reminder.create", {
220
- description: "创建一条提醒",
372
+ description: "⚠️ 仅写 DB,不主动推送(libero-mcp 无 background sender)。" +
373
+ "在 hermes 等有 cronjob 工具的平台上,用户要'X 时间提醒我 Y'请调宿主 cronjob(action=create, deliver=origin),不要用本工具。" +
374
+ "本工具仅用于:① 无 cron 能力的平台兜底;② 查询场景配合 getDue/getMissed。",
221
375
  inputSchema: {
222
- user_id: z.string().min(1),
376
+ user_id: z.string().optional(),
377
+ target_user_id: z.string().min(1).optional(),
223
378
  content: z.string().min(1),
224
379
  remind_at: z.string().min(1),
225
380
  channel: z.string().optional(),
226
381
  timeblock_id: z.string().optional(),
227
382
  },
228
- }, async (input) => asContent(await reminder.create(input)));
383
+ }, async (rawInput) => {
384
+ const r = injector(rawInput);
385
+ if ("error" in r)
386
+ return errorContent(r.error);
387
+ return asContent(await reminder.create(r));
388
+ });
229
389
  server.registerTool("reminder.getDue", {
230
390
  description: "获取到期且待发送的提醒列表",
231
391
  inputSchema: {
@@ -242,9 +402,14 @@ export function buildServer(db) {
242
402
  server.registerTool("reminder.getMissed", {
243
403
  description: "获取过期但未送达的提醒(被动补发用)",
244
404
  inputSchema: {
245
- user_id: z.string().min(1),
405
+ user_id: z.string().optional(),
246
406
  },
247
- }, async ({ user_id }) => asContent(await reminder.getMissedReminders({ user_id })));
407
+ }, async (rawInput) => {
408
+ const r = injector(rawInput);
409
+ if ("error" in r)
410
+ return errorContent(r.error);
411
+ return asContent(await reminder.getMissedReminders({ user_id: r.user_id }));
412
+ });
248
413
  server.registerTool("reminder.markSent", {
249
414
  description: "标记提醒为已发送(幂等)",
250
415
  inputSchema: { id: z.string().min(1) },
@@ -257,9 +422,15 @@ export function buildServer(db) {
257
422
  // scheduler (3 tools — S27, reuses timeblockRepo)
258
423
  // ============================================================
259
424
  server.registerTool("scheduler.createSchedule", {
260
- description: "创建一个日程(planned 时间块)",
425
+ description: "创建一个日程(planned 时间块)。" +
426
+ "⚠️ MV-COACH-3 Slice 4 v2 教练闭环(用户主权模型):建完 schedule 后,**必须**在同一回复里完成:" +
427
+ "① 调 cronjob(action=create, deliver=origin) 建开工 cron(schedule=日程开始时间),cron prompt 必须用 libero-coach skill 的'开工 cron 标准 prompt 模板'——**不允许自创结构**。" +
428
+ "② 那条标准 prompt 里写死了 3 步:推提醒 + 调 timeblock.update(status=**unverified**,不是 in_progress) + 建追问 cron(时间动态算:有耗时→过半;无耗时→+30min)。" +
429
+ "③ 顺带问用户'要不要每天 22:00 做个简短复盘?'(B 类推送必须先问再建)。" +
430
+ "关键:状态机用户主权——LLM 只能到点标 unverified 等用户核实,**不能擅自升级到 completed**。" +
431
+ "详见 libero-coach skill 的'推送抓手 + 状态机一条龙'段。",
261
432
  inputSchema: {
262
- user_id: z.string().min(1),
433
+ user_id: z.string().optional(),
263
434
  title: z.string().min(1),
264
435
  category_id: z.string().optional(),
265
436
  start_time: z.string(),
@@ -269,23 +440,40 @@ export function buildServer(db) {
269
440
  rating_reason: z.string().optional(),
270
441
  notes: z.string().optional(),
271
442
  },
272
- }, async (input) => asContent(await scheduler.createSchedule(input)));
443
+ }, async (rawInput) => {
444
+ const r = injector(rawInput);
445
+ if ("error" in r)
446
+ return errorContent(r.error);
447
+ return asContent(await scheduler.createSchedule(r));
448
+ });
273
449
  server.registerTool("scheduler.listSchedules", {
274
- description: "列出用户的日程(仅 planned 时间块)",
450
+ description: "内部工具:查询 planned 日程块用于排程冲突检查。" +
451
+ "⚠️ 用户「看今天/看日程/复盘/在干啥」请调 schedule.timeline(含矩阵+表格+全状态视图,不要调本工具)。" +
452
+ "本工具仅用于排程编排时的内部查询。",
275
453
  inputSchema: {
276
- user_id: z.string().min(1),
454
+ user_id: z.string().optional(),
277
455
  from: z.string().optional(),
278
456
  to: z.string().optional(),
279
457
  },
280
- }, async (input) => asContent(await scheduler.listSchedules(input)));
458
+ }, async (rawInput) => {
459
+ const r = injector(rawInput);
460
+ if ("error" in r)
461
+ return errorContent(r.error);
462
+ return asContent(await scheduler.listSchedules(r));
463
+ });
281
464
  server.registerTool("scheduler.checkConflict", {
282
465
  description: "检查时间段是否与已有日程冲突",
283
466
  inputSchema: {
284
- user_id: z.string().min(1),
467
+ user_id: z.string().optional(),
285
468
  start_time: z.string(),
286
469
  end_time: z.string(),
287
470
  },
288
- }, async (input) => asContent(await scheduler.checkConflict(input)));
471
+ }, async (rawInput) => {
472
+ const r = injector(rawInput);
473
+ if ("error" in r)
474
+ return errorContent(r.error);
475
+ return asContent(await scheduler.checkConflict(r));
476
+ });
289
477
  // ============================================================
290
478
  // task (7 tools — S29 P2-3 + 事一 draft/commit, takes raw db)
291
479
  // ============================================================
@@ -296,12 +484,18 @@ export function buildServer(db) {
296
484
  // 底层 createTask 仍保留 parent_id 能力(task.test.ts 单测不破)。
297
485
  inputSchema: z
298
486
  .object({
299
- user_id: z.string().min(1),
487
+ user_id: z.string().optional(),
488
+ target_user_id: z.string().min(1).optional(),
300
489
  title: z.string().min(1),
301
490
  est_minutes: z.number().optional(),
302
491
  })
303
492
  .strict(),
304
- }, async (input) => asContent(await task.createTask(input)));
493
+ }, async (rawInput) => {
494
+ const r = injector(rawInput);
495
+ if ("error" in r)
496
+ return errorContent(r.error);
497
+ return asContent(await task.createTask(r));
498
+ });
305
499
  server.registerTool("task.get", {
306
500
  description: "按 id 获取任务(含递归子树)",
307
501
  inputSchema: { id: z.string().min(1) },
@@ -309,10 +503,16 @@ export function buildServer(db) {
309
503
  server.registerTool("task.list", {
310
504
  description: "列出任务(parent_id=null 仅根;给定则该父的子;省略则全部)",
311
505
  inputSchema: {
312
- user_id: z.string().min(1),
506
+ user_id: z.string().optional(),
507
+ target_user_id: z.string().min(1).optional(),
313
508
  parent_id: z.string().nullable().optional(),
314
509
  },
315
- }, async (input) => asContent(await task.listTasks(input)));
510
+ }, async (rawInput) => {
511
+ const r = injector(rawInput);
512
+ if ("error" in r)
513
+ return errorContent(r.error);
514
+ return asContent(await task.listTasks(r));
515
+ });
316
516
  server.registerTool("task.update", {
317
517
  description: "更新任务(title/est_minutes/status;状态机校验非法流转)",
318
518
  inputSchema: {
@@ -332,10 +532,15 @@ export function buildServer(db) {
332
532
  server.registerTool("task.findPlan", {
333
533
  description: "按事件名 exact-match 查拆解计划:返回根任务 + 第一块未完成子任务(record 开工时链 task_id,驱动闭环正反馈)",
334
534
  inputSchema: {
335
- user_id: z.string().min(1),
535
+ user_id: z.string().optional(),
336
536
  title: z.string().min(1),
337
537
  },
338
- }, async (input) => asContent(await task.findPlan(input)));
538
+ }, async (rawInput) => {
539
+ const r = injector(rawInput);
540
+ if ("error" in r)
541
+ return errorContent(r.error);
542
+ return asContent(await task.findPlan(r));
543
+ });
339
544
  // S33 v3: task.draftPlan / task.commitPlan 从 MCP 工具列表移除(不再暴露给 mimo)。
340
545
  // 拆解唯一入口 = decompose.split(见下方 decompose 段)。
341
546
  // 底层 draftPlan/commitPlan 方法保留在 mcp/src/task/index.ts(被 decompose.split 复用 + task.test.ts 单测覆盖)。
@@ -347,7 +552,7 @@ export function buildServer(db) {
347
552
  server.registerTool("decompose.split", {
348
553
  description: "拆解 timeblock 为结构化子任务树(拆解唯一入口)。第一次调用出方案(不写库),第二次带 confirm_token 落地。状态机强制 draft→confirm→commit。⚠️ 拆子任务请用本工具,不要用 task.create。",
349
554
  inputSchema: {
350
- user_id: z.string().min(1),
555
+ user_id: z.string().optional(),
351
556
  timeblock_id: z.string().min(1),
352
557
  children: z
353
558
  .array(z.object({
@@ -357,8 +562,11 @@ export function buildServer(db) {
357
562
  .min(1),
358
563
  confirm_token: z.string().optional(),
359
564
  },
360
- }, async (input) => {
361
- const result = await task.splitDecompose(input);
565
+ }, async (rawInput) => {
566
+ const r = injector(rawInput);
567
+ if ("error" in r)
568
+ return errorContent(r.error);
569
+ const result = await task.splitDecompose(r);
362
570
  // DecomposeErrorResult → isError content(不 throw,避 hermes 熔断器)
363
571
  if ("ok" in result && result.ok === false) {
364
572
  return {
@@ -0,0 +1,48 @@
1
+ export function createProfileConfirmTools(db) {
2
+ return {
3
+ async confirmDefault(input) {
4
+ if (!input.platform_user_id || !input.platform_user_id.trim()) {
5
+ throw new Error("platform_user_id 为必填参数");
6
+ }
7
+ if (!input.display_name || !input.display_name.trim()) {
8
+ throw new Error("display_name 为必填参数");
9
+ }
10
+ const displayName = input.display_name.trim();
11
+ const platformUserId = input.platform_user_id.trim();
12
+ const taken = db
13
+ .prepare(`SELECT id FROM user_profile
14
+ WHERE display_name = ? AND id != ? AND deleted_at IS NULL`)
15
+ .get(displayName, platformUserId);
16
+ if (taken) {
17
+ return { status: "rejected", reason: "display_name_taken" };
18
+ }
19
+ const now = new Date().toISOString().replace("T", " ").replace("Z", "");
20
+ const existing = db
21
+ .prepare(`SELECT id FROM user_profile WHERE id = ? AND deleted_at IS NULL`)
22
+ .get(platformUserId);
23
+ if (existing) {
24
+ db.prepare(`UPDATE user_profile
25
+ SET display_name = ?, default_confirmed_at = ?, updated_at = ?
26
+ WHERE id = ?`).run(displayName, now, now, platformUserId);
27
+ }
28
+ else {
29
+ db.prepare(`INSERT INTO user_profile
30
+ (id, display_name, expectations, settings, default_confirmed_at, created_at, updated_at)
31
+ VALUES (?, ?, NULL, NULL, ?, ?, ?)`).run(platformUserId, displayName, now, now, now);
32
+ }
33
+ const row = db
34
+ .prepare(`SELECT id, display_name, expectations, settings, default_confirmed_at, created_at, updated_at
35
+ FROM user_profile WHERE id = ?`)
36
+ .get(platformUserId);
37
+ return {
38
+ id: row.id,
39
+ display_name: row.display_name,
40
+ default_confirmed_at: row.default_confirmed_at,
41
+ expectations: row.expectations,
42
+ settings: row.settings != null ? JSON.parse(row.settings) : null,
43
+ created_at: row.created_at,
44
+ updated_at: row.updated_at,
45
+ };
46
+ },
47
+ };
48
+ }