import { type ScanFinding, type DirScanFinding } from '@chat-recall/engine/core/secret-scanner.js'; import { type SessionRef } from '@chat-recall/engine/core/tool-backend.js'; /** An Error that already knows its own failure class. */ export declare function classedError(errorClass: FailureClass, message: string): Error; /** True when this error must not be retried. Exported for the tests. */ export declare function isStandingFailure(err: unknown): boolean; export type FailureClass = 'rate_limited' | 'payment_required' | 'auth' | 'timeout' | 'refused' | 'dns' | 'network' | 'insecure_transport' | 'server_error' | 'other'; /** * Reduce an error message to a bounded CLASS. * * Telemetry must never carry a raw error string: server errors quote paths, * payload fragments and occasionally tokens. An enum is enough to answer "what * is breaking for customers", and cannot leak. * * A tag from the throw site always wins. The message tests remain for errors * thrown by fetch itself, and by code that predates the tag. */ export declare function classifyError(msg: string, err?: unknown): FailureClass; import '@chat-recall/engine/core/backends/index.js'; export interface Credentials { serverUrl: string; token: string; } /** * Multi-target: logging in ADDS a server (or updates its token). Every * sync pushes to ALL targets; the per-server ledger keeps coverage * independent. File shape is {targets:[...]} with the legacy single-object * form still readable. */ export declare function saveCredentials(c: Credentials): void; export declare function loadAllCredentials(): Credentials[]; export declare function _resetTenantSecurityConfigCache(): void; interface TenantSyncConfig { excludeTools: string[]; excludeProjects: string[]; excludeSources: string[]; approveSources: string[]; } declare function fetchTenantSyncConfig(cred: Credentials): Promise; /** Test seam. The function itself is internal — one caller, inside the sync — * but the three failure outcomes it now distinguishes are exactly what has to * be pinned, and reaching them through a full sync would need a server. */ export declare const _fetchTenantSyncConfigForTests: typeof fetchTenantSyncConfig; export declare function _resetTenantSyncConfigCache(): void; /** Legacy single-target accessor — first target. */ /** The ACTIVE target = most recent login (saveCredentials unshifts). */ export declare function loadCredentials(): Credentials | null; export declare function removeCredentials(serverUrl: string): boolean; export interface SyncResult { uploaded: number; skipped: number; redactions: number; items: number; links: number; findings: number; derived: number; kgEntities: number; kgTriples: number; /** Sessions secret-scanned this run. */ scanned: number; /** Total wall-clock ms spent scanning this run. */ scanMs: number; /** * THIS WALK'S OUTCOME PER TARGET, keyed by server URL. * * The aggregate above cannot answer "did target X accept anything", and the * daemon needs exactly that: it writes the per-target health file that * `chat-recall doctor` reads. Without this it marked EVERY target with the * walk's aggregate verdict, so a walk where the SaaS succeeded and a LAN box * refused the connection wrote `lastOkAt = now, failures = 0` for both — and * `doctor` cheerfully reported a target that had not accepted a byte in days. * * Set by `syncSessions` (the multi-target walk) and absent from a * single-target result, so a consumer must treat "absent" as "unknown" and * mark nothing, never as "all fine". */ perTarget?: Record; } /** One target's outcome for one walk. */ export interface TargetOutcome { ok: boolean; /** Why it failed, or why it was not attempted. Absent when ok. */ error?: string; /** * Uploads this target ACCEPTED during the walk, whether or not the walk then * failed. * * `ok` alone answers "did the whole walk finish", which is not the question a * user asks. A walk that delivered 38 batches and then hit one fatal error * was recorded as a plain failure, so `lastOkAt` never moved and doctor said * "nothing has synced in 7h" about a machine whose rows were landing as it * spoke. Data arriving and a walk completing are two different facts and the * health file needs both. */ accepted?: number; } export declare function _acceptedUploads(url: string): number; export declare function syncSessions(opts?: { sinceMs?: number; cleartextPaths?: boolean; limit?: number; throttleMs?: number; prune?: boolean; useLedger?: boolean; walk?: 'full' | 'changed'; }): Promise; export interface ReconcileResult { sessions: number; scanned: number; pushed: number; absent: number; perTarget: Record; } /** * Manual entrypoint (CLI `reconcile`): reconcile derived fields for ALL targets. * Normal `sync` already runs reconciliation each pass; this is the explicit * on-demand backfill / forced re-scan (also what a UI "re-scan this field" * action drives, via forceFieldRescan + a sync). */ export declare function reconcileFields(opts?: { force?: boolean; }): Promise; /** * Reap batch-scan temp dirs leaked by a previous run. * * The scan below deletes its dir in a `finally` — which a SIGKILL or the OOM * killer skips entirely, and this loop is exactly the one that has OOM-killed * the daemon before. Each leaked dir holds one file per session: one of them * was found at 3.4GB / 309k files, and together with other cruft it filled a * 31GB tmpfs, at which point *everything* on the box that needed /tmp broke. * * So: before starting a scan, delete our own stale dirs. 6h is far longer than * any real scan, so an in-flight dir from a concurrent process is never at risk. */ export declare function reapStaleScanDirs(dir?: string, maxAgeMs?: number, now?: number): number; /** * Run the optional external detectors (gitleaks/trufflehog) over many sessions * with a BOUNDED amount of pre-redaction text on disk at any one time. * * The detectors take a path, so their input has to be materialized — and that * input is raw session text, i.e. real credentials in cleartext. Writing all of * it at once is what left 3.4GB / 309k such files in /tmp when the daemon was * OOM-killed mid-scan (the `finally` never ran). So we fill a temp dir up to * `maxBytes`, scan it, delete it, and repeat: worst-case exposure — and * worst-case leak if this process is killed — is one slice, not the whole walk. * * Cost of slicing is 2 detector spawns per extra slice, still far below the * 2·N of per-session scanning. * * `text()` returning null (unexportable session) is skipped silently: it just * gets no external findings, the builtin scan still covers it. * `scan` is injectable so tests don't need the binaries installed. */ export declare function batchScanExternal(items: Array<{ id: string; text: () => string | null; }>, opts?: { verifyOnly?: boolean; maxBytes?: number; scan?: (dir: string, o: { verifyOnly?: boolean; }) => DirScanFinding[]; }): { findings: Map; scanned: number; slices: number; scanMs: number; }; export declare function _resetCapsCacheForTests(): void; /** * Learn the tenant-scoped ingest allowance from an authenticated response. * * `verifyServerApi` sets the same map from /api/capabilities, which is pre-auth * and therefore can only report the CLASS ceiling. This overrides it with the * number that actually applies to this tenant. */ export declare function setServerIngestConcurrency(serverUrl: string, n: number): void; /** Does this server accept `Content-Encoding: gzip` on a request body? */ export declare function serverAcceptsGzipBody(serverUrl: string): boolean; /** * How many uploads may be in flight at once, according to the SERVER. * * Returns null when the server does not advertise it (older image) — the caller * then keeps its own default. */ export declare function serverIngestConcurrency(serverUrl: string): number | null; /** * Build the live per-session conversation payload — the THIN-COLLECTOR core. * Reads ONLY the raw transcript (via `parseTranscript` + `parseSessionFile`), * never the local store. Returns the conversation row exactly as the server * ingest expects it, plus the redacted plain text (for live KG extraction) * and the redaction count. Returns null when there's nothing worth shipping. * * Exported so the payload can be exercised in tests with no server and no * pre-populated index (see sync-client.test.ts). */ export interface BuiltConversation { conv: Record; /** Redacted conversation text — feed to `extractEntities` for the KG. */ kgText: string; /** Real project path resolved from the transcript cwd (for KG context). */ projectPath: string; redactions: number; /** Masked secret findings from the raw session (built-in regex always, plus * gitleaks/trufflehog/tenant rules when installed), shipped to the server's secret_findings. */ findings: Array<{ session_id: string; detector: string; rule: string; line: number; preview: string; verified_at?: string | null; }>; /** Wall-clock ms spent scanning this session (0 if nothing to scan). */ scanMs: number; /** Content fingerprint of the transcript this payload was built from * (containerSrcHash of the raw export). The walk stamps it into the ledger so * a later mtime-only bump with identical content can skip the whole rebuild. * Undefined for the oversized-tail path (no full container is materialized). */ srcHash?: string; } /** buildConversationSync's early-out when the caller passed a priorContentHash * that matches the freshly-exported content: nothing changed since the last * FULL sync, so the caller just re-stamps the ledger mtime and skips. */ export interface UnchangedConversation { unchanged: true; srcHash: string; } /** Transcript bytes beyond which a FULL sync must not materialize the whole * file. The FULL pipeline copies the transcript ~5× (parse, trim+redact, * raw export, raw-text join, redacted container) — a 350MB session transiently * needs >1.5GB of heap, which OOM-killed the watch daemon in a restart loop * (ledger never acked → same file retried on every startup tick, forever). */ export declare const FULL_BUILD_MAX_BYTES: number; /** Ceiling on PRE-REDACTION session text materialized on disk at any one moment * by the optional external-detector batch scan. The batch dir is flushed and * deleted every time it crosses this, so a SIGKILL/OOM mid-scan can strand at * most this many bytes of cleartext (the incident stranded 3.4GB), and the * window in which they exist is proportionally short. Raising it buys fewer * detector spawns; lowering it buys a smaller blast radius. */ export declare const BATCH_SCAN_MAX_BYTES: number; /** Bytes an append-only session occupies on disk (0 for non-AO backends). */ export declare function sessionFileBytes(ref: SessionRef): number; export declare function buildConversationSync(ref: SessionRef, mtime: number, opts?: { mapPath?: (p: string) => string; includeRaw?: boolean; includeMeta?: boolean; scanSecrets?: boolean; verifySecrets?: boolean; tenantRules?: Array<{ name: string; regex: string; }>; /** Batch-scan results for THIS session (gitleaks/trufflehog already run * over a directory). When provided, skip the per-session subprocess scan * and use these instead — built-in regex + tenant rules still run inline. */ precomputedExternal?: ScanFinding[]; /** The content hash this target last FULL-synced (ledger `h`). When the * freshly-exported transcript hashes equal to it, the whole build is * redundant — we bail immediately with { unchanged, srcHash } so the caller * just advances the ledger mtime. Only pass it when the ledger row is at the * current extractor version (else a re-derive is genuinely owed). */ priorContentHash?: string; }): Promise; /** * Build the tail-only conversation payload for an APPEND sync (see * docs/SYNC-INCREMENTAL.md). Reads ONLY the new bytes from `fromOffset` via * the backend's `readFromOffset`, parses them into messages, and ships a * minimal append envelope — no raw_b64, no telemetry meta, no title/ * first_prompt (all head-derived; the server keeps its prior values), no * derived (deferred to the FULL re-sync when the session goes quiet). * * Returns null when the tail is empty / unparseable / all-internal — the * caller treats that as "nothing to ship this tick" and leaves the ledger * cursor unchanged. */ export declare function buildConversationTail(ref: SessionRef, fromOffset: number, opts?: { mapPath?: (p: string) => string; tenantRules?: Array<{ name: string; regex: string; }>; }): Promise<{ conv: Record; redactions: number; newOffset: number; findings: BuiltConversation['findings']; } | null>; /** * Per-session derived payload: the four compute kinds the server's deep-dive * routes read (diff/outcome/commits/markers) + the outcome-badge row. ALWAYS * computed live from the transcript/git (no local compute/outcome cache is * read or warmed — thin collector). Everything is deep-redacted — diffs carry * file content. */ /** * Incremental sync for the watch daemon and parameterless `chat-recall sync`: * push only sessions modified after `settings.sync.lastSyncAt`, then advance * the watermark. The watermark is captured BEFORE the walk so sessions that * change mid-sync are re-pushed next time instead of being skipped forever. * * Returns null when sync isn't configured (no credentials) — callers treat * that as "feature off", not an error. */ /** Why an incremental sync tick did no work. Distinct outcomes because the * right reaction differs: 'lock-held' is routine writer election (be quiet), * 'no-credentials'/'paused' are user-actionable. Historically all three were * a bare `null` and every skip logged as "not logged in" — thousands of * misleading lines in the watch log. */ export interface SyncSkip { skipped: 'no-credentials' | 'paused' | 'lock-held'; } export declare function isSyncSkip(r: SyncResult | SyncSkip): r is SyncSkip; export declare function syncIncremental(opts?: { scope?: 'full' | 'changed'; }): Promise; export {}; //# sourceMappingURL=sync-client.d.ts.map