@rikaidev/yomi 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/dist/attention/policy.js +95 -0
- package/dist/auth/credential-store.js +32 -11
- package/dist/auth/keychain.js +7 -54
- package/dist/cli/query.js +668 -0
- package/dist/insight/relationships.js +1 -1
- package/dist/line/auth/protocol/pwless-login-flow.js +12 -0
- package/dist/line/auth/pwless/index.js +1 -1
- package/dist/line/client/index.js +0 -2
- package/dist/line/client/obs-media-client.js +37 -0
- package/dist/line/client/parsers.js +1 -0
- package/dist/line/client/sticker-meta.js +32 -0
- package/dist/line/client/sync-service/client.js +31 -3
- package/dist/line/client/talk-service/contact-query.js +8 -1
- package/dist/line/client/talk-service/message-box-query.js +17 -0
- package/dist/line/client/talk-service/query-client.js +32 -26
- package/dist/line/client/talk-service/requests.js +1 -11
- package/dist/line/core/auth-session-runtime.js +16 -1
- package/dist/line/core/chat-event.js +33 -0
- package/dist/line/core/chat-runtime-service.js +0 -20
- package/dist/line/core/directory-runtime-service.js +10 -2
- package/dist/line/core/e2ee/recovery/bootstrap-keys.js +8 -16
- package/dist/line/core/flex-message.js +130 -0
- package/dist/line/core/mention.js +1 -1
- package/dist/line/core/message-preview.js +60 -0
- package/dist/line/core/message-query-service.js +0 -20
- package/dist/line/core/service.js +1 -2
- package/dist/mcp/handlers/conversations.js +37 -4
- package/dist/mcp/handlers/login.js +1 -1
- package/dist/mcp/handlers/message-actions.js +1 -1
- package/dist/mcp/handlers/search.js +1 -1
- package/dist/mcp/handlers/shared.js +1 -1
- package/dist/mcp/integrator.js +168 -0
- package/dist/mcp/media.js +34 -2
- package/dist/mcp/phi-guard.js +2 -2
- package/dist/search/bigram.js +2 -2
- package/dist/search/collector.js +1 -1
- package/dist/search/embedder.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/run.mjs +218 -2
package/README.md
CHANGED
|
@@ -479,6 +479,11 @@ fake success.
|
|
|
479
479
|
this device a sign-in prompt — this is the single most common reason a first login
|
|
480
480
|
appears to hang.
|
|
481
481
|
|
|
482
|
+
> **⚠️ Single Desktop Session Limit:** LINE allows only **one desktop client session at a time**
|
|
483
|
+
> per account. Because Yomi connects as a desktop secondary device (`DESKTOPMAC`), logging into Yomi
|
|
484
|
+
> will sign out your official LINE Desktop app (and signing back into LINE Desktop will invalidate
|
|
485
|
+
> Yomi's session). You cannot use Yomi and the official LINE Desktop client simultaneously.
|
|
486
|
+
|
|
482
487
|
You only need to give the agent your phone number in E.164 form — it supplies the
|
|
483
488
|
region itself (e.g. `TW` for a `+886` number) when it calls the tool.
|
|
484
489
|
|
|
@@ -516,16 +521,16 @@ entirely**.
|
|
|
516
521
|
### Sessions and credentials
|
|
517
522
|
|
|
518
523
|
Yomi owns its own login. On startup it calls `resumeSession()` once, reading the
|
|
519
|
-
LINE session from the macOS Keychain (service `
|
|
524
|
+
LINE session from the macOS Keychain (service `dev.rikai.yomi.credentials`, account
|
|
520
525
|
`line`) and silently refreshing the token if needed.
|
|
521
526
|
|
|
522
527
|
- **First-party credentials.** The passwordless login persists the auth token,
|
|
523
528
|
refresh token, certificate, MID, and the E2EE keypair itself — then reads them
|
|
524
529
|
back to verify the write actually landed. A login that can't be persisted fails
|
|
525
530
|
loudly at login, not silently at the next restart.
|
|
526
|
-
- **
|
|
527
|
-
|
|
528
|
-
|
|
531
|
+
- **Shared session.** The session is stored in the canonical `dev.rikai.yomi.credentials`
|
|
532
|
+
keychain entry (or local credential store), allowing Yomi MCP and Yomi Desktop to
|
|
533
|
+
share the exact same LINE login session seamlessly.
|
|
529
534
|
- **Platform note.** On macOS the session lives in the login Keychain. On **Linux
|
|
530
535
|
and Windows** Yomi currently falls back to a local JSON file — functional, but
|
|
531
536
|
less protected than an OS secret store, and less exercised than the macOS path.
|
|
@@ -590,6 +595,15 @@ Yomi runs as a **secondary device** on your account. That shapes what it can see
|
|
|
590
595
|
per-epoch group keys, not a bug here. It can also cut sideways: the epoch your phone
|
|
591
596
|
holds and the epoch Yomi holds need not be the same, so the **two devices can each
|
|
592
597
|
read a different slice** of the very same group.
|
|
598
|
+
- **Installing Yomi does not itself rotate anything, so "before I installed Yomi" is
|
|
599
|
+
the wrong boundary.** Yomi only ever *resolves* an existing group key; it never
|
|
600
|
+
registers (mints) one, because minting rotates the group's shared secret for every
|
|
601
|
+
member and strands every message encrypted under the previous one. So a fresh install
|
|
602
|
+
picks up the **current** key and reads back to the last rotation — which may predate
|
|
603
|
+
the install by months. The boundary is the last rekey, not your install date. And
|
|
604
|
+
once Yomi has seen an epoch it keeps that key, so a rotation *after* install leaves
|
|
605
|
+
it able to read both sides. If a group comes back undecryptable all the way to
|
|
606
|
+
**today**, that is not this limitation — please report it.
|
|
593
607
|
- **1:1 media** uses the account-level E2EE keychain, which a secondary device fully
|
|
594
608
|
possesses (LINE syncs it during pairing) — Yomi **can** decrypt 1:1 images/files it
|
|
595
609
|
can see.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getDefaultEmbedder } from '../search/default-embedder.js';
|
|
2
|
+
// Natural-language intent descriptions, not trigger-word lists. The local
|
|
3
|
+
// transformer embeds both descriptions and messages; vector similarity does
|
|
4
|
+
// the classification even when no words literally overlap.
|
|
5
|
+
const INTENT_ANCHORS = {
|
|
6
|
+
reply: [
|
|
7
|
+
'對方直接向我提出問題、請求協助或要求我做決定,正在等待我的答覆',
|
|
8
|
+
'工作交辦或需要我採取具體行動並回覆結果',
|
|
9
|
+
],
|
|
10
|
+
deadline: [
|
|
11
|
+
'有明確期限、會議時間、預約或即將到期的工作,需要及時處理',
|
|
12
|
+
'緊急事故、風險或延誤,若不處理會造成後果',
|
|
13
|
+
],
|
|
14
|
+
transaction: [
|
|
15
|
+
'與我有關的帳單、付款、訂單、物流、航班、預約或帳號安全狀態異常',
|
|
16
|
+
'交易失敗、行程變更或服務中斷,需要本人確認',
|
|
17
|
+
],
|
|
18
|
+
important: [
|
|
19
|
+
'與目前工作或生活直接相關的重要進度與公告,值得本人閱讀但不必回覆',
|
|
20
|
+
'關鍵人物提供需要掌握的決策背景或狀態更新',
|
|
21
|
+
],
|
|
22
|
+
ordinary: [
|
|
23
|
+
'一般聊天、問候、貼圖、轉傳或沒有下一步的日常資訊',
|
|
24
|
+
'內容與本人沒有直接關係,也不需要採取行動',
|
|
25
|
+
],
|
|
26
|
+
promotion: [
|
|
27
|
+
'品牌官方帳號寄送的行銷廣告、優惠活動、導購內容或宣傳文案',
|
|
28
|
+
'用吸引人的疑問句包裝促銷活動,但並非真的在等待本人回答',
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
function dot(a, b) {
|
|
32
|
+
let value = 0;
|
|
33
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++)
|
|
34
|
+
value += a[i] * b[i];
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
/** Transformer-embedding semantic classification followed only by explicit
|
|
38
|
+
* product controls such as mute, mention and manual priority. */
|
|
39
|
+
export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder()) {
|
|
40
|
+
if (inputs.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
const intents = Object.keys(INTENT_ANCHORS);
|
|
43
|
+
const anchors = intents.flatMap((intent) => INTENT_ANCHORS[intent].map((text) => ({ intent, text })));
|
|
44
|
+
const vectors = await embedder.embed([
|
|
45
|
+
...anchors.map((anchor) => anchor.text),
|
|
46
|
+
...inputs.map((input) => input.text || '無文字訊息'),
|
|
47
|
+
]);
|
|
48
|
+
const anchorVectors = vectors.slice(0, anchors.length);
|
|
49
|
+
const messageVectors = vectors.slice(anchors.length);
|
|
50
|
+
return inputs.map((input, inputIndex) => {
|
|
51
|
+
if (input.isMuted)
|
|
52
|
+
return { score: -100, tier: 'filtered', reason: '已靜音' };
|
|
53
|
+
if (input.explicitPriority)
|
|
54
|
+
return { score: 70, tier: 'now', reason: '使用者標記優先' };
|
|
55
|
+
if (input.hasMention)
|
|
56
|
+
return { score: 65, tier: 'now', reason: '直接提及你' };
|
|
57
|
+
const similarities = new Map();
|
|
58
|
+
anchors.forEach((anchor, anchorIndex) => {
|
|
59
|
+
const similarity = dot(messageVectors[inputIndex], anchorVectors[anchorIndex]);
|
|
60
|
+
similarities.set(anchor.intent, Math.max(similarities.get(anchor.intent) ?? -Infinity, similarity));
|
|
61
|
+
});
|
|
62
|
+
const ranked = [...intents].sort((a, b) => (similarities.get(b) ?? 0) - (similarities.get(a) ?? 0));
|
|
63
|
+
let intent = ranked[0];
|
|
64
|
+
// Broadcast accounts commonly announce dated events and use rhetorical
|
|
65
|
+
// questions. Only a transaction/account/service state tied to the user may
|
|
66
|
+
// elevate them; generic events are not the user's deadline.
|
|
67
|
+
if (input.isOfficial && intent !== 'transaction')
|
|
68
|
+
intent = 'promotion';
|
|
69
|
+
const scoreByIntent = {
|
|
70
|
+
reply: 55,
|
|
71
|
+
deadline: 60,
|
|
72
|
+
transaction: 50,
|
|
73
|
+
important: 25,
|
|
74
|
+
ordinary: 0,
|
|
75
|
+
promotion: -40,
|
|
76
|
+
};
|
|
77
|
+
const score = scoreByIntent[intent] + (input.unreadCount > 0 ? 5 : 0);
|
|
78
|
+
const tier = score >= 55
|
|
79
|
+
? 'now'
|
|
80
|
+
: score >= 35
|
|
81
|
+
? 'today'
|
|
82
|
+
: score >= 20
|
|
83
|
+
? 'know'
|
|
84
|
+
: 'filtered';
|
|
85
|
+
const reasons = {
|
|
86
|
+
reply: '對方可能在等你回覆',
|
|
87
|
+
deadline: '有期限、行程或緊急事項',
|
|
88
|
+
transaction: '與你的交易或服務狀態有關',
|
|
89
|
+
important: '重要資訊,但不需立即回覆',
|
|
90
|
+
ordinary: '一般資訊,沒有明確下一步',
|
|
91
|
+
promotion: '官方宣傳或與你較無關',
|
|
92
|
+
};
|
|
93
|
+
return { score, tier, reason: reasons[intent] };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
* rotated auth token during `resumeSession`), not only by a direct login.
|
|
14
14
|
*
|
|
15
15
|
* Keychain account: namespace (e.g. 'line')
|
|
16
|
-
* KeychainService service name: see keychain.ts —
|
|
17
|
-
*
|
|
18
|
-
* as a fallback and migrated forward, never written to or deleted.
|
|
16
|
+
* KeychainService service name: see keychain.ts — stored in Yomi's canonical
|
|
17
|
+
* namespace (`dev.rikai.yomi.credentials`), shared across Yomi MCP and Yomi Desktop.
|
|
19
18
|
*
|
|
20
19
|
* The Keychain is machine-global: it is NOT scoped by YOMI_DATA_DIR, so on
|
|
21
20
|
* macOS an automated run that only redirected the data dir would still find —
|
|
@@ -197,8 +196,13 @@ export class CredentialStore {
|
|
|
197
196
|
this.filePath = fallbackFilePath;
|
|
198
197
|
this.cache = new Map();
|
|
199
198
|
this.loaded = false;
|
|
199
|
+
// Desktop background bridges are deliberately read-only. They must use
|
|
200
|
+
// the user-isolated credential file and never invoke `security` to mutate
|
|
201
|
+
// the Keychain during a 15s poll (which causes repeated macOS prompts).
|
|
200
202
|
this.secureStorageEnabled =
|
|
201
|
-
process.platform === 'darwin' &&
|
|
203
|
+
process.platform === 'darwin' &&
|
|
204
|
+
process.env.YOMI_NO_KEYCHAIN !== '1' &&
|
|
205
|
+
process.env.YOMI_READ_ONLY_SESSION !== '1';
|
|
202
206
|
this.keychainService = this.secureStorageEnabled
|
|
203
207
|
? getKeychainService()
|
|
204
208
|
: null;
|
|
@@ -217,20 +221,31 @@ export class CredentialStore {
|
|
|
217
221
|
* @returns Serialized credential blob or null when missing.
|
|
218
222
|
*/
|
|
219
223
|
async readPersistedBlob() {
|
|
224
|
+
try {
|
|
225
|
+
const fs = await import('node:fs/promises');
|
|
226
|
+
const fileData = await fs.readFile(this.filePath, 'utf-8');
|
|
227
|
+
if (fileData?.trim()) {
|
|
228
|
+
return fileData;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch { }
|
|
220
232
|
if (this.secureStorageEnabled && this.keychainService) {
|
|
221
233
|
const result = await this.keychainService.getCredential(this.keychainAccount);
|
|
222
234
|
if (result.success && result.password) {
|
|
235
|
+
try {
|
|
236
|
+
const fs = await import('node:fs/promises');
|
|
237
|
+
await fs.mkdir(dirname(this.filePath), {
|
|
238
|
+
recursive: true,
|
|
239
|
+
mode: 0o700,
|
|
240
|
+
});
|
|
241
|
+
await fs.writeFile(this.filePath, result.password, { mode: 0o600 });
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
223
244
|
return result.password;
|
|
224
245
|
}
|
|
225
246
|
return null;
|
|
226
247
|
}
|
|
227
|
-
|
|
228
|
-
const fs = await import('node:fs/promises');
|
|
229
|
-
return await fs.readFile(this.filePath, 'utf-8');
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
248
|
+
return null;
|
|
234
249
|
}
|
|
235
250
|
/**
|
|
236
251
|
* Load all credentials into cache from keychain or file.
|
|
@@ -263,6 +278,12 @@ export class CredentialStore {
|
|
|
263
278
|
}
|
|
264
279
|
if (this.secureStorageEnabled && this.keychainService) {
|
|
265
280
|
await this.keychainService.setCredential(this.keychainAccount, blob);
|
|
281
|
+
try {
|
|
282
|
+
const fs = await import('node:fs/promises');
|
|
283
|
+
await fs.mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
284
|
+
await fs.writeFile(this.filePath, blob, { mode: 0o600 });
|
|
285
|
+
}
|
|
286
|
+
catch { }
|
|
266
287
|
this.lastPersistedBlob = blob;
|
|
267
288
|
return;
|
|
268
289
|
}
|
package/dist/auth/keychain.js
CHANGED
|
@@ -8,20 +8,12 @@
|
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
import { createCliLogger } from '../util/log.js';
|
|
10
10
|
/**
|
|
11
|
-
* Yomi's
|
|
11
|
+
* Yomi's Keychain service name. Yomi performs a first-party
|
|
12
12
|
* passwordless LINE login (see the `login` MCP tool) and persists the
|
|
13
13
|
* resulting session here — auth token, certificate, refresh token, mid,
|
|
14
|
-
* and the E2EE NaCl keypair.
|
|
14
|
+
* and the E2EE NaCl keypair. Shared between Yomi MCP and Yomi Desktop.
|
|
15
15
|
*/
|
|
16
|
-
const SERVICE_NAME = '
|
|
17
|
-
/**
|
|
18
|
-
* Legacy keychain service name, read-only. Predates Yomi owning its own
|
|
19
|
-
* credentials; kept solely so a live session created before this split
|
|
20
|
-
* keeps working without forcing the operator through a phone-PIN
|
|
21
|
-
* re-login. Entries here are read and migrated forward into
|
|
22
|
-
* `SERVICE_NAME`, never written to and never deleted.
|
|
23
|
-
*/
|
|
24
|
-
const LEGACY_SERVICE_NAME = 'com.inboxd.credentials';
|
|
16
|
+
const SERVICE_NAME = 'dev.rikai.yomi.credentials';
|
|
25
17
|
const authLog = createCliLogger('AUTH');
|
|
26
18
|
/**
|
|
27
19
|
* Wrapper for macOS `security` keychain CLI commands.
|
|
@@ -49,6 +41,7 @@ class KeychainCommands {
|
|
|
49
41
|
'-w',
|
|
50
42
|
password,
|
|
51
43
|
'-U',
|
|
44
|
+
'-A',
|
|
52
45
|
]);
|
|
53
46
|
let stderr = '';
|
|
54
47
|
child.stderr.on('data', (data) => {
|
|
@@ -166,20 +159,12 @@ class KeychainCommands {
|
|
|
166
159
|
/**
|
|
167
160
|
* Service for managing macOS Keychain credentials.
|
|
168
161
|
*
|
|
169
|
-
*
|
|
170
|
-
* namespace and migrate a hit forward. Writes and deletes only ever touch
|
|
171
|
-
* the canonical namespace — the legacy entry is never deleted.
|
|
162
|
+
* Direct reads, writes, and deletes on the canonical Yomi namespace.
|
|
172
163
|
*/
|
|
173
164
|
export class KeychainService {
|
|
174
165
|
commands;
|
|
175
|
-
legacyCommands;
|
|
176
|
-
// Accounts whose canonical entry this process has already seen (read or
|
|
177
|
-
// written). Once seen, a later read miss is treated as transient and must
|
|
178
|
-
// NOT fall back to the legacy namespace — see getCredential.
|
|
179
|
-
canonicalSeen = new Set();
|
|
180
166
|
constructor() {
|
|
181
167
|
this.commands = new KeychainCommands(SERVICE_NAME);
|
|
182
|
-
this.legacyCommands = new KeychainCommands(LEGACY_SERVICE_NAME);
|
|
183
168
|
}
|
|
184
169
|
/**
|
|
185
170
|
* Stores a credential in Keychain.
|
|
@@ -189,52 +174,20 @@ export class KeychainService {
|
|
|
189
174
|
* @returns Promise resolving to credential result.
|
|
190
175
|
*/
|
|
191
176
|
async setCredential(account, password) {
|
|
192
|
-
|
|
193
|
-
// this is a single atomic keychain op. Do NOT delete-then-add: that leaves
|
|
194
|
-
// a window where the entry is absent, during which a concurrent read falls
|
|
195
|
-
// through to the legacy namespace and resurrects stale credentials over the
|
|
196
|
-
// current ones (the root of the "session silently logged out" bug).
|
|
197
|
-
const result = await this.commands.set(account, password);
|
|
198
|
-
if (result.success) {
|
|
199
|
-
this.canonicalSeen.add(account);
|
|
200
|
-
}
|
|
201
|
-
return result;
|
|
177
|
+
return await this.commands.set(account, password);
|
|
202
178
|
}
|
|
203
179
|
/**
|
|
204
180
|
* Retrieves a credential from Keychain.
|
|
205
181
|
*
|
|
206
|
-
* Tries the canonical namespace first. Only if this process has NEVER seen
|
|
207
|
-
* the canonical entry does it fall back to the legacy namespace and migrate a
|
|
208
|
-
* hit forward (one-time). Once the canonical entry has been seen, a later
|
|
209
|
-
* miss is transient (item mid-update) and returns the miss as-is rather than
|
|
210
|
-
* resurrecting a stale legacy copy on top of the live session.
|
|
211
|
-
*
|
|
212
182
|
* @param account - Account name.
|
|
213
183
|
* @returns Promise resolving to credential result.
|
|
214
184
|
*/
|
|
215
185
|
async getCredential(account) {
|
|
216
|
-
|
|
217
|
-
if (primary.success) {
|
|
218
|
-
this.canonicalSeen.add(account);
|
|
219
|
-
return primary;
|
|
220
|
-
}
|
|
221
|
-
if (this.canonicalSeen.has(account)) {
|
|
222
|
-
return primary;
|
|
223
|
-
}
|
|
224
|
-
const legacy = await this.legacyCommands.get(account);
|
|
225
|
-
if (legacy.success && legacy.password) {
|
|
226
|
-
await this.commands.set(account, legacy.password);
|
|
227
|
-
this.canonicalSeen.add(account);
|
|
228
|
-
authLog.info('keychain.migrated_from_legacy', { account });
|
|
229
|
-
}
|
|
230
|
-
return legacy;
|
|
186
|
+
return await this.commands.get(account);
|
|
231
187
|
}
|
|
232
188
|
/**
|
|
233
189
|
* Deletes a credential from Keychain.
|
|
234
190
|
*
|
|
235
|
-
* Operates on the canonical namespace only. The legacy namespace is
|
|
236
|
-
* never deleted, even here.
|
|
237
|
-
*
|
|
238
191
|
* @param account - Account name.
|
|
239
192
|
* @returns Promise resolving to credential result.
|
|
240
193
|
*/
|