@rikaidev/yomi 0.2.6 → 0.3.1
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 +38 -11
- 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/contacts.js +4 -16
- package/dist/mcp/handlers/conversations.js +5 -11
- package/dist/mcp/handlers/index.js +1 -0
- package/dist/mcp/handlers/insight.js +129 -0
- package/dist/mcp/handlers/login.js +3 -3
- package/dist/mcp/handlers/scope.js +5 -11
- package/dist/mcp/handlers/search.js +32 -10
- package/dist/mcp/handlers/send.js +16 -16
- package/dist/mcp/handlers/shared.js +21 -6
- package/dist/mcp/handlers/stickers.js +3 -12
- package/dist/mcp/media.js +2 -2
- package/dist/mcp/names.js +1 -1
- package/dist/mcp/server.js +11 -4
- package/dist/mcp/tools.js +17 -0
- package/dist/search/collector.js +1 -1
- package/dist/search/store.js +58 -14
- package/dist/util/data-dir.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -3
- 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
|
@@ -2,7 +2,6 @@ import { tryDecryptInner } from './message-decrypt.js';
|
|
|
2
2
|
const PEER_KEY_MISMATCH_PATTERN = /Peer E2EE key mismatch/i;
|
|
3
3
|
const MISSING_GROUP_SENDER_KEY_PATTERN = /Missing group sender key/i;
|
|
4
4
|
const GCM_AUTH_FAILURE_PATTERN = /unable to authenticate data|Unsupported state or unable to authenticate/i;
|
|
5
|
-
export { decryptV1, decryptV2, decryptV2WithCandidates, extractTextPayload, generateAAD, getIntBytes, toChunkKeyId, } from './message-crypto.js';
|
|
6
5
|
/**
|
|
7
6
|
* Build one structured decrypt-failure log payload from a raw LINE message.
|
|
8
7
|
*
|
|
@@ -52,7 +52,7 @@ function readNegotiatedValue(publicKey, raw, fieldId) {
|
|
|
52
52
|
* @param keyData - Raw key payload from thrift or object responses.
|
|
53
53
|
* @returns Buffer instance or null when unsupported.
|
|
54
54
|
*/
|
|
55
|
-
|
|
55
|
+
function toKeyBuffer(keyData) {
|
|
56
56
|
if (Buffer.isBuffer(keyData)) {
|
|
57
57
|
return keyData;
|
|
58
58
|
}
|
|
@@ -43,7 +43,7 @@ function hasDecryptMaterial(resolved) {
|
|
|
43
43
|
* @param msg - Raw message object
|
|
44
44
|
* @returns Parsed E2EE chunk data, or null if the message is not E2EE
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
function parseE2EEChunks(ctx, msg) {
|
|
47
47
|
const chunks = msg.chunks;
|
|
48
48
|
if (!chunks || !Array.isArray(chunks) || chunks.length < 5) {
|
|
49
49
|
return null;
|
|
@@ -59,7 +59,7 @@ export async function getPeerPublicKey(ctx, mid, keyId) {
|
|
|
59
59
|
* @param keyId - Public key id
|
|
60
60
|
* @param data - 32-byte public key
|
|
61
61
|
*/
|
|
62
|
-
|
|
62
|
+
async function persistPeerPublicKey(ctx, mid, keyId, data) {
|
|
63
63
|
const store = ctx.getStore();
|
|
64
64
|
const record = JSON.stringify({
|
|
65
65
|
mid,
|
|
@@ -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';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createCliLogger } from '../../util/log.js';
|
|
2
2
|
import { resolveUserNames } from '../names.js';
|
|
3
|
-
import { jsonResult, toolError } from './shared.js';
|
|
3
|
+
import { jsonResult, toolError, toonResult } from './shared.js';
|
|
4
4
|
const log = createCliLogger('Yomi');
|
|
5
5
|
/**
|
|
6
6
|
* Fetch and normalize the authenticated user's full friend list in one
|
|
@@ -43,11 +43,7 @@ async function fetchAllContacts(service) {
|
|
|
43
43
|
*/
|
|
44
44
|
export async function handleListContacts(service) {
|
|
45
45
|
const contacts = await fetchAllContacts(service);
|
|
46
|
-
return
|
|
47
|
-
content: [
|
|
48
|
-
{ type: 'text', text: JSON.stringify(contacts, null, 2) },
|
|
49
|
-
],
|
|
50
|
-
};
|
|
46
|
+
return toonResult(contacts);
|
|
51
47
|
}
|
|
52
48
|
/**
|
|
53
49
|
* Handle `find_contact` — case-insensitive substring match over the
|
|
@@ -66,11 +62,7 @@ export async function handleFindContact(service, args) {
|
|
|
66
62
|
const needle = args.name.toLowerCase();
|
|
67
63
|
const contacts = await fetchAllContacts(service);
|
|
68
64
|
const matches = contacts.filter((contact) => contact.displayName?.toLowerCase().includes(needle));
|
|
69
|
-
return
|
|
70
|
-
content: [
|
|
71
|
-
{ type: 'text', text: JSON.stringify(matches, null, 2) },
|
|
72
|
-
],
|
|
73
|
-
};
|
|
65
|
+
return toonResult(matches);
|
|
74
66
|
}
|
|
75
67
|
/**
|
|
76
68
|
* Handle `get_group_members` — the raw member list of one LINE group/room.
|
|
@@ -110,11 +102,7 @@ export async function handleGetGroupMembers(service, args) {
|
|
|
110
102
|
invited: true,
|
|
111
103
|
})),
|
|
112
104
|
];
|
|
113
|
-
return
|
|
114
|
-
content: [
|
|
115
|
-
{ type: 'text', text: JSON.stringify(summaries, null, 2) },
|
|
116
|
-
],
|
|
117
|
-
};
|
|
105
|
+
return toonResult(summaries);
|
|
118
106
|
}
|
|
119
107
|
/**
|
|
120
108
|
* Handle `add_friend` — REALLY adds a person to THIS account's LINE friends now
|
|
@@ -4,7 +4,7 @@ import { createCliLogger } from '../../util/log.js';
|
|
|
4
4
|
import { resolveLineMediaDescriptor, resolveLineMediaType } from '../media.js';
|
|
5
5
|
import { resolveConversationNames, resolveSenderNames } from '../names.js';
|
|
6
6
|
import { createPhiAccumulator, maskInto, phiNote } from '../phi-guard.js';
|
|
7
|
-
import { toolError } from './shared.js';
|
|
7
|
+
import { jsonResult, toolError, toonText } from './shared.js';
|
|
8
8
|
const log = createCliLogger('Yomi');
|
|
9
9
|
/**
|
|
10
10
|
* Handle `list_conversations` — list LINE conversations (chats, groups, rooms)
|
|
@@ -42,7 +42,7 @@ export async function handleListConversations(service, args) {
|
|
|
42
42
|
};
|
|
43
43
|
}));
|
|
44
44
|
const content = [
|
|
45
|
-
{ type: 'text', text:
|
|
45
|
+
{ type: 'text', text: toonText(conversations) },
|
|
46
46
|
];
|
|
47
47
|
const note = phiNote(acc);
|
|
48
48
|
if (note)
|
|
@@ -113,9 +113,7 @@ export async function handleGetChatMessages(service, args) {
|
|
|
113
113
|
? { e2eeIntegrityVerified: false }
|
|
114
114
|
: {}),
|
|
115
115
|
}));
|
|
116
|
-
const content = [
|
|
117
|
-
{ type: 'text', text: JSON.stringify(shaped, null, 2) },
|
|
118
|
-
];
|
|
116
|
+
const content = [{ type: 'text', text: toonText(shaped) }];
|
|
119
117
|
const note = phiNote(acc);
|
|
120
118
|
if (note)
|
|
121
119
|
content.push(note);
|
|
@@ -137,9 +135,7 @@ export async function handleMarkRead(service, args) {
|
|
|
137
135
|
}
|
|
138
136
|
const result = await service.markChatRead(args.chatId, args.messageId);
|
|
139
137
|
log.info('mark_read.done', { chatId: args.chatId, marked: result.marked });
|
|
140
|
-
return
|
|
141
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
142
|
-
};
|
|
138
|
+
return jsonResult(result);
|
|
143
139
|
}
|
|
144
140
|
/**
|
|
145
141
|
* Handle `get_unread_digest` — one-shot composite: every conversation with
|
|
@@ -183,9 +179,7 @@ export async function handleGetUnreadDigest(service, args) {
|
|
|
183
179
|
})),
|
|
184
180
|
};
|
|
185
181
|
}));
|
|
186
|
-
const content = [
|
|
187
|
-
{ type: 'text', text: JSON.stringify(digest, null, 2) },
|
|
188
|
-
];
|
|
182
|
+
const content = [{ type: 'text', text: toonText(digest) }];
|
|
189
183
|
const note = phiNote(acc);
|
|
190
184
|
if (note)
|
|
191
185
|
content.push(note);
|
|
@@ -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
|
+
}
|
|
@@ -27,7 +27,7 @@ import { runPwlessLogin } from '../../cli/login.js';
|
|
|
27
27
|
import { LINE_PIN_CODE_LIFETIME_MS, PAAK_AUTH_CLIENT_CEILING_MS, PIN_VERIFY_CLIENT_CEILING_MS, } from '../../line/auth/pwless/index.js';
|
|
28
28
|
import { createCliLogger } from '../../util/log.js';
|
|
29
29
|
import { supportsMcpApps } from '../ui/capability.js';
|
|
30
|
-
import { toolError } from './shared.js';
|
|
30
|
+
import { jsonText, toolError } from './shared.js';
|
|
31
31
|
const log = createCliLogger('Yomi');
|
|
32
32
|
/** LINE region codes offered on the login elicitation form. */
|
|
33
33
|
const LOGIN_REGIONS = ['TW', 'JP', 'TH', 'ID', 'US'];
|
|
@@ -176,7 +176,7 @@ async function handleLoginElicitation(server, service, args) {
|
|
|
176
176
|
content: [
|
|
177
177
|
{
|
|
178
178
|
type: 'text',
|
|
179
|
-
text:
|
|
179
|
+
text: jsonText({ loggedIn: true, mid, displayName }),
|
|
180
180
|
},
|
|
181
181
|
],
|
|
182
182
|
};
|
|
@@ -461,7 +461,7 @@ export async function handleLoginComplete() {
|
|
|
461
461
|
content: [
|
|
462
462
|
{
|
|
463
463
|
type: 'text',
|
|
464
|
-
text:
|
|
464
|
+
text: jsonText({ loggedIn: true, mid, displayName }),
|
|
465
465
|
},
|
|
466
466
|
],
|
|
467
467
|
};
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { addExcludedChatIds, deleteMessagesForChats, getExcludedChatIds, removeExcludedChatIds, } from '../../search/scope.js';
|
|
13
13
|
import { resolveConversationNames } from '../names.js';
|
|
14
14
|
import { getPrivacyPolicyText } from '../policy.js';
|
|
15
|
-
import { toolError } from './shared.js';
|
|
15
|
+
import { jsonResult, jsonText, toolError } from './shared.js';
|
|
16
16
|
/**
|
|
17
17
|
* Read the current exclusion denylist and pair each chatId with a
|
|
18
18
|
* best-effort display name. Name resolution only runs when a live LINE
|
|
@@ -54,7 +54,7 @@ export async function handleExcludeChats(args) {
|
|
|
54
54
|
content: [
|
|
55
55
|
{
|
|
56
56
|
type: 'text',
|
|
57
|
-
text:
|
|
57
|
+
text: jsonText({ excluded, purgedMessages }),
|
|
58
58
|
},
|
|
59
59
|
],
|
|
60
60
|
};
|
|
@@ -74,9 +74,7 @@ export async function handleIncludeChats(args) {
|
|
|
74
74
|
}
|
|
75
75
|
const included = removeExcludedChatIds(args.chatIds);
|
|
76
76
|
return {
|
|
77
|
-
content: [
|
|
78
|
-
{ type: 'text', text: JSON.stringify({ included }, null, 2) },
|
|
79
|
-
],
|
|
77
|
+
content: [{ type: 'text', text: jsonText({ included }) }],
|
|
80
78
|
};
|
|
81
79
|
}
|
|
82
80
|
/**
|
|
@@ -91,9 +89,7 @@ export async function handleIncludeChats(args) {
|
|
|
91
89
|
*/
|
|
92
90
|
export async function handleListExcludedChats(service) {
|
|
93
91
|
const result = await listExcludedChatsWithNames(service);
|
|
94
|
-
return
|
|
95
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
96
|
-
};
|
|
92
|
+
return jsonResult(result);
|
|
97
93
|
}
|
|
98
94
|
/**
|
|
99
95
|
* Handle `get_scope_policy` — return Yomi's data-capture privacy policy (the
|
|
@@ -114,7 +110,5 @@ export async function handleGetScopePolicy(service) {
|
|
|
114
110
|
policy: getPrivacyPolicyText(),
|
|
115
111
|
excludedChats,
|
|
116
112
|
};
|
|
117
|
-
return
|
|
118
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
119
|
-
};
|
|
113
|
+
return jsonResult(result);
|
|
120
114
|
}
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { collectMessages } from '../../search/collector.js';
|
|
26
26
|
import { getDefaultEmbedder } from '../../search/default-embedder.js';
|
|
27
|
-
import { getEmbeddingCount, getIndexedMessageCount, searchMessages, semanticSearch, } from '../../search/store.js';
|
|
27
|
+
import { getEmbeddingCount, getIndexedMessageCount, getMessageContext, searchMessages, semanticSearch, } from '../../search/store.js';
|
|
28
28
|
import { createCliLogger } from '../../util/log.js';
|
|
29
29
|
import { createPhiAccumulator, maskInto, phiNote } from '../phi-guard.js';
|
|
30
|
-
import { toolError } from './shared.js';
|
|
30
|
+
import { jsonText, toolError, toonText } from './shared.js';
|
|
31
31
|
const log = createCliLogger('Yomi');
|
|
32
32
|
/**
|
|
33
33
|
* Reciprocal Rank Fusion constant. The standard k=60 from Cormack et al.:
|
|
@@ -50,7 +50,7 @@ const RRF_K = 60;
|
|
|
50
50
|
* @param limit - Maximum number of fused results to return.
|
|
51
51
|
* @returns Fused results, best-first, each carrying its RRF score.
|
|
52
52
|
*/
|
|
53
|
-
function fuseByRrf(lists, limit) {
|
|
53
|
+
export function fuseByRrf(lists, limit) {
|
|
54
54
|
const scores = new Map();
|
|
55
55
|
const rows = new Map();
|
|
56
56
|
for (const list of lists) {
|
|
@@ -69,6 +69,26 @@ function fuseByRrf(lists, limit) {
|
|
|
69
69
|
score,
|
|
70
70
|
}));
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Keep one active conversation from flooding the top results. Skipped rows are
|
|
74
|
+
* backfilled only when there are not enough distinct conversations, so a query
|
|
75
|
+
* scoped by its natural matches still returns the requested number of hits.
|
|
76
|
+
*/
|
|
77
|
+
export function applyConversationDiversity(rows, limit, perChat = 3) {
|
|
78
|
+
const selected = [];
|
|
79
|
+
const deferred = [];
|
|
80
|
+
const counts = new Map();
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
if ((counts.get(row.chatId) ?? 0) < perChat) {
|
|
83
|
+
selected.push(row);
|
|
84
|
+
counts.set(row.chatId, (counts.get(row.chatId) ?? 0) + 1);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
deferred.push(row);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [...selected, ...deferred].slice(0, limit);
|
|
91
|
+
}
|
|
72
92
|
/**
|
|
73
93
|
* Handle `collect_messages` — explicit bulk fetch of recent messages from
|
|
74
94
|
* one or more LINE conversations into the local search index, batch-
|
|
@@ -88,9 +108,7 @@ export async function handleCollectMessages(service, args) {
|
|
|
88
108
|
embedder: getDefaultEmbedder(),
|
|
89
109
|
});
|
|
90
110
|
return {
|
|
91
|
-
content: [
|
|
92
|
-
{ type: 'text', text: JSON.stringify(summary, null, 2) },
|
|
93
|
-
],
|
|
111
|
+
content: [{ type: 'text', text: jsonText(summary) }],
|
|
94
112
|
};
|
|
95
113
|
}
|
|
96
114
|
/**
|
|
@@ -168,25 +186,29 @@ export async function handleSearchMessages(service, args) {
|
|
|
168
186
|
let results;
|
|
169
187
|
if (semantic.length > 0 && keyword.length > 0) {
|
|
170
188
|
mode = 'hybrid';
|
|
171
|
-
results = fuseByRrf([keyword, semantic], limit);
|
|
189
|
+
results = applyConversationDiversity(fuseByRrf([keyword, semantic], pool), limit);
|
|
172
190
|
}
|
|
173
191
|
else if (semantic.length > 0) {
|
|
174
192
|
mode = 'semantic';
|
|
175
|
-
results = semantic
|
|
193
|
+
results = applyConversationDiversity(semantic, limit);
|
|
176
194
|
}
|
|
177
195
|
else {
|
|
178
196
|
mode = 'keyword';
|
|
179
|
-
results = keyword
|
|
197
|
+
results = applyConversationDiversity(keyword, limit);
|
|
180
198
|
}
|
|
181
199
|
const acc = createPhiAccumulator();
|
|
182
200
|
const maskedResults = results.map((row) => ({
|
|
183
201
|
...row,
|
|
184
202
|
text: maskInto(acc, row.text),
|
|
203
|
+
context: getMessageContext(row.messageId).map((message) => ({
|
|
204
|
+
...message,
|
|
205
|
+
text: maskInto(acc, message.text),
|
|
206
|
+
})),
|
|
185
207
|
}));
|
|
186
208
|
const content = [
|
|
187
209
|
{
|
|
188
210
|
type: 'text',
|
|
189
|
-
text:
|
|
211
|
+
text: toonText({ mode, results: maskedResults }),
|
|
190
212
|
},
|
|
191
213
|
];
|
|
192
214
|
const note = phiNote(acc);
|