/** * CLEO blob store backed by `llmtxt/blob`'s {@link BlobFsAdapter}. * * Wraps llmtxt's content-addressed blob filesystem store into a CLEO-shaped * API that mirrors the surface of {@link ../store/attachment-store.ts} for * future drop-in replacement. Satisfies owner Constraint #4 (zero primitive * duplication) per GitHub issue #96 — CLEO no longer rolls its own SHA-256 * hashing, tmp-rename atomic writes, or orphan tracking; those primitives * are sourced from llmtxt's WASM-backed core and tracked on the llmtxt * release cadence. * * Wiring model: * - `BlobFsAdapter` requires a `NodeSQLiteDatabase>` * that owns a `blob_attachments` table (manifest + LWW). * - Bytes live at `/.cleo/blobs/blobs/`. * The nested `blobs/` subdir is BlobFsAdapter's internal convention. * - We lazy-load `node:sqlite` + `drizzle-orm/node-sqlite` — both ship * with Node 24 and drizzle-orm v1.0.0-beta, so no optional peer deps * are required. A pre-constructed DB may also be injected via * {@link CleoBlobStoreOptions.db}. * * Retirement plan (Wave B): the existing `packages/core/src/store/attachment-store.ts` * (643 LoC) will be migrated caller-by-caller to this store. Both stores * stay operational in parallel during the transition — do NOT delete * `attachment-store.ts` in this wave. * * @epic T947 * @see packages/core/src/store/attachment-store.ts (legacy, kept in parallel) * @see https://github.com/kryptobaseddev/llmtxt (llmtxt/blob subpath) */ import type { AttachBlobParams, BlobAttachment, BlobData } from 'llmtxt/blob'; import { BlobAccessDeniedError, BlobCorruptError, BlobNameInvalidError, BlobNotFoundError, BlobTooLargeError } from 'llmtxt/blob'; export type { AttachBlobParams, BlobAttachment, BlobData }; export { BlobAccessDeniedError, BlobCorruptError, BlobNameInvalidError, BlobNotFoundError, BlobTooLargeError, }; /** * Options for constructing a {@link CleoBlobStore}. */ export interface CleoBlobStoreOptions { /** * Absolute path to the project root. The store writes to * `/.cleo/blobs/` by default. */ readonly projectRoot: string; /** * Optional SQLite file path for the blob manifest. Defaults to * `/.cleo/blobs/manifest.db`. */ readonly manifestDbPath?: string; /** * Optional absolute storage path override. Defaults to * `/.cleo/blobs`. BlobFsAdapter will write bytes to * `/blobs/` beneath this directory. */ readonly storagePath?: string; /** * Maximum blob size in bytes. Defaults to 100 MiB (llmtxt default). */ readonly maxBlobSizeBytes?: number; /** * Injected pre-constructed Drizzle database. When provided, takes * precedence over `manifestDbPath`. Useful for tests that want to * share a single in-memory DB or for integrations that already own * a Drizzle connection. * * Typed as `unknown` to avoid a hard compile-time dependency on * `drizzle-orm/node-sqlite` — the runtime shape is validated by * BlobFsAdapter's constructor. */ readonly db?: unknown; } /** * Result returned by {@link CleoBlobStore.attach}. */ export interface CleoBlobAttachResult { /** Unique attachment id (nanoid, 21 chars). */ readonly attachmentId: string; /** Lowercase hex SHA-256 digest (64 chars) — content-address. */ readonly sha256: string; /** Byte size of the stored blob. */ readonly size: number; /** MIME content type recorded at attach time. */ readonly contentType: string; } /** * Content-addressed blob store backed by `llmtxt/blob.BlobFsAdapter`. * * Mirrors the behavioural contract of {@link ../attachment-store.ts}: * - Same bytes attached twice produce the same SHA-256 (dedup). * - Detach is a soft delete; bytes remain on disk until orphan GC. * - Hash is verified on every `get(includeData=true)` read. * * Use a per-task scoping model: CLEO passes `taskId` as the llmtxt `docSlug` * so the LWW semantics (newest upload wins for `(docSlug, blobName)`) apply * naturally to CLEO attachments. * * @example * ```ts * import { CleoBlobStore } from '@cleocode/core/store/llmtxt-blob-adapter'; * * const store = new CleoBlobStore({ projectRoot: process.cwd() }); * await store.open(); * * const { attachmentId, sha256 } = await store.attach( * 'T123', * 'design.png', * new Uint8Array(Buffer.from('fake png bytes')), * 'image/png', * ); * const blob = await store.get('T123', 'design.png'); * // ... * await store.close(); * ``` */ export declare class CleoBlobStore { private readonly opts; private adapter; /** * The node:sqlite native Database handle. Retained so {@link close} * can release the file descriptor. `null` when the caller injected a * pre-constructed DB (ownership stays with the caller). */ private ownedNativeDb; /** * Construct a new store. Call {@link open} to initialize the backing * SQLite manifest + blob directory before any attach/get/list/detach. */ constructor(opts: CleoBlobStoreOptions); /** * Initialize the backing Drizzle database and BlobFsAdapter. * * Idempotent — a second call is a no-op. Creates the `blob_attachments` * manifest table and ensures the storage directory exists. */ open(): Promise; /** * Close the store, releasing the SQLite file descriptor when this * store owns it. Safe to call multiple times. */ close(): Promise; /** * Attach a blob to a task. Returns attachment id + sha256. * * @param taskId Task identifier (used as llmtxt docSlug). * @param name User-visible attachment name (e.g. "design.png"). Must pass * {@link llmtxt/blob.validateBlobName} — no path separators, * no path traversal, no null bytes, ≤255 UTF-8 bytes. * @param data Raw bytes. Buffer or Uint8Array accepted. * @param contentType MIME type. Defaults to `application/octet-stream`. * * @throws BlobNameInvalidError on bad `name`. * @throws BlobTooLargeError when `data` exceeds the configured max size. */ attach(taskId: string, name: string, data: Uint8Array, contentType?: string): Promise; /** * Retrieve the blob manifest row + bytes for a `(taskId, name)` pair. * * Returns `null` (NOT throws) when no active attachment exists for the * requested name. Throws {@link BlobCorruptError} when on-disk bytes * do not match the recorded hash. */ get(taskId: string, name: string): Promise; /** * List active (non-detached) attachments for a task, manifest-only * (no bytes). */ list(taskId: string): Promise; /** * Detach (soft-delete) an attachment. Returns silently when no * active attachment exists for the given name. */ detach(taskId: string, name: string): Promise; /** * Compute the SHA-256 content hash of raw bytes WITHOUT storing them. * * Delegates to `llmtxt/blob.hashBlob` (WASM-backed, matches the Rust * `llmtxt-core::hash_blob` primitive exactly). * * @returns Lowercase hex SHA-256 digest (64 chars). */ static hash(data: Uint8Array): string; /** @internal */ private ensureOpen; } //# sourceMappingURL=llmtxt-blob-adapter.d.ts.map