@rikaidev/yomi 0.5.0 → 0.6.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.
Files changed (63) hide show
  1. package/README.md +28 -0
  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 +22 -16
  13. package/dist/auth/credential-store.js +25 -12
  14. package/dist/cli/query.js +276 -69
  15. package/dist/cli/work-item.js +24 -0
  16. package/dist/line/client/auth-service/client.js +7 -8
  17. package/dist/line/client/buddy-service/client.js +73 -0
  18. package/dist/line/client/index.js +28 -1
  19. package/dist/line/client/obs-media-client.js +2 -1
  20. package/dist/line/client/square-service/client.js +298 -0
  21. package/dist/line/client/talk-service/command-client.js +9 -1
  22. package/dist/line/client/talk-service/message-id-query.js +27 -3
  23. package/dist/line/client/talk-service/requests.js +9 -0
  24. package/dist/line/core/auth-session-runtime.js +1 -1
  25. package/dist/line/core/chat-event.js +2 -0
  26. package/dist/line/core/chat-runtime-service.js +18 -3
  27. package/dist/line/core/config.js +1 -0
  28. package/dist/line/core/flex-message.js +323 -0
  29. package/dist/line/core/service.js +0 -1
  30. package/dist/mcp/handlers/contacts.js +12 -6
  31. package/dist/mcp/handlers/conversations.js +54 -0
  32. package/dist/mcp/handlers/send.js +8 -11
  33. package/dist/mcp/integrator.js +21 -7
  34. package/dist/mcp/media.js +101 -2
  35. package/dist/mcp/names.js +78 -6
  36. package/dist/search/default-embedder.js +12 -3
  37. package/dist/version.js +1 -1
  38. package/dist/work-item/adapter.js +1 -0
  39. package/dist/work-item/destination-contracts.js +1 -0
  40. package/dist/work-item/event-context.js +73 -0
  41. package/dist/work-item/gold-contract.js +101 -0
  42. package/dist/work-item/gold-coverage.js +96 -0
  43. package/dist/work-item/gold-derivation.js +67 -0
  44. package/dist/work-item/gold-split.js +143 -0
  45. package/dist/work-item/index.js +23 -0
  46. package/dist/work-item/line-adapter.js +111 -0
  47. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  48. package/dist/work-item/local-provider.js +38 -0
  49. package/dist/work-item/local-work-event-provider.js +56 -0
  50. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  51. package/dist/work-item/provider.js +21 -0
  52. package/dist/work-item/registry.js +19 -0
  53. package/dist/work-item/service.js +85 -0
  54. package/dist/work-item/store.js +97 -0
  55. package/dist/work-item/types.js +79 -0
  56. package/dist/work-item/work-event-graph-engine.js +63 -0
  57. package/dist/work-item/work-event-inference.js +267 -0
  58. package/dist/work-item/work-event-matcher.js +105 -0
  59. package/dist/work-item/work-event-model-manifest.js +39 -0
  60. package/dist/work-item/work-event-store.js +125 -0
  61. package/dist/work-item/work-event.js +123 -0
  62. package/package.json +25 -4
  63. package/run.mjs +186 -9
@@ -0,0 +1,128 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const CURRENT_SUMMARY_ENGINE_VERSION = 1;
3
+ const EPISODE_GAP_MS = 45 * 60 * 1000; // 45 minutes
4
+ const ROLLUP_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
5
+ function computeDigest(content) {
6
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
7
+ }
8
+ function inferDialogueActs(events) {
9
+ const acts = new Set();
10
+ for (const event of events) {
11
+ const text = event.text ?? '';
12
+ const isQuestion = /[??]/.test(text);
13
+ if (isQuestion) {
14
+ acts.add('ask');
15
+ }
16
+ if (['image', 'video', 'file'].includes(event.contentType)) {
17
+ acts.add('deliver');
18
+ }
19
+ if (event.mentionsSelf || (event.metadata.mentionMids?.length ?? 0) > 0) {
20
+ acts.add('directive');
21
+ }
22
+ if (event.contentType === 'sticker') {
23
+ acts.add('acknowledge');
24
+ }
25
+ if (text.length > 0 && !isQuestion) {
26
+ acts.add('inform');
27
+ }
28
+ }
29
+ return [...acts];
30
+ }
31
+ export function buildEpisodeSummaries(events, engineVersion = CURRENT_SUMMARY_ENGINE_VERSION) {
32
+ if (events.length === 0)
33
+ return [];
34
+ const byChat = new Map();
35
+ for (const event of events) {
36
+ const list = byChat.get(event.chatId) ?? [];
37
+ list.push(event);
38
+ byChat.set(event.chatId, list);
39
+ }
40
+ const summaries = [];
41
+ for (const [chatId, chatEvents] of byChat) {
42
+ const sorted = [...chatEvents].sort((a, b) => a.sentAt - b.sentAt);
43
+ let currentEpisode = [];
44
+ const flush = () => {
45
+ if (currentEpisode.length === 0)
46
+ return;
47
+ const minSentAt = currentEpisode[0].sentAt;
48
+ const maxSentAt = currentEpisode[currentEpisode.length - 1].sentAt;
49
+ const messageIds = currentEpisode.map((e) => e.messageId);
50
+ const participantNames = [
51
+ ...new Set(currentEpisode
52
+ .map((e) => e.senderName)
53
+ .filter((n) => Boolean(n))),
54
+ ];
55
+ const ownerHints = [
56
+ ...new Set(currentEpisode.flatMap((e) => e.metadata.mentionNames ?? [])),
57
+ ];
58
+ const hasMentionsSelf = currentEpisode.some((e) => e.mentionsSelf);
59
+ const hasQuestions = currentEpisode.some((e) => /[??]/.test(e.text ?? ''));
60
+ const hasMedia = currentEpisode.some((e) => ['image', 'video', 'file', 'audio'].includes(e.contentType));
61
+ const dialogueActs = inferDialogueActs(currentEpisode);
62
+ const hasSelfQuestion = currentEpisode.some((e) => e.isSelf && /[??]/.test(e.text ?? ''));
63
+ const selfQuestionIdx = currentEpisode.findIndex((e) => e.isSelf && /[??]/.test(e.text ?? ''));
64
+ const hasReplyAfterSelfQuestion = selfQuestionIdx >= 0 &&
65
+ currentEpisode.slice(selfQuestionIdx + 1).some((e) => !e.isSelf);
66
+ const responseState = hasReplyAfterSelfQuestion
67
+ ? 'received'
68
+ : hasSelfQuestion
69
+ ? 'waiting'
70
+ : 'observing';
71
+ const digest = computeDigest(`${chatId}:${minSentAt}:${messageIds.join(',')}`);
72
+ const episodeId = `ep-${chatId.slice(0, 8)}-${minSentAt}`;
73
+ summaries.push({
74
+ episodeId,
75
+ chatId,
76
+ minSentAt,
77
+ maxSentAt,
78
+ messageIds,
79
+ participantNames,
80
+ dialogueActs,
81
+ ownerHints,
82
+ responseState,
83
+ hasMentionsSelf,
84
+ hasQuestions,
85
+ hasMedia,
86
+ candidateCount: hasMentionsSelf || hasSelfQuestion ? 1 : 0,
87
+ digest,
88
+ engineVersion,
89
+ });
90
+ currentEpisode = [];
91
+ };
92
+ for (const event of sorted) {
93
+ if (currentEpisode.length > 0 &&
94
+ event.sentAt - currentEpisode[currentEpisode.length - 1].sentAt >
95
+ EPISODE_GAP_MS) {
96
+ flush();
97
+ }
98
+ currentEpisode.push(event);
99
+ }
100
+ flush();
101
+ }
102
+ return summaries.sort((a, b) => a.minSentAt - b.minSentAt);
103
+ }
104
+ export function buildRollupSummary(windowStart, windowEnd, episodes, engineVersion = CURRENT_SUMMARY_ENGINE_VERSION) {
105
+ const matching = episodes.filter((ep) => ep.maxSentAt >= windowStart && ep.minSentAt <= windowEnd);
106
+ const activeChatIds = [...new Set(matching.map((ep) => ep.chatId))];
107
+ const episodeIds = matching.map((ep) => ep.episodeId);
108
+ const totalEvents = matching.reduce((sum, ep) => sum + ep.messageIds.length, 0);
109
+ const unresolvedFrontierCount = matching.filter((ep) => ep.responseState === 'waiting' || ep.hasMentionsSelf).length;
110
+ const topics = matching
111
+ .filter((ep) => ep.dialogueActs.length > 0)
112
+ .map((ep) => `${ep.chatId}:${ep.dialogueActs.join('+')}`)
113
+ .slice(0, 10);
114
+ const rollupId = `rollup-${Math.floor(windowStart / ROLLUP_INTERVAL_MS)}`;
115
+ const digest = computeDigest(`${rollupId}:${episodeIds.join(',')}:${totalEvents}`);
116
+ return {
117
+ rollupId,
118
+ windowStart,
119
+ windowEnd,
120
+ activeChatIds,
121
+ episodeIds,
122
+ totalEvents,
123
+ unresolvedFrontierCount,
124
+ topics,
125
+ digest,
126
+ engineVersion,
127
+ };
128
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for Hierarchical Summaries (10-minute Episodes & 6-hour Rollups).
3
+ */
4
+ export {};
@@ -49,23 +49,29 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
49
49
  const messageVectors = vectors.slice(anchors.length);
50
50
  return inputs.map((input, inputIndex) => {
51
51
  if (input.isMuted)
52
- return { score: -100, tier: 'filtered', reason: '已靜音' };
52
+ return { score: -100, tier: 'filtered', reason: 'muted' };
53
53
  if (input.explicitPriority)
54
- return { score: 70, tier: 'now', reason: '使用者標記優先' };
54
+ return { score: 70, tier: 'now', reason: 'manual_priority' };
55
55
  if (input.hasMention)
56
- return { score: 65, tier: 'now', reason: '直接提及你' };
56
+ return { score: 65, tier: 'now', reason: 'direct_mention' };
57
+ if (input.isOfficial &&
58
+ /^\[yomi:(?:image|rich|sticker|video|audio|file|extended_image)\]$/.test(input.text.trim())) {
59
+ return { score: 0, tier: 'filtered', reason: 'ordinary' };
60
+ }
57
61
  const similarities = new Map();
58
62
  anchors.forEach((anchor, anchorIndex) => {
59
63
  const similarity = dot(messageVectors[inputIndex], anchorVectors[anchorIndex]);
60
64
  similarities.set(anchor.intent, Math.max(similarities.get(anchor.intent) ?? -Infinity, similarity));
61
65
  });
62
66
  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';
67
+ const intent = ranked[0];
68
+ if (input.isOfficial && intent !== 'transaction') {
69
+ return {
70
+ score: -40,
71
+ tier: 'filtered',
72
+ reason: intent === 'promotion' ? 'promotion' : 'ordinary',
73
+ };
74
+ }
69
75
  const scoreByIntent = {
70
76
  reply: 55,
71
77
  deadline: 60,
@@ -74,7 +80,7 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
74
80
  ordinary: 0,
75
81
  promotion: -40,
76
82
  };
77
- const score = scoreByIntent[intent] + (input.unreadCount > 0 ? 5 : 0);
83
+ const score = scoreByIntent[intent];
78
84
  const tier = score >= 55
79
85
  ? 'now'
80
86
  : score >= 35
@@ -83,12 +89,12 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
83
89
  ? 'know'
84
90
  : 'filtered';
85
91
  const reasons = {
86
- reply: '對方可能在等你回覆',
87
- deadline: '有期限、行程或緊急事項',
88
- transaction: '與你的交易或服務狀態有關',
89
- important: '重要資訊,但不需立即回覆',
90
- ordinary: '一般資訊,沒有明確下一步',
91
- promotion: '官方宣傳或與你較無關',
92
+ reply: 'reply_expected',
93
+ deadline: 'deadline',
94
+ transaction: 'transaction',
95
+ important: 'important',
96
+ ordinary: 'ordinary',
97
+ promotion: 'promotion',
92
98
  };
93
99
  return { score, tier, reason: reasons[intent] };
94
100
  });
@@ -86,7 +86,7 @@ class VolatileCacheStore {
86
86
  cache = new Map();
87
87
  loaded = false;
88
88
  dirty = false;
89
- writing = false;
89
+ flushPromise = null;
90
90
  constructor(filePath) {
91
91
  this.filePath = filePath;
92
92
  }
@@ -139,11 +139,10 @@ class VolatileCacheStore {
139
139
  /** Coalesce bursts of writes into a single atomic flush cycle. */
140
140
  scheduleFlush() {
141
141
  this.dirty = true;
142
- if (this.writing) {
142
+ if (this.flushPromise) {
143
143
  return;
144
144
  }
145
- this.writing = true;
146
- void (async () => {
145
+ this.flushPromise = (async () => {
147
146
  const fs = await import('node:fs/promises');
148
147
  while (this.dirty) {
149
148
  this.dirty = false;
@@ -169,8 +168,13 @@ class VolatileCacheStore {
169
168
  // Best-effort cache persistence; in-memory copy still serves reads.
170
169
  }
171
170
  }
172
- this.writing = false;
173
- })();
171
+ })().finally(() => {
172
+ this.flushPromise = null;
173
+ });
174
+ }
175
+ async flush() {
176
+ while (this.flushPromise)
177
+ await this.flushPromise;
174
178
  }
175
179
  }
176
180
  /**
@@ -196,11 +200,12 @@ export class CredentialStore {
196
200
  this.filePath = fallbackFilePath;
197
201
  this.cache = new Map();
198
202
  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).
203
+ // The user-isolated credential file (0600 mode) is fast, reliable, and avoids
204
+ // triggering disruptive macOS SecurityAgent GUI prompts. Mutating the Keychain
205
+ // via `security` CLI is opt-in (via YOMI_USE_KEYCHAIN=1) rather than default.
202
206
  this.secureStorageEnabled =
203
207
  process.platform === 'darwin' &&
208
+ process.env.YOMI_USE_KEYCHAIN === '1' &&
204
209
  process.env.YOMI_NO_KEYCHAIN !== '1' &&
205
210
  process.env.YOMI_READ_ONLY_SESSION !== '1';
206
211
  this.keychainService = this.secureStorageEnabled
@@ -229,8 +234,12 @@ export class CredentialStore {
229
234
  }
230
235
  }
231
236
  catch { }
232
- if (this.secureStorageEnabled && this.keychainService) {
233
- const result = await this.keychainService.getCredential(this.keychainAccount);
237
+ const keychain = this.keychainService ??
238
+ (process.platform === 'darwin' && process.env.YOMI_NO_KEYCHAIN !== '1'
239
+ ? getKeychainService()
240
+ : null);
241
+ if (keychain) {
242
+ const result = await keychain.getCredential(this.keychainAccount);
234
243
  if (result.success && result.password) {
235
244
  try {
236
245
  const fs = await import('node:fs/promises');
@@ -243,7 +252,6 @@ export class CredentialStore {
243
252
  catch { }
244
253
  return result.password;
245
254
  }
246
- return null;
247
255
  }
248
256
  return null;
249
257
  }
@@ -361,6 +369,11 @@ export class CredentialStore {
361
369
  return persisted;
362
370
  });
363
371
  }
372
+ /** Await durable and volatile write queues, primarily for orderly shutdown. */
373
+ async flush() {
374
+ await this.persistQueue;
375
+ await this.volatile.flush();
376
+ }
364
377
  /**
365
378
  * Delete a credential and persist.
366
379
  * @param key - Credential key to delete.