Server (SvelteKit + SQLite Docker container) + per-machine agent that parses Claude Code JSONL transcripts. docker-compose one-command deploy; raw transcript view + search gated behind SHOW_TRANSCRIPTS (hidden by default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
/**
|
|
* toknmtr agent — per-file byte-offset cursor.
|
|
*
|
|
* Persists, per transcript file, the byte offset up to which we've already parsed and
|
|
* pushed events, so re-runs only emit NEW bytes appended since last time. Stored as a
|
|
* single JSON file at ~/.toknmtr/cursors.json (one process at a time is assumed — there's
|
|
* no file locking).
|
|
*
|
|
* Handles truncation/rotation: if a file's current size is smaller than the recorded
|
|
* offset, the file was truncated or replaced (e.g. a session id got reused, or the file
|
|
* was edited out from under us) — getOffset() resets to 0 so the whole file is reparsed.
|
|
*/
|
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
export const CURSOR_FILE = join(homedir(), '.toknmtr', 'cursors.json');
|
|
|
|
interface CursorEntry {
|
|
offset: number;
|
|
size: number;
|
|
}
|
|
|
|
type CursorMap = Record<string, CursorEntry>;
|
|
|
|
let cache: CursorMap | null = null;
|
|
|
|
function load(): CursorMap {
|
|
if (cache) return cache;
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(CURSOR_FILE, 'utf8')) as unknown;
|
|
cache = parsed && typeof parsed === 'object' ? (parsed as CursorMap) : {};
|
|
} catch {
|
|
cache = {};
|
|
}
|
|
return cache;
|
|
}
|
|
|
|
/** Persist the in-memory cursor map to disk. Call once after a batch of setOffset() calls. */
|
|
export function save(): void {
|
|
const map = load();
|
|
mkdirSync(dirname(CURSOR_FILE), { recursive: true });
|
|
writeFileSync(CURSOR_FILE, JSON.stringify(map, null, 2));
|
|
}
|
|
|
|
/**
|
|
* Byte offset to resume reading `filePath` from, given its current size on disk.
|
|
* Returns 0 (full reparse) if there's no recorded cursor, or if the file shrank since
|
|
* last run (truncated/rotated).
|
|
*/
|
|
export function getOffset(filePath: string, currentSize: number): number {
|
|
const entry = load()[filePath];
|
|
if (!entry) return 0;
|
|
if (entry.offset > currentSize) return 0;
|
|
return entry.offset;
|
|
}
|
|
|
|
/** Record the new offset/size for `filePath` in memory. Call save() to persist. */
|
|
export function setOffset(filePath: string, offset: number, size: number): void {
|
|
load()[filePath] = { offset, size };
|
|
}
|
|
|
|
/** Forget a file's cursor entirely (forces a full reparse on next run). */
|
|
export function resetOffset(filePath: string): void {
|
|
delete load()[filePath];
|
|
}
|