iris-chatbot 0.2.4

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/bin/iris.mjs +267 -0
  4. package/package.json +61 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +49 -0
  7. package/template/eslint.config.mjs +18 -0
  8. package/template/next.config.ts +7 -0
  9. package/template/package-lock.json +9193 -0
  10. package/template/package.json +46 -0
  11. package/template/postcss.config.mjs +7 -0
  12. package/template/public/file.svg +1 -0
  13. package/template/public/globe.svg +1 -0
  14. package/template/public/next.svg +1 -0
  15. package/template/public/vercel.svg +1 -0
  16. package/template/public/window.svg +1 -0
  17. package/template/src/app/api/chat/route.ts +2445 -0
  18. package/template/src/app/api/connections/models/route.ts +255 -0
  19. package/template/src/app/api/connections/test/route.ts +124 -0
  20. package/template/src/app/api/local-sync/route.ts +74 -0
  21. package/template/src/app/api/tool-approval/route.ts +47 -0
  22. package/template/src/app/favicon.ico +0 -0
  23. package/template/src/app/globals.css +808 -0
  24. package/template/src/app/layout.tsx +74 -0
  25. package/template/src/app/page.tsx +444 -0
  26. package/template/src/components/ChatView.tsx +1537 -0
  27. package/template/src/components/Composer.tsx +160 -0
  28. package/template/src/components/MapView.tsx +244 -0
  29. package/template/src/components/MessageCard.tsx +955 -0
  30. package/template/src/components/SearchModal.tsx +72 -0
  31. package/template/src/components/SettingsModal.tsx +1257 -0
  32. package/template/src/components/Sidebar.tsx +153 -0
  33. package/template/src/components/TopBar.tsx +164 -0
  34. package/template/src/lib/connections.ts +275 -0
  35. package/template/src/lib/data.ts +324 -0
  36. package/template/src/lib/db.ts +49 -0
  37. package/template/src/lib/hooks.ts +76 -0
  38. package/template/src/lib/local-sync.ts +192 -0
  39. package/template/src/lib/memory.ts +695 -0
  40. package/template/src/lib/model-presets.ts +251 -0
  41. package/template/src/lib/store.ts +36 -0
  42. package/template/src/lib/tooling/approvals.ts +78 -0
  43. package/template/src/lib/tooling/providers/anthropic.ts +155 -0
  44. package/template/src/lib/tooling/providers/ollama.ts +73 -0
  45. package/template/src/lib/tooling/providers/openai.ts +267 -0
  46. package/template/src/lib/tooling/providers/openai_compatible.ts +16 -0
  47. package/template/src/lib/tooling/providers/types.ts +44 -0
  48. package/template/src/lib/tooling/registry.ts +103 -0
  49. package/template/src/lib/tooling/runtime.ts +189 -0
  50. package/template/src/lib/tooling/safety.ts +165 -0
  51. package/template/src/lib/tooling/tools/apps.ts +108 -0
  52. package/template/src/lib/tooling/tools/apps_plus.ts +153 -0
  53. package/template/src/lib/tooling/tools/communication.ts +883 -0
  54. package/template/src/lib/tooling/tools/files.ts +395 -0
  55. package/template/src/lib/tooling/tools/music.ts +988 -0
  56. package/template/src/lib/tooling/tools/notes.ts +461 -0
  57. package/template/src/lib/tooling/tools/notes_plus.ts +294 -0
  58. package/template/src/lib/tooling/tools/numbers.ts +175 -0
  59. package/template/src/lib/tooling/tools/schedule.ts +579 -0
  60. package/template/src/lib/tooling/tools/system.ts +142 -0
  61. package/template/src/lib/tooling/tools/web.ts +212 -0
  62. package/template/src/lib/tooling/tools/workflow.ts +218 -0
  63. package/template/src/lib/tooling/types.ts +27 -0
  64. package/template/src/lib/types.ts +309 -0
  65. package/template/src/lib/utils.ts +108 -0
  66. package/template/tsconfig.json +34 -0
@@ -0,0 +1,695 @@
1
+ import { nanoid } from "nanoid";
2
+ import { db } from "./db";
3
+ import type {
4
+ MemoryContextPayload,
5
+ MemoryEntry,
6
+ MemoryKind,
7
+ MemoryMusicAlias,
8
+ MemoryScope,
9
+ MemorySettings,
10
+ MemorySource,
11
+ } from "./types";
12
+
13
+ const MAX_MEMORY_ENTRIES = 300;
14
+ const MAX_MEMORY_CONTEXT_CHARS = 1800;
15
+ const DAY_MS = 24 * 60 * 60 * 1000;
16
+
17
+ type CapturedMemoryCandidate = {
18
+ kind: MemoryKind;
19
+ scope: MemoryScope;
20
+ conversationId?: string;
21
+ key: string;
22
+ value: string;
23
+ normalizedKey: string;
24
+ source: MemorySource;
25
+ confidence: number;
26
+ };
27
+
28
+ export function normalizeMemoryKey(input: string): string {
29
+ return input
30
+ .normalize("NFKD")
31
+ .replace(/[\u0300-\u036f]/g, "")
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9\s]/g, " ")
34
+ .replace(/\s+/g, " ")
35
+ .trim();
36
+ }
37
+
38
+ function trimSentence(input: string): string {
39
+ return input
40
+ .trim()
41
+ .replace(/["'`]+$/g, "")
42
+ .replace(/^["'`]+/g, "")
43
+ .replace(/[.?!\s]+$/g, "")
44
+ .trim();
45
+ }
46
+
47
+ function detectScope(text: string): MemoryScope {
48
+ return /\b(for this chat|in this chat|for this thread)\b/i.test(text)
49
+ ? "conversation"
50
+ : "global";
51
+ }
52
+
53
+ function looksSensitive(text: string): boolean {
54
+ const patterns = [
55
+ /\b(?:password|passcode|pin)\b\s*(?:is|=|:)?\s*\S+/i,
56
+ /\b(?:api[_\s-]?key|access[_\s-]?token|secret|private key)\b\s*(?:is|=|:)?\s*\S+/i,
57
+ /\bsk-[a-zA-Z0-9]{12,}\b/,
58
+ /\b\d{3}-\d{2}-\d{4}\b/,
59
+ /\b(?:\d[ -]*?){13,19}\b/,
60
+ /\b[A-Za-z0-9+/_-]{24,}\.[A-Za-z0-9+/_-]{10,}\.[A-Za-z0-9+/_-]{10,}\b/,
61
+ ];
62
+ return patterns.some((pattern) => pattern.test(text));
63
+ }
64
+
65
+ function createCandidate(input: {
66
+ kind: MemoryKind;
67
+ scope: MemoryScope;
68
+ conversationId?: string;
69
+ key: string;
70
+ value: string;
71
+ source: MemorySource;
72
+ confidence: number;
73
+ }): CapturedMemoryCandidate | null {
74
+ const key = trimSentence(input.key);
75
+ const value = trimSentence(input.value);
76
+ if (!key || !value) {
77
+ return null;
78
+ }
79
+ const normalizedKey = normalizeMemoryKey(key);
80
+ if (!normalizedKey) {
81
+ return null;
82
+ }
83
+ return {
84
+ kind: input.kind,
85
+ scope: input.scope,
86
+ conversationId: input.scope === "conversation" ? input.conversationId : undefined,
87
+ key,
88
+ value,
89
+ normalizedKey,
90
+ source: input.source,
91
+ confidence: input.confidence,
92
+ };
93
+ }
94
+
95
+ function parseMusicTarget(target: string): { query: string; title?: string; artist?: string } {
96
+ const trimmed = trimSentence(target);
97
+ const bySplit = trimmed.split(/\s+by\s+/i);
98
+ if (bySplit.length > 1) {
99
+ const title = trimSentence(bySplit[0] ?? "");
100
+ const artist = trimSentence(bySplit.slice(1).join(" by "));
101
+ return {
102
+ query: trimmed,
103
+ title: title || undefined,
104
+ artist: artist || undefined,
105
+ };
106
+ }
107
+ return { query: trimmed };
108
+ }
109
+
110
+ function parseStoredMusicAlias(entry: MemoryEntry): MemoryMusicAlias | null {
111
+ try {
112
+ const parsed = JSON.parse(entry.value) as {
113
+ query?: unknown;
114
+ title?: unknown;
115
+ artist?: unknown;
116
+ };
117
+ if (typeof parsed.query !== "string" || !parsed.query.trim()) {
118
+ return null;
119
+ }
120
+ return {
121
+ alias: entry.key,
122
+ query: parsed.query.trim(),
123
+ title: typeof parsed.title === "string" && parsed.title.trim() ? parsed.title.trim() : undefined,
124
+ artist: typeof parsed.artist === "string" && parsed.artist.trim() ? parsed.artist.trim() : undefined,
125
+ };
126
+ } catch {
127
+ const query = trimSentence(entry.value);
128
+ if (!query) {
129
+ return null;
130
+ }
131
+ return {
132
+ alias: entry.key,
133
+ query,
134
+ };
135
+ }
136
+ }
137
+
138
+ function extractExplicitCandidates(params: {
139
+ text: string;
140
+ scope: MemoryScope;
141
+ conversationId: string;
142
+ }): CapturedMemoryCandidate[] {
143
+ const { text, scope, conversationId } = params;
144
+ const candidates: CapturedMemoryCandidate[] = [];
145
+
146
+ const aliasMeaningMatch = text.match(
147
+ /\bwhen\s+i\s+say\s+["']?(.+?)["']?\s+i\s+mean\s+["']?(.+?)["']?(?:[.?!]|$)/i,
148
+ );
149
+ if (aliasMeaningMatch?.[1] && aliasMeaningMatch?.[2]) {
150
+ const candidate = createCandidate({
151
+ kind: "person_alias",
152
+ scope,
153
+ conversationId,
154
+ key: aliasMeaningMatch[1],
155
+ value: aliasMeaningMatch[2],
156
+ source: "explicit",
157
+ confidence: 1,
158
+ });
159
+ if (candidate) {
160
+ candidates.push(candidate);
161
+ }
162
+ }
163
+
164
+ const aliasMessageMatch = text.match(
165
+ /\bwhen\s+i\s+say\s+["']?(.+?)["']?\s+(?:text|message)\s+["']?(.+?)["']?(?:[.?!]|$)/i,
166
+ );
167
+ if (aliasMessageMatch?.[1] && aliasMessageMatch?.[2]) {
168
+ const candidate = createCandidate({
169
+ kind: "person_alias",
170
+ scope,
171
+ conversationId,
172
+ key: aliasMessageMatch[1],
173
+ value: aliasMessageMatch[2],
174
+ source: "explicit",
175
+ confidence: 1,
176
+ });
177
+ if (candidate) {
178
+ candidates.push(candidate);
179
+ }
180
+ }
181
+
182
+ const aliasPlayMatch = text.match(
183
+ /\bwhen\s+i\s+say\s+["']?(.+?)["']?\s+play\s+["']?(.+?)["']?(?:[.?!]|$)/i,
184
+ );
185
+ if (aliasPlayMatch?.[1] && aliasPlayMatch?.[2]) {
186
+ const parsedTarget = parseMusicTarget(aliasPlayMatch[2]);
187
+ const candidate = createCandidate({
188
+ kind: "music_alias",
189
+ scope,
190
+ conversationId,
191
+ key: aliasPlayMatch[1],
192
+ value: JSON.stringify(parsedTarget),
193
+ source: "explicit",
194
+ confidence: 1,
195
+ });
196
+ if (candidate) {
197
+ candidates.push(candidate);
198
+ }
199
+ }
200
+
201
+ const rememberMatch = text.match(/\bremember(?:\s+that)?\s+(.+)$/i);
202
+ if (!rememberMatch?.[1]) {
203
+ return candidates;
204
+ }
205
+
206
+ const remembered = trimSentence(rememberMatch[1]);
207
+ if (!remembered) {
208
+ return candidates;
209
+ }
210
+
211
+ const nameMatch = remembered.match(/^my\s+name\s+is\s+(.+)$/i);
212
+ if (nameMatch?.[1]) {
213
+ const candidate = createCandidate({
214
+ kind: "profile",
215
+ scope,
216
+ conversationId,
217
+ key: "name",
218
+ value: nameMatch[1],
219
+ source: "explicit",
220
+ confidence: 1,
221
+ });
222
+ if (candidate) {
223
+ candidates.push(candidate);
224
+ }
225
+ return candidates;
226
+ }
227
+
228
+ const callMeMatch = remembered.match(/^call\s+me\s+(.+)$/i);
229
+ if (callMeMatch?.[1]) {
230
+ const candidate = createCandidate({
231
+ kind: "profile",
232
+ scope,
233
+ conversationId,
234
+ key: "name",
235
+ value: callMeMatch[1],
236
+ source: "explicit",
237
+ confidence: 1,
238
+ });
239
+ if (candidate) {
240
+ candidates.push(candidate);
241
+ }
242
+ return candidates;
243
+ }
244
+
245
+ if (/\b(i\s+prefer|default\s+to|always\s+use)\b/i.test(remembered)) {
246
+ const candidate = createCandidate({
247
+ kind: "preference",
248
+ scope,
249
+ conversationId,
250
+ key: remembered,
251
+ value: remembered,
252
+ source: "explicit",
253
+ confidence: 1,
254
+ });
255
+ if (candidate) {
256
+ candidates.push(candidate);
257
+ }
258
+ return candidates;
259
+ }
260
+
261
+ const noteCandidate = createCandidate({
262
+ kind: "note",
263
+ scope,
264
+ conversationId,
265
+ key: remembered.slice(0, 80),
266
+ value: remembered,
267
+ source: "explicit",
268
+ confidence: 1,
269
+ });
270
+ if (noteCandidate) {
271
+ candidates.push(noteCandidate);
272
+ }
273
+
274
+ return candidates;
275
+ }
276
+
277
+ function extractAutoCandidates(params: {
278
+ text: string;
279
+ scope: MemoryScope;
280
+ conversationId: string;
281
+ }): CapturedMemoryCandidate[] {
282
+ const { text, scope, conversationId } = params;
283
+ const candidates: CapturedMemoryCandidate[] = [];
284
+
285
+ const nameMatch = text.match(/\bmy\s+name\s+is\s+([^.!?\n]+)(?:[.!?]|$)/i);
286
+ if (nameMatch?.[1]) {
287
+ const candidate = createCandidate({
288
+ kind: "profile",
289
+ scope,
290
+ conversationId,
291
+ key: "name",
292
+ value: nameMatch[1],
293
+ source: "auto",
294
+ confidence: 0.75,
295
+ });
296
+ if (candidate) {
297
+ candidates.push(candidate);
298
+ }
299
+ }
300
+
301
+ const callMeMatch = text.match(/\bcall\s+me\s+([^.!?\n]+)(?:[.!?]|$)/i);
302
+ if (callMeMatch?.[1]) {
303
+ const candidate = createCandidate({
304
+ kind: "profile",
305
+ scope,
306
+ conversationId,
307
+ key: "name",
308
+ value: callMeMatch[1],
309
+ source: "auto",
310
+ confidence: 0.75,
311
+ });
312
+ if (candidate) {
313
+ candidates.push(candidate);
314
+ }
315
+ }
316
+
317
+ const preferenceMatchers: Array<{ pattern: RegExp; prefix: string }> = [
318
+ { pattern: /\bi\s+prefer\s+([^.!?\n]+)(?:[.!?]|$)/i, prefix: "I prefer" },
319
+ { pattern: /\bdefault\s+to\s+([^.!?\n]+)(?:[.!?]|$)/i, prefix: "Default to" },
320
+ { pattern: /\balways\s+use\s+([^.!?\n]+)(?:[.!?]|$)/i, prefix: "Always use" },
321
+ ];
322
+
323
+ for (const matcher of preferenceMatchers) {
324
+ const match = text.match(matcher.pattern);
325
+ if (!match?.[1]) {
326
+ continue;
327
+ }
328
+ const value = `${matcher.prefix} ${trimSentence(match[1])}`;
329
+ const candidate = createCandidate({
330
+ kind: "preference",
331
+ scope,
332
+ conversationId,
333
+ key: trimSentence(match[1]),
334
+ value,
335
+ source: "auto",
336
+ confidence: 0.75,
337
+ });
338
+ if (candidate) {
339
+ candidates.push(candidate);
340
+ }
341
+ }
342
+
343
+ return candidates;
344
+ }
345
+
346
+ async function upsertMemoryCandidate(candidate: CapturedMemoryCandidate): Promise<void> {
347
+ const now = Date.now();
348
+ const existing = await db.memories
349
+ .where("normalizedKey")
350
+ .equals(candidate.normalizedKey)
351
+ .filter(
352
+ (entry) =>
353
+ entry.kind === candidate.kind &&
354
+ entry.scope === candidate.scope &&
355
+ (candidate.scope === "conversation"
356
+ ? entry.conversationId === candidate.conversationId
357
+ : !entry.conversationId),
358
+ )
359
+ .first();
360
+
361
+ if (!existing) {
362
+ await db.memories.add({
363
+ id: nanoid(),
364
+ ...candidate,
365
+ createdAt: now,
366
+ updatedAt: now,
367
+ lastUsedAt: now,
368
+ });
369
+ return;
370
+ }
371
+
372
+ const shouldReplaceValue = candidate.source !== "auto" || existing.source === "auto";
373
+ await db.memories.update(existing.id, {
374
+ ...(shouldReplaceValue
375
+ ? {
376
+ key: candidate.key,
377
+ value: candidate.value,
378
+ normalizedKey: candidate.normalizedKey,
379
+ source: candidate.source,
380
+ confidence: candidate.confidence,
381
+ }
382
+ : {}),
383
+ updatedAt: now,
384
+ });
385
+ }
386
+
387
+ async function enforceMemoryCap(): Promise<void> {
388
+ const total = await db.memories.count();
389
+ if (total <= MAX_MEMORY_ENTRIES) {
390
+ return;
391
+ }
392
+ const overflow = total - MAX_MEMORY_ENTRIES;
393
+ const oldest = await db.memories.orderBy("lastUsedAt").limit(overflow).toArray();
394
+ if (oldest.length === 0) {
395
+ return;
396
+ }
397
+ await db.memories.bulkDelete(oldest.map((entry) => entry.id));
398
+ }
399
+
400
+ function dedupeCandidates(candidates: CapturedMemoryCandidate[]): CapturedMemoryCandidate[] {
401
+ const byKey = new Map<string, CapturedMemoryCandidate>();
402
+ for (const candidate of candidates) {
403
+ const composite = [
404
+ candidate.kind,
405
+ candidate.scope,
406
+ candidate.conversationId ?? "",
407
+ candidate.normalizedKey,
408
+ ].join("::");
409
+ const existing = byKey.get(composite);
410
+ if (!existing) {
411
+ byKey.set(composite, candidate);
412
+ continue;
413
+ }
414
+ if (candidate.source === "explicit" || (candidate.source === "manual" && existing.source === "auto")) {
415
+ byKey.set(composite, candidate);
416
+ }
417
+ }
418
+ return [...byKey.values()];
419
+ }
420
+
421
+ export async function captureMemoriesFromUserTurn(params: {
422
+ text: string;
423
+ conversationId: string;
424
+ settingsMemory?: MemorySettings | null;
425
+ }): Promise<void> {
426
+ const rawText = params.text?.trim();
427
+ if (!rawText) {
428
+ return;
429
+ }
430
+
431
+ const settings = params.settingsMemory;
432
+ if (settings?.enabled === false) {
433
+ return;
434
+ }
435
+ if (looksSensitive(rawText)) {
436
+ return;
437
+ }
438
+
439
+ const scope = detectScope(rawText);
440
+ const explicitCandidates = extractExplicitCandidates({
441
+ text: rawText,
442
+ scope,
443
+ conversationId: params.conversationId,
444
+ });
445
+ const autoCandidates = settings?.autoCapture === false
446
+ ? []
447
+ : extractAutoCandidates({
448
+ text: rawText,
449
+ scope,
450
+ conversationId: params.conversationId,
451
+ });
452
+
453
+ const candidates = dedupeCandidates([...explicitCandidates, ...autoCandidates]);
454
+ if (candidates.length === 0) {
455
+ return;
456
+ }
457
+
458
+ for (const candidate of candidates) {
459
+ await upsertMemoryCandidate(candidate);
460
+ }
461
+ await enforceMemoryCap();
462
+ }
463
+
464
+ function tokenize(input: string): string[] {
465
+ const normalized = normalizeMemoryKey(input);
466
+ if (!normalized) {
467
+ return [];
468
+ }
469
+ return normalized.split(" ").filter(Boolean);
470
+ }
471
+
472
+ function scoreMemoryEntry(entry: MemoryEntry, query: string): number {
473
+ const queryNormalized = normalizeMemoryKey(query);
474
+ const queryTokens = tokenize(query);
475
+ const keyTokens = tokenize(entry.key);
476
+ const valueTokens = tokenize(entry.value);
477
+
478
+ let score = 0;
479
+ if (queryNormalized) {
480
+ if (entry.normalizedKey === queryNormalized) {
481
+ score += 140;
482
+ }
483
+ if (
484
+ entry.normalizedKey.length >= 3 &&
485
+ queryNormalized.includes(entry.normalizedKey)
486
+ ) {
487
+ score += 80;
488
+ }
489
+ if (queryNormalized.length >= 3 && entry.normalizedKey.includes(queryNormalized)) {
490
+ score += 45;
491
+ }
492
+ }
493
+
494
+ if (queryTokens.length > 0) {
495
+ const tokenSet = new Set([...keyTokens, ...valueTokens]);
496
+ let overlap = 0;
497
+ for (const token of queryTokens) {
498
+ if (tokenSet.has(token)) {
499
+ overlap += 1;
500
+ }
501
+ }
502
+ score += overlap * 14;
503
+ }
504
+
505
+ if (entry.source === "manual") {
506
+ score += 32;
507
+ } else if (entry.source === "explicit") {
508
+ score += 26;
509
+ } else {
510
+ score += 10;
511
+ }
512
+
513
+ const ageDays = Math.max(0, Math.floor((Date.now() - entry.updatedAt) / DAY_MS));
514
+ score += Math.max(0, 20 - ageDays);
515
+ score += Math.round(Math.max(0, Math.min(1, entry.confidence)) * 15);
516
+
517
+ return score;
518
+ }
519
+
520
+ function selectMemoryEntries(candidates: MemoryEntry[], query: string): MemoryEntry[] {
521
+ const ranked = [...candidates]
522
+ .map((entry) => ({ entry, score: scoreMemoryEntry(entry, query) }))
523
+ .sort((left, right) => {
524
+ if (right.score !== left.score) {
525
+ return right.score - left.score;
526
+ }
527
+ return right.entry.updatedAt - left.entry.updatedAt;
528
+ })
529
+ .map((item) => item.entry);
530
+
531
+ const selected: MemoryEntry[] = [];
532
+ let profilePreferenceCount = 0;
533
+ let personAliasCount = 0;
534
+ let musicAliasCount = 0;
535
+ let noteCount = 0;
536
+
537
+ for (const entry of ranked) {
538
+ if ((entry.kind === "profile" || entry.kind === "preference") && profilePreferenceCount >= 3) {
539
+ continue;
540
+ }
541
+ if (entry.kind === "person_alias" && personAliasCount >= 3) {
542
+ continue;
543
+ }
544
+ if (entry.kind === "music_alias" && musicAliasCount >= 2) {
545
+ continue;
546
+ }
547
+ if (entry.kind === "note" && noteCount >= 2) {
548
+ continue;
549
+ }
550
+
551
+ selected.push(entry);
552
+
553
+ if (entry.kind === "profile" || entry.kind === "preference") {
554
+ profilePreferenceCount += 1;
555
+ } else if (entry.kind === "person_alias") {
556
+ personAliasCount += 1;
557
+ } else if (entry.kind === "music_alias") {
558
+ musicAliasCount += 1;
559
+ } else if (entry.kind === "note") {
560
+ noteCount += 1;
561
+ }
562
+ }
563
+
564
+ return selected;
565
+ }
566
+
567
+ function buildSystemMessage(entries: MemoryEntry[]): string | null {
568
+ if (entries.length === 0) {
569
+ return null;
570
+ }
571
+
572
+ const profiles = entries.filter((entry) => entry.kind === "profile" || entry.kind === "preference");
573
+ const people = entries.filter((entry) => entry.kind === "person_alias");
574
+ const music = entries.filter((entry) => entry.kind === "music_alias");
575
+ const notes = entries.filter((entry) => entry.kind === "note");
576
+
577
+ const lines: string[] = [
578
+ "On-device memory context (local user profile and aliases):",
579
+ "Use this as helpful context. If memory conflicts with the current request, ask a concise clarification.",
580
+ ];
581
+
582
+ if (profiles.length > 0) {
583
+ lines.push("", "### Profile & Preferences");
584
+ for (const entry of profiles) {
585
+ lines.push(`- **${entry.key}:** ${entry.value}`);
586
+ }
587
+ }
588
+
589
+ if (people.length > 0) {
590
+ lines.push("", "### Person Aliases");
591
+ for (const entry of people) {
592
+ lines.push(`- \"${entry.key}\" refers to ${entry.value}`);
593
+ }
594
+ }
595
+
596
+ if (music.length > 0) {
597
+ lines.push("", "### Music Aliases");
598
+ for (const entry of music) {
599
+ const parsed = parseStoredMusicAlias(entry);
600
+ if (!parsed) {
601
+ continue;
602
+ }
603
+ const suffix = parsed.artist ? ` by ${parsed.artist}` : "";
604
+ lines.push(`- \"${parsed.alias}\" means play ${parsed.query}${suffix}`);
605
+ }
606
+ }
607
+
608
+ if (notes.length > 0) {
609
+ lines.push("", "### Notes");
610
+ for (const entry of notes) {
611
+ lines.push(`- ${entry.value}`);
612
+ }
613
+ }
614
+
615
+ let message = "";
616
+ for (const line of lines) {
617
+ const next = message ? `${message}\n${line}` : line;
618
+ if (next.length > MAX_MEMORY_CONTEXT_CHARS) {
619
+ break;
620
+ }
621
+ message = next;
622
+ }
623
+
624
+ return message || null;
625
+ }
626
+
627
+ function buildAliasPayload(entries: MemoryEntry[], toolInfluence: boolean): MemoryContextPayload["aliases"] {
628
+ if (!toolInfluence) {
629
+ return { people: [], music: [] };
630
+ }
631
+
632
+ const people = entries
633
+ .filter((entry) => entry.kind === "person_alias")
634
+ .map((entry) => ({ alias: entry.key, target: entry.value }));
635
+
636
+ const music: MemoryMusicAlias[] = [];
637
+ for (const entry of entries) {
638
+ if (entry.kind !== "music_alias") {
639
+ continue;
640
+ }
641
+ const parsed = parseStoredMusicAlias(entry);
642
+ if (!parsed) {
643
+ continue;
644
+ }
645
+ music.push(parsed);
646
+ }
647
+
648
+ return { people, music };
649
+ }
650
+
651
+ export async function buildMemoryContextForPrompt(params: {
652
+ query: string;
653
+ conversationId: string;
654
+ enabled: boolean;
655
+ toolInfluence?: boolean;
656
+ }): Promise<{
657
+ systemMessage: string | null;
658
+ payload: MemoryContextPayload;
659
+ usedEntryIds: string[];
660
+ }> {
661
+ if (!params.enabled) {
662
+ return {
663
+ systemMessage: null,
664
+ payload: {
665
+ enabled: false,
666
+ aliases: { people: [], music: [] },
667
+ },
668
+ usedEntryIds: [],
669
+ };
670
+ }
671
+
672
+ const allMemories = await db.memories.toArray();
673
+ const inScope = allMemories.filter(
674
+ (entry) => entry.scope === "global" || entry.conversationId === params.conversationId,
675
+ );
676
+ const selected = selectMemoryEntries(inScope, params.query);
677
+ const aliases = buildAliasPayload(selected, params.toolInfluence !== false);
678
+
679
+ return {
680
+ systemMessage: buildSystemMessage(selected),
681
+ payload: {
682
+ enabled: true,
683
+ aliases,
684
+ },
685
+ usedEntryIds: selected.map((entry) => entry.id),
686
+ };
687
+ }
688
+
689
+ export async function markMemoriesUsed(ids: string[]): Promise<void> {
690
+ if (!Array.isArray(ids) || ids.length === 0) {
691
+ return;
692
+ }
693
+ const now = Date.now();
694
+ await Promise.all(ids.map((id) => db.memories.update(id, { lastUsedAt: now })));
695
+ }