@rikaidev/yomi 0.6.0 → 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.
- package/dist/auth/credential-store.js +10 -6
- package/dist/line/core/message-box-recency.js +27 -0
- package/dist/mcp/handlers/conversations.js +10 -12
- package/dist/mcp/integrator.js +21 -7
- package/dist/mcp/tools.js +2 -2
- package/dist/search/collector.js +2 -6
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/run.mjs +24 -0
|
@@ -200,11 +200,12 @@ export class CredentialStore {
|
|
|
200
200
|
this.filePath = fallbackFilePath;
|
|
201
201
|
this.cache = new Map();
|
|
202
202
|
this.loaded = false;
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
203
|
+
// The user-isolated credential file (0600 mode) is fast, reliable, and avoids
|
|
204
|
+
// triggering disruptive macOS SecurityAgent GUI prompts. Mutating the Keychain
|
|
205
|
+
// via `security` CLI is opt-in (via YOMI_USE_KEYCHAIN=1) rather than default.
|
|
206
206
|
this.secureStorageEnabled =
|
|
207
207
|
process.platform === 'darwin' &&
|
|
208
|
+
process.env.YOMI_USE_KEYCHAIN === '1' &&
|
|
208
209
|
process.env.YOMI_NO_KEYCHAIN !== '1' &&
|
|
209
210
|
process.env.YOMI_READ_ONLY_SESSION !== '1';
|
|
210
211
|
this.keychainService = this.secureStorageEnabled
|
|
@@ -233,8 +234,12 @@ export class CredentialStore {
|
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
236
|
catch { }
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
const keychain = this.keychainService ??
|
|
238
|
+
(process.platform === 'darwin' && process.env.YOMI_NO_KEYCHAIN !== '1'
|
|
239
|
+
? getKeychainService()
|
|
240
|
+
: null);
|
|
241
|
+
if (keychain) {
|
|
242
|
+
const result = await keychain.getCredential(this.keychainAccount);
|
|
238
243
|
if (result.success && result.password) {
|
|
239
244
|
try {
|
|
240
245
|
const fs = await import('node:fs/promises');
|
|
@@ -247,7 +252,6 @@ export class CredentialStore {
|
|
|
247
252
|
catch { }
|
|
248
253
|
return result.password;
|
|
249
254
|
}
|
|
250
|
-
return null;
|
|
251
255
|
}
|
|
252
256
|
return null;
|
|
253
257
|
}
|
|
@@ -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
|
|
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
|
|
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/integrator.js
CHANGED
|
@@ -79,19 +79,33 @@ description: Manage and interact with user's LINE account via Yomi E2EE MCP Serv
|
|
|
79
79
|
|
|
80
80
|
# Yomi LINE Copilot Skill
|
|
81
81
|
|
|
82
|
-
You have real-time access to the user's LINE account through the Yomi Letter-Sealing E2EE
|
|
82
|
+
You have real-time access to the user's LINE account through the Yomi Letter-Sealing E2EE server. Discover the current tools and complete a scoped read before claiming that access; the names below describe expected capabilities, not proof they are connected.
|
|
83
83
|
|
|
84
|
-
##
|
|
85
|
-
1. \`
|
|
86
|
-
2. \`
|
|
87
|
-
3. \`
|
|
88
|
-
4. \`
|
|
84
|
+
## Reading
|
|
85
|
+
1. \`list_conversations\`: List the chats the account can see.
|
|
86
|
+
2. \`get_chat_messages\`: Read the recent decrypted messages of one chat.
|
|
87
|
+
3. \`collect_messages\`: Collect messages across several chats.
|
|
88
|
+
4. \`search_messages\`: Full-text search across conversations.
|
|
89
|
+
5. \`get_unread_digest\`: Summarize what is unread.
|
|
90
|
+
6. \`get_insight\`: Ask the account model for an insight about the messages.
|
|
91
|
+
7. \`find_contact\` and \`list_contacts\`: Resolve a person or a group to its chat id.
|
|
92
|
+
|
|
93
|
+
## Writing
|
|
94
|
+
1. \`send_message\`: Send a text message to a chat.
|
|
95
|
+
2. \`unsend_message\`: Recall a message the account sent.
|
|
96
|
+
|
|
97
|
+
## Operating the server
|
|
98
|
+
- The Model Context Protocol server speaks stdio and lives only while its stdin stays open. A command that closes stdin ends the server, so keep the pipe open for as long as the session needs it.
|
|
99
|
+
- The \`yomi messages\` CLI reports an empty list; only the Model Context Protocol tools above return real messages.
|
|
100
|
+
- \`unsend_message\` is irreversible, and LINE accepts a recall only within one hour of sending. A recall past that window is refused by LINE itself, not by the tool.
|
|
89
101
|
|
|
90
102
|
## Guidelines
|
|
91
103
|
- Summarize conversations concisely and highlight action items or mentions needing attention.
|
|
92
|
-
-
|
|
104
|
+
- Drafting does not authorize sending. Require explicit sending authorization for the recipient and content; reuse that exact authorization when already given.
|
|
105
|
+
- A recall writes to the conversation as well, so it needs the same explicit authorization as sending.
|
|
93
106
|
`;
|
|
94
107
|
const skillDirs = [
|
|
108
|
+
join(home, '.agents', 'skills', 'yomi-line-manager'),
|
|
95
109
|
join(home, '.claude', 'skills', 'yomi-line-manager'),
|
|
96
110
|
join(home, '.gemini', 'antigravity-cli', 'builtin', 'skills', 'yomi-line-manager'),
|
|
97
111
|
join(home, '.codex', 'skills', 'yomi-line-manager'),
|
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
|
|
128
|
+
description: 'Maximum number of unread conversations to return, most recently active first (default: all of them).',
|
|
129
129
|
},
|
|
130
130
|
},
|
|
131
131
|
},
|
package/dist/search/collector.js
CHANGED
|
@@ -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
|
|
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
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.
|
|
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",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"desktop:windows:qc": "npm run typecheck && npm run lint && npm run lint:comments && cargo test --manifest-path desktop/Cargo.toml && npm run desktop:windows:lint",
|
|
68
68
|
"desktop:windows:i18n": "node scripts/windows-i18n-harness.mjs",
|
|
69
69
|
"desktop:windows:harness": "node scripts/windows-desktop-harness.mjs",
|
|
70
|
-
"version": "node scripts/sync-version.mjs && git add src/version.ts",
|
|
70
|
+
"version": "node scripts/sync-version.mjs && git add src/version.ts desktop/mac-native/Info.plist desktop/Cargo.toml",
|
|
71
71
|
"prepublishOnly": "npm run build",
|
|
72
72
|
"test:fixture-hygiene": "node scripts/test-fixture-hygiene.mjs",
|
|
73
73
|
"desktop:install-local": "bash scripts/install-swift-desktop-local.sh"
|
package/run.mjs
CHANGED
|
@@ -268,6 +268,29 @@ switch (cmd) {
|
|
|
268
268
|
}));
|
|
269
269
|
process.exit(0);
|
|
270
270
|
}
|
|
271
|
+
case 'unsend-message':
|
|
272
|
+
case 'unsend': {
|
|
273
|
+
const messageId = args[1];
|
|
274
|
+
if (!messageId) {
|
|
275
|
+
console.error(JSON.stringify({ error: 'messageId required' }));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
const { LineProtocolService } = await load('line/core/service');
|
|
279
|
+
const service = new LineProtocolService();
|
|
280
|
+
if (!(await service.resumeSession())) process.exit(1);
|
|
281
|
+
try {
|
|
282
|
+
const res = await service.unsendMessage(messageId);
|
|
283
|
+
console.log(JSON.stringify({
|
|
284
|
+
unsent: true,
|
|
285
|
+
messageId,
|
|
286
|
+
...res,
|
|
287
|
+
}));
|
|
288
|
+
process.exit(0);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error(JSON.stringify({ error: err.message || String(err) }));
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
271
294
|
case 'send-image': {
|
|
272
295
|
const chatId = args[1];
|
|
273
296
|
const imagePath = args[2];
|
|
@@ -419,6 +442,7 @@ Usage:
|
|
|
419
442
|
yomi Start the MCP stdio server (default)
|
|
420
443
|
yomi serve Same as above, explicit
|
|
421
444
|
yomi login [...] Run the passwordless login flow in a terminal
|
|
445
|
+
yomi unsend <id> Retract (unsend) a message sent within the last hour
|
|
422
446
|
yomi help Show this help
|
|
423
447
|
yomi version Print version
|
|
424
448
|
|