@tiongcreative/assistant-mcp 0.0.0-stage → 0.1.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.
@@ -0,0 +1,427 @@
1
+ // Gift Keeper: people, their occasions, gift ideas and gifts.
2
+ // Talks to the app's local API with the per-launch token it writes while
3
+ // "Allow assistant access" is on (gift-keeper/server/assistant.js).
4
+ import { z } from 'zod';
5
+ import { appConnection, apiCall, notAvailable } from '../connection.js';
6
+ import { todayLocal } from '../dates.js';
7
+
8
+ export const app = {
9
+ id: 'gift-keeper',
10
+ name: 'Gift Keeper',
11
+ bundleName: 'Gift Keeper',
12
+ dataDir: 'gift-keeper',
13
+ };
14
+
15
+ function summariseOccasion(u) {
16
+ return {
17
+ occasionId: u.occasion.id,
18
+ name: u.occasion.name,
19
+ type: u.occasion.type,
20
+ // nextDate is local midnight as an ISO timestamp; format it in local time, or the
21
+ // calendar day shifts back by one for anyone east of UTC.
22
+ date: todayLocal(new Date(u.nextDate)),
23
+ daysUntil: u.daysUntil,
24
+ people: u.people.map((p) => p.name),
25
+ giftChosen: u.hasGiftSelected,
26
+ openGifts: u.openGiftCount,
27
+ budget: u.budget ?? null,
28
+ spent: u.spent,
29
+ gifts: u.gifts
30
+ .filter((g) => g.status !== 'cancelled')
31
+ .map((g) => ({ id: g.id, name: g.name, status: g.status, price: g.price ?? null })),
32
+ };
33
+ }
34
+
35
+ function summarisePerson(p) {
36
+ return {
37
+ id: p.id,
38
+ name: p.name,
39
+ relationship: p.relationship || undefined,
40
+ interests: p.interests?.length ? p.interests : undefined,
41
+ sizes: p.sizes && Object.keys(p.sizes).length ? p.sizes : undefined,
42
+ preferences: p.preferences || undefined,
43
+ avoid: p.avoid || undefined,
44
+ notes: p.notes || undefined,
45
+ };
46
+ }
47
+
48
+ async function findPerson(conn, name) {
49
+ const people = await apiCall(conn, 'GET', '/api/people');
50
+ const wanted = name.trim().toLowerCase();
51
+ return {
52
+ person: people.find((p) => p.name.toLowerCase() === wanted),
53
+ names: people.map((p) => p.name),
54
+ };
55
+ }
56
+
57
+ export const tools = [
58
+ {
59
+ name: 'gift_keeper_upcoming',
60
+ description:
61
+ 'Upcoming occasions in Gift Keeper (birthdays, anniversaries, holidays and custom dates) with who they are for, whether a gift has been chosen, the budget and what has been spent so far.',
62
+ input: z.object({
63
+ withinDays: z
64
+ .number()
65
+ .int()
66
+ .min(0)
67
+ .max(730)
68
+ .optional()
69
+ .describe('Only occasions within this many days'),
70
+ person: z.string().max(100).optional().describe("Only occasions for this person's name"),
71
+ }),
72
+ async run(input) {
73
+ const conn = await appConnection(app);
74
+ if (!conn.ok) return notAvailable(app, conn);
75
+ let list = (await apiCall(conn, 'GET', '/api/upcoming')).map(summariseOccasion);
76
+ if (input.withinDays !== undefined)
77
+ list = list.filter((u) => u.daysUntil <= input.withinDays);
78
+ if (input.person) {
79
+ const who = input.person.toLowerCase();
80
+ list = list.filter((u) => u.people.some((n) => n.toLowerCase() === who));
81
+ }
82
+ return { app: app.name, count: list.length, occasions: list };
83
+ },
84
+ },
85
+ {
86
+ name: 'gift_keeper_people',
87
+ description:
88
+ 'The people in Gift Keeper with what helps choose a gift: relationship, interests, sizes, preferences and things to avoid. Give a name for one person, or leave it out for everyone.',
89
+ input: z.object({ name: z.string().max(100).optional() }),
90
+ async run(input) {
91
+ const conn = await appConnection(app);
92
+ if (!conn.ok) return notAvailable(app, conn);
93
+ const people = await apiCall(conn, 'GET', '/api/people');
94
+ let list = people.filter((p) => !p.archived);
95
+ if (input.name) {
96
+ const who = input.name.toLowerCase();
97
+ list = list.filter((p) => p.name.toLowerCase() === who);
98
+ }
99
+ return { app: app.name, count: list.length, people: list.map(summarisePerson) };
100
+ },
101
+ },
102
+ {
103
+ name: 'gift_keeper_ideas',
104
+ description:
105
+ "Gift ideas saved in Gift Keeper: the pool of possible gifts, including ones not yet tied to an occasion. Filter by person's name or search the idea text. This is where to look before suggesting something new.",
106
+ input: z.object({
107
+ person: z.string().max(100).optional(),
108
+ search: z.string().max(100).optional(),
109
+ }),
110
+ async run(input) {
111
+ const conn = await appConnection(app);
112
+ if (!conn.ok) return notAvailable(app, conn);
113
+ const [ideas, people, occasions] = await Promise.all([
114
+ apiCall(conn, 'GET', '/api/ideas'),
115
+ apiCall(conn, 'GET', '/api/people'),
116
+ apiCall(conn, 'GET', '/api/occasions'),
117
+ ]);
118
+ const personName = new Map(people.map((p) => [p.id, p.name]));
119
+ const occasionName = new Map(occasions.map((o) => [o.id, o.name]));
120
+ let list = ideas.map((i) => ({
121
+ id: i.id,
122
+ idea: i.name,
123
+ person: i.personId ? (personName.get(i.personId) ?? null) : null,
124
+ occasion: i.occasionId ? (occasionName.get(i.occasionId) ?? null) : null,
125
+ price: i.price ?? null,
126
+ priority: i.priority ?? 'normal',
127
+ url: i.url || undefined,
128
+ notes: i.notes || undefined,
129
+ added: i.dateAdded ?? null,
130
+ }));
131
+ if (input.person) {
132
+ const who = input.person.toLowerCase();
133
+ list = list.filter((i) => i.person?.toLowerCase() === who);
134
+ }
135
+ if (input.search) {
136
+ const q = input.search.toLowerCase();
137
+ list = list.filter(
138
+ (i) => i.idea.toLowerCase().includes(q) || (i.notes ?? '').toLowerCase().includes(q),
139
+ );
140
+ }
141
+ return { app: app.name, count: list.length, ideas: list };
142
+ },
143
+ },
144
+ {
145
+ name: 'gift_keeper_gifts',
146
+ description:
147
+ 'Gifts being tracked in Gift Keeper at any stage (idea, shortlisted, ordered, bought, arrived, wrapped, given, cancelled), with who and which occasion each is for. Filter by person or status. Given and cancelled gifts are left out unless asked for.',
148
+ input: z.object({
149
+ person: z.string().max(100).optional(),
150
+ status: z
151
+ .enum([
152
+ 'open',
153
+ 'idea',
154
+ 'shortlisted',
155
+ 'ordered',
156
+ 'bought',
157
+ 'arrived',
158
+ 'wrapped',
159
+ 'given',
160
+ 'cancelled',
161
+ 'all',
162
+ ])
163
+ .default('open')
164
+ .describe('open = everything not yet given or cancelled'),
165
+ }),
166
+ async run(input) {
167
+ const conn = await appConnection(app);
168
+ if (!conn.ok) return notAvailable(app, conn);
169
+ const [gifts, people, occasions] = await Promise.all([
170
+ apiCall(conn, 'GET', '/api/gifts'),
171
+ apiCall(conn, 'GET', '/api/people'),
172
+ apiCall(conn, 'GET', '/api/occasions'),
173
+ ]);
174
+ const personName = new Map(people.map((p) => [p.id, p.name]));
175
+ const occasionName = new Map(occasions.map((o) => [o.id, o.name]));
176
+ let list = gifts.map((g) => ({
177
+ id: g.id,
178
+ name: g.name,
179
+ status: g.status,
180
+ person: g.personId ? (personName.get(g.personId) ?? null) : null,
181
+ occasion: g.occasionId ? (occasionName.get(g.occasionId) ?? null) : null,
182
+ price: g.price ?? null,
183
+ plannedDate: g.plannedDate ?? null,
184
+ notes: g.notes || undefined,
185
+ }));
186
+ if (input.status === 'open')
187
+ list = list.filter((g) => !['given', 'cancelled'].includes(g.status));
188
+ else if (input.status !== 'all') list = list.filter((g) => g.status === input.status);
189
+ if (input.person) {
190
+ const who = input.person.toLowerCase();
191
+ list = list.filter((g) => g.person?.toLowerCase() === who);
192
+ }
193
+ return { app: app.name, count: list.length, gifts: list };
194
+ },
195
+ },
196
+ {
197
+ name: 'gift_keeper_add_idea',
198
+ description:
199
+ 'Save a gift idea in Gift Keeper for a person, by name. If the name does not match, the reply lists the people to choose from. Optional link, price, notes and priority.',
200
+ input: z.object({
201
+ idea: z.string().trim().min(1).max(200),
202
+ person: z.string().min(1).max(100),
203
+ url: z.string().url().max(2000).optional(),
204
+ price: z.number().min(0).optional(),
205
+ notes: z.string().max(2000).optional(),
206
+ priority: z.enum(['low', 'normal', 'high']).optional(),
207
+ }),
208
+ write: true,
209
+ async run(input) {
210
+ const conn = await appConnection(app);
211
+ if (!conn.ok) return notAvailable(app, conn);
212
+ const { person, names } = await findPerson(conn, input.person);
213
+ if (!person) {
214
+ return {
215
+ app: app.name,
216
+ added: false,
217
+ message: `No one called "${input.person}" in Gift Keeper. Choose one of these and try again.`,
218
+ people: names,
219
+ };
220
+ }
221
+ const body = {
222
+ name: input.idea,
223
+ personId: person.id,
224
+ ...(input.url ? { url: input.url } : {}),
225
+ ...(input.price !== undefined ? { price: input.price } : {}),
226
+ ...(input.notes ? { notes: input.notes } : {}),
227
+ ...(input.priority ? { priority: input.priority } : {}),
228
+ };
229
+ const idea = await apiCall(conn, 'POST', '/api/ideas', body);
230
+ return {
231
+ app: app.name,
232
+ added: true,
233
+ idea: { id: idea.id, name: idea.name, person: person.name, priority: idea.priority },
234
+ };
235
+ },
236
+ },
237
+ {
238
+ name: 'gift_keeper_mark_gift_given',
239
+ description: 'Mark a gift in Gift Keeper as given. Use a gift id from gift_keeper_upcoming.',
240
+ input: z.object({ giftId: z.string().min(1) }),
241
+ write: true,
242
+ async run(input) {
243
+ const conn = await appConnection(app);
244
+ if (!conn.ok) return notAvailable(app, conn);
245
+ const gift = await apiCall(
246
+ conn,
247
+ 'POST',
248
+ `/api/gifts/${encodeURIComponent(input.giftId)}/mark-given`,
249
+ );
250
+ return {
251
+ app: app.name,
252
+ changed: 'given',
253
+ gift: { id: gift.id, name: gift.name, status: gift.status },
254
+ };
255
+ },
256
+ },
257
+ {
258
+ name: 'gift_keeper_update_gift',
259
+ description:
260
+ 'Change a gift in Gift Keeper: its status (idea, shortlisted, ordered, bought, arrived, wrapped, given, cancelled), price, name or notes. Use a gift id from gift_keeper_upcoming.',
261
+ input: z.object({
262
+ giftId: z.string().min(1),
263
+ status: z
264
+ .enum([
265
+ 'idea',
266
+ 'shortlisted',
267
+ 'ordered',
268
+ 'bought',
269
+ 'arrived',
270
+ 'wrapped',
271
+ 'given',
272
+ 'cancelled',
273
+ ])
274
+ .optional(),
275
+ price: z.number().min(0).nullable().optional(),
276
+ name: z.string().trim().min(1).max(200).optional(),
277
+ notes: z.string().max(2000).optional(),
278
+ }),
279
+ write: true,
280
+ async run(input) {
281
+ const conn = await appConnection(app);
282
+ if (!conn.ok) return notAvailable(app, conn);
283
+ const { giftId, ...changes } = input;
284
+ if (Object.keys(changes).length === 0) {
285
+ return {
286
+ app: app.name,
287
+ changed: false,
288
+ message: 'Nothing to change: give a status, price, name or notes.',
289
+ };
290
+ }
291
+ const gift = await apiCall(conn, 'PUT', `/api/gifts/${encodeURIComponent(giftId)}`, changes);
292
+ return {
293
+ app: app.name,
294
+ changed: true,
295
+ gift: { id: gift.id, name: gift.name, status: gift.status, price: gift.price ?? null },
296
+ };
297
+ },
298
+ },
299
+ {
300
+ name: 'gift_keeper_add_person',
301
+ description: 'Add a person to Gift Keeper, with optional relationship, interests and notes.',
302
+ input: z.object({
303
+ name: z.string().trim().min(1).max(100),
304
+ relationship: z.string().max(100).optional(),
305
+ interests: z.array(z.string().max(100)).max(50).optional(),
306
+ notes: z.string().max(2000).optional(),
307
+ }),
308
+ write: true,
309
+ async run(input) {
310
+ const conn = await appConnection(app);
311
+ if (!conn.ok) return notAvailable(app, conn);
312
+ const { names } = await findPerson(conn, input.name);
313
+ if (names.some((n) => n.toLowerCase() === input.name.trim().toLowerCase())) {
314
+ return { app: app.name, added: false, message: `${input.name} is already in Gift Keeper.` };
315
+ }
316
+ const person = await apiCall(conn, 'POST', '/api/people', {
317
+ name: input.name,
318
+ ...(input.relationship ? { relationship: input.relationship } : {}),
319
+ ...(input.interests ? { interests: input.interests } : {}),
320
+ ...(input.notes ? { notes: input.notes } : {}),
321
+ });
322
+ return { app: app.name, added: true, person: summarisePerson(person) };
323
+ },
324
+ },
325
+ {
326
+ name: 'gift_keeper_add_occasion',
327
+ description:
328
+ 'Add an occasion in Gift Keeper (a birthday, anniversary, holiday or custom date) for one or more people by name, yearly or one-off, with an optional budget. If a name does not match, the reply lists the people.',
329
+ input: z.object({
330
+ name: z.string().trim().min(1).max(200),
331
+ date: z
332
+ .string()
333
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
334
+ .describe('YYYY-MM-DD; for a yearly occasion, the next one'),
335
+ people: z.array(z.string().min(1).max(100)).min(1).max(20),
336
+ repeats: z.enum(['yearly', 'one-off']).default('yearly'),
337
+ type: z.string().max(50).optional().describe('e.g. birthday, anniversary, christmas, custom'),
338
+ budget: z.number().min(0).optional(),
339
+ }),
340
+ write: true,
341
+ async run(input) {
342
+ const conn = await appConnection(app);
343
+ if (!conn.ok) return notAvailable(app, conn);
344
+ const people = await apiCall(conn, 'GET', '/api/people');
345
+ const ids = [];
346
+ const missing = [];
347
+ for (const wanted of input.people) {
348
+ const p = people.find((x) => x.name.toLowerCase() === wanted.trim().toLowerCase());
349
+ if (p) ids.push(p.id);
350
+ else missing.push(wanted);
351
+ }
352
+ if (missing.length) {
353
+ return {
354
+ app: app.name,
355
+ added: false,
356
+ message: `No one called ${missing.map((m) => `"${m}"`).join(', ')} in Gift Keeper.`,
357
+ people: people.map((p) => p.name),
358
+ };
359
+ }
360
+ const occasion = await apiCall(conn, 'POST', '/api/occasions', {
361
+ name: input.name,
362
+ date: input.date,
363
+ personIds: ids,
364
+ recurrence: input.repeats,
365
+ ...(input.type ? { type: input.type } : {}),
366
+ ...(input.budget !== undefined ? { budget: input.budget } : {}),
367
+ });
368
+ return {
369
+ app: app.name,
370
+ added: true,
371
+ occasion: {
372
+ id: occasion.id,
373
+ name: occasion.name,
374
+ date: occasion.date,
375
+ repeats: occasion.recurrence,
376
+ people: input.people,
377
+ },
378
+ };
379
+ },
380
+ },
381
+ {
382
+ name: 'gift_keeper_complete_occasion',
383
+ description:
384
+ 'Mark an occasion in Gift Keeper as done for this year, so it leaves the upcoming list until next time. Use an occasion id from gift_keeper_upcoming. The app refuses while a gift for it is still open (not given or cancelled), so settle those first with gift_keeper_update_gift.',
385
+ input: z.object({ occasionId: z.string().min(1) }),
386
+ write: true,
387
+ async run(input) {
388
+ const conn = await appConnection(app);
389
+ if (!conn.ok) return notAvailable(app, conn);
390
+ const occasion = await apiCall(
391
+ conn,
392
+ 'POST',
393
+ `/api/occasions/${encodeURIComponent(input.occasionId)}/complete`,
394
+ {},
395
+ );
396
+ return {
397
+ app: app.name,
398
+ changed: 'completed',
399
+ occasion: {
400
+ id: occasion.id,
401
+ name: occasion.name,
402
+ completedYears: occasion.completedYears ?? [],
403
+ },
404
+ };
405
+ },
406
+ },
407
+ ];
408
+
409
+ /** Lines for the cross-app "what's on" view. */
410
+ export async function attention(days) {
411
+ const conn = await appConnection(app);
412
+ if (!conn.ok) return { app: app.name, available: false, reason: conn.reason, items: [] };
413
+ const upcoming = (await apiCall(conn, 'GET', '/api/upcoming')).map(summariseOccasion);
414
+ const items = upcoming
415
+ .filter((u) => u.daysUntil <= days)
416
+ .map((u) => ({
417
+ app: app.name,
418
+ id: u.occasionId,
419
+ title: `${u.name}${u.people.length ? ` (${u.people.join(', ')})` : ''}`,
420
+ date: u.date,
421
+ daysUntilDue: u.daysUntil,
422
+ status: u.giftChosen ? 'gift chosen' : 'no gift yet',
423
+ expectedCost: u.budget,
424
+ currency: null,
425
+ }));
426
+ return { app: app.name, available: true, items };
427
+ }
@@ -0,0 +1,166 @@
1
+ // Lists & Notes: checklists and markdown notes. Read-only for now: the app's list
2
+ // endpoint replaces the whole file from the request, so writes wait until that is
3
+ // hardened (see the plan). Talks to the app's local API with the per-launch token it
4
+ // writes while "Allow assistant access" is on (list-manager/assistant-access.js).
5
+ import { z } from 'zod';
6
+ import { appConnection, apiCall, notAvailable } from '../connection.js';
7
+
8
+ export const app = {
9
+ id: 'lists-and-notes',
10
+ name: 'Lists & Notes',
11
+ bundleName: 'Lists & Notes',
12
+ dataDir: 'lists-and-notes',
13
+ // The app keeps its files in the data dir itself, not a local_data folder.
14
+ tokenFile: 'assistant.json',
15
+ };
16
+
17
+ function summariseList(l) {
18
+ const items = l.items ?? [];
19
+ return {
20
+ id: l.id,
21
+ name: l.name,
22
+ category: l.category,
23
+ priority: l.priority,
24
+ favourite: l.favorite === true || undefined,
25
+ tags: l.tags?.length ? l.tags : undefined,
26
+ items: items.length,
27
+ done: items.filter((i) => i.completed).length,
28
+ modified: l.metadata?.modified ?? null,
29
+ };
30
+ }
31
+
32
+ function summariseNote(n) {
33
+ return {
34
+ id: n.id,
35
+ title: n.title,
36
+ category: n.category,
37
+ tags: n.tags?.length ? n.tags : undefined,
38
+ favourite: n.favorite === true || undefined,
39
+ preview: n.preview,
40
+ summary: n.content?.tldr || undefined,
41
+ words: n.metadata?.wordCount ?? null,
42
+ modified: n.metadata?.modified ?? null,
43
+ };
44
+ }
45
+
46
+ function matches(text, q) {
47
+ return typeof text === 'string' && text.toLowerCase().includes(q);
48
+ }
49
+
50
+ export const tools = [
51
+ {
52
+ name: 'lists_and_notes_lists',
53
+ description:
54
+ 'The lists in Lists & Notes (checklists and plain lists) with their category, how many items each has and how many are done. Give a name to get one list with its items.',
55
+ input: z.object({
56
+ name: z.string().max(75).optional().describe('A list name, for that list with its items'),
57
+ search: z.string().max(100).optional().describe('Case-insensitive match on list names'),
58
+ }),
59
+ async run(input) {
60
+ const conn = await appConnection(app);
61
+ if (!conn.ok) return notAvailable(app, conn);
62
+ const lists = await apiCall(conn, 'GET', '/api/data/lists');
63
+ if (input.name) {
64
+ const wanted = input.name.trim().toLowerCase();
65
+ const list = lists.find((l) => l.name.toLowerCase() === wanted);
66
+ if (!list) {
67
+ return {
68
+ app: app.name,
69
+ found: false,
70
+ message: `No list called "${input.name}". These exist.`,
71
+ lists: lists.map((l) => l.name),
72
+ };
73
+ }
74
+ return {
75
+ app: app.name,
76
+ found: true,
77
+ list: {
78
+ ...summariseList(list),
79
+ items: (list.items ?? []).map((i) => ({
80
+ id: i.id,
81
+ text: i.text,
82
+ done: i.completed === true,
83
+ notes: i.notes || undefined,
84
+ })),
85
+ },
86
+ };
87
+ }
88
+ let out = lists.map(summariseList);
89
+ if (input.search) {
90
+ const q = input.search.toLowerCase();
91
+ out = out.filter((l) => matches(l.name, q));
92
+ }
93
+ return { app: app.name, count: out.length, lists: out };
94
+ },
95
+ },
96
+ {
97
+ name: 'lists_and_notes_notes',
98
+ description:
99
+ 'Notes in Lists & Notes: title, category, tags and a short preview of each. Search matches the title, preview, summary and tags. Use lists_and_notes_note for the full text of one note.',
100
+ input: z.object({
101
+ search: z.string().max(100).optional(),
102
+ category: z.string().max(100).optional(),
103
+ limit: z.number().int().min(1).max(200).default(50),
104
+ }),
105
+ async run(input) {
106
+ const conn = await appConnection(app);
107
+ if (!conn.ok) return notAvailable(app, conn);
108
+ let notes = (await apiCall(conn, 'GET', '/api/data/notes')).map(summariseNote);
109
+ if (input.category) {
110
+ const c = input.category.toLowerCase();
111
+ notes = notes.filter((n) => (n.category ?? '').toLowerCase() === c);
112
+ }
113
+ if (input.search) {
114
+ const q = input.search.toLowerCase();
115
+ notes = notes.filter(
116
+ (n) =>
117
+ matches(n.title, q) ||
118
+ matches(n.preview, q) ||
119
+ matches(n.summary, q) ||
120
+ (n.tags ?? []).some((t) => matches(t, q)),
121
+ );
122
+ }
123
+ notes.sort((a, b) => String(b.modified ?? '').localeCompare(String(a.modified ?? '')));
124
+ return { app: app.name, count: notes.length, notes: notes.slice(0, input.limit) };
125
+ },
126
+ },
127
+ {
128
+ name: 'lists_and_notes_note',
129
+ description:
130
+ 'The full markdown text of one note in Lists & Notes, by title. If the title does not match, the reply lists the closest titles.',
131
+ input: z.object({ title: z.string().min(1).max(75) }),
132
+ async run(input) {
133
+ const conn = await appConnection(app);
134
+ if (!conn.ok) return notAvailable(app, conn);
135
+ const notes = await apiCall(conn, 'GET', '/api/data/notes');
136
+ const wanted = input.title.trim().toLowerCase();
137
+ const note =
138
+ notes.find((n) => n.title.toLowerCase() === wanted) ??
139
+ notes.find((n) => n.title.toLowerCase().includes(wanted));
140
+ if (!note) {
141
+ return {
142
+ app: app.name,
143
+ found: false,
144
+ message: `No note called "${input.title}". These are close.`,
145
+ titles: notes
146
+ .filter((n) => wanted.split(/\s+/).some((w) => w.length > 2 && matches(n.title, w)))
147
+ .slice(0, 20)
148
+ .map((n) => n.title),
149
+ };
150
+ }
151
+ const full = await apiCall(conn, 'GET', `/api/data/notes/${encodeURIComponent(note.id)}`);
152
+ return {
153
+ app: app.name,
154
+ found: true,
155
+ note: { ...summariseNote(note), preview: undefined, content: full.content },
156
+ };
157
+ },
158
+ },
159
+ ];
160
+
161
+ /** Lists & Notes has no dated items, so it adds nothing to "what's on". */
162
+ export async function attention() {
163
+ const conn = await appConnection(app);
164
+ if (!conn.ok) return { app: app.name, available: false, reason: conn.reason, items: [] };
165
+ return { app: app.name, available: true, items: [] };
166
+ }