@sentry/junior-memory 0.124.0 → 0.124.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.
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { type MemoryDb, type MemoryRecord } from "./store";
3
- import type { MemoryRuntimeContext } from "./types";
3
+ import { type MemoryRuntimeContext } from "./types";
4
4
  declare const personalMemoryCursorSchema: z.ZodObject<{
5
5
  createdAtMs: z.ZodNumber;
6
6
  id: z.ZodString;
@@ -10,15 +10,47 @@ declare const personalMemoryPageInputSchema: z.ZodObject<{
10
10
  createdAtMs: z.ZodNumber;
11
11
  id: z.ZodString;
12
12
  }, z.core.$strict>>;
13
+ kind: z.ZodOptional<z.ZodEnum<{
14
+ preference: "preference";
15
+ procedure: "procedure";
16
+ knowledge: "knowledge";
17
+ }>>;
13
18
  limit: z.ZodNumber;
19
+ origin: z.ZodOptional<z.ZodEnum<{
20
+ automatic: "automatic";
21
+ explicit: "explicit";
22
+ }>>;
14
23
  query: z.ZodOptional<z.ZodString>;
15
24
  }, z.core.$strict>;
16
25
  export type PersonalMemoryCursor = z.output<typeof personalMemoryCursorSchema>;
17
26
  export type PersonalMemoryPageInput = z.output<typeof personalMemoryPageInputSchema>;
18
27
  export interface PersonalMemoryPage {
19
- memories: MemoryRecord[];
28
+ memories: PersonalMemoryRecord[];
20
29
  nextCursor?: PersonalMemoryCursor;
21
30
  }
31
+ /** Safe provenance attached to one viewer-owned personal memory. */
32
+ export type PersonalMemoryRecord = MemoryRecord & {
33
+ origin: "automatic" | "explicit" | "other";
34
+ sourcePlatform: "local" | "slack";
35
+ };
36
+ /** Viewer-scoped active memory totals used by the dashboard. */
37
+ export interface PersonalMemoryStats {
38
+ active: number;
39
+ automatic: number;
40
+ createdThirtyDays: number;
41
+ embedded: number;
42
+ explicit: number;
43
+ knowledge: number;
44
+ preference: number;
45
+ procedure: number;
46
+ }
47
+ /** Viewer-scoped memory creation totals for one UTC calendar day. */
48
+ export interface PersonalMemoryDay {
49
+ date: string;
50
+ knowledge: number;
51
+ preference: number;
52
+ procedure: number;
53
+ }
22
54
  /** Expected failure when a viewer does not own the requested memory. */
23
55
  export declare class PersonalMemoryNotFoundError extends Error {
24
56
  constructor();
@@ -31,6 +63,12 @@ export interface PersonalMemoryCollection {
31
63
  get(id: string): Promise<MemoryRecord>;
32
64
  /** List one stable page across every linked personal scope. */
33
65
  list(input: PersonalMemoryPageInput): Promise<PersonalMemoryPage>;
66
+ /** Summarize active memories across every linked personal scope. */
67
+ stats(): Promise<PersonalMemoryStats>;
68
+ /** Read memory creation history across every linked personal scope. */
69
+ timeline(input: {
70
+ days: number;
71
+ }): Promise<PersonalMemoryDay[]>;
34
72
  }
35
73
  /** Build storage operations for every personal scope linked to one viewer. */
36
74
  export declare function createPersonalMemoryCollection(db: MemoryDb, runtimeContexts: MemoryRuntimeContext[], options?: {
@@ -1,12 +1,16 @@
1
1
  import type { PluginUserPageActor } from "@sentry/junior-plugin-api";
2
+ import { type PersonalMemoryRecord } from "./personal-store";
2
3
  import type { MemoryDb, MemoryRecord } from "./store";
4
+ import type { MemoryKind } from "./types";
3
5
  export interface ViewerMemoryPage {
4
- memories: MemoryRecord[];
6
+ memories: PersonalMemoryRecord[];
5
7
  nextCursor?: string;
6
8
  }
7
9
  export interface ViewerMemoryPageInput {
8
10
  cursor?: string;
11
+ kind?: MemoryKind;
9
12
  limit: number;
13
+ origin?: "automatic" | "explicit";
10
14
  query?: string;
11
15
  }
12
16
  export declare class InvalidMemoryCursorError extends Error {
@@ -18,4 +22,8 @@ export declare function createViewerMemories(db: MemoryDb, actors: PluginUserPag
18
22
  archive(id: string): Promise<MemoryRecord>;
19
23
  get(id: string): Promise<MemoryRecord>;
20
24
  list(input: ViewerMemoryPageInput): Promise<ViewerMemoryPage>;
25
+ stats(): Promise<import("./personal-store").PersonalMemoryStats>;
26
+ timeline(input: {
27
+ days: number;
28
+ }): Promise<import("./personal-store").PersonalMemoryDay[]>;
21
29
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-memory",
3
- "version": "0.124.0",
3
+ "version": "0.124.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -27,7 +27,7 @@
27
27
  "commander": "^14.0.3",
28
28
  "drizzle-orm": "^0.45.2",
29
29
  "zod": "^4.4.3",
30
- "@sentry/junior-plugin-api": "0.124.0"
30
+ "@sentry/junior-plugin-api": "0.124.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",
package/src/api.ts CHANGED
@@ -36,7 +36,38 @@ export const memoryListResponseSchema = z
36
36
  })
37
37
  .strict();
38
38
 
39
+ const memoryDashboardDaySchema = z
40
+ .object({
41
+ date: z.iso.date(),
42
+ knowledge: z.number().int().min(0),
43
+ preference: z.number().int().min(0),
44
+ procedure: z.number().int().min(0),
45
+ })
46
+ .strict();
47
+
48
+ export const memoryDashboardResponseSchema = z
49
+ .object({
50
+ days: z.array(memoryDashboardDaySchema).length(90),
51
+ generatedAt: z.iso.datetime(),
52
+ stats: z
53
+ .object({
54
+ active: z.number().int().min(0),
55
+ automatic: z.number().int().min(0),
56
+ createdThirtyDays: z.number().int().min(0),
57
+ embedded: z.number().int().min(0),
58
+ explicit: z.number().int().min(0),
59
+ knowledge: z.number().int().min(0),
60
+ preference: z.number().int().min(0),
61
+ procedure: z.number().int().min(0),
62
+ })
63
+ .strict(),
64
+ })
65
+ .strict();
66
+
39
67
  export type MemoryApi = z.output<typeof memoryApiSchema>;
68
+ export type MemoryDashboardResponse = z.output<
69
+ typeof memoryDashboardResponseSchema
70
+ >;
40
71
  export type MemoryListResponse = z.output<typeof memoryListResponseSchema>;
41
72
 
42
73
  const memoryListQuerySchema = z
@@ -94,13 +125,35 @@ export function createMemoryApi(options: MemoryApiOptions): PluginRouteApp {
94
125
  const url = new URL(request.url);
95
126
  const memoryPath = /^\/memories\/([^/]+)$/.exec(url.pathname);
96
127
  const isCollection = url.pathname === "/memories";
97
- if (!isCollection && !memoryPath) {
128
+ const isDashboard = url.pathname === "/dashboard";
129
+ if (!isCollection && !isDashboard && !memoryPath) {
98
130
  return json({ error: "Not found." }, 404);
99
131
  }
100
132
 
101
133
  const actors = await options.actors(email);
102
134
  const memories = createViewerMemories(options.db, actors);
103
135
  try {
136
+ if (
137
+ isDashboard &&
138
+ (request.method === "GET" || request.method === "HEAD")
139
+ ) {
140
+ const [stats, days] = await Promise.all([
141
+ memories.stats(),
142
+ memories.timeline({ days: 90 }),
143
+ ]);
144
+ const body = memoryDashboardResponseSchema.parse({
145
+ days,
146
+ generatedAt: new Date().toISOString(),
147
+ stats,
148
+ });
149
+ return request.method === "HEAD"
150
+ ? new Response(null, {
151
+ headers: { "cache-control": "no-store" },
152
+ status: 200,
153
+ })
154
+ : json(body);
155
+ }
156
+
104
157
  if (
105
158
  isCollection &&
106
159
  (request.method === "GET" || request.method === "HEAD")
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { memoryPlugin } from "./plugin";
2
2
  export {
3
3
  memoryApiSchema,
4
+ memoryDashboardResponseSchema,
4
5
  memoryListResponseSchema,
5
6
  type MemoryApi,
7
+ type MemoryDashboardResponse,
6
8
  type MemoryListResponse,
7
9
  } from "./api";
8
10
  export type { MemoryPluginOptions } from "./plugin";
@@ -4,7 +4,7 @@
4
4
  * A viewer may resolve to several runtime actors. This store combines their
5
5
  * personal scopes without treating one actor as the canonical identity.
6
6
  */
7
- import { and, asc, desc, eq, gt, ilike, lt, or, sql } from "drizzle-orm";
7
+ import { and, asc, desc, eq, gt, ilike, like, lt, or, sql } from "drizzle-orm";
8
8
  import { z } from "zod";
9
9
  import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
10
10
  import { deriveMemoryScope, type ResolvedMemoryScope } from "./scope";
@@ -15,7 +15,7 @@ import {
15
15
  type MemoryDb,
16
16
  type MemoryRecord,
17
17
  } from "./store";
18
- import type { MemoryRuntimeContext } from "./types";
18
+ import { MEMORY_KINDS, type MemoryRuntimeContext } from "./types";
19
19
 
20
20
  const nonEmptyStringSchema = z.string().min(1);
21
21
  const personalMemoryCursorSchema = z
@@ -27,10 +27,18 @@ const personalMemoryCursorSchema = z
27
27
  const personalMemoryPageInputSchema = z
28
28
  .object({
29
29
  cursor: personalMemoryCursorSchema.optional(),
30
+ kind: z.enum(MEMORY_KINDS).optional(),
30
31
  limit: z.number().int().min(1).max(50),
32
+ origin: z.enum(["automatic", "explicit"]).optional(),
31
33
  query: z.string().max(200).optional(),
32
34
  })
33
35
  .strict();
36
+ const personalMemoryTimelineInputSchema = z
37
+ .object({
38
+ days: z.number().int().min(1).max(365),
39
+ })
40
+ .strict();
41
+ const DAY_MS = 24 * 60 * 60 * 1_000;
34
42
 
35
43
  export type PersonalMemoryCursor = z.output<typeof personalMemoryCursorSchema>;
36
44
 
@@ -39,10 +47,36 @@ export type PersonalMemoryPageInput = z.output<
39
47
  >;
40
48
 
41
49
  export interface PersonalMemoryPage {
42
- memories: MemoryRecord[];
50
+ memories: PersonalMemoryRecord[];
43
51
  nextCursor?: PersonalMemoryCursor;
44
52
  }
45
53
 
54
+ /** Safe provenance attached to one viewer-owned personal memory. */
55
+ export type PersonalMemoryRecord = MemoryRecord & {
56
+ origin: "automatic" | "explicit" | "other";
57
+ sourcePlatform: "local" | "slack";
58
+ };
59
+
60
+ /** Viewer-scoped active memory totals used by the dashboard. */
61
+ export interface PersonalMemoryStats {
62
+ active: number;
63
+ automatic: number;
64
+ createdThirtyDays: number;
65
+ embedded: number;
66
+ explicit: number;
67
+ knowledge: number;
68
+ preference: number;
69
+ procedure: number;
70
+ }
71
+
72
+ /** Viewer-scoped memory creation totals for one UTC calendar day. */
73
+ export interface PersonalMemoryDay {
74
+ date: string;
75
+ knowledge: number;
76
+ preference: number;
77
+ procedure: number;
78
+ }
79
+
46
80
  /** Expected failure when a viewer does not own the requested memory. */
47
81
  export class PersonalMemoryNotFoundError extends Error {
48
82
  constructor() {
@@ -59,6 +93,10 @@ export interface PersonalMemoryCollection {
59
93
  get(id: string): Promise<MemoryRecord>;
60
94
  /** List one stable page across every linked personal scope. */
61
95
  list(input: PersonalMemoryPageInput): Promise<PersonalMemoryPage>;
96
+ /** Summarize active memories across every linked personal scope. */
97
+ stats(): Promise<PersonalMemoryStats>;
98
+ /** Read memory creation history across every linked personal scope. */
99
+ timeline(input: { days: number }): Promise<PersonalMemoryDay[]>;
62
100
  }
63
101
 
64
102
  function personalScopes(
@@ -74,6 +112,22 @@ function personalScopes(
74
112
  ];
75
113
  }
76
114
 
115
+ function personalScopePredicate(scopes: ResolvedMemoryScope[]) {
116
+ if (scopes.length === 0) return undefined;
117
+ return or(
118
+ ...scopes.map((scope) =>
119
+ and(
120
+ eq(juniorMemoryMemories.scope, scope.scope),
121
+ eq(juniorMemoryMemories.scopeKey, scope.scopeKey),
122
+ ),
123
+ ),
124
+ );
125
+ }
126
+
127
+ function utcDate(ms: number): string {
128
+ return new Date(ms).toISOString().slice(0, 10);
129
+ }
130
+
77
131
  function searchTerms(query: string): string[] {
78
132
  return [
79
133
  ...new Set(
@@ -86,6 +140,24 @@ function searchTerms(query: string): string[] {
86
140
  ];
87
141
  }
88
142
 
143
+ function memoryOrigin(
144
+ idempotencyKey: string | null,
145
+ ): PersonalMemoryRecord["origin"] {
146
+ if (idempotencyKey?.startsWith("session:")) return "automatic";
147
+ if (idempotencyKey?.startsWith("tool:")) return "explicit";
148
+ return "other";
149
+ }
150
+
151
+ function personalMemoryRecord(
152
+ row: typeof juniorMemoryMemories.$inferSelect,
153
+ ): PersonalMemoryRecord {
154
+ return {
155
+ ...parseMemoryRow(row),
156
+ origin: memoryOrigin(row.idempotencyKey),
157
+ sourcePlatform: row.sourcePlatform,
158
+ };
159
+ }
160
+
89
161
  /** Build storage operations for every personal scope linked to one viewer. */
90
162
  export function createPersonalMemoryCollection(
91
163
  db: MemoryDb,
@@ -167,17 +239,26 @@ export function createPersonalMemoryCollection(
167
239
  ilike(juniorMemoryMemories.content, `%${term}%`),
168
240
  ),
169
241
  );
242
+ const kind = input.kind
243
+ ? eq(juniorMemoryMemories.kind, input.kind)
244
+ : undefined;
245
+ const origin =
246
+ input.origin === "automatic"
247
+ ? like(juniorMemoryMemories.idempotencyKey, "session:%")
248
+ : input.origin === "explicit"
249
+ ? like(juniorMemoryMemories.idempotencyKey, "tool:%")
250
+ : undefined;
170
251
  const rows = await db
171
252
  .select()
172
253
  .from(juniorMemoryMemories)
173
- .where(and(active, cursor, search))
254
+ .where(and(active, cursor, search, kind, origin))
174
255
  .orderBy(
175
256
  desc(juniorMemoryMemories.createdAtMs),
176
257
  asc(juniorMemoryMemories.id),
177
258
  )
178
259
  .limit(input.limit + 1);
179
260
  const hasNextPage = rows.length > input.limit;
180
- const memories = rows.slice(0, input.limit).map(parseMemoryRow);
261
+ const memories = rows.slice(0, input.limit).map(personalMemoryRecord);
181
262
  const last = memories.at(-1);
182
263
  return {
183
264
  memories,
@@ -191,5 +272,122 @@ export function createPersonalMemoryCollection(
191
272
  : {}),
192
273
  };
193
274
  },
275
+
276
+ async stats() {
277
+ const nowMs = getNowMs();
278
+ await archiveExpiredMemoryBatch({ db, nowMs, scopes });
279
+ const active = activeVisiblePredicate({ nowMs, scopes });
280
+ if (!active) {
281
+ return {
282
+ active: 0,
283
+ automatic: 0,
284
+ createdThirtyDays: 0,
285
+ embedded: 0,
286
+ explicit: 0,
287
+ knowledge: 0,
288
+ preference: 0,
289
+ procedure: 0,
290
+ };
291
+ }
292
+ const [counts] = await db
293
+ .select({
294
+ active: sql<number>`count(*)`.mapWith(Number),
295
+ automatic:
296
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'session:%')`.mapWith(
297
+ Number,
298
+ ),
299
+ createdThirtyDays:
300
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.createdAtMs} >= ${nowMs - 30 * 24 * 60 * 60 * 1_000})`.mapWith(
301
+ Number,
302
+ ),
303
+ embedded:
304
+ sql<number>`count(${juniorMemoryEmbeddings.memoryId})`.mapWith(
305
+ Number,
306
+ ),
307
+ explicit:
308
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'tool:%')`.mapWith(
309
+ Number,
310
+ ),
311
+ knowledge:
312
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
313
+ Number,
314
+ ),
315
+ preference:
316
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
317
+ Number,
318
+ ),
319
+ procedure:
320
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
321
+ Number,
322
+ ),
323
+ })
324
+ .from(juniorMemoryMemories)
325
+ .leftJoin(
326
+ juniorMemoryEmbeddings,
327
+ eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
328
+ )
329
+ .where(active);
330
+ return {
331
+ active: counts?.active ?? 0,
332
+ automatic: counts?.automatic ?? 0,
333
+ createdThirtyDays: counts?.createdThirtyDays ?? 0,
334
+ embedded: counts?.embedded ?? 0,
335
+ explicit: counts?.explicit ?? 0,
336
+ knowledge: counts?.knowledge ?? 0,
337
+ preference: counts?.preference ?? 0,
338
+ procedure: counts?.procedure ?? 0,
339
+ };
340
+ },
341
+
342
+ async timeline(input) {
343
+ input = personalMemoryTimelineInputSchema.parse(input);
344
+ const todayMs = Date.parse(`${utcDate(getNowMs())}T00:00:00.000Z`);
345
+ const startMs = todayMs - (input.days - 1) * DAY_MS;
346
+ const ownership = personalScopePredicate(scopes);
347
+ if (!ownership) {
348
+ return Array.from({ length: input.days }, (_, index) => ({
349
+ date: utcDate(startMs + index * DAY_MS),
350
+ knowledge: 0,
351
+ preference: 0,
352
+ procedure: 0,
353
+ }));
354
+ }
355
+ const rows = await db
356
+ .select({
357
+ date: sql<string>`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`.as(
358
+ "date",
359
+ ),
360
+ knowledge:
361
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
362
+ Number,
363
+ ),
364
+ preference:
365
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
366
+ Number,
367
+ ),
368
+ procedure:
369
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
370
+ Number,
371
+ ),
372
+ })
373
+ .from(juniorMemoryMemories)
374
+ .where(
375
+ and(ownership, gt(juniorMemoryMemories.createdAtMs, startMs - 1)),
376
+ )
377
+ .groupBy(
378
+ sql`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`,
379
+ );
380
+ const byDate = new Map(rows.map((row) => [row.date, row]));
381
+ return Array.from({ length: input.days }, (_, index) => {
382
+ const date = utcDate(startMs + index * DAY_MS);
383
+ const row = byDate.get(date);
384
+ return {
385
+ date,
386
+ knowledge: row?.knowledge ?? 0,
387
+ preference: row?.preference ?? 0,
388
+ procedure: row?.procedure ?? 0,
389
+ };
390
+ });
391
+ },
194
392
  };
195
393
  }
package/src/personal.ts CHANGED
@@ -6,27 +6,34 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
  import type { PluginUserPageActor } from "@sentry/junior-plugin-api";
9
- import { createPersonalMemoryCollection } from "./personal-store";
9
+ import {
10
+ createPersonalMemoryCollection,
11
+ type PersonalMemoryRecord,
12
+ } from "./personal-store";
10
13
  import type { MemoryDb, MemoryRecord } from "./store";
11
- import type { MemoryRuntimeContext } from "./types";
14
+ import type { MemoryKind, MemoryRuntimeContext } from "./types";
12
15
 
13
16
  const cursorSchema = z
14
17
  .object({
15
18
  createdAtMs: z.number().finite(),
16
19
  id: z.string().min(1),
20
+ kind: z.enum(["preference", "procedure", "knowledge"]).optional(),
21
+ origin: z.enum(["automatic", "explicit"]).optional(),
17
22
  query: z.string().max(200).optional(),
18
23
  version: z.literal(1),
19
24
  })
20
25
  .strict();
21
26
 
22
27
  export interface ViewerMemoryPage {
23
- memories: MemoryRecord[];
28
+ memories: PersonalMemoryRecord[];
24
29
  nextCursor?: string;
25
30
  }
26
31
 
27
32
  export interface ViewerMemoryPageInput {
28
33
  cursor?: string;
34
+ kind?: MemoryKind;
29
35
  limit: number;
36
+ origin?: "automatic" | "explicit";
30
37
  query?: string;
31
38
  }
32
39
 
@@ -61,13 +68,20 @@ function runtimeContext(actor: PluginUserPageActor): MemoryRuntimeContext {
61
68
  };
62
69
  }
63
70
 
64
- function decodeCursor(value: string | undefined, query: string | undefined) {
71
+ function decodeCursor(
72
+ value: string | undefined,
73
+ input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query">,
74
+ ) {
65
75
  if (!value) return undefined;
66
76
  try {
67
77
  const parsed = cursorSchema.parse(
68
78
  JSON.parse(Buffer.from(value, "base64url").toString("utf8")),
69
79
  );
70
- if (parsed.query !== query) {
80
+ if (
81
+ parsed.query !== input.query ||
82
+ parsed.kind !== input.kind ||
83
+ parsed.origin !== input.origin
84
+ ) {
71
85
  throw new InvalidMemoryCursorError();
72
86
  }
73
87
  return { createdAtMs: parsed.createdAtMs, id: parsed.id };
@@ -78,10 +92,16 @@ function decodeCursor(value: string | undefined, query: string | undefined) {
78
92
 
79
93
  function encodeCursor(
80
94
  cursor: { createdAtMs: number; id: string },
81
- query: string | undefined,
95
+ input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query">,
82
96
  ): string {
83
97
  return Buffer.from(
84
- JSON.stringify({ ...cursor, ...(query ? { query } : {}), version: 1 }),
98
+ JSON.stringify({
99
+ ...cursor,
100
+ ...(input.query ? { query: input.query } : {}),
101
+ ...(input.kind ? { kind: input.kind } : {}),
102
+ ...(input.origin ? { origin: input.origin } : {}),
103
+ version: 1,
104
+ }),
85
105
  "utf8",
86
106
  ).toString("base64url");
87
107
  }
@@ -104,17 +124,28 @@ export function createViewerMemories(
104
124
  },
105
125
  async list(input: ViewerMemoryPageInput): Promise<ViewerMemoryPage> {
106
126
  const query = input.query?.trim() || undefined;
127
+ const filters = {
128
+ ...(input.kind ? { kind: input.kind } : {}),
129
+ ...(input.origin ? { origin: input.origin } : {}),
130
+ ...(query ? { query } : {}),
131
+ };
107
132
  const page = await collection.list({
108
- cursor: decodeCursor(input.cursor, query),
133
+ cursor: decodeCursor(input.cursor, filters),
134
+ ...filters,
109
135
  limit: input.limit,
110
- ...(query ? { query } : {}),
111
136
  });
112
137
  return {
113
138
  memories: page.memories,
114
139
  ...(page.nextCursor
115
- ? { nextCursor: encodeCursor(page.nextCursor, query) }
140
+ ? { nextCursor: encodeCursor(page.nextCursor, filters) }
116
141
  : {}),
117
142
  };
118
143
  },
144
+ async stats() {
145
+ return await collection.stats();
146
+ },
147
+ async timeline(input: { days: number }) {
148
+ return await collection.timeline(input);
149
+ },
119
150
  };
120
151
  }
package/src/user-pages.ts CHANGED
@@ -1,39 +1,66 @@
1
1
  /** Project viewer-owned memories into Junior's core-rendered user page. */
2
2
  import type { PluginUserPageDefinition } from "@sentry/junior-plugin-api";
3
3
  import { createViewerMemories } from "./personal";
4
- import type { MemoryDb, MemoryRecord } from "./store";
4
+ import type { PersonalMemoryRecord } from "./personal-store";
5
+ import type { MemoryDb } from "./store";
5
6
 
6
- function memoryKindLabel(kind: MemoryRecord["kind"]): string {
7
- return kind.charAt(0).toUpperCase() + kind.slice(1);
7
+ function titleCase(value: string): string {
8
+ return value.charAt(0).toUpperCase() + value.slice(1);
8
9
  }
9
10
 
10
11
  function rememberedDate(createdAtMs: number): string {
11
12
  return new Intl.DateTimeFormat("en-US", {
12
13
  dateStyle: "medium",
14
+ timeStyle: "short",
13
15
  timeZone: "UTC",
14
16
  }).format(new Date(createdAtMs));
15
17
  }
16
18
 
19
+ function originLabel(origin: PersonalMemoryRecord["origin"]): string {
20
+ if (origin === "automatic") return "Automatic";
21
+ if (origin === "explicit") return "Explicit";
22
+ return "Other";
23
+ }
24
+
25
+ function pageFilter(filter: string | undefined): {
26
+ kind?: "preference";
27
+ origin?: "automatic" | "explicit";
28
+ } {
29
+ if (filter === "preferences") return { kind: "preference" };
30
+ if (filter === "automatic") return { origin: "automatic" };
31
+ if (filter === "explicit") return { origin: "explicit" };
32
+ return {};
33
+ }
34
+
35
+ function pageEmptyText(input: { filter?: string; query?: string }): string {
36
+ if (input.query) return "No memories matched your search.";
37
+ if (input.filter === "preferences") return "No preferences yet.";
38
+ if (input.filter === "automatic") return "No learned memories yet.";
39
+ if (input.filter === "explicit") return "No saved memories yet.";
40
+ return "No personal memories yet.";
41
+ }
42
+
17
43
  /** Create the interactive personal Memories dashboard page. */
18
44
  export function createMemoryUserPage(): PluginUserPageDefinition {
19
45
  return {
20
46
  id: "memories",
21
47
  label: "Memories",
48
+ navigation: "primary",
22
49
  description: "Personal facts and preferences Junior remembers about you.",
23
50
  async read(ctx, input) {
24
- const page = await createViewerMemories(
51
+ const memories = createViewerMemories(
25
52
  ctx.db as MemoryDb,
26
53
  ctx.viewer.actors,
27
- ).list({
54
+ );
55
+ const page = await memories.list({
28
56
  cursor: input.cursor,
57
+ ...pageFilter(input.filter),
29
58
  limit: input.limit,
30
59
  ...(input.query ? { query: input.query } : {}),
31
60
  });
32
61
  return {
33
62
  type: "list",
34
- emptyText: input.query
35
- ? "No memories matched your search."
36
- : "No personal memories yet.",
63
+ emptyText: pageEmptyText(input),
37
64
  ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
38
65
  searchPlaceholder: "Search memories",
39
66
  records: page.memories.map((memory) => ({
@@ -49,8 +76,18 @@ export function createMemoryUserPage(): PluginUserPageDefinition {
49
76
  id: memory.id,
50
77
  title: memory.content,
51
78
  metadata: [
52
- { label: "Type", value: memoryKindLabel(memory.kind) },
79
+ { label: "Type", value: titleCase(memory.kind) },
80
+ { label: "Learned", value: originLabel(memory.origin) },
81
+ { label: "Source", value: titleCase(memory.sourcePlatform) },
82
+ { label: "Visibility", value: "Only you" },
53
83
  { label: "Remembered", value: rememberedDate(memory.createdAtMs) },
84
+ { label: "Observed", value: rememberedDate(memory.observedAtMs) },
85
+ {
86
+ label: "Expires",
87
+ value: memory.expiresAtMs
88
+ ? rememberedDate(memory.expiresAtMs)
89
+ : "Never",
90
+ },
54
91
  ],
55
92
  })),
56
93
  };