@rikaidev/yomi 0.4.2 → 0.6.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.
Files changed (86) hide show
  1. package/README.md +46 -4
  2. package/dist/ambient/ambient-event-graph.js +172 -0
  3. package/dist/ambient/ambient-observer.js +71 -0
  4. package/dist/ambient/event-graph-reducer.js +300 -0
  5. package/dist/ambient/ingestion-adapter.js +203 -0
  6. package/dist/ambient/journal/crypto.js +58 -0
  7. package/dist/ambient/journal/journal-store.js +284 -0
  8. package/dist/ambient/journal/types.js +7 -0
  9. package/dist/ambient/summaries/retriever.js +55 -0
  10. package/dist/ambient/summaries/summary-engine.js +128 -0
  11. package/dist/ambient/summaries/types.js +4 -0
  12. package/dist/attention/policy.js +101 -0
  13. package/dist/auth/credential-store.js +47 -17
  14. package/dist/auth/keychain.js +7 -54
  15. package/dist/cli/query.js +875 -0
  16. package/dist/cli/work-item.js +24 -0
  17. package/dist/insight/relationships.js +1 -1
  18. package/dist/line/auth/protocol/pwless-login-flow.js +12 -0
  19. package/dist/line/auth/pwless/index.js +1 -1
  20. package/dist/line/client/auth-service/client.js +7 -8
  21. package/dist/line/client/buddy-service/client.js +73 -0
  22. package/dist/line/client/index.js +28 -3
  23. package/dist/line/client/obs-media-client.js +38 -0
  24. package/dist/line/client/parsers.js +1 -0
  25. package/dist/line/client/square-service/client.js +298 -0
  26. package/dist/line/client/sticker-meta.js +32 -0
  27. package/dist/line/client/sync-service/client.js +31 -3
  28. package/dist/line/client/talk-service/command-client.js +9 -1
  29. package/dist/line/client/talk-service/contact-query.js +8 -1
  30. package/dist/line/client/talk-service/message-box-query.js +17 -0
  31. package/dist/line/client/talk-service/message-id-query.js +27 -3
  32. package/dist/line/client/talk-service/query-client.js +32 -26
  33. package/dist/line/client/talk-service/requests.js +10 -11
  34. package/dist/line/core/auth-session-runtime.js +17 -2
  35. package/dist/line/core/chat-event.js +35 -0
  36. package/dist/line/core/chat-runtime-service.js +18 -23
  37. package/dist/line/core/config.js +1 -0
  38. package/dist/line/core/directory-runtime-service.js +10 -2
  39. package/dist/line/core/e2ee/recovery/bootstrap-keys.js +8 -16
  40. package/dist/line/core/flex-message.js +453 -0
  41. package/dist/line/core/mention.js +1 -1
  42. package/dist/line/core/message-preview.js +60 -0
  43. package/dist/line/core/message-query-service.js +0 -20
  44. package/dist/line/core/service.js +1 -3
  45. package/dist/mcp/handlers/contacts.js +12 -6
  46. package/dist/mcp/handlers/conversations.js +91 -4
  47. package/dist/mcp/handlers/login.js +1 -1
  48. package/dist/mcp/handlers/message-actions.js +1 -1
  49. package/dist/mcp/handlers/search.js +1 -1
  50. package/dist/mcp/handlers/send.js +8 -11
  51. package/dist/mcp/handlers/shared.js +1 -1
  52. package/dist/mcp/integrator.js +168 -0
  53. package/dist/mcp/media.js +135 -4
  54. package/dist/mcp/names.js +78 -6
  55. package/dist/mcp/phi-guard.js +2 -2
  56. package/dist/search/bigram.js +2 -2
  57. package/dist/search/collector.js +1 -1
  58. package/dist/search/default-embedder.js +12 -3
  59. package/dist/search/embedder.js +1 -1
  60. package/dist/version.js +1 -1
  61. package/dist/work-item/adapter.js +1 -0
  62. package/dist/work-item/destination-contracts.js +1 -0
  63. package/dist/work-item/event-context.js +73 -0
  64. package/dist/work-item/gold-contract.js +101 -0
  65. package/dist/work-item/gold-coverage.js +96 -0
  66. package/dist/work-item/gold-derivation.js +67 -0
  67. package/dist/work-item/gold-split.js +143 -0
  68. package/dist/work-item/index.js +23 -0
  69. package/dist/work-item/line-adapter.js +111 -0
  70. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  71. package/dist/work-item/local-provider.js +38 -0
  72. package/dist/work-item/local-work-event-provider.js +56 -0
  73. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  74. package/dist/work-item/provider.js +21 -0
  75. package/dist/work-item/registry.js +19 -0
  76. package/dist/work-item/service.js +85 -0
  77. package/dist/work-item/store.js +97 -0
  78. package/dist/work-item/types.js +79 -0
  79. package/dist/work-item/work-event-graph-engine.js +63 -0
  80. package/dist/work-item/work-event-inference.js +267 -0
  81. package/dist/work-item/work-event-matcher.js +105 -0
  82. package/dist/work-item/work-event-model-manifest.js +39 -0
  83. package/dist/work-item/work-event-store.js +125 -0
  84. package/dist/work-item/work-event.js +123 -0
  85. package/package.json +26 -2
  86. package/run.mjs +371 -2
@@ -0,0 +1,453 @@
1
+ import { createHash } from 'node:crypto';
2
+ function flexLayoutFingerprint(root) {
3
+ return createHash('sha256').update(JSON.stringify(root)).digest('hex');
4
+ }
5
+ const FLEX_METRIC = {
6
+ none: 0,
7
+ xxs: 2,
8
+ xs: 4,
9
+ sm: 6,
10
+ md: 8,
11
+ lg: 12,
12
+ xl: 16,
13
+ xxl: 20,
14
+ };
15
+ function resolveMetric(value, fallback = 0) {
16
+ if (typeof value !== 'string')
17
+ return fallback;
18
+ if (/^\d+(\.\d+)?px$/.test(value))
19
+ return Number(value.slice(0, -2));
20
+ return FLEX_METRIC[value] ?? fallback;
21
+ }
22
+ function resolveOptionalMetric(value) {
23
+ return typeof value === 'string' ? resolveMetric(value) : null;
24
+ }
25
+ function resolveDimension(value) {
26
+ if (typeof value !== 'string')
27
+ return { points: null, fraction: null };
28
+ if (/^\d+(\.\d+)?px$/.test(value))
29
+ return { points: Number(value.slice(0, -2)), fraction: null };
30
+ if (/^\d+(\.\d+)?%$/.test(value))
31
+ return { points: null, fraction: Number(value.slice(0, -1)) / 100 };
32
+ return { points: null, fraction: null };
33
+ }
34
+ function resolveFontSize(value) {
35
+ if (typeof value === 'string' && /^\d+(\.\d+)?px$/.test(value))
36
+ return Number(value.slice(0, -2));
37
+ const sizes = {
38
+ xxs: 10,
39
+ xs: 11,
40
+ sm: 12,
41
+ md: 13,
42
+ lg: 15,
43
+ xl: 18,
44
+ xxl: 22,
45
+ '3xl': 26,
46
+ '4xl': 30,
47
+ '5xl': 34,
48
+ };
49
+ return sizes[typeof value === 'string' ? value : 'md'] ?? 13;
50
+ }
51
+ function resolveAspectRatio(value) {
52
+ if (typeof value !== 'string')
53
+ return 1;
54
+ const [width, height] = value.split(':').map(Number);
55
+ return width > 0 && height > 0 ? width / height : 1;
56
+ }
57
+ function normalizeGradient(value) {
58
+ const raw = parseObject(value);
59
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
60
+ return null;
61
+ const gradient = raw;
62
+ if (gradient.type !== 'linearGradient' ||
63
+ typeof gradient.startColor !== 'string' ||
64
+ typeof gradient.endColor !== 'string')
65
+ return null;
66
+ const angle = typeof gradient.angle === 'string'
67
+ ? Number(gradient.angle.replace(/deg$/, ''))
68
+ : 0;
69
+ const centerPosition = typeof gradient.centerPosition === 'string'
70
+ ? Number(gradient.centerPosition.replace(/%$/, '')) / 100
71
+ : null;
72
+ return {
73
+ type: 'linearGradient',
74
+ angle: Number.isFinite(angle) ? angle : 0,
75
+ startColor: gradient.startColor,
76
+ centerColor: typeof gradient.centerColor === 'string' ? gradient.centerColor : null,
77
+ endColor: gradient.endColor,
78
+ centerPosition: centerPosition !== null && Number.isFinite(centerPosition)
79
+ ? centerPosition
80
+ : null,
81
+ };
82
+ }
83
+ function resolveFlexStyle(node) {
84
+ const all = resolveMetric(node.paddingAll);
85
+ const width = resolveDimension(node.width);
86
+ const maxWidth = resolveDimension(node.maxWidth);
87
+ const height = resolveDimension(node.height);
88
+ const maxHeight = resolveDimension(node.maxHeight);
89
+ return {
90
+ axis: ['horizontal', 'vertical', 'baseline'].includes(node.layout)
91
+ ? node.layout
92
+ : null,
93
+ spacing: resolveMetric(node.spacing, 8),
94
+ paddingTop: resolveMetric(node.paddingTop, all),
95
+ paddingEnd: resolveMetric(node.paddingEnd, all),
96
+ paddingBottom: resolveMetric(node.paddingBottom, all),
97
+ paddingStart: resolveMetric(node.paddingStart, all),
98
+ marginBefore: resolveMetric(node.margin),
99
+ fontSize: resolveFontSize(node.size),
100
+ fontWeight: node.weight === 'bold' ? 700 : 400,
101
+ lineLimit: Number.isFinite(node.maxLines)
102
+ ? Number(node.maxLines)
103
+ : node.wrap === true
104
+ ? null
105
+ : 1,
106
+ aspectRatio: resolveAspectRatio(node.aspectRatio),
107
+ contentMode: node.aspectMode === 'cover' ? 'cover' : 'fit',
108
+ buttonStyle: node.style === 'primary' || node.style === 'secondary'
109
+ ? node.style
110
+ : 'link',
111
+ buttonHeight: node.height === 'sm' ? 34 : 40,
112
+ cornerRadius: resolveMetric(node.cornerRadius),
113
+ borderWidth: resolveMetric(node.borderWidth),
114
+ flexGrow: Number.isFinite(node.flex) ? Math.max(0, Number(node.flex)) : 0,
115
+ position: node.position === 'absolute' ? 'absolute' : 'relative',
116
+ offsetTop: resolveOptionalMetric(node.offsetTop),
117
+ offsetBottom: resolveOptionalMetric(node.offsetBottom),
118
+ offsetStart: resolveOptionalMetric(node.offsetStart),
119
+ offsetEnd: resolveOptionalMetric(node.offsetEnd),
120
+ widthPoints: width.points,
121
+ widthFraction: width.fraction,
122
+ maxWidthPoints: maxWidth.points,
123
+ maxWidthFraction: maxWidth.fraction,
124
+ heightPoints: height.points,
125
+ heightFraction: height.fraction,
126
+ maxHeightPoints: maxHeight.points,
127
+ maxHeightFraction: maxHeight.fraction,
128
+ };
129
+ }
130
+ const FLEX_PAYLOAD_KEYS = [
131
+ 'FLEX_JSON',
132
+ 'FLEX_CONTENTS',
133
+ 'FLEX_MESSAGE',
134
+ 'CONTENTS',
135
+ 'MARKUP_JSON',
136
+ ];
137
+ function parseObject(value) {
138
+ if (typeof value !== 'string')
139
+ return value;
140
+ const trimmed = value.trim();
141
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('['))
142
+ return null;
143
+ try {
144
+ return JSON.parse(trimmed);
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ function webUrl(value) {
151
+ if (typeof value !== 'string')
152
+ return null;
153
+ try {
154
+ const url = new URL(value);
155
+ return url.protocol === 'http:' || url.protocol === 'https:'
156
+ ? url.toString()
157
+ : null;
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ export function resolveImagemapImageUrl(rawUrl, _canvasWidth, targetPixelWidth = 640) {
164
+ const base = webUrl(rawUrl);
165
+ if (!base)
166
+ return null;
167
+ const supported = [240, 300, 460, 700, 1040];
168
+ const requested = Math.max(1, targetPixelWidth);
169
+ const width = supported.find((candidate) => candidate >= requested) ??
170
+ supported[supported.length - 1];
171
+ const url = new URL(base);
172
+ if (!url.pathname.endsWith(`/${width}`)) {
173
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/${width}`;
174
+ }
175
+ return url.toString();
176
+ }
177
+ function flexAction(raw) {
178
+ const parsed = parseObject(raw);
179
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
180
+ return null;
181
+ const record = parsed;
182
+ const params = record.params && typeof record.params === 'object' ? record.params : record;
183
+ const rawType = String(record.type ?? '').toLowerCase();
184
+ const uri = webUrl(params.linkUri) || webUrl(record.uri);
185
+ const text = typeof params.text === 'string'
186
+ ? params.text
187
+ : typeof record.text === 'string'
188
+ ? record.text
189
+ : null;
190
+ const label = String(record.label ?? record.text ?? params.displayText ?? '').trim();
191
+ const data = typeof record.data === 'string'
192
+ ? record.data
193
+ : typeof params.data === 'string'
194
+ ? params.data
195
+ : null;
196
+ const type = rawType === 'web' || rawType === 'uri'
197
+ ? 'uri'
198
+ : rawType === 'message' || rawType === 'sendmessage'
199
+ ? 'message'
200
+ : rawType === 'postback'
201
+ ? 'postback'
202
+ : 'unknown';
203
+ return { type, label, uri, text, data };
204
+ }
205
+ function normalizeFlexNode(raw, depth = 0, block = null, blockStyle = null) {
206
+ if (depth > 24)
207
+ return null;
208
+ const parsed = parseObject(raw);
209
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
210
+ return null;
211
+ const node = parsed;
212
+ const nodeType = String(node.type ?? 'box').toLowerCase();
213
+ const parsedBlockStyle = parseObject(blockStyle);
214
+ const inherited = parsedBlockStyle &&
215
+ typeof parsedBlockStyle === 'object' &&
216
+ !Array.isArray(parsedBlockStyle)
217
+ ? parsedBlockStyle
218
+ : {};
219
+ const childValues = Array.isArray(node.contents)
220
+ ? node.contents.map((child) => [child, null, null])
221
+ : ['header', 'hero', 'body', 'footer']
222
+ .filter((key) => Boolean(node[key]))
223
+ .map((key) => [node[key], key, node.styles?.[key]]);
224
+ if (node.altContent)
225
+ childValues.push([node.altContent, null, null]);
226
+ return {
227
+ type: nodeType,
228
+ block,
229
+ layout: typeof node.layout === 'string' ? node.layout : null,
230
+ text: typeof node.text === 'string' ? node.text : null,
231
+ url: webUrl(node.url) || webUrl(node.previewUrl),
232
+ color: typeof node.color === 'string' ? node.color : null,
233
+ backgroundColor: typeof node.backgroundColor === 'string'
234
+ ? node.backgroundColor
235
+ : typeof inherited.backgroundColor === 'string'
236
+ ? inherited.backgroundColor
237
+ : nodeType === 'bubble'
238
+ ? '#FFFFFF'
239
+ : null,
240
+ background: normalizeGradient(node.background),
241
+ size: typeof node.size === 'string' ? node.size : null,
242
+ weight: typeof node.weight === 'string' ? node.weight : null,
243
+ wrap: typeof node.wrap === 'boolean' ? node.wrap : null,
244
+ margin: typeof node.margin === 'string' ? node.margin : null,
245
+ spacing: typeof node.spacing === 'string' ? node.spacing : null,
246
+ paddingAll: typeof node.paddingAll === 'string' ? node.paddingAll : null,
247
+ paddingTop: typeof node.paddingTop === 'string' ? node.paddingTop : null,
248
+ paddingBottom: typeof node.paddingBottom === 'string' ? node.paddingBottom : null,
249
+ paddingStart: typeof node.paddingStart === 'string' ? node.paddingStart : null,
250
+ paddingEnd: typeof node.paddingEnd === 'string' ? node.paddingEnd : null,
251
+ style: typeof node.style === 'string' ? node.style : null,
252
+ height: typeof node.height === 'string' ? node.height : null,
253
+ width: typeof node.width === 'string' ? node.width : null,
254
+ maxWidth: typeof node.maxWidth === 'string' ? node.maxWidth : null,
255
+ maxHeight: typeof node.maxHeight === 'string' ? node.maxHeight : null,
256
+ justifyContent: typeof node.justifyContent === 'string' ? node.justifyContent : null,
257
+ alignItems: typeof node.alignItems === 'string' ? node.alignItems : null,
258
+ cornerRadius: typeof node.cornerRadius === 'string' ? node.cornerRadius : null,
259
+ borderColor: typeof node.borderColor === 'string' ? node.borderColor : null,
260
+ borderWidth: typeof node.borderWidth === 'string' ? node.borderWidth : null,
261
+ separator: inherited.separator === true,
262
+ separatorColor: typeof inherited.separatorColor === 'string'
263
+ ? inherited.separatorColor
264
+ : null,
265
+ maxLines: Number.isFinite(node.maxLines) ? Number(node.maxLines) : null,
266
+ decoration: typeof node.decoration === 'string' ? node.decoration : null,
267
+ position: typeof node.position === 'string' ? node.position : null,
268
+ offsetTop: typeof node.offsetTop === 'string' ? node.offsetTop : null,
269
+ offsetBottom: typeof node.offsetBottom === 'string' ? node.offsetBottom : null,
270
+ offsetStart: typeof node.offsetStart === 'string' ? node.offsetStart : null,
271
+ offsetEnd: typeof node.offsetEnd === 'string' ? node.offsetEnd : null,
272
+ resolved: resolveFlexStyle(node),
273
+ aspectRatio: typeof node.aspectRatio === 'string' ? node.aspectRatio : null,
274
+ aspectMode: node.aspectMode === 'fit' || node.aspectMode === 'cover'
275
+ ? node.aspectMode
276
+ : null,
277
+ align: typeof node.align === 'string' ? node.align : null,
278
+ gravity: typeof node.gravity === 'string' ? node.gravity : null,
279
+ flex: Number.isFinite(node.flex) ? Number(node.flex) : null,
280
+ action: flexAction(node.action),
281
+ children: childValues
282
+ .map(([child, childBlock, childStyle]) => normalizeFlexNode(child, depth + 1, childBlock, childStyle))
283
+ .filter((child) => Boolean(child)),
284
+ };
285
+ }
286
+ export function processFlexMessage(message) {
287
+ const metadata = message?.contentMetadata && typeof message.contentMetadata === 'object'
288
+ ? message.contentMetadata
289
+ : {};
290
+ const explicitAlt = typeof metadata.ALT_TEXT === 'string' ? metadata.ALT_TEXT : '';
291
+ const markup = parseObject(metadata.MARKUP_JSON);
292
+ if (markup && typeof markup === 'object' && !Array.isArray(markup)) {
293
+ const record = markup;
294
+ const canvas = record.canvas;
295
+ const scene = record.scenes?.[canvas?.initialScene];
296
+ if (canvas && scene && Array.isArray(scene.listeners)) {
297
+ const regions = scene.listeners.flatMap((listener) => {
298
+ const [x, y, width, height] = listener?.params ?? [];
299
+ const action = flexAction(record.actions?.[listener?.action]);
300
+ if (![x, y, width, height].every(Number.isFinite) || !action)
301
+ return [];
302
+ return [{ x, y, width, height, action }];
303
+ });
304
+ return {
305
+ version: 1,
306
+ layoutVersion: 1,
307
+ layoutFingerprint: flexLayoutFingerprint(null),
308
+ kind: 'imagemap',
309
+ altText: explicitAlt,
310
+ width: Number(canvas.width) || null,
311
+ height: Number(canvas.height) || null,
312
+ imageUrl: resolveImagemapImageUrl(metadata.DOWNLOAD_URL, canvas.width),
313
+ regions,
314
+ root: null,
315
+ };
316
+ }
317
+ }
318
+ for (const key of FLEX_PAYLOAD_KEYS) {
319
+ if (key === 'MARKUP_JSON')
320
+ continue;
321
+ const parsed = parseObject(metadata[key]);
322
+ if (!parsed)
323
+ continue;
324
+ const root = normalizeFlexNode(parsed);
325
+ if (root) {
326
+ return {
327
+ version: 1,
328
+ layoutVersion: 1,
329
+ layoutFingerprint: flexLayoutFingerprint(root),
330
+ kind: 'flex',
331
+ altText: explicitAlt,
332
+ width: null,
333
+ height: null,
334
+ imageUrl: null,
335
+ regions: [],
336
+ root,
337
+ };
338
+ }
339
+ }
340
+ const textPayload = parseObject(message?.text);
341
+ const root = normalizeFlexNode(textPayload);
342
+ return root
343
+ ? {
344
+ version: 1,
345
+ layoutVersion: 1,
346
+ layoutFingerprint: flexLayoutFingerprint(root),
347
+ kind: 'flex',
348
+ altText: explicitAlt,
349
+ width: null,
350
+ height: null,
351
+ imageUrl: null,
352
+ regions: [],
353
+ root,
354
+ }
355
+ : null;
356
+ }
357
+ /** Interpret LINE bubble/carousel content in header→hero→body→footer order. */
358
+ export function interpretFlexPayload(payload) {
359
+ const texts = [];
360
+ const images = [];
361
+ const actions = [];
362
+ const seen = new Set();
363
+ let visited = 0;
364
+ const addText = (value) => {
365
+ if (typeof value !== 'string')
366
+ return;
367
+ const clean = value.replace(/\s+/g, ' ').trim();
368
+ if (clean && !texts.includes(clean))
369
+ texts.push(clean);
370
+ };
371
+ const walk = (raw, depth = 0) => {
372
+ if (depth > 24 || visited >= 800)
373
+ return;
374
+ const value = parseObject(raw);
375
+ if (!value || typeof value !== 'object' || seen.has(value))
376
+ return;
377
+ seen.add(value);
378
+ visited += 1;
379
+ if (Array.isArray(value)) {
380
+ for (const item of value)
381
+ walk(item, depth + 1);
382
+ return;
383
+ }
384
+ const node = value;
385
+ const type = typeof node.type === 'string' ? node.type.toLowerCase() : '';
386
+ if (type === 'text' || type === 'span')
387
+ addText(node.text);
388
+ if (type === 'image' || type === 'video') {
389
+ const image = webUrl(node.url) || webUrl(node.previewUrl);
390
+ if (image && !images.includes(image))
391
+ images.push(image);
392
+ }
393
+ const action = parseObject(node.action);
394
+ if (action && typeof action === 'object' && !Array.isArray(action)) {
395
+ const record = action;
396
+ const label = [record.label, record.displayText, record.text].find((item) => typeof item === 'string' && item.trim().length > 0) ?? '';
397
+ const url = webUrl(record.uri);
398
+ if (label || url)
399
+ actions.push({ label: label.trim(), url });
400
+ }
401
+ const ordered = [
402
+ 'header',
403
+ 'hero',
404
+ 'body',
405
+ 'footer',
406
+ 'contents',
407
+ 'altContent',
408
+ ];
409
+ for (const key of ordered)
410
+ if (key in node)
411
+ walk(node[key], depth + 1);
412
+ for (const [key, child] of Object.entries(node)) {
413
+ if (ordered.includes(key) ||
414
+ ['action', 'type', 'text', 'url', 'previewUrl'].includes(key))
415
+ continue;
416
+ if (child && typeof child === 'object')
417
+ walk(child, depth + 1);
418
+ }
419
+ };
420
+ walk(payload);
421
+ const machineLabels = new Set(['uri', 'url', 'link', 'open', 'action']);
422
+ const actionLabels = actions.map((item) => item.label).filter(Boolean);
423
+ const readableActionLabels = actionLabels.filter((label) => !machineLabels.has(label.toLowerCase()));
424
+ const readable = [
425
+ ...texts,
426
+ ...readableActionLabels.filter((label) => !texts.includes(label)),
427
+ ];
428
+ return {
429
+ summary: readable.join(' · ').slice(0, 700),
430
+ imageUrl: images[0] ?? null,
431
+ actionUrl: actions.find((item) => item.url)?.url ?? null,
432
+ actionLabels,
433
+ };
434
+ }
435
+ export function interpretFlexMessage(message) {
436
+ const metadata = message?.contentMetadata && typeof message.contentMetadata === 'object'
437
+ ? message.contentMetadata
438
+ : {};
439
+ for (const key of FLEX_PAYLOAD_KEYS) {
440
+ const parsed = parseObject(metadata[key]);
441
+ if (parsed)
442
+ return interpretFlexPayload(parsed);
443
+ }
444
+ const textPayload = parseObject(message?.text);
445
+ return textPayload
446
+ ? interpretFlexPayload(textPayload)
447
+ : {
448
+ summary: '',
449
+ imageUrl: null,
450
+ actionUrl: null,
451
+ actionLabels: [],
452
+ };
453
+ }
@@ -10,7 +10,7 @@
10
10
  * - `M` is the mentioned user's MID; `S`/`E` are the start/end offsets as
11
11
  * *strings*, not numbers.
12
12
  * - The range is half-open `[S, E)`, measured in UTF-16 code units — i.e.
13
- * plain JS string indices (`"@鄭富源".length === 4` matched `S:"0", E:"4"`).
13
+ * plain JavaScript string indices (a three-code-unit name plus `@` matched `S:"0", E:"4"`).
14
14
  * - Key order inside a mentionee object is not significant.
15
15
  * - MENTION rides in `contentMetadata`, which is never part of an E2EE
16
16
  * ciphertext payload — it was observed in the clear on a real message.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Convert raw/decrypted LINE payloads into safe user-facing preview text.
3
+ *
4
+ * E2EE media envelopes are transport data, not message text. They commonly
5
+ * contain keyMaterial, keys, chunks, ciphertext, or nonce fields. This
6
+ * boundary must be shared by every conversation/message output path so a raw
7
+ * message cannot bypass the decrypted-path sanitizer.
8
+ */
9
+ const OPAQUE_FIELD = /(?:^|["'\s,{])(keymaterial|keys|chunks|ciphertext|nonce)\s*["']?\s*:/i;
10
+ const OPAQUE_TOKEN = /\b(?:keymaterial|ciphertext|nonce)\b/i;
11
+ function isRecord(value) {
12
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
13
+ }
14
+ function containsOpaqueField(value) {
15
+ if (typeof value === 'string') {
16
+ return (OPAQUE_FIELD.test(value) ||
17
+ (value.trim().startsWith('{') && OPAQUE_TOKEN.test(value)));
18
+ }
19
+ if (Array.isArray(value))
20
+ return value.some(containsOpaqueField);
21
+ if (!isRecord(value))
22
+ return false;
23
+ return Object.entries(value).some(([key, nested]) => /^(keymaterial|keys|chunks|ciphertext|nonce)$/i.test(key) ||
24
+ containsOpaqueField(nested));
25
+ }
26
+ /** Return only actual user text; opaque E2EE envelopes return an empty string. */
27
+ export function sanitizeMessagePreview(value) {
28
+ if (containsOpaqueField(value))
29
+ return '';
30
+ if (typeof value !== 'string') {
31
+ if (!isRecord(value))
32
+ return '';
33
+ for (const key of ['text', 'message', 'body', 'content', 'caption']) {
34
+ const nested = sanitizeMessagePreview(value[key]);
35
+ if (nested)
36
+ return nested;
37
+ }
38
+ return '';
39
+ }
40
+ const text = value.trim();
41
+ if (!text)
42
+ return '';
43
+ if (!text.startsWith('{') && !text.startsWith('['))
44
+ return text;
45
+ try {
46
+ const parsed = JSON.parse(text);
47
+ if (!isRecord(parsed))
48
+ return '';
49
+ for (const key of ['text', 'message', 'body', 'content', 'caption']) {
50
+ const nested = sanitizeMessagePreview(parsed[key]);
51
+ if (nested)
52
+ return nested;
53
+ }
54
+ return '';
55
+ }
56
+ catch {
57
+ // A non-JSON message beginning with punctuation is still normal text.
58
+ return text;
59
+ }
60
+ }
@@ -137,25 +137,5 @@ export function createMessageQueryService(getClient, e2eeManager) {
137
137
  });
138
138
  return decryptLineMessages(e2eeManager, messages, chatId);
139
139
  },
140
- /**
141
- * Download the original LINE content bytes for one message.
142
- *
143
- * @param messageId - LINE message identifier
144
- * @param requestId - Optional request identifier
145
- * @returns Original message content bytes
146
- */
147
- async downloadMessageContent(messageId, requestId = `yomi-${Date.now()}`) {
148
- return requireLineClient(getClient).downloadMessageContent(messageId, requestId);
149
- },
150
- /**
151
- * Download the LINE preview bytes for one message.
152
- *
153
- * @param messageId - LINE message identifier
154
- * @param requestId - Optional request identifier
155
- * @returns Preview content bytes
156
- */
157
- async downloadMessageContentPreview(messageId, requestId = `yomi-${Date.now()}`) {
158
- return requireLineClient(getClient).downloadMessageContentPreview(messageId, requestId);
159
- },
160
140
  };
161
141
  }
@@ -58,10 +58,10 @@ export class LineProtocolService extends EventEmitter {
58
58
  invalidateSession;
59
59
  tryRefreshToken;
60
60
  resumeSession;
61
- resolveName;
62
61
  getContact;
63
62
  getGroup;
64
63
  listStickerPackages;
64
+ getAllMessageBoxes;
65
65
  searchStickerPackages;
66
66
  sendMessage;
67
67
  sendImage;
@@ -73,8 +73,6 @@ export class LineProtocolService extends EventEmitter {
73
73
  sendLocation;
74
74
  getRecentMessages;
75
75
  getPreviousMessages;
76
- downloadMessageContent;
77
- downloadMessageContentPreview;
78
76
  markChatRead;
79
77
  renameGroup;
80
78
  inviteToGroup;
@@ -1,5 +1,5 @@
1
1
  import { createCliLogger } from '../../util/log.js';
2
- import { resolveUserNames } from '../names.js';
2
+ import { contactDisplayName, resolveUserNames } from '../names.js';
3
3
  import { jsonResult, toolError, toonResult } from './shared.js';
4
4
  const log = createCliLogger('Yomi');
5
5
  /**
@@ -23,12 +23,16 @@ async function fetchAllContacts(service) {
23
23
  if (!contact?.mid) {
24
24
  continue;
25
25
  }
26
- if (contact.displayName) {
27
- service.nameCache.set(contact.mid, contact.displayName);
26
+ const name = contactDisplayName(contact);
27
+ if (name) {
28
+ service.nameCache.set(contact.mid, name);
28
29
  }
29
30
  summaries.push({
30
31
  mid: contact.mid,
31
- displayName: contact.displayName ?? null,
32
+ displayName: name,
33
+ ...(contact.displayNameOverridden && contact.displayName
34
+ ? { profileName: contact.displayName }
35
+ : {}),
32
36
  });
33
37
  }
34
38
  return summaries;
@@ -47,7 +51,8 @@ export async function handleListContacts(service) {
47
51
  }
48
52
  /**
49
53
  * Handle `find_contact` — case-insensitive substring match over the
50
- * friend list's displayName, so a caller can resolve a person's name to
54
+ * friend list's displayName (and profileName, when the user renamed the
55
+ * friend), so a caller can resolve a person's name to
51
56
  * the MID `send_message` needs for a 1:1. Pure lookup: no fuzzy scoring,
52
57
  * no ranking by interaction history.
53
58
  *
@@ -61,7 +66,8 @@ export async function handleFindContact(service, args) {
61
66
  }
62
67
  const needle = args.name.toLowerCase();
63
68
  const contacts = await fetchAllContacts(service);
64
- const matches = contacts.filter((contact) => contact.displayName?.toLowerCase().includes(needle));
69
+ const matches = contacts.filter((contact) => contact.displayName?.toLowerCase().includes(needle) ||
70
+ contact.profileName?.toLowerCase().includes(needle));
65
71
  return toonResult(matches);
66
72
  }
67
73
  /**