Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
7.0 kB · 235 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236/** * Pasted text blobs are represented in the editor as `[pasted #N]` markers while the * text payload is held aside in a registry keyed by N. The markers present in * the submitted text decide which pasted blobs are written to disk and replaced with * stubs, so deleting a marker drops its payload and restoring it (undo, history, retry) * brings it back as long as the payload is still in the registry. */
import { createHash } from "node:crypto";import { chmodSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";import { join } from "node:path";import { getAgentDir } from "../../config.js";
/** Matches `[pasted #N]` markers inserted when a text blob is moved into a file from the editor. */const PASTED_MARKER_REGEX = /\[pasted #(\d+)\]/g;
export type PastedMode = "blob" | "secret";
export interface PastedEntry { id: number; content: string; mode: PastedMode; name?: string; timestamp?: number;}
export interface PastedStubInfo { path: string; bytes: number; lines: number; sha256: string; mode: PastedMode;}
/** The marker text inserted into the editor for pasted text blob `id`. */export function formatPastedMarker(id: number): string { return `[pasted #${id}]`;}
/** Marker ids that appear in `text`, in order of appearance. */export function pastedMarkerIds(text: string): number[] { return [...text.matchAll(PASTED_MARKER_REGEX)] .map((match) => Number(match[1])) .filter((id) => Number.isSafeInteger(id));}
/** Replace every pasted-marker spelling whose numeric id appears in `remaps`. */export function remapPastedMarkers(text: string, remaps: ReadonlyMap<number, number>): string { return text.replace(PASTED_MARKER_REGEX, (marker, id: string) => { const replacement = remaps.get(Number(id)); return replacement === undefined ? marker : formatPastedMarker(replacement); });}
/** * Pasted entries from `pending` whose marker still appears in `text`, in paste order * (the map's insertion order). Each entry is returned at most once even if its * marker is duplicated in the text. */export function collectMarkedPastes<T>(pending: ReadonlyMap<number, T>, text: string): T[] { if (pending.size === 0) { return []; } const present = new Set(pastedMarkerIds(text)); const pasted: T[] = []; for (const [id, entry] of pending) { if (present.has(id)) { pasted.push(entry); } } return pasted;}
/** * Evict oldest entries (insertion order) from `pastes` until the total of * `sizeOf` is within `maxBytes`. Ids in `keep` are never evicted, so a pasted * whose marker is still live retains its content even if that holds the total * above the cap. */export function evictPastesToBudget<T>( pastes: Map<number, T>, sizeOf: (value: T) => number, maxBytes: number, keep: ReadonlySet<number>,): void { let total = 0; for (const value of pastes.values()) { total += sizeOf(value); } for (const key of [...pastes.keys()]) { if (total <= maxBytes) { break; } if (keep.has(key)) { continue; } const value = pastes.get(key); if (value !== undefined) { total -= sizeOf(value); pastes.delete(key); } }}
/** * Modes a live pasted file cycles through. A legacy terminal cannot deliver `shift+alt+k` * (a shifted letter arrives as its uppercase form, which the key matcher cannot tell * apart from the unshifted key), so `alt+k` has to be able to reach secret mode on its own. */export function toggledPastedMode(current: PastedMode): PastedMode { return current === "secret" ? "blob" : "secret";}
/** * One-line stand-in for a pasted blob. The reading rules live in the system prompt * (`PASTED_FILE_PROMPT`), not here, so the user's own message stays one line plus a blank. */export function formatPastedStub(info: PastedStubInfo): string { const label = info.mode === "secret" ? "pasted secret" : "pasted"; return `[${label}: ${info.path} · ${info.bytes} B]\n`;}
function formatHHMMSS(date: Date): string { const pad = (n: number) => String(n).padStart(2, "0"); return `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;}
export interface WritePastedFileResult { path: string; bytes: number; lines: number; sha256: string; stub: string;}
/** * Write a pasted blob to `~/.prime/agent/pastes/<sessionId>/<HHMMSS>-<name>.txt` * with directory mode 0700 and file mode 0600 atomically (via `.partial` then rename). */export function writePastedFile( entry: PastedEntry, options: { sessionId: string; agentDir?: string; now?: Date; },): WritePastedFileResult { const baseDir = options.agentDir ?? getAgentDir(); const pastesDir = join(baseDir, "pastes", options.sessionId);
mkdirSync(pastesDir, { recursive: true, mode: 0o700 }); chmodSync(pastesDir, 0o700);
const timestamp = formatHHMMSS(options.now ?? new Date()); const name = entry.name ?? `paste-${entry.id}`; const fileName = `${timestamp}-${name}.txt`; const filePath = join(pastesDir, fileName); const partialPath = `${filePath}.partial`;
const content = entry.content; writeFileSync(partialPath, content, { encoding: "utf-8", mode: 0o600 }); chmodSync(partialPath, 0o600); renameSync(partialPath, filePath);
const bytes = Buffer.byteLength(content, "utf-8"); const lines = content.split("\n").length; const sha256 = createHash("sha256").update(content, "utf-8").digest("hex");
const stub = formatPastedStub({ path: filePath, bytes, lines, sha256, mode: entry.mode, });
return { path: filePath, bytes, lines, sha256, stub, };}
/** * Replace all `[pasted #N]` markers present in `text` with their written file stubs. */export function resolvePastesInText( text: string, pastes: ReadonlyMap<number, PastedEntry>, options: { sessionId: string; agentDir?: string; now?: Date; },): { text: string; writtenFiles: Array<{ id: number; path: string; mode: PastedMode }> } { const writtenFiles: Array<{ id: number; path: string; mode: PastedMode }> = []; const writtenById = new Map<number, WritePastedFileResult>();
const replacedText = text.replace(PASTED_MARKER_REGEX, (marker, idStr) => { const id = Number(idStr); const entry = pastes.get(id); if (!entry) { return marker; } let written = writtenById.get(id); if (!written) { written = writePastedFile(entry, options); writtenById.set(id, written); writtenFiles.push({ id, path: written.path, mode: entry.mode }); } return written.stub; });
return { text: replacedText, writtenFiles };}
/** * Best-effort delete a session's pasted-file directory and return names of files removed. */export function reapSessionPastes(sessionId: string, agentDir?: string): string[] { const baseDir = agentDir ?? getAgentDir(); const pastesDir = join(baseDir, "pastes", sessionId); if (!existsSync(pastesDir)) { return []; } try { const files = readdirSync(pastesDir); rmSync(pastesDir, { recursive: true, force: true }); return files; } catch { return []; }}