donguri-journal 0.1.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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +181 -0
  3. package/README.md +184 -0
  4. package/dist/db/schema.d.ts +21 -0
  5. package/dist/db/schema.js +53 -0
  6. package/dist/db/schema.js.map +1 -0
  7. package/dist/db/store.d.ts +136 -0
  8. package/dist/db/store.js +0 -0
  9. package/dist/db/store.js.map +1 -0
  10. package/dist/embedding/provider.d.ts +37 -0
  11. package/dist/embedding/provider.js +53 -0
  12. package/dist/embedding/provider.js.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +49 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/kernel/config.d.ts +17 -0
  17. package/dist/kernel/config.js +26 -0
  18. package/dist/kernel/config.js.map +1 -0
  19. package/dist/kernel/context.d.ts +26 -0
  20. package/dist/kernel/context.js +7 -0
  21. package/dist/kernel/context.js.map +1 -0
  22. package/dist/kernel/module.d.ts +13 -0
  23. package/dist/kernel/module.js +7 -0
  24. package/dist/kernel/module.js.map +1 -0
  25. package/dist/kernel/plugin.d.ts +68 -0
  26. package/dist/kernel/plugin.js +144 -0
  27. package/dist/kernel/plugin.js.map +1 -0
  28. package/dist/kernel/result.d.ts +14 -0
  29. package/dist/kernel/result.js +11 -0
  30. package/dist/kernel/result.js.map +1 -0
  31. package/dist/management/module.d.ts +2 -0
  32. package/dist/management/module.js +39 -0
  33. package/dist/management/module.js.map +1 -0
  34. package/dist/management/server.d.ts +18 -0
  35. package/dist/management/server.js +216 -0
  36. package/dist/management/server.js.map +1 -0
  37. package/dist/management/ui.d.ts +10 -0
  38. package/dist/management/ui.js +159 -0
  39. package/dist/management/ui.js.map +1 -0
  40. package/dist/modules/core.d.ts +2 -0
  41. package/dist/modules/core.js +386 -0
  42. package/dist/modules/core.js.map +1 -0
  43. package/dist/modules/plugins.d.ts +2 -0
  44. package/dist/modules/plugins.js +177 -0
  45. package/dist/modules/plugins.js.map +1 -0
  46. package/dist/originals/store.d.ts +50 -0
  47. package/dist/originals/store.js +185 -0
  48. package/dist/originals/store.js.map +1 -0
  49. package/dist/review/charts.d.ts +16 -0
  50. package/dist/review/charts.js +69 -0
  51. package/dist/review/charts.js.map +1 -0
  52. package/dist/review/patterns.d.ts +33 -0
  53. package/dist/review/patterns.js +73 -0
  54. package/dist/review/patterns.js.map +1 -0
  55. package/dist/review/review.d.ts +30 -0
  56. package/dist/review/review.js +82 -0
  57. package/dist/review/review.js.map +1 -0
  58. package/dist/review/window.d.ts +18 -0
  59. package/dist/review/window.js +57 -0
  60. package/dist/review/window.js.map +1 -0
  61. package/package.json +62 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Original-artifact store.
3
+ *
4
+ * The two-layer design keeps originals verbatim and the vector index disposable.
5
+ * When the front-end LLM captures media, it sends the original bytes over MCP and
6
+ * the server persists them here, then points `original_ref` at the saved object.
7
+ * The server never interprets the bytes (no vision/audio models) — it only stores
8
+ * and serves them so the LLM can re-view / re-extract later.
9
+ *
10
+ * Objects are addressed purely by content hash, so identical bytes always map to
11
+ * the same ref regardless of the supplied filename/MIME. The MIME type is kept as
12
+ * separate sidecar metadata rather than baked into the ref.
13
+ *
14
+ * The backend is pluggable behind `OriginalStore`. The default is a local
15
+ * directory; an Eagle backend can be added as an opt-in later without touching
16
+ * callers.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ import { existsSync } from "node:fs";
20
+ import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
21
+ import { homedir } from "node:os";
22
+ import { join, resolve } from "node:path";
23
+ const EXT_TO_MIME = {
24
+ png: "image/png",
25
+ jpg: "image/jpeg",
26
+ jpeg: "image/jpeg",
27
+ gif: "image/gif",
28
+ webp: "image/webp",
29
+ svg: "image/svg+xml",
30
+ bmp: "image/bmp",
31
+ tiff: "image/tiff",
32
+ avif: "image/avif",
33
+ heic: "image/heic",
34
+ mp3: "audio/mpeg",
35
+ wav: "audio/wav",
36
+ ogg: "audio/ogg",
37
+ flac: "audio/flac",
38
+ aac: "audio/aac",
39
+ m4a: "audio/mp4",
40
+ weba: "audio/webm",
41
+ mp4: "video/mp4",
42
+ mov: "video/quicktime",
43
+ webm: "video/webm",
44
+ pdf: "application/pdf",
45
+ txt: "text/plain",
46
+ md: "text/markdown",
47
+ json: "application/json",
48
+ };
49
+ /** Resolve a MIME type from the explicit value, else the filename extension. */
50
+ function resolveMime(input) {
51
+ if (input.mime && input.mime.length > 0)
52
+ return input.mime.toLowerCase();
53
+ if (input.filename) {
54
+ const dot = input.filename.lastIndexOf(".");
55
+ if (dot >= 0) {
56
+ const ext = input.filename
57
+ .slice(dot + 1)
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]/g, "");
60
+ const mime = EXT_TO_MIME[ext];
61
+ if (mime)
62
+ return mime;
63
+ }
64
+ }
65
+ return undefined;
66
+ }
67
+ /**
68
+ * Content-addressed store under a local directory. Each original is stored as a
69
+ * blob named `<sha256>` plus a `<sha256>.json` sidecar holding its MIME type and
70
+ * original filename.
71
+ */
72
+ export class LocalDirStore {
73
+ kind = "local";
74
+ #baseDir;
75
+ constructor(baseDir) {
76
+ this.#baseDir = resolve(baseDir);
77
+ }
78
+ async save(input) {
79
+ const sha = createHash("sha256").update(input.data).digest("hex");
80
+ const mime = resolveMime(input);
81
+ const blobPath = join(this.#baseDir, sha);
82
+ const metaPath = join(this.#baseDir, `${sha}.json`);
83
+ if (!existsSync(blobPath)) {
84
+ await mkdir(this.#baseDir, { recursive: true });
85
+ await writeFile(blobPath, input.data);
86
+ }
87
+ // Create the sidecar, or backfill fields a previous save left empty (e.g. a
88
+ // first save without a MIME followed by one that knows it).
89
+ let existing = {};
90
+ if (existsSync(metaPath)) {
91
+ try {
92
+ existing = JSON.parse(await readFile(metaPath, "utf8"));
93
+ }
94
+ catch {
95
+ // Corrupt sidecar — rewrite it from the current input.
96
+ }
97
+ }
98
+ const merged = {
99
+ mime: existing.mime ?? mime,
100
+ filename: existing.filename ?? input.filename,
101
+ };
102
+ if (!existsSync(metaPath) ||
103
+ merged.mime !== existing.mime ||
104
+ merged.filename !== existing.filename) {
105
+ await mkdir(this.#baseDir, { recursive: true });
106
+ await writeFile(metaPath, JSON.stringify(merged));
107
+ }
108
+ return { ref: `local:${sha}`, bytes: input.data.length, mime: merged.mime };
109
+ }
110
+ async get(ref) {
111
+ const prefix = "local:";
112
+ if (!ref.startsWith(prefix))
113
+ return null;
114
+ const sha = ref.slice(prefix.length);
115
+ // Pure hex sha256: also rules out path traversal.
116
+ if (!/^[a-f0-9]{64}$/.test(sha))
117
+ return null;
118
+ const blobPath = join(this.#baseDir, sha);
119
+ if (!blobPath.startsWith(this.#baseDir) || !existsSync(blobPath))
120
+ return null;
121
+ const data = await readFile(blobPath);
122
+ let mime;
123
+ const metaPath = join(this.#baseDir, `${sha}.json`);
124
+ if (existsSync(metaPath)) {
125
+ try {
126
+ const meta = JSON.parse(await readFile(metaPath, "utf8"));
127
+ if (typeof meta.mime === "string")
128
+ mime = meta.mime;
129
+ }
130
+ catch {
131
+ // Corrupt sidecar — fall back to no MIME rather than failing the read.
132
+ }
133
+ }
134
+ return { data, mime, path: blobPath };
135
+ }
136
+ async delete(ref) {
137
+ const prefix = "local:";
138
+ if (!ref.startsWith(prefix))
139
+ return false;
140
+ const sha = ref.slice(prefix.length);
141
+ if (!/^[a-f0-9]{64}$/.test(sha))
142
+ return false;
143
+ const blobPath = join(this.#baseDir, sha);
144
+ const metaPath = join(this.#baseDir, `${sha}.json`);
145
+ let removed = false;
146
+ if (existsSync(blobPath)) {
147
+ await unlink(blobPath);
148
+ removed = true;
149
+ }
150
+ if (existsSync(metaPath)) {
151
+ await unlink(metaPath);
152
+ }
153
+ return removed;
154
+ }
155
+ async stats() {
156
+ if (!existsSync(this.#baseDir))
157
+ return { count: 0, bytes: 0 };
158
+ const names = await readdir(this.#baseDir);
159
+ let count = 0;
160
+ let bytes = 0;
161
+ for (const name of names) {
162
+ if (name.endsWith(".json"))
163
+ continue;
164
+ const info = await stat(join(this.#baseDir, name));
165
+ if (info.isFile()) {
166
+ count += 1;
167
+ bytes += info.size;
168
+ }
169
+ }
170
+ return { count, bytes };
171
+ }
172
+ }
173
+ /**
174
+ * Choose an original store. Today this is always the local directory; an Eagle
175
+ * backend (when JOURNAL_EAGLE_API is set) can be slotted in here later.
176
+ */
177
+ export function createOriginalStore(baseDir) {
178
+ if (baseDir && baseDir.length > 0) {
179
+ return new LocalDirStore(baseDir);
180
+ }
181
+ const fromEnv = process.env.JOURNAL_ORIGINALS_DIR;
182
+ const dir = fromEnv && fromEnv.length > 0 ? fromEnv : join(homedir(), ".journal-mcp", "originals");
183
+ return new LocalDirStore(dir);
184
+ }
185
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/originals/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Tiny dependency-free SVG bar-chart builder, rasterized to PNG via sharp.
3
+ *
4
+ * Reviews return rendered PNG charts (plus structured data and presentation
5
+ * hints) so the front-end LLM can present richly — markdown alone is not enough.
6
+ * sharp is imported lazily so the server starts even where it is unavailable;
7
+ * callers fall back to data-only output if rendering throws.
8
+ */
9
+ export interface BarDatum {
10
+ label: string;
11
+ value: number;
12
+ }
13
+ /** Build a vertical bar chart as an SVG string. */
14
+ export declare function barChartSvg(title: string, data: BarDatum[]): string;
15
+ /** Rasterize an SVG string to a PNG buffer using sharp (lazy-loaded). */
16
+ export declare function renderPng(svg: string): Promise<Buffer>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Tiny dependency-free SVG bar-chart builder, rasterized to PNG via sharp.
3
+ *
4
+ * Reviews return rendered PNG charts (plus structured data and presentation
5
+ * hints) so the front-end LLM can present richly — markdown alone is not enough.
6
+ * sharp is imported lazily so the server starts even where it is unavailable;
7
+ * callers fall back to data-only output if rendering throws.
8
+ */
9
+ const COLORS = {
10
+ bg: "#ffffff",
11
+ bar: "#4f46e5",
12
+ axis: "#9ca3af",
13
+ text: "#1f2937",
14
+ subtext: "#6b7280",
15
+ };
16
+ function esc(s) {
17
+ return s
18
+ .replace(/&/g, "&amp;")
19
+ .replace(/</g, "&lt;")
20
+ .replace(/>/g, "&gt;")
21
+ .replace(/"/g, "&quot;");
22
+ }
23
+ /** Build a vertical bar chart as an SVG string. */
24
+ export function barChartSvg(title, data) {
25
+ const marginTop = 40;
26
+ const marginBottom = 64;
27
+ const marginLeft = 44;
28
+ const marginRight = 16;
29
+ const n = Math.max(data.length, 1);
30
+ const slot = Math.max(18, Math.min(56, Math.floor(640 / n)));
31
+ const barW = Math.max(8, slot - 10);
32
+ const plotW = n * slot;
33
+ const plotH = 200;
34
+ // Width must fit both the bars and the (left-aligned) title; ~9.5px/char at 15px bold.
35
+ const barsWidth = marginLeft + plotW + marginRight;
36
+ const titleWidth = marginLeft + Math.ceil(title.length * 9.5) + marginRight;
37
+ const width = Math.max(barsWidth, titleWidth);
38
+ const height = marginTop + plotH + marginBottom;
39
+ const maxValue = Math.max(1, ...data.map((d) => d.value));
40
+ const baselineY = marginTop + plotH;
41
+ const parts = [];
42
+ parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" font-family="system-ui, -apple-system, Segoe UI, sans-serif">`);
43
+ parts.push(`<rect width="${width}" height="${height}" fill="${COLORS.bg}"/>`);
44
+ parts.push(`<text x="${marginLeft}" y="24" font-size="15" font-weight="600" fill="${COLORS.text}">${esc(title)}</text>`);
45
+ // y-axis max label + baseline
46
+ parts.push(`<text x="${marginLeft - 8}" y="${marginTop + 4}" font-size="11" text-anchor="end" fill="${COLORS.subtext}">${maxValue}</text>`);
47
+ parts.push(`<line x1="${marginLeft}" y1="${baselineY}" x2="${marginLeft + plotW}" y2="${baselineY}" stroke="${COLORS.axis}" stroke-width="1"/>`);
48
+ data.forEach((d, i) => {
49
+ const h = Math.round((d.value / maxValue) * plotH);
50
+ const x = marginLeft + i * slot + (slot - barW) / 2;
51
+ const y = baselineY - h;
52
+ parts.push(`<rect x="${x}" y="${y}" width="${barW}" height="${h}" rx="2" fill="${COLORS.bar}"/>`);
53
+ if (d.value > 0) {
54
+ parts.push(`<text x="${x + barW / 2}" y="${y - 4}" font-size="10" text-anchor="middle" fill="${COLORS.subtext}">${d.value}</text>`);
55
+ }
56
+ const cx = x + barW / 2;
57
+ const ly = baselineY + 14;
58
+ parts.push(`<text x="${cx}" y="${ly}" font-size="10" text-anchor="end" fill="${COLORS.text}" transform="rotate(-45 ${cx} ${ly})">${esc(d.label)}</text>`);
59
+ });
60
+ parts.push("</svg>");
61
+ return parts.join("");
62
+ }
63
+ /** Rasterize an SVG string to a PNG buffer using sharp (lazy-loaded). */
64
+ export async function renderPng(svg) {
65
+ const sharpModule = await import("sharp");
66
+ const sharp = sharpModule.default;
67
+ return sharp(Buffer.from(svg)).png().toBuffer();
68
+ }
69
+ //# sourceMappingURL=charts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"charts.js","sourceRoot":"","sources":["../../src/review/charts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * surface_patterns — find recurring themes ("you wrote something similar
3
+ * before"). For each recent entry we look for semantically similar OLDER
4
+ * entries; clusters of echoes are returned as structured data + a PNG chart of
5
+ * the strongest echoes + presentation hints. The front-end LLM judges which
6
+ * echoes are meaningful and weaves the narrative.
7
+ */
8
+ import type { Entry, JournalStore, RecallHit } from "../db/store.js";
9
+ export interface SurfacePatternsInput {
10
+ /** How far back to treat entries as "recent". Default 30 days. */
11
+ lookback_days?: number;
12
+ /** Max recent entries to examine. Default 50. */
13
+ max_recent?: number;
14
+ /** Neighbours to consider per recent entry. Default 5. */
15
+ per_entry?: number;
16
+ /** Distance cutoff; only echoes at or below this are kept. Default 1.3. */
17
+ max_distance?: number;
18
+ }
19
+ export interface Echo {
20
+ recent: Pick<Entry, "id" | "body" | "created_at" | "occurred_at" | "tags">;
21
+ related: Array<Pick<RecallHit, "id" | "body" | "created_at" | "distance">>;
22
+ }
23
+ export interface PatternsOutput {
24
+ structured: {
25
+ lookback_days: number;
26
+ max_distance: number;
27
+ examined: number;
28
+ echoes: Echo[];
29
+ };
30
+ chartPng: Buffer | null;
31
+ presentation_hints: Record<string, unknown>;
32
+ }
33
+ export declare function surfacePatterns(store: JournalStore, input: SurfacePatternsInput): Promise<PatternsOutput>;
@@ -0,0 +1,73 @@
1
+ import { barChartSvg, renderPng } from "./charts.js";
2
+ function shortLabel(body) {
3
+ const oneLine = body.replace(/\s+/g, " ").trim();
4
+ return oneLine.length > 18 ? `${oneLine.slice(0, 17)}…` : oneLine;
5
+ }
6
+ export async function surfacePatterns(store, input) {
7
+ const lookbackDays = Math.min(Math.max(Math.trunc(input.lookback_days ?? 30), 1), 3650);
8
+ const maxRecent = Math.min(Math.max(Math.trunc(input.max_recent ?? 50), 1), 200);
9
+ const perEntry = Math.min(Math.max(Math.trunc(input.per_entry ?? 5), 1), 20);
10
+ const maxDistance = input.max_distance ?? 1.3;
11
+ const since = new Date(Date.now() - lookbackDays * 86_400_000).toISOString();
12
+ const recent = store.query({ since, time_field: "created_at", limit: maxRecent });
13
+ const echoes = [];
14
+ for (const entry of recent) {
15
+ // Over-fetch: self and newer entries can crowd the top neighbours, so we
16
+ // need a larger candidate pool to still find `perEntry` OLDER echoes.
17
+ const candidateK = Math.min(100, perEntry * 4 + 5);
18
+ const neighbours = await store.recall(entry.body, candidateK);
19
+ const related = neighbours
20
+ .filter((h) => h.id !== entry.id && h.created_at < entry.created_at && h.distance <= maxDistance)
21
+ .slice(0, perEntry)
22
+ .map((h) => ({ id: h.id, body: h.body, created_at: h.created_at, distance: h.distance }));
23
+ if (related.length > 0) {
24
+ echoes.push({
25
+ recent: {
26
+ id: entry.id,
27
+ body: entry.body,
28
+ created_at: entry.created_at,
29
+ occurred_at: entry.occurred_at,
30
+ tags: entry.tags,
31
+ },
32
+ related,
33
+ });
34
+ }
35
+ }
36
+ echoes.sort((a, b) => b.related.length - a.related.length);
37
+ let chartPng = null;
38
+ if (echoes.length > 0) {
39
+ try {
40
+ const svg = barChartSvg("Recurring echoes (recent entries with past matches)", echoes
41
+ .slice(0, 12)
42
+ .map((e) => ({ label: shortLabel(e.recent.body), value: e.related.length })));
43
+ chartPng = await renderPng(svg);
44
+ }
45
+ catch (err) {
46
+ console.error("[donguri-journal] patterns chart render failed:", err);
47
+ chartPng = null;
48
+ }
49
+ }
50
+ const presentation_hints = {
51
+ headline: echoes.length > 0
52
+ ? `Found ${echoes.length} recent ${echoes.length === 1 ? "entry that echoes" : "entries that echo"} earlier ones`
53
+ : "No clear recurring themes in this window",
54
+ interpretation: "Each echo is a candidate recurrence, not a certainty — judge relevance yourself before " +
55
+ "presenting. Distances are L2 (smaller = closer); ~<1.2 is usually a strong match.",
56
+ tone: "Curious and gentle: 'you've returned to this before…'. Avoid over-claiming.",
57
+ chart: chartPng
58
+ ? "A PNG chart of the strongest echoes is attached."
59
+ : "No chart rendered (no echoes, or rendering unavailable).",
60
+ next: "Use query_entries or recall_related to pull full context for any echo worth discussing.",
61
+ };
62
+ return {
63
+ structured: {
64
+ lookback_days: lookbackDays,
65
+ max_distance: maxDistance,
66
+ examined: recent.length,
67
+ echoes,
68
+ },
69
+ chartPng,
70
+ presentation_hints,
71
+ };
72
+ }
73
+ //# sourceMappingURL=patterns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/review/patterns.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * generate_review — pull entries in a time window and return rich aggregates:
3
+ * structured data + a rendered PNG activity chart + presentation hints, so the
4
+ * front-end LLM can present a reflective review (not just a markdown list).
5
+ */
6
+ import { type DayCount, type JournalStore, type SourceKindCount, type TagCount } from "../db/store.js";
7
+ import { type ReviewPeriod, type ReviewWindow } from "./window.js";
8
+ export interface GenerateReviewInput {
9
+ period?: ReviewPeriod;
10
+ anchor?: string;
11
+ since?: string;
12
+ until?: string;
13
+ time_field?: "created_at" | "occurred_at";
14
+ }
15
+ export interface ReviewStructured {
16
+ window: ReviewWindow;
17
+ time_field: "created_at" | "occurred_at";
18
+ total: number;
19
+ busiest_day: DayCount | null;
20
+ by_day: DayCount[];
21
+ by_source_kind: SourceKindCount[];
22
+ top_tags: TagCount[];
23
+ }
24
+ export interface ReviewOutput {
25
+ structured: ReviewStructured;
26
+ /** PNG chart of entries per day, if rendering succeeded. */
27
+ chartPng: Buffer | null;
28
+ presentation_hints: Record<string, unknown>;
29
+ }
30
+ export declare function generateReview(store: JournalStore, input: GenerateReviewInput): Promise<ReviewOutput>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * generate_review — pull entries in a time window and return rich aggregates:
3
+ * structured data + a rendered PNG activity chart + presentation hints, so the
4
+ * front-end LLM can present a reflective review (not just a markdown list).
5
+ */
6
+ import { normalizeTimestamp, } from "../db/store.js";
7
+ import { barChartSvg, renderPng } from "./charts.js";
8
+ import { computeWindow } from "./window.js";
9
+ export async function generateReview(store, input) {
10
+ const timeField = input.time_field === "occurred_at" ? "occurred_at" : "created_at";
11
+ // A custom window needs both bounds; one-sided input must fail loudly rather
12
+ // than silently falling back to `period` and returning a different range.
13
+ const { since, until } = input;
14
+ if ((since === undefined) !== (until === undefined)) {
15
+ throw new Error("A custom window requires both `since` and `until` (or neither — use `period`).");
16
+ }
17
+ let window;
18
+ if (since !== undefined && until !== undefined) {
19
+ const s = normalizeTimestamp(since);
20
+ const u = normalizeTimestamp(until);
21
+ if (s > u) {
22
+ throw new Error(`Invalid custom window: since (${s}) is after until (${u}).`);
23
+ }
24
+ window = { since: s, until: u, label: `${s}–${u} (custom)` };
25
+ }
26
+ else {
27
+ window = computeWindow(input.period ?? "week", input.anchor);
28
+ }
29
+ const filter = {
30
+ since: window.since,
31
+ until: window.until,
32
+ time_field: timeField,
33
+ };
34
+ const byDay = store.aggregateByDay(filter);
35
+ const total = store.countInWindow(filter);
36
+ const bySourceKind = store.aggregateBySourceKind(filter);
37
+ const topTags = store.aggregateTags(filter, 10);
38
+ const busiestDay = byDay.reduce((best, d) => (best === null || d.count > best.count ? d : best), null);
39
+ const structured = {
40
+ window,
41
+ time_field: timeField,
42
+ total,
43
+ busiest_day: busiestDay,
44
+ by_day: byDay,
45
+ by_source_kind: bySourceKind,
46
+ top_tags: topTags,
47
+ };
48
+ let chartPng = null;
49
+ if (byDay.length > 0) {
50
+ try {
51
+ // Keep the year in labels when the window spans more than one (MM-DD alone
52
+ // would be ambiguous across years); otherwise MM-DD keeps labels compact.
53
+ const includeYear = new Set(byDay.map((d) => d.day.slice(0, 4))).size > 1;
54
+ const svg = barChartSvg(`Entries per day — ${window.label}`, byDay.map((d) => ({ label: includeYear ? d.day : d.day.slice(5), value: d.count })));
55
+ chartPng = await renderPng(svg);
56
+ }
57
+ catch (err) {
58
+ console.error("[donguri-journal] review chart render failed:", err);
59
+ chartPng = null;
60
+ }
61
+ }
62
+ const presentation_hints = {
63
+ headline: `Review of ${window.label}: ${total} ${total === 1 ? "entry" : "entries"}`,
64
+ highlight: {
65
+ total,
66
+ busiest_day: busiestDay,
67
+ top_tags: topTags.slice(0, 5),
68
+ },
69
+ chart: chartPng
70
+ ? "A PNG bar chart of entries per day is attached; show it alongside your summary."
71
+ : "No chart was rendered (no entries in window, or rendering unavailable).",
72
+ tone: "Reflective and concise. Surface notable themes; invite the user to reflect.",
73
+ suggestions: [
74
+ total === 0
75
+ ? "Window is empty — gently note it and suggest capturing more."
76
+ : "Call out the busiest day and the recurring tags.",
77
+ "If patterns recur, consider calling surface_patterns for deeper echoes.",
78
+ ],
79
+ };
80
+ return { structured, chartPng, presentation_hints };
81
+ }
82
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/review/review.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Calendar window helpers for reviews. All windows are computed in UTC so they
3
+ * line up with the stored `...Z` timestamps (see normalizeTimestamp in store).
4
+ */
5
+ export type ReviewPeriod = "day" | "week" | "month";
6
+ export interface ReviewWindow {
7
+ /** Inclusive ISO-8601 lower bound. */
8
+ since: string;
9
+ /** Inclusive ISO-8601 upper bound. */
10
+ until: string;
11
+ /** Human-readable label, e.g. "2026-06 (month)". */
12
+ label: string;
13
+ }
14
+ /**
15
+ * Compute the calendar window (day / ISO-week Mon–Sun / month) containing
16
+ * `anchorIso` (defaults to now).
17
+ */
18
+ export declare function computeWindow(period: ReviewPeriod, anchorIso?: string): ReviewWindow;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Calendar window helpers for reviews. All windows are computed in UTC so they
3
+ * line up with the stored `...Z` timestamps (see normalizeTimestamp in store).
4
+ */
5
+ function startOfDayUTC(d) {
6
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
7
+ }
8
+ /** End of an inclusive window: 1 ms before the start of the next window. */
9
+ function inclusiveEnd(exclusiveStartOfNext) {
10
+ return new Date(exclusiveStartOfNext.getTime() - 1).toISOString();
11
+ }
12
+ function dayLabel(d) {
13
+ return d.toISOString().slice(0, 10);
14
+ }
15
+ /**
16
+ * Compute the calendar window (day / ISO-week Mon–Sun / month) containing
17
+ * `anchorIso` (defaults to now).
18
+ */
19
+ export function computeWindow(period, anchorIso) {
20
+ const anchor = anchorIso ? new Date(anchorIso) : new Date();
21
+ if (Number.isNaN(anchor.getTime())) {
22
+ throw new Error(`Invalid anchor timestamp: ${anchorIso}`);
23
+ }
24
+ if (period === "day") {
25
+ const start = startOfDayUTC(anchor);
26
+ const next = new Date(start);
27
+ next.setUTCDate(start.getUTCDate() + 1);
28
+ return {
29
+ since: start.toISOString(),
30
+ until: inclusiveEnd(next),
31
+ label: `${dayLabel(start)} (day)`,
32
+ };
33
+ }
34
+ if (period === "week") {
35
+ const start = startOfDayUTC(anchor);
36
+ // Shift back to Monday (getUTCDay: 0=Sun..6=Sat -> 0=Mon..6=Sun).
37
+ const mondayOffset = (start.getUTCDay() + 6) % 7;
38
+ start.setUTCDate(start.getUTCDate() - mondayOffset);
39
+ const next = new Date(start);
40
+ next.setUTCDate(start.getUTCDate() + 7);
41
+ const sunday = new Date(next.getTime() - 1);
42
+ return {
43
+ since: start.toISOString(),
44
+ until: inclusiveEnd(next),
45
+ label: `${dayLabel(start)}–${dayLabel(sunday)} (week)`,
46
+ };
47
+ }
48
+ // month
49
+ const start = new Date(Date.UTC(anchor.getUTCFullYear(), anchor.getUTCMonth(), 1));
50
+ const next = new Date(Date.UTC(anchor.getUTCFullYear(), anchor.getUTCMonth() + 1, 1));
51
+ return {
52
+ since: start.toISOString(),
53
+ until: inclusiveEnd(next),
54
+ label: `${start.toISOString().slice(0, 7)} (month)`,
55
+ };
56
+ }
57
+ //# sourceMappingURL=window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"window.js","sourceRoot":"","sources":["../../src/review/window.ts"],"names":[],"mappings":"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"}