whats_new
List files created or modified since a given timestamp or relative duration. Use at session start to identify what changed since your last checkpoint.
Instructions
List files created or modified since a checkpoint. since accepts ISO-8601 (2025-01-15T00:00:00Z) or relative durations (1h, 7d, 2w); invalid formats throw. Read-only; no side effects, auth, or rate limits. Returns annotated rows plus aggregate total_est_tokens so you can decide what to read next. CAVEAT: hard-deleted files are NOT surfaced — only mtime-driven changes. Defaults: include_tags=true, limit=200. project_id: null = KB only; omit = everything. Use at session start to catch up.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| since | Yes | ISO 8601 timestamp or relative duration (e.g. "1h", "7d", "2w"). | |
| project_id | No | Filter to a single project. Pass null for knowledge-base-only files. Omit for all. | |
| include_tags | No | Attach tags[] to each file. Default true. | |
| limit | No | Max files returned. Default 200. |
Implementation Reference
- Core handler function that queries the files table for records with updated_at >= since, classifies each as 'created' or 'modified', optionally attaches tags, and returns the result with resolved timestamps.
export function whatsNew(opts: WhatsNewOptions): WhatsNewResult { const now = new Date(); const cutoff = resolveSince(opts.since, now); const limit = opts.limit ?? 200; const includeTags = opts.include_tags !== false; const db = getDatabase(); let sql = ` SELECT *, CASE WHEN created_at >= ? THEN 'created' ELSE 'modified' END AS change FROM files WHERE updated_at >= ? `; const params: unknown[] = [cutoff, cutoff]; if (opts.project_id !== undefined) { if (opts.project_id === null) { sql += " AND project_id IS NULL"; } else { sql += " AND project_id = ?"; params.push(opts.project_id); } } sql += " ORDER BY updated_at DESC LIMIT ?"; params.push(limit); const rows = db.prepare(sql).all(...params) as (FileRecord & { change: ChangeKind })[]; let entries: WhatsNewEntry[] = rows; if (includeTags && rows.length > 0) { const tagMap = getTagsForFiles(rows.map((r) => r.id)); entries = rows.map((r) => ({ ...r, tags: tagMap.get(r.id) ?? [] })); } return { since: new Date(cutoff.replace(" ", "T") + "Z").toISOString(), until: now.toISOString(), count: entries.length, files: entries, }; } - Type definitions for the whats_new tool: input options (since, project_id, include_tags, limit), entry shape with change kind and optional tags, and result structure with resolved timestamps and count.
export interface WhatsNewOptions { /** * Cutoff. Either: * - ISO 8601 timestamp ("2026-04-30T12:00:00Z") * - Relative duration ("30s", "15m", "2h", "1d", "7d", "1w") * Records with updated_at >= since are returned. */ since: string; project_id?: number | null; /** If true, fetch and attach tags[] for each file. Default true. */ include_tags?: boolean; /** Max rows to return. Default 200. */ limit?: number; } export interface WhatsNewEntry extends FileRecord { change: ChangeKind; tags?: string[]; } export interface WhatsNewResult { /** Resolved cutoff as ISO timestamp. */ since: string; /** Server "now" at query time, ISO. */ until: string; count: number; files: WhatsNewEntry[]; } - apps/mcp/src/index.ts:1991-2034 (registration)MCP server tool registration for 'whats_new' — defines the Zod schema for input parameters, calls the core whatsNew() handler, annotates results with token estimates, and wraps in try/catch for error responses.
server.tool( "whats_new", "List files created or modified since a checkpoint. `since` accepts ISO-8601 (`2025-01-15T00:00:00Z`) or relative durations (`1h`, `7d`, `2w`); invalid formats throw. Read-only; no side effects, auth, or rate limits. Returns annotated rows plus aggregate `total_est_tokens` so you can decide what to read next. CAVEAT: hard-deleted files are NOT surfaced — only mtime-driven changes. Defaults: include_tags=true, limit=200. `project_id: null` = KB only; omit = everything. Use at session start to catch up.", { since: z.string().describe("ISO 8601 timestamp or relative duration (e.g. \"1h\", \"7d\", \"2w\")."), project_id: z.number().nullable().optional().describe("Filter to a single project. Pass null for knowledge-base-only files. Omit for all."), include_tags: z.boolean().optional().describe("Attach tags[] to each file. Default true."), limit: z.number().optional().describe("Max files returned. Default 200."), }, async ({ since, project_id, include_tags, limit }) => { try { const opts: any = { since }; if (project_id !== undefined) opts.project_id = project_id; if (include_tags !== undefined) opts.include_tags = include_tags; if (limit !== undefined) opts.limit = limit; const result = whatsNew(opts); const annotated = result.files.map(annotateTokens); const total_est_tokens = annotated.reduce((s, f) => s + (f.est_tokens ?? 0), 0); return { content: [ { type: "text", text: JSON.stringify( { since: result.since, until: result.until, count: result.count, total_est_tokens, files: annotated, }, null, 2 ), }, ], }; } catch (e) { return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: (e as Error).message }, null, 2) }], }; } } ); - Helper that parses the 'since' parameter — accepts ISO 8601 timestamps or relative durations (s/m/h/d/w) and converts to a SQLite-comparable UTC datetime string. Also validates the cutoff is not in the future.
/** * Resolve `since` (ISO timestamp OR relative duration) to a sqlite-comparable * UTC datetime string ("YYYY-MM-DD HH:MM:SS"). Throws RangeError if unparseable * or if the resulting time is in the future. */ export function resolveSince(since: string, now: Date = new Date()): string { const trimmed = since.trim(); if (!trimmed) throw new RangeError("`since` is required"); const dur = DURATION_RE.exec(trimmed); let cutoff: Date; if (dur) { const n = parseInt(dur[1], 10); const unit = dur[2].toLowerCase(); cutoff = new Date(now.getTime() - n * UNIT_MS[unit]); } else { const t = Date.parse(trimmed); if (Number.isNaN(t)) { throw new RangeError( `Cannot parse \`since\`: ${JSON.stringify(since)}. Expected ISO 8601 timestamp or relative duration like "1h", "7d", "2w".` ); } cutoff = new Date(t); } if (cutoff.getTime() > now.getTime()) { throw new RangeError(`\`since\` is in the future: ${cutoff.toISOString()}`); } return toSqliteTime(cutoff); } function toSqliteTime(d: Date): string { // sqlite stores datetime('now') as "YYYY-MM-DD HH:MM:SS" in UTC. // Comparisons in our tables expect that exact shape. return d.toISOString().replace("T", " ").slice(0, 19); } - Helper used by the whatsNew handler to bulk-fetch tags for all returned files in a single query, avoiding N+1 round-trips.
/** * Bulk-fetch tag names for many files in a single query. * Returns a Map keyed by file_id; missing keys = file has no tags. * Used by list_files / search / whats_new / project_map to inline tags * without an N+1 round-trip per file. */ export function getTagsForFiles(fileIds: number[]): Map<number, string[]> { const out = new Map<number, string[]>(); if (fileIds.length === 0) return out; const db = getDatabase(); const placeholders = fileIds.map(() => "?").join(","); const rows = db .prepare( `SELECT ft.file_id AS file_id, t.name AS name FROM file_tags ft JOIN tags t ON t.id = ft.tag_id WHERE ft.file_id IN (${placeholders}) ORDER BY t.name ASC` ) .all(...fileIds) as { file_id: number; name: string }[]; for (const r of rows) { const arr = out.get(r.file_id); if (arr) arr.push(r.name); else out.set(r.file_id, [r.name]); } return out; }