toknmtr — self-hostable Claude Code usage & analytics dashboard
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>
This commit is contained in:
commit
71a60ab054
74 changed files with 15613 additions and 0 deletions
66
agent/cursor.ts
Normal file
66
agent/cursor.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* 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];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue