curt-format API
The reference implementation ships as the zero-dependency npm package
curt-format (TypeScript,
MIT). Current release: 1.2.0 — the first npm release (1.0.0 and 1.1.0
were tagged in-repo but never published).
npm install curt-format
Constants
export const CURT_MAGIC = 'curtis-conversation';
export const CURT_VERSION = 1;
Types
export interface CurtMessage {
id: string;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
/** Epoch milliseconds. */
timestamp: number;
/** Provider model id, recorded on assistant messages when known. */
model?: string;
/** Implementations may attach extra fields; readers ignore unknowns. */
[key: string]: unknown;
}
export interface CurtConversation {
id: string;
title: string;
/** Epoch milliseconds. */
createdAt: number;
/** Epoch milliseconds. */
updatedAt: number;
messages: CurtMessage[];
[key: string]: unknown;
}
parseCurt
export function parseCurt(raw: string): CurtConversation | null;
Parse a .curt body. Returns null when the magic field, version, or
conversation shape fails validation — a corrupted file is reported, never
half-parsed (whole-file rejection, SPEC "Validation rules"). A single
leading UTF-8 BOM is ignored. Unknown fields on the conversation and its
messages are preserved for round-tripping; unknown top-level envelope
fields are rebuilt on serialization and therefore not preserved
(preservation is best-effort, never normative).
import { parseCurt } from 'curt-format';
const conversation = parseCurt(fileText);
if (conversation === null) {
// invalid: reject whole-file
}
serializeCurt
export function serializeCurt(
conversation: CurtConversation,
generator?: string,
extensions?: string[],
): string;
Serialize a conversation as a .curt file body (tab-indented JSON,
version-1 envelope). generator is provenance in "writer-name@version"
form; readers ignore it. extensions declares registry names (see the
extension registry).
import { serializeCurt } from 'curt-format';
isCurtMessage / isCurtConversation
export function isCurtMessage(v: unknown): v is CurtMessage;
export function isCurtConversation(v: unknown): v is CurtConversation;
The spec's validation predicates as type guards. isCurtConversation is
the exact predicate parseCurt applies to the conversation field;
isCurtMessage is the per-message rule ("Each message in messages").
Exported so third-party validators share the canonical behavior instead of
re-deriving it.
curtFilename
export function curtFilename(conversation: CurtConversation): string;
The recommended file name, "<YYYY-MM-DD> <slug> <suffix>.curt". It
derives only from the immutable fields createdAt and id, so the name
survives title edits and later updates. <date> is createdAt as a UTC
date (1970-01-01 when not a finite timestamp); <slug> is the lowercased
title joined on -, capped at 50 characters (untitled when nothing
remains); <suffix> is the part of id after the last _, else the last
8 characters of id.
import { curtFilename } from 'curt-format';
curtFilename(conversation); // "2025-10-09 fixture-conversation 001.curt"
JSON Schema
For non-TypeScript validators, a non-normative draft-07 schema ships in the
package — tested to agree with parseCurt:
curt-format/schema/curt-v1.schema.json
See Schema for a validate-your-file snippet.