@rikaidev/yomi 0.6.1 → 0.6.2

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,27 @@
1
+ function messageTimestamp(message) {
2
+ return Number(message?.deliveredTime || message?.createdTime || 0);
3
+ }
4
+ /** Newest activity time of one message box, in epoch milliseconds. */
5
+ export function boxActivity(box) {
6
+ const cursorTime = Number(box?.lastDeliveredMessageId?.deliveredTime || 0);
7
+ const lastTimes = Array.isArray(box?.lastMessages)
8
+ ? box.lastMessages.map(messageTimestamp)
9
+ : [];
10
+ return Math.max(cursorTime, ...lastTimes, 0);
11
+ }
12
+ /**
13
+ * Every message box, most recently active first.
14
+ *
15
+ * LINE returns message boxes in chat-MID order (`c…` groups, then `m…`, then
16
+ * `u…` 1:1 chats) and one page holds only the requested count, so a single
17
+ * `getMessageBoxes` call is neither recent nor complete: it silently drops
18
+ * every chat whose MID sorts past the page. Page through all of them, then
19
+ * order by activity so a caller-supplied limit keeps the newest chats.
20
+ */
21
+ export async function listBoxesByRecency(service, options) {
22
+ const result = await service.client.getAllMessageBoxes({
23
+ lastMessagesPerMessageBoxCount: 1,
24
+ withUnreadCount: options.withUnreadCount,
25
+ });
26
+ return [...(result.messageBoxes || [])].sort((a, b) => boxActivity(b) - boxActivity(a));
27
+ }
@@ -1,4 +1,5 @@
1
1
  import { isSquareChatMid } from '../../line/client/square-service/client.js';
2
+ import { listBoxesByRecency } from '../../line/core/message-box-recency.js';
2
3
  import { sanitizeMessagePreview } from '../../line/core/message-preview.js';
3
4
  import { decryptLineMessage } from '../../line/core/message-query-service.js';
4
5
  import { getExcludedChatIds } from '../../search/scope.js';
@@ -34,12 +35,7 @@ export async function handleListConversations(service, args) {
34
35
  }
35
36
  }
36
37
  catch { }
37
- const result = await service.client.getMessageBoxes({
38
- lastMessagesPerMessageBoxCount: 1,
39
- messageBoxCountLimit: args.limit ?? 20,
40
- withUnreadCount: true,
41
- });
42
- const boxes = result.messageBoxes || [];
38
+ const boxes = (await listBoxesByRecency(service, { withUnreadCount: true })).slice(0, args.limit ?? 20);
43
39
  const names = await resolveConversationNames(service, boxes.map((box) => box.id).filter(Boolean));
44
40
  // The last message embedded in getMessageBoxes is E2EE-wrapped for most
45
41
  // chats; run it through the same local decrypt path get_chat_messages
@@ -237,13 +233,9 @@ export async function handleMarkRead(service, args) {
237
233
  * @returns MCP tool result.
238
234
  */
239
235
  export async function handleGetUnreadDigest(service, args) {
240
- const result = await service.client.getMessageBoxes({
241
- lastMessagesPerMessageBoxCount: 1,
242
- messageBoxCountLimit: args.limit ?? 20,
243
- withUnreadCount: true,
244
- });
245
236
  const excluded = getExcludedChatIds();
246
- const unreadBoxes = (result.messageBoxes || []).filter((box) => (box.unreadCount ?? 0) > 0 && box.id && !excluded.has(box.id));
237
+ const allUnread = (await listBoxesByRecency(service, { withUnreadCount: true })).filter((box) => (box.unreadCount ?? 0) > 0 && box.id && !excluded.has(box.id));
238
+ const unreadBoxes = args.limit && args.limit > 0 ? allUnread.slice(0, args.limit) : allUnread;
247
239
  const names = await resolveConversationNames(service, unreadBoxes.map((box) => box.id));
248
240
  const perChat = args.perChat ?? 10;
249
241
  const acc = createPhiAccumulator();
@@ -267,6 +259,12 @@ export async function handleGetUnreadDigest(service, args) {
267
259
  };
268
260
  }));
269
261
  const content = [{ type: 'text', text: toonText(digest) }];
262
+ if (unreadBoxes.length < allUnread.length) {
263
+ content.push({
264
+ type: 'text',
265
+ text: `Showing the ${unreadBoxes.length} most recently active of ${allUnread.length} unread conversations (limit=${args.limit}). Omit limit to list all.`,
266
+ });
267
+ }
270
268
  const note = phiNote(acc);
271
269
  if (note)
272
270
  content.push(note);
package/dist/mcp/tools.js CHANGED
@@ -115,7 +115,7 @@ export const TOOLS = [
115
115
  name: 'get_message_media',
116
116
  },
117
117
  {
118
- description: 'One-shot unread digest: every LINE conversation with unread messages, each with its most recent messages (default 10), E2EE-decrypted, with resolved sender names. Saves calling list_conversations then get_chat_messages per chat. Read-only: never marks anything read, never touches the search index; denylist-excluded conversations are omitted. Returns an empty list when nothing is unread — never fabricated.',
118
+ description: 'One-shot unread digest: every LINE conversation with unread messages (all pages of the inbox, newest activity first), each with its most recent messages (default 10), E2EE-decrypted, with resolved sender names. Saves calling list_conversations then get_chat_messages per chat. Read-only: never marks anything read, never touches the search index; denylist-excluded conversations are omitted. Returns an empty list when nothing is unread — never fabricated.',
119
119
  inputSchema: {
120
120
  type: 'object',
121
121
  properties: {
@@ -125,7 +125,7 @@ export const TOOLS = [
125
125
  },
126
126
  limit: {
127
127
  type: 'number',
128
- description: 'Maximum number of conversations to scan for unread (default 20).',
128
+ description: 'Maximum number of unread conversations to return, most recently active first (default: all of them).',
129
129
  },
130
130
  },
131
131
  },
@@ -26,6 +26,7 @@
26
26
  * messages + bigram search_text are always stored regardless of whether
27
27
  * embedding succeeded, so keyword search keeps working either way.
28
28
  */
29
+ import { listBoxesByRecency } from '../line/core/message-box-recency.js';
29
30
  import { resolveConversationNames, resolveSenderNames } from '../mcp/names.js';
30
31
  import { createCliLogger } from '../util/log.js';
31
32
  import { getExcludedChatIds } from './scope.js';
@@ -46,12 +47,7 @@ async function resolveTargetChats(service, chatIds) {
46
47
  if (chatIds && chatIds.length > 0) {
47
48
  return resolveConversationNames(service, chatIds);
48
49
  }
49
- const result = await service.client.getMessageBoxes({
50
- lastMessagesPerMessageBoxCount: 1,
51
- messageBoxCountLimit: DEFAULT_CHAT_LIMIT,
52
- withUnreadCount: false,
53
- });
54
- const boxes = result.messageBoxes || [];
50
+ const boxes = (await listBoxesByRecency(service, { withUnreadCount: false })).slice(0, DEFAULT_CHAT_LIMIT);
55
51
  const ids = boxes.map((box) => box.id).filter(Boolean);
56
52
  return resolveConversationNames(service, ids);
57
53
  }
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * virtual filesystem). MUST stay equal to the "version" field in
5
5
  * package.json — the test in version.test.ts enforces that.
6
6
  */
7
- export const YOMI_VERSION = '0.6.1';
7
+ export const YOMI_VERSION = '0.6.2';
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=22.13.0 <23.0.0 || >=23.4.0"
5
5
  },
6
- "version": "0.6.1",
6
+ "version": "0.6.2",
7
7
  "description": "Open-source personal LINE MCP server for AI agents. Read, reply, send images, and search LINE conversations locally with Letter-Sealing E2EE.",
8
8
  "type": "module",
9
9
  "license": "MIT",