@rikaidev/yomi 0.2.6 → 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.
- package/README.md +14 -4
- package/dist/auth/credential-store.js +0 -34
- package/dist/insight/graph.js +90 -0
- package/dist/insight/index.js +61 -0
- package/dist/insight/pending.js +92 -0
- package/dist/insight/relationships.js +125 -0
- package/dist/insight/rhythm.js +50 -0
- package/dist/line/auth/protocol/index.js +0 -2
- package/dist/line/auth/pwless/index.js +1 -1
- package/dist/line/client/index.js +1 -1
- package/dist/line/client/talk-service/chat-query.js +3 -3
- package/dist/line/client/talk-service/contact-query.js +1 -1
- package/dist/line/client/talk-service/requests.js +1 -1
- package/dist/line/core/e2ee/crypto/crypto-primitives.js +2 -15
- package/dist/line/core/e2ee/index.js +0 -1
- package/dist/line/core/e2ee/keys/decrypt.js +0 -1
- package/dist/line/core/e2ee/keys/group-key.js +0 -1
- package/dist/line/core/e2ee/keys/key-payload.js +1 -1
- package/dist/line/core/e2ee/keys/message-decrypt.js +1 -1
- package/dist/line/core/e2ee/keys/peer-key.js +1 -1
- package/dist/line/core/media-decrypt.js +1 -1
- package/dist/line/core/mention.js +1 -1
- package/dist/line/core/message-query-service.js +2 -2
- package/dist/line/core/session-state/persist.js +1 -1
- package/dist/line/core/thrift/binary/type-codec-dsl.js +1 -1
- package/dist/line/core/thrift/compact/type-codec-dsl.js +1 -43
- package/dist/line/core/thrift/fields/builders.js +1 -11
- package/dist/line/core/thrift/index.js +1 -6
- package/dist/line/core/thrift/primitives/varint.js +1 -1
- package/dist/line/core/thrift/types.js +0 -6
- package/dist/mcp/handlers/index.js +1 -0
- package/dist/mcp/handlers/insight.js +129 -0
- package/dist/mcp/handlers/shared.js +2 -2
- package/dist/mcp/media.js +2 -2
- package/dist/mcp/names.js +1 -1
- package/dist/mcp/server.js +7 -4
- package/dist/mcp/tools.js +17 -0
- package/dist/search/collector.js +1 -1
- package/dist/search/store.js +31 -14
- package/dist/util/data-dir.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/dist/line/core/e2ee/types.js +0 -4
- package/dist/line/core/store.js +0 -8
- package/dist/line/core/thrift/binary/binary-reader.js +0 -151
- package/dist/line/core/thrift/compact/compact-reader.js +0 -180
- package/dist/line/core/thrift/compact/compact-writer.js +0 -161
|
@@ -24,7 +24,7 @@ async function deriveLineMediaKeyMaterial(keyMaterial) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
/** Thrown when a media object's MAC does not match its ciphertext. */
|
|
27
|
-
|
|
27
|
+
class MediaMacVerificationError extends Error {
|
|
28
28
|
constructor(message) {
|
|
29
29
|
super(message);
|
|
30
30
|
this.name = 'MediaMacVerificationError';
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* @param mentions - Proposed mentions.
|
|
32
32
|
* @throws Error describing the first invalid mention or overlap found.
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
function validateMentions(text, mentions) {
|
|
35
35
|
const sorted = [...mentions].sort((a, b) => a.start - b.start);
|
|
36
36
|
for (const mention of sorted) {
|
|
37
37
|
const { mid, start, end } = mention;
|
|
@@ -27,7 +27,7 @@ function inferLineToType(chatId) {
|
|
|
27
27
|
* @param chatId - Requested LINE chat MID.
|
|
28
28
|
* @returns Message with stable chat context.
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
function normalizeLineMessageContext(message, chatId = null) {
|
|
31
31
|
const resolvedChatId = message?.chatMid || chatId || null;
|
|
32
32
|
const inferredToType = inferLineToType(resolvedChatId);
|
|
33
33
|
return {
|
|
@@ -97,7 +97,7 @@ export async function decryptLineMessage(e2eeManager, message, chatId = null) {
|
|
|
97
97
|
* @param chatId - Optional chat id to attach to each message.
|
|
98
98
|
* @returns Messages with decrypted text flags when available.
|
|
99
99
|
*/
|
|
100
|
-
|
|
100
|
+
async function decryptLineMessages(e2eeManager, messages, chatId = null) {
|
|
101
101
|
return Promise.all(messages.map((message) => decryptLineMessage(e2eeManager, message, chatId)));
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
@@ -11,7 +11,7 @@ const persistLog = createCliLogger('SESSION-PERSIST');
|
|
|
11
11
|
* @param value - Raw credential value.
|
|
12
12
|
* @returns Persistable string value.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
function toPersistedKeyValue(value) {
|
|
15
15
|
if (!value) {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
@@ -1,37 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export const COMPACT_TYPE_CODEC_DEFINITIONS = [
|
|
3
|
-
{
|
|
4
|
-
type: THRIFT_TYPE.BOOL,
|
|
5
|
-
readHandler: 'readBoolValue',
|
|
6
|
-
writeHandler: 'writeBoolValue',
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
type: THRIFT_TYPE.BYTE,
|
|
10
|
-
readHandler: 'readByteValue',
|
|
11
|
-
writeHandler: 'writeByteValue',
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
type: THRIFT_TYPE.I16,
|
|
15
|
-
readHandler: 'readI16Value',
|
|
16
|
-
writeHandler: 'writeI16Value',
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
type: THRIFT_TYPE.I32,
|
|
20
|
-
readHandler: 'readI32Value',
|
|
21
|
-
writeHandler: 'writeI32Value',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
type: THRIFT_TYPE.I64,
|
|
25
|
-
readHandler: 'readI64Value',
|
|
26
|
-
writeHandler: 'writeI64Value',
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
type: THRIFT_TYPE.STRING,
|
|
30
|
-
readHandler: 'readStringValue',
|
|
31
|
-
writeHandler: 'writeStringValue',
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
export const COMPACT_STRUCT_TYPE_DEFINITIONS = [
|
|
1
|
+
const COMPACT_STRUCT_TYPE_DEFINITIONS = [
|
|
35
2
|
{ type: 0, handler: 'readNullValue' },
|
|
36
3
|
{ type: 1, handler: 'readTrueValue' },
|
|
37
4
|
{ type: 2, handler: 'readFalseValue' },
|
|
@@ -46,15 +13,6 @@ export const COMPACT_STRUCT_TYPE_DEFINITIONS = [
|
|
|
46
13
|
{ type: 11, handler: 'readMapValue' },
|
|
47
14
|
{ type: 12, handler: 'readStructValue' },
|
|
48
15
|
];
|
|
49
|
-
/**
|
|
50
|
-
* Resolve one compact thrift type into its configured codec definition.
|
|
51
|
-
*
|
|
52
|
-
* @param type - Thrift wire type.
|
|
53
|
-
* @returns Codec definition, if defined.
|
|
54
|
-
*/
|
|
55
|
-
export function getCompactTypeCodecDefinition(type) {
|
|
56
|
-
return (COMPACT_TYPE_CODEC_DEFINITIONS.find((candidate) => candidate.type === type) ?? null);
|
|
57
|
-
}
|
|
58
16
|
/**
|
|
59
17
|
* Resolve one compact struct field type into its configured reader handler.
|
|
60
18
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @param value - Field value.
|
|
10
10
|
* @returns Thrift field tuple.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
function field(type, fieldId, value) {
|
|
13
13
|
return [type, fieldId, value];
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
@@ -32,16 +32,6 @@ export function boolField(fieldId, value) {
|
|
|
32
32
|
export function byteField(fieldId, value) {
|
|
33
33
|
return field(3, fieldId, value);
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
* Build an I16 field tuple.
|
|
37
|
-
*
|
|
38
|
-
* @param fieldId - Thrift field ID.
|
|
39
|
-
* @param value - I16 value.
|
|
40
|
-
* @returns Thrift field tuple.
|
|
41
|
-
*/
|
|
42
|
-
export function i16Field(fieldId, value) {
|
|
43
|
-
return field(6, fieldId, value);
|
|
44
|
-
}
|
|
45
35
|
/**
|
|
46
36
|
* Build an I32 field tuple.
|
|
47
37
|
*
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thrift - exports all modules
|
|
3
3
|
*/
|
|
4
|
-
export {
|
|
5
|
-
export { TBinaryWriter } from './binary/binary-writer.js';
|
|
6
|
-
export { TCompactReader } from './compact/compact-reader.js';
|
|
7
|
-
export { TCompactWriter } from './compact/compact-writer.js';
|
|
8
|
-
export { boolField, byteField, field, i16Field, i32Field, i64Field, listField, mapField, setField, stringField, structField, } from './fields/builders.js';
|
|
4
|
+
export { boolField, byteField, i32Field, i64Field, listField, mapField, setField, stringField, structField, } from './fields/builders.js';
|
|
9
5
|
export { decodeResponseMessage } from './message/decoder.js';
|
|
10
6
|
export { encodeCallMessage } from './message/encoder.js';
|
|
11
7
|
export { readStructFields } from './struct/reader.js';
|
|
12
|
-
export { MSG_TYPE, THRIFT_TYPE } from './types.js';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { encode } from '@toon-format/toon';
|
|
2
|
+
import { computeInsights } from '../../insight/index.js';
|
|
3
|
+
import { getDefaultEmbedder } from '../../search/default-embedder.js';
|
|
4
|
+
import { getExcludedChatIds } from '../../search/scope.js';
|
|
5
|
+
import { createPhiAccumulator, maskInto, phiNote } from '../phi-guard.js';
|
|
6
|
+
/**
|
|
7
|
+
* get_insight — return structured attention context for the agent to judge.
|
|
8
|
+
*
|
|
9
|
+
* This is a first-stage tool: it does not decide what needs attention, it
|
|
10
|
+
* assembles a compact context network that can be computed without reading
|
|
11
|
+
* illocutionary force. Nodes are cross-conversation connectors and
|
|
12
|
+
* conversations; `open` are directed edges (last speaker → addressee) for
|
|
13
|
+
* threads whose latest message is not yours, each with reply-rhythm, overdue
|
|
14
|
+
* ratio, a boilerplate-shape hint, an address hint, and a message-id pointer —
|
|
15
|
+
* but no thread text. The agent dereferences the pointer (get_chat_messages)
|
|
16
|
+
* only for the edges it chooses to judge, and decides open-request vs
|
|
17
|
+
* closing-ack, a judgement the topic embedding cannot make. See ../../insight
|
|
18
|
+
* for why the split is drawn exactly there.
|
|
19
|
+
*
|
|
20
|
+
* Denylist-excluded chats (../../search/scope) are removed from every section.
|
|
21
|
+
*
|
|
22
|
+
* @param service - Live LINE service (used for the owner's identity).
|
|
23
|
+
* @param args - Optional focus chat and lookback window.
|
|
24
|
+
* @returns MCP tool result: the JSON context package plus any PHI-masking note.
|
|
25
|
+
*/
|
|
26
|
+
export async function handleGetInsight(service, args) {
|
|
27
|
+
const pkg = computeInsights({
|
|
28
|
+
focusChatId: args.chatId,
|
|
29
|
+
sinceHours: args.sinceHours,
|
|
30
|
+
selfMid: service.profile?.mid ?? null,
|
|
31
|
+
embedder: getDefaultEmbedder(),
|
|
32
|
+
});
|
|
33
|
+
const excluded = getExcludedChatIds();
|
|
34
|
+
const filtered = {
|
|
35
|
+
window: pkg.window,
|
|
36
|
+
relationships: pkg.relationships.filter((r) => !excluded.has(r.chatId)),
|
|
37
|
+
connectors: pkg.connectors,
|
|
38
|
+
open: pkg.open.filter((o) => !excluded.has(o.chatId)),
|
|
39
|
+
};
|
|
40
|
+
if (filtered.window.messages === 0) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: 'The semantic index is empty — run collect_messages (or any search) first, then retry.',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const acc = createPhiAccumulator();
|
|
51
|
+
const masked = maskPackage(filtered, acc);
|
|
52
|
+
// TOON encoding (github.com/toon-format/toon) over a lean projection: uniform
|
|
53
|
+
// arrays become a one-line column header plus CSV rows (~half the tokens of
|
|
54
|
+
// JSON), and the projection drops fields the agent can derive or does not need
|
|
55
|
+
// — long MIDs/pointers, and low-signal one-off conversations.
|
|
56
|
+
const guide = 'A context network over your local LINE index, encoded as TOON — objects are `key: value`, uniform arrays are `name[count]{columns}:` then one comma-separated row per item. NOT verdicts. `connectors`: people across ≥2 of your chats (hubs; `isBridge` = removing them splits your contact graph). `relationships`: per-conversation engagement (`msgs` total, `yours` from you, `rhythmH` = your usual reply latency in hours) for conversations that matter. `open`: conversations whose latest message is not yours, most overdue vs that rhythm first (`overdueR`), with the last speaker (`from`) and a `preview`. Read each `preview` to judge — who it addresses (a group message may name someone else, who then owns it), open-request vs closing-acknowledgement, and nickname identities — then fetch the thread via get_chat_messages(chatId) only for the few worth it. `typicality` hints boilerplate shape but also buries polite requests, so never filter on it.';
|
|
57
|
+
const content = [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: `${guide}\n\n${encode(roundNumbers(project(masked)))}`,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
const note = phiNote(acc);
|
|
64
|
+
if (note) {
|
|
65
|
+
content.push(note);
|
|
66
|
+
}
|
|
67
|
+
return { content };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Project the package to a lean shape for encoding: drop long MIDs and message
|
|
71
|
+
* pointers, drop values the agent can derive (reciprocity, absolute times), and
|
|
72
|
+
* keep only conversations with signal — an open thread, or real back-and-forth
|
|
73
|
+
* (≥5 messages). One-off two-line chats are noise in a "what needs attention"
|
|
74
|
+
* scaffold.
|
|
75
|
+
*/
|
|
76
|
+
function project(pkg) {
|
|
77
|
+
const openChatIds = new Set(pkg.open.map((o) => o.chatId));
|
|
78
|
+
return {
|
|
79
|
+
window: pkg.window,
|
|
80
|
+
connectors: pkg.connectors.map((c) => ({
|
|
81
|
+
name: c.name,
|
|
82
|
+
chats: c.chatCount,
|
|
83
|
+
isBridge: c.isBridge,
|
|
84
|
+
})),
|
|
85
|
+
relationships: pkg.relationships
|
|
86
|
+
.filter((r) => openChatIds.has(r.chatId) || r.totalMessages >= 5)
|
|
87
|
+
.map((r) => ({
|
|
88
|
+
chatId: r.chatId,
|
|
89
|
+
name: r.chatName,
|
|
90
|
+
isDM: r.isDM,
|
|
91
|
+
msgs: r.totalMessages,
|
|
92
|
+
yours: r.yourMessages,
|
|
93
|
+
rhythmH: r.rhythmHours,
|
|
94
|
+
})),
|
|
95
|
+
open: pkg.open.map((o) => ({
|
|
96
|
+
chatId: o.chatId,
|
|
97
|
+
name: o.chatName,
|
|
98
|
+
isDM: o.isDM,
|
|
99
|
+
from: o.fromName,
|
|
100
|
+
waitH: o.waitHours,
|
|
101
|
+
overdueR: o.overdueRatio,
|
|
102
|
+
typicality: o.typicality,
|
|
103
|
+
preview: o.preview,
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Round every number to 2 decimals — full float precision is pure token cost. */
|
|
108
|
+
function roundNumbers(value) {
|
|
109
|
+
if (typeof value === 'number') {
|
|
110
|
+
return (Math.round(value * 100) / 100);
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(value)) {
|
|
113
|
+
return value.map(roundNumbers);
|
|
114
|
+
}
|
|
115
|
+
if (value && typeof value === 'object') {
|
|
116
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, roundNumbers(v)]));
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
/** Mask PHI in the free-text previews the package carries. */
|
|
121
|
+
function maskPackage(pkg, acc) {
|
|
122
|
+
return {
|
|
123
|
+
...pkg,
|
|
124
|
+
open: pkg.open.map((o) => ({
|
|
125
|
+
...o,
|
|
126
|
+
preview: maskInto(acc, o.preview) ?? o.preview,
|
|
127
|
+
})),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -11,7 +11,7 @@ export const NO_CREDENTIALS_MESSAGE = 'No persisted LINE session. Call the `logi
|
|
|
11
11
|
// "never logged in" so the model tells the user the right story: the MCP
|
|
12
12
|
// connection is healthy, LINE revoked this device's token (usually because
|
|
13
13
|
// the same account logged in somewhere else).
|
|
14
|
-
|
|
14
|
+
const SESSION_REVOKED_MESSAGE = 'LINE signed this device out — usually because the same account logged in ' +
|
|
15
15
|
'somewhere else (another device, or a terminal `login` run). The MCP ' +
|
|
16
16
|
'connection itself is fine. Call the `login` tool to reconnect; cached ' +
|
|
17
17
|
'credentials usually complete without a new PIN.';
|
|
@@ -30,7 +30,7 @@ export function sessionRequiredError() {
|
|
|
30
30
|
// Recovery is the same `login` call as a revocation, but the cause is not — no
|
|
31
31
|
// competing login happened, so claiming one sends the user hunting for a device
|
|
32
32
|
// that never signed in.
|
|
33
|
-
|
|
33
|
+
const SESSION_EXPIRED_MESSAGE = 'The saved LINE session expired and the automatic token refresh failed. ' +
|
|
34
34
|
'Nothing signed this device out — the token simply aged out, and the MCP ' +
|
|
35
35
|
'connection itself is fine. Call the `login` tool to reconnect; cached ' +
|
|
36
36
|
'credentials usually complete without a new PIN.';
|
package/dist/mcp/media.js
CHANGED
|
@@ -127,14 +127,14 @@ export function resolveLineMediaDescriptor(message) {
|
|
|
127
127
|
export class LineMediaAccessError extends Error {
|
|
128
128
|
}
|
|
129
129
|
/** Thrown when the message body is missing the OBS/E2EE material required to decrypt its media. */
|
|
130
|
-
|
|
130
|
+
class MissingDecryptMaterialError extends LineMediaAccessError {
|
|
131
131
|
code = 'missing_decrypt_material';
|
|
132
132
|
constructor(reason) {
|
|
133
133
|
super(`missing_decrypt_material: ${reason}`);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
/** Thrown when the message's content type is not a downloadable media type (e.g. plain text, sticker ref). */
|
|
137
|
-
|
|
137
|
+
class UnsupportedMediaTypeError extends LineMediaAccessError {
|
|
138
138
|
code = 'unsupported_media_type';
|
|
139
139
|
constructor(contentType, messageId) {
|
|
140
140
|
const label = resolveLineMediaType(contentType) ?? `contentType=${contentType}`;
|
package/dist/mcp/names.js
CHANGED
|
@@ -80,7 +80,7 @@ export async function resolveSenderNames(service, mids) {
|
|
|
80
80
|
* @param chatIds - Group/room chat MIDs to resolve.
|
|
81
81
|
* @returns Map of chat MID to resolved chat title (unresolved MIDs are omitted).
|
|
82
82
|
*/
|
|
83
|
-
|
|
83
|
+
async function resolveGroupNames(service, chatIds) {
|
|
84
84
|
const unique = Array.from(new Set(chatIds.filter(Boolean)));
|
|
85
85
|
const resolved = new Map();
|
|
86
86
|
const missing = [];
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Yomi MCP server — LINE query + reply surface over stdio.
|
|
3
3
|
*
|
|
4
|
-
* On startup resumes any persisted LINE session. Exposes forty tools:
|
|
4
|
+
* On startup resumes any persisted LINE session. Exposes forty-one tools:
|
|
5
5
|
* login, login_complete, list_conversations, get_chat_messages,
|
|
6
|
-
* get_message_image, get_message_media, get_unread_digest,
|
|
6
|
+
* get_message_image, get_message_media, get_unread_digest, get_insight,
|
|
7
|
+
* mark_read,
|
|
7
8
|
* send_message, send_image, send_file, send_audio, send_video,
|
|
8
9
|
* send_location, send_contact, send_sticker, list_stickers, search_stickers,
|
|
9
10
|
* preview_sticker, find_contact, list_contacts, get_group_members,
|
|
@@ -71,7 +72,7 @@ import { startCapture } from '../search/capture.js';
|
|
|
71
72
|
import { getDefaultEmbedder } from '../search/default-embedder.js';
|
|
72
73
|
import { createCliLogger } from '../util/log.js';
|
|
73
74
|
import { YOMI_VERSION } from '../version.js';
|
|
74
|
-
import { handleAcceptInvitation, handleAddFriend, handleBlockContact, handleCancelReaction, handleCollectMessages, handleCreateGroup, handleExcludeChats, handleFindContact, handleGetChatMessages, handleGetGroupMembers, handleGetMessageImage, handleGetMessageMedia, handleGetScopePolicy, handleGetUnreadDigest, handleIncludeChats, handleInviteMember, handleKickMember, handleLeaveGroup, handleListContacts, handleListConversations, handleListExcludedChats, handleListStickers, handleLogin, handleLoginComplete, handleMarkRead, handlePreviewSticker, handleReactMessage, handleRenameGroup, handleSearchMessages, handleSearchStickers, handleSendAudio, handleSendContact, handleSendFile, handleSendImage, handleSendLocation, handleSendMessage, handleSendSticker, handleSendVideo, handleUnblockContact, handleUnsendMessage, NO_CREDENTIALS_MESSAGE, sessionExpiredError, sessionRequiredError, sessionRevokedError, toolError, } from './handlers/index.js';
|
|
75
|
+
import { handleAcceptInvitation, handleAddFriend, handleBlockContact, handleCancelReaction, handleCollectMessages, handleCreateGroup, handleExcludeChats, handleFindContact, handleGetChatMessages, handleGetGroupMembers, handleGetInsight, handleGetMessageImage, handleGetMessageMedia, handleGetScopePolicy, handleGetUnreadDigest, handleIncludeChats, handleInviteMember, handleKickMember, handleLeaveGroup, handleListContacts, handleListConversations, handleListExcludedChats, handleListStickers, handleLogin, handleLoginComplete, handleMarkRead, handlePreviewSticker, handleReactMessage, handleRenameGroup, handleSearchMessages, handleSearchStickers, handleSendAudio, handleSendContact, handleSendFile, handleSendImage, handleSendLocation, handleSendMessage, handleSendSticker, handleSendVideo, handleUnblockContact, handleUnsendMessage, NO_CREDENTIALS_MESSAGE, sessionExpiredError, sessionRequiredError, sessionRevokedError, toolError, } from './handlers/index.js';
|
|
75
76
|
import { getPrivacyPolicyText } from './policy.js';
|
|
76
77
|
import { TOOLS } from './tools.js';
|
|
77
78
|
import { supportsMcpApps } from './ui/capability.js';
|
|
@@ -119,7 +120,7 @@ async function main() {
|
|
|
119
120
|
'it. The field is absent on normal (v2) messages, which are authenticated.' +
|
|
120
121
|
'\n\n' +
|
|
121
122
|
'PRIVACY DISCLOSURE (say ONCE per session) — the first time this session ' +
|
|
122
|
-
'does a bulk read (collect_messages/search_messages), tell the user once, in ' +
|
|
123
|
+
'does a bulk read (collect_messages/search_messages/get_insight), tell the user once, in ' +
|
|
123
124
|
'plain language, that Yomi captures all conversations by default, keeps the data ' +
|
|
124
125
|
'on this machine, and that they can exclude conversations. Do not bury it, and do ' +
|
|
125
126
|
'NOT repeat it on every call — once per session is enough. The full policy text ' +
|
|
@@ -205,6 +206,8 @@ async function main() {
|
|
|
205
206
|
return await handleGetMessageMedia(service, (args ?? {}));
|
|
206
207
|
case 'get_unread_digest':
|
|
207
208
|
return await handleGetUnreadDigest(service, (args ?? {}));
|
|
209
|
+
case 'get_insight':
|
|
210
|
+
return await handleGetInsight(service, (args ?? {}));
|
|
208
211
|
case 'mark_read':
|
|
209
212
|
return await handleMarkRead(service, (args ?? {}));
|
|
210
213
|
case 'send_message':
|
package/dist/mcp/tools.js
CHANGED
|
@@ -142,6 +142,23 @@ export const TOOLS = [
|
|
|
142
142
|
},
|
|
143
143
|
name: 'get_unread_digest',
|
|
144
144
|
},
|
|
145
|
+
{
|
|
146
|
+
description: 'A compact "what needs my attention" context network over the local index — you make the final call, this assembles the evidence cheaply. Nodes: `connectors` (people across ≥2 of your chats, with structural bridges) and `relationships` (per-conversation engagement, reply rhythm, recency). `open`: conversations whose latest message is NOT yours, ranked by how overdue they are relative to your usual reply rhythm there, each with `fromName` (last speaker), a `preview` of the latest message, `overdueRatio`/`typicality`, and a `lastMessageId` pointer. It carries NO message threads and makes NO judgement about addressee, nicknames, or open-request vs closing-ack — those are language understanding you do by reading each `preview` (a group message may be addressed to someone else, who then owns it), fetching the full thread with get_chat_messages only for the few worth it. Reads across all conversations (denylist-excluded dropped). Empty only when the index is empty.',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
chatId: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: 'Optional focus: restrict `relationships` and `pending` to this chat (as returned by list_conversations). Omit to scan all conversations.',
|
|
153
|
+
},
|
|
154
|
+
sinceHours: {
|
|
155
|
+
type: 'number',
|
|
156
|
+
description: 'Lookback window in hours, measured back from the newest captured message (not wall-clock). Default 504 (21 days).',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
name: 'get_insight',
|
|
161
|
+
},
|
|
145
162
|
{
|
|
146
163
|
description: 'Sends a text message to a LINE conversation immediately (not a draft). Always E2EE (pairwise for 1:1, group key for group/room); fails honestly rather than sending plaintext if the key cannot be resolved. One send per call. To @mention someone, put the visible "@name " into `text` AND pass a matching `mentions` entry — without `mentions`, "@name" is plain text and notifies no one. Resolve MIDs via get_group_members or find_contact first.',
|
|
147
164
|
inputSchema: {
|
package/dist/search/collector.js
CHANGED
|
@@ -121,7 +121,7 @@ export async function collectMessages(service, options = {}) {
|
|
|
121
121
|
* @param embedder - Injected embedder to embed missing rows with.
|
|
122
122
|
* @returns Number of messages newly embedded and stored.
|
|
123
123
|
*/
|
|
124
|
-
|
|
124
|
+
async function backfillMissingEmbeddings(embedder) {
|
|
125
125
|
const missing = getMessagesMissingEmbedding(embedder.modelLabel);
|
|
126
126
|
if (missing.length === 0) {
|
|
127
127
|
return 0;
|
package/dist/search/store.js
CHANGED
|
@@ -437,22 +437,39 @@ export async function semanticSearch(query, limit, embedder) {
|
|
|
437
437
|
return scored.slice(0, limit);
|
|
438
438
|
}
|
|
439
439
|
/**
|
|
440
|
-
*
|
|
440
|
+
* Load every indexed message that has an embedding for `model`, newest-window
|
|
441
|
+
* first bounded by `sinceMs`, with its vector decoded. Empty-text and
|
|
442
|
+
* timestamp-less rows are excluded — clustering needs both a vector to compare
|
|
443
|
+
* and a time to gate on, and a row missing either can only add noise.
|
|
441
444
|
*
|
|
442
|
-
*
|
|
445
|
+
* This is a full scan of the embedded corpus (personal-scale: thousands of
|
|
446
|
+
* rows), left to the caller to cluster; the store deliberately holds no
|
|
447
|
+
* opinion on thresholds or windowing beyond the `sinceMs` cutoff.
|
|
448
|
+
*
|
|
449
|
+
* @param model - Embedder.modelLabel whose vectors to load.
|
|
450
|
+
* @param sinceMs - Optional lower bound on createdTime (epoch ms); omit for all.
|
|
451
|
+
* @returns Messages with decoded vectors, oldest first.
|
|
443
452
|
*/
|
|
444
|
-
export function
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
453
|
+
export function getMessagesWithVectors(model, sinceMs) {
|
|
454
|
+
const handle = getDb();
|
|
455
|
+
const rows = handle
|
|
456
|
+
.prepare(`
|
|
457
|
+
SELECT m.messageId as messageId, m.chatId as chatId, m.chatName as chatName,
|
|
458
|
+
m.fromMid as fromMid, m.fromName as fromName, m.text as text,
|
|
459
|
+
m.createdTime as createdTime, e.vector as vector
|
|
460
|
+
FROM embeddings e
|
|
461
|
+
JOIN messages m ON m.messageId = e.messageId
|
|
462
|
+
WHERE e.model = $model
|
|
463
|
+
AND m.createdTime IS NOT NULL
|
|
464
|
+
AND ($since IS NULL OR m.createdTime >= $since)
|
|
465
|
+
AND length(m.text) > 0
|
|
466
|
+
ORDER BY m.createdTime ASC;
|
|
467
|
+
`)
|
|
468
|
+
.all({ $model: model, $since: sinceMs ?? null });
|
|
469
|
+
return rows.map((row) => {
|
|
470
|
+
const { vector, ...rest } = row;
|
|
471
|
+
return { ...rest, vector: decodeVector(vector) };
|
|
472
|
+
});
|
|
456
473
|
}
|
|
457
474
|
/**
|
|
458
475
|
* Persist the current capture cursor, overwriting the single stored row.
|
package/dist/util/data-dir.js
CHANGED
|
@@ -28,7 +28,7 @@ import { join } from 'node:path';
|
|
|
28
28
|
*
|
|
29
29
|
* @returns Absolute path to the directory Yomi's state lives in.
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
function resolveYomiDataDir() {
|
|
32
32
|
const override = process.env.YOMI_DATA_DIR;
|
|
33
33
|
if (override) {
|
|
34
34
|
return override;
|
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
|
+
"version": "0.3.0",
|
|
7
7
|
"description": "Yomi (読み) — reads your LINE (decrypting Letter-Sealing E2EE) and sends on your behalf, via a stdio MCP server, so an agent can catch up on your LINE for you.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"license": "MIT",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@huggingface/transformers": "^4.2.0",
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
54
|
+
"@toon-format/toon": "^2.3.1",
|
|
54
55
|
"curve25519-js": "^0.0.4",
|
|
55
56
|
"tweetnacl": "^1.0.3"
|
|
56
57
|
},
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"@biomejs/biome": "^2.5.3",
|
|
59
60
|
"@types/bun": "^1.3.14",
|
|
60
61
|
"@types/node": "^22.10.0",
|
|
62
|
+
"knip": "^6.27.0",
|
|
61
63
|
"typescript": "^7.0.0"
|
|
62
64
|
}
|
|
63
65
|
}
|
package/dist/line/core/store.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LINE Credential Stores — re-exports
|
|
3
|
-
*
|
|
4
|
-
* Both stores live in Yomi's vendored auth/credential-store module.
|
|
5
|
-
* This module re-exports them under their original names for
|
|
6
|
-
* compatibility with the LINE protocol core.
|
|
7
|
-
*/
|
|
8
|
-
export { CredentialStore as FileCredentialStore, InMemoryStore, } from '../../auth/credential-store.js';
|