# txt — Full API & SDK Reference # Generated: 2026-09-20T03:48:25.974Z # Base URL: https://api.n0t.es =========================================================================== ## 0. Mental model — remote text filesystem =========================================================================== txt notes and attachments are files. Cloud sync (dots) exposes them as a read/write HTTP filesystem for other apps (CLI, React Native, MCP, desktop, agents). Attachment bytes stay in the user's private dots blob; txt manages their workspace metadata and links. Stable developer routes use /api/v1. Existing /api/me and /api/files routes remain supported for installed clients. GET /api/v1 is the live discovery document. v1 adds scoped/expiring keys, cursor pagination, CORS, DB-backed quotas, X-Request-ID, If-Match conditional writes, and Idempotency-Key replay. list / readdir GET /api/me | /api/me/files | /api/me/folders skills GET /api/v1/skills read GET /api/me/files/:id | …/raw write PUT /api/me/files/:id create POST /api/me/files upload bytes POST /api/me/files/upload?name=FILE_NAME import URL POST /api/me/files/import temp link POST /api/me/files/:id/share identities GET|PATCH /api/v1/identities ideate GET|POST /api/v1/ideation | POST /api/v1/files/:id/ideate render POST /api/v1/artifacts unlink DELETE /api/me/files/:id stat HEAD /api/me/files/:id | …/raw connect GET /api/me/files/:id/connections Every file and folder can carry: context human/agent-readable purpose, scope, people, and constraints tags normalized reusable labels for deterministic connections Performance contract: - Directory listings NEVER include file bodies (only size + mtime). - GET file/raw returns ETag + Last-Modified. - If-None-Match → 304 (empty body) when the revision is unchanged. - Folder GET defaults to metadata; pass ?include=content for bodies. - Versions list omits bodies unless ?include=content. Human API page: https://api.n0t.es/api Interactive docs: https://api.n0t.es/api/playground Developer dashboard: https://api.n0t.es/api/dashboard Short agent doc: https://api.n0t.es/llms.txt Mint a key: https://txt.mn/sync (vanity redirect to the working sync page) =========================================================================== ## 1. Authentication =========================================================================== All API endpoints accept authentication in two forms: 1. Browser session cookie (set automatically when signed in with dots at https://api.n0t.es/auth/sign-in) 2. Bearer API key: Authorization: Bearer txt_ First-party native clients may instead send a dots OAuth Authorization Code + PKCE access token as Bearer auth. Store it in Keychain/secure storage and refresh it without discarding offline edits. Generate API keys at https://txt.mn/sync or via POST /api/me/keys (session). Keys are prefixed with "txt_" and hashed at rest. Key creation accepts scopes plus expiresInDays (1, 7, 30, 90, 365, or null). Available scopes: files:read, files:write, folders:read, folders:write, collaboration:read, collaboration:write, shares:read, shares:write, ai:use. Raw keys are returned once. Rotation preserves scope/expiry; revocation is immediate. =========================================================================== ## 2. REST API Reference =========================================================================== All responses are JSON unless noted. v1 infrastructure errors return { error: { code, message, requestId } }; legacy route errors retain { error: string } for compatibility. Every v1 response includes X-Request-ID. --------------------------------------------------------------------------- ### 2.1 User & File Tree (directory listing) --------------------------------------------------------------------------- GET /api/me Returns the file tree — metadata only, no bodies. Response: { user: { id, email, name }, folders: [ { id, name, context, tags, fileCount, createdAt, updatedAt, endpoint, files: [...] } ], rootFiles: [ { id, name, context, tags, type, size, createdAt, updatedAt, endpoint, raw } ] } GET /api/me/files Flat file index (preferred by desktop / SDK). Metadata only. POST /api/me/files Create a file. Body: { name, content?, context?, tags?, fileType?, folderId?, availability? } availability is "cloud" or "cloud+local" (default). A local-only file is intentionally outside the cloud API until a client uploads it. DELETE /api/me/files/:id Unlink a file. --------------------------------------------------------------------------- ### 2.2 Files (read / write) --------------------------------------------------------------------------- GET /api/me/files/:id Returns full file metadata and content. content is null for dots-backed attachments; use /raw to download their bytes. Headers: ETag, Last-Modified, Cache-Control: private, no-cache If-None-Match → 304 when unchanged. Response: { id, name, content, context, tags, type, availability, folderId, size, createdAt, updatedAt } HEAD /api/me/files/:id Stat only (ETag, Content-Length, no body). GET /api/me/files/:id/raw Raw bytes (no JSON) with the stored Content-Type. Same ETag / 304 / HEAD behavior as JSON GET. POST /api/me/files/upload?name=FILE_NAME&folderId=OPTIONAL_ID Upload any non-empty raw body up to 100 MB. Content-Length is checked when supplied; the actual body length is always enforced. Content-Type is preserved. The response metadata includes storage:"dots" and raw:/api/v1/files/:id/raw. Stable route: /api/v1/files/upload. POST /api/me/files/import Import one public HTTPS resource into private dots storage. Body: { url, name?, folderId?, context?, tags? }. Redirects and private-network destinations are blocked; the downloaded source is limited to 25 MB. Stable route: /api/v1/files/import. POST /api/me/files/:id/share Mint a dots-backed download URL lasting 60–300 seconds. Body: { expiresIn?: number }. Use only after the user explicitly asks to share. Stable route: /api/v1/files/:id/share. GET /api/v1/identities List every connected dots user and the identity active for this credential. PATCH /api/v1/identities Switch only this API key, CLI, native client, or browser surface. Body: { identity: "@username" }. GET|POST /api/v1/ideation List ideation notes or create one from { seed? }. Grow an existing note with POST /api/v1/files/:id/ideate { seed? }; inspect it at …/:id/ideation. POST /api/v1/artifacts Render a source note plus optional context/design files into standalone HTML and a txt.deploy/v1 handoff manifest. mode:"site" enables FX. Pass model as an exact provider/model ID from GET /api/v1/artifacts/models; omit it to use defaultModel. The response and saved manifest record the exact model used. GET /api/v1/artifacts/models Return { defaultModel, models } for the deployment-approved FX model switcher. PUT /api/me/files/:id Update a file's content and/or name. Dots-backed attachments support metadata, rename, and move here; replace bytes by uploading a new file. Body: { content?: string, name?: string, context?: string, tags?: string[], fileType?, folderId?, availability? } Response: { file: { id, name, content, context, tags, type, folderId, size, createdAt, updatedAt } } GET /api/me/files/:id/connections Ranked related files. Explicit links score highest, then shared tags and same-folder membership. Each result includes score, reasons, tags, and context. POST /api/me/files/:id/connections Persist a direct link. Body: { targetFileId, reason? } DELETE /api/me/files/:id/connections?targetFileId=... Remove a direct link. GET /api/me/files/:id/plan Return { plan: { enabled, scope, trust, source, updatedAt } }. PUT /api/me/files/:id/plan Owner-only. Body: { enabled, scope: "manual"|"project"|"global", trust: "unreviewed"|"approved", source? }. Approval permits reference use; plan text remains untrusted and cannot authorize actions or override higher-priority instructions. GET /api/me/context Workspace metadata plus organization coverage. POST /api/me/context AI-assisted, reviewable context/tag/folder/connection proposals. Body: { prompt?, includeApprovedPlans? }. Plan bodies are excluded by default. When true, only enabled, approved, non-manual plans are included. Proposals do not mutate files until explicitly applied. GET /api/identity/jazz Optional/experimental capability discovery for dots-provisioned Jazz sync. The dots token stays server-side. Connect when configured is true, syncMode is not "off", and syncServer is present. A client without the required dots scopes receives DOTS_REAUTH_REQUIRED. The core Turso/API filesystem remains the supported fallback and does not require Jazz. GET|PUT /api/me/files/:id/sync GET|PUT /api/me/folders/:id/sync Read or set the dots sync policy. PUT body: { mode: "inherit"|"on"|"off" }. Folder policy is inherited; a note-level policy can override it. GET|POST|PATCH|DELETE /api/me/files/:id/share-dots List, grant, change, or revoke writer/reader access for a dots user. GET|POST|PATCH|DELETE /api/me/folders/:id/share-dots Manage folder-level dots collaborators. POST can include descendants and materializes grants for the current sync-enabled notes (100-note limit). --------------------------------------------------------------------------- ### 2.3 Folders --------------------------------------------------------------------------- GET /api/me/folders List folder metadata. POST /api/me/folders Create a folder or subfolder. Body: { name, context?, tags?, parentFolderId? } GET /api/me/folders/:id Directory listing. Metadata only by default. Pass ?include=content to embed bodies (heavier). Response: { id, name, context, tags, parentFolderId, createdAt, updatedAt, files: [...] } PUT /api/me/folders/:id Update folder name, context, tags, or parentFolderId. Circular nesting is rejected. DELETE /api/me/folders/:id Delete the folder and promote direct files and subfolders one level. --------------------------------------------------------------------------- ### 2.4 Version History --------------------------------------------------------------------------- GET /api/me/files/:id/versions List immutable versions, newest first (metadata by default). Pass ?include=content for bodies. Response includes version_number, content_hash, byte_size, author, source, label, and created_at. POST /api/me/files/:id/versions Commit an immutable version. Body: { content?, label?, source?, commitId? }. Omit content to snapshot the server working copy. commitId makes retries idempotent. Writers may commit; readers may only list/fetch. GET /api/me/files/:id/versions/:versionId Fetch one immutable version including content. --------------------------------------------------------------------------- ### 2.5 Comments (multiplayer) --------------------------------------------------------------------------- GET /api/files/:id/comments Browser-session only. Response fields include id, file_id, user_id, user_email, user_name, body, selection_start, selection_end, selected_text, resolved, resolved_by, resolved_at, created_at, and updated_at. Polls every 5 s in the web app UI. POST /api/files/:id/comments Body: { body, selectionStart?, selectionEnd?, selectedText? }. Response: { comment: {...} } PATCH /api/files/:id/comments Resolve or re-open a comment. Body: { commentId, resolved }. DELETE /api/files/:id/comments Query: ?commentId=... --------------------------------------------------------------------------- ### 2.6 Suggestions (track-changes) --------------------------------------------------------------------------- GET /api/files/:id/suggestions Browser-session only. Response fields include original_text, suggested_text, selection_start, selection_end, status, reviewer, and timestamps. status is one of: pending | accepted | rejected POST /api/files/:id/suggestions Propose a change without modifying the live file. Body: { originalText?, suggestedText, selectionStart, selectionEnd }. Response: { suggestion: {...} } PATCH /api/files/:id/suggestions Accept or reject a suggestion. Body: { suggestionId, status: "accepted" | "rejected" }. --------------------------------------------------------------------------- ### 2.7 API Keys --------------------------------------------------------------------------- GET /api/me/keys Response: { keys: [ { id, label, token_prefix, created_at, last_used_at } ] } POST /api/me/keys Body: { label?: string } Response: { id, label, token: "txt_...", token_prefix } IMPORTANT: the full token is only returned once — store it securely. PATCH /api/me/keys Rotate an existing key. Body: { id: string } Response: { id, label, token: "txt_...", token_prefix, rotated_from } IMPORTANT: the old key is revoked and the replacement token is shown once. DELETE /api/me/keys Body: { id: string } --------------------------------------------------------------------------- ### 2.8 AI credentials and usage --------------------------------------------------------------------------- Human configuration: https://api.n0t.es/ai GET /api/me/ai User-activated provider preference, usage, and credential metadata only. AI is off when provider is null. Accepts a browser session or Bearer txt_ key. Never returns provider secrets. PUT /api/me/ai Select an already-configured user-funded provider: { provider: "gateway"|"openai"|"anthropic" } GET|PUT|DELETE /api/me/ai/credentials Browser-session-only write-only vault. Provider credentials are verified, AES-256-GCM encrypted, user/provider-bound, and never returned. Do not ask an agent to collect, print, save, or rotate a human's provider key. POST /api/ai/assist Uses the account preference and rejects request-scoped X-Txt-AI-Key headers. Accepts text, action/customPrompt, fileName, scope (selection|document), and targetFormat (preserve|markdown|plain-text|html|json|yaml|mermaid|fountain). Selection scope returns replacement text. Document scope returns a complete transformed document; AI Gateway document transforms run through fx. txt-cli BYOK bypasses this endpoint and calls the chosen provider directly. --------------------------------------------------------------------------- ### 2.9 Public links --------------------------------------------------------------------------- POST /api/shares Create a live capability link from { fileId, mode, password? }. The server reads the canonical file; callers do not upload a second content snapshot. Modes may be editable, readonly, password, or password-readonly. Defaults to a compact random txt.mn URL. Optional name creates a chosen alias; linkStyle selects short or named. Returns publicUrl, shortUrl, namedUrl, tokenUrl, and a separate nav.link tracking descriptor when configured. PATCH /api/shares Owner-only token + linkStyle and/or name changes the URL preference without rotating the capability or changing its password. Name conflicts return 409. GET /api/shares?slug=... or ?token=... Read the current canonical file. Password modes use X-Txt-Share-Password; passwords never enter URLs or logs. GET /api/shares?fileId=... Authenticated owner lookup for the file's active link; returns { share: null } when the file has no link. PUT /api/shares Update the canonical file through an editable link with { token, content, password? }. DELETE /api/shares Revoke an owned link with { token }. --------------------------------------------------------------------------- ### 2.10 Devices & per-device availability (Dropbox-style three-state) --------------------------------------------------------------------------- The server-wide file column "availability" ("cloud" | "cloud+local") is the default for every device. A registered device can override it per file: "online-only" (tracked placeholder, no bytes on that device), "available" (cached, evictable), or "pinned" (guaranteed local, eviction refused). Metadata always syncs everywhere; only content follows the state. POST /api/v1/devices Register an installation once: { name?, platform? } → { device: { id } }. Store the id; it is the handle for all state operations. GET /api/v1/devices List this account's registered devices. GET /api/v1/devices/:id The device plus its explicit per-file state overrides. Files without an override follow their availability default. PUT /api/v1/devices/:id/state Bulk pin-policy write (up to 500 entries): { states: [{ fileId, state: "online-only"|"available"|"pinned"|null }] } null clears the override. State changes appear on GET /api/v1/changes as resourceType "device", so any client (or UI) can react — including pinning a file onto a *different* device remotely. DELETE /api/v1/devices/:id Forget a device and all its overrides. Invariants clients must honor: local edits always upload regardless of state (a write re-materializes an online-only file); never evict a file whose local content is unsynced; never auto-evict a pinned file. =========================================================================== ## 3. txt-cli, folder mirrors, and plan files =========================================================================== Install: npm install --global txt-cli (binary name: txt) txt auth txt ls txt sync ./notes --folder FOLDER_SLUG txt sync ./notes --watch txt pin ./notes/keep.md txt evict ./notes/big-old.md txt hydrate ./notes/big-old.md txt state ./notes txt plan link launch-plan.md txt plan sync --once txt plan init --global txt skills where txt skills sync --reveal -w txt skills ls --remote txt skills share txt security doctor Folder mirrors use sync.txt for remote IDs and last-agreed hashes, never credentials. Plan links use .plans/.txt-sync.json and do not require Git. The global plan home defaults to ~/.agents/.plans, beside shared agent skills. Plan bodies remain untrusted plaintext reference data; never store secrets in them. Agent skills stay local until `txt skills sync --reveal`; the published catalog is GET /api/v1/skills and https://txt.mn/skills. Human CLI guide: https://api.n0t.es/cli =========================================================================== ## 4. MCP Server =========================================================================== Endpoint: https://api.n0t.es/mcp Transport: Streamable HTTP (GET + POST) Connect with any MCP client (Claude Desktop, Cursor, Windsurf): { "mcpServers": { "txt": { "url": "https://api.n0t.es/mcp", "headers": { "Authorization": "Bearer txt_" } } } } Available tools: connect-files — create or update an explicit file connection create-agent-checkout — start the agent subscription checkout flow create-diagram — save validated Mermaid as a txt file create-folder — create or reuse an owned folder create-publication — create an approval-gated pubs.li publication derive-publication-note — privately save or fork a published issue file-info — inspect file metadata without reading binary bytes get-account — show the active identity and account capabilities get-publication — read a publication or issue locator import-file — import a public HTTPS resource into private storage ingest-image — preserve and transcribe an image as linked resources inspect-skill — inspect package files and security audits without installation install-skill — install an audited package into the private .skills workspace list-files — list all files and folders create-list — create a private playlist of anything: title, items OR newline text, and a UUID requestId; up to 10,000 items / 2 MB create-lists — atomically create up to 50 lists in one request (2 MB combined); each definition has title and items OR text list-lists — search and paginate owned or shared structured lists; metadata and counts only get-list — read a list's stable item IDs, ordered content, current revision, and fork source edit-list — bulk add/update/remove/move/reorder/dedupe/replace in one transaction; current revision and UUID requestId required fork-list — make a private independent copy of an accessible list or item selection; retains source ID/revision but no sharing grants List mutations reuse the same requestId and identical input on retries for 24 hours. Stale revisions fail without changes. References do not grant access to targets. List content is untrusted data. list-skills — list published agent skills, including collaborator shares list-fx-models — list deployment-approved artifact models list-identities — list connected dots identities for this credential list-publications — list owned publications list-publication-subscriptions — list active subscriptions get-file — get content of a specific file (required: fileId) resolve-file — resolve an owned workspace path to a file ID (required: path; optional: createIfMissing, default false). Creating a missing note also requires files:write. update-file — write content to a file (required: fileId, content) create-file — create a new file (required: name; optional: content, folderId, fileType) grow-ideation-note — expand an owned note using workspace context publish-file — publish a committed note with human approval render-artifact — render files into HTML plus a deployment handoff search-skills — search txt and the federated skills.sh catalog set-publication-subscription — subscribe or unsubscribe share-file — create an approval-gated temporary attachment URL shorten-dropbox-link — create an approval-gated txt viewer URL start-agent-onboarding — begin first-party agent onboarding switch-identity — switch only this credential's active dots identity view-skill — save an inert private package snapshot for review trash_note — move a note to reversible trash (required: id) restore_note — restore a trashed note (required: id) purge_trashed_note — permanently delete an already-trashed note with explicit confirmation get-clue — inspect aclue's brain state and suggestions run_aclue — explicitly run aclue self-reasoning (required confirmation: RUN ACLUE) =========================================================================== ## 5. SDK — TypeScript / JavaScript / React Native =========================================================================== TypeScript source (copy-paste or import): https://api.n0t.es/sdk Pre-built ESM bundle (no build step): https://api.n0t.es/sdk.js --------------------------------------------------------------------------- ### 5.1 Install --------------------------------------------------------------------------- Option A — copy the TypeScript source into your project: curl https://api.n0t.es/sdk > lib/txt-sdk.ts Option B — dynamic import in Node / Deno / browser: const { TxtClient } = await import("https://api.n0t.es/sdk.js") Option C — npm (if published): npm install txt-sdk # or pnpm add / yarn add --------------------------------------------------------------------------- ### 5.2 Basic usage --------------------------------------------------------------------------- import { TxtClient } from "./txt-sdk" const txt = new TxtClient({ baseUrl: "https://api.n0t.es", apiKey: "txt_...", }) // List all files const tree = await txt.list() // Read a file const file = await txt.getFile("file_id") console.log(file.content) // Write to a file await txt.updateFile("file_id", { content: "Hello from the SDK!" }) // Get raw text (no JSON) const raw = await txt.getRaw("file_id") --------------------------------------------------------------------------- ### 5.3 Real-time sync (ETag polling) --------------------------------------------------------------------------- // Uses If-None-Match — unchanged polls return 304 (no body) const stop = txt.sync("file_id", (content, file) => { console.log("File updated:", content) }, 5000) // Stop syncing stop() // Watch multiple files at once const stopAll = txt.syncMany( ["file_a", "file_b"], (fileId, content) => console.log(fileId, content), ) --------------------------------------------------------------------------- ### 5.4 React hook (web + React Native) --------------------------------------------------------------------------- import { TxtClient, useTxtFile } from "./txt-sdk" const client = new TxtClient({ baseUrl: "https://api.n0t.es", apiKey: "txt_..." }) function NoteScreen({ fileId }) { const { content, loading, error, update } = useTxtFile(client, fileId) if (loading) return return ( update(text)} multiline /> ) } --------------------------------------------------------------------------- ### 5.5 React Native — quick notes app pattern --------------------------------------------------------------------------- // 1. Store the API key securely import * as SecureStore from "expo-secure-store" await SecureStore.setItemAsync("txt_key", "txt_...") const key = await SecureStore.getItemAsync("txt_key") // 2. Create the client const txt = new TxtClient({ baseUrl: "https://api.n0t.es", apiKey: key }) // 3. List notes on mount useEffect(() => { txt.list().then(tree => setFiles(tree.rootFiles)) }, []) // 4. Sync a single note live useEffect(() => { if (!activeFileId) return return txt.sync(activeFileId, (content) => setNote(content)) }, [activeFileId]) // 5. Save on blur / debounce const save = useDebouncedCallback( (content) => txt.updateFile(activeFileId, { content }), 800, ) --------------------------------------------------------------------------- ### 5.6 Comments & suggestions from code --------------------------------------------------------------------------- // Post a comment anchored to selected text await txt.createComment(fileId, "Check this phrasing", "selected text here") // Propose a change without editing the live doc await txt.createSuggestion(fileId, "old text", "new text") // Accept a suggestion (applies the diff to the file) await txt.reviewSuggestion(fileId, suggestionId, "accepted") =========================================================================== ## 6. SDK Source =========================================================================== import type { PublicLinkStats } from "../public-link-tracking" import type { EditStamp } from "../edit-origin" import type { ListOperation, ListRecord, ListSummary } from '../lists-model' export type { ListItem, ListDocument, ListRecord, ListSummary, ListOperation } from '../lists-model' export interface TxtListInput { title: string; description?: string; items?: unknown[]; text?: string } /** * txt SDK — zero-dependency client for the txt filesystem API. * * Treats remote notes as a read/write text filesystem: * list() / listFiles() → directory listing (metadata) * getFile() / getRaw() → read * updateFile() → write * createFile() / deleteFile() → create / unlink * * Works in Node.js, browsers, React Native (Expo), and any fetch-capable runtime. * * Quick start: * import { TxtClient } from "@/lib/sdk" * const txt = new TxtClient({ baseUrl: "https://your-app.vercel.app", apiKey: "txt_..." }) * const tree = await txt.list() * const file = await txt.getFile(tree.rootFiles[0].id) * * Efficient sync (uses If-None-Match → 304 when unchanged): * const stop = txt.sync("file_id", (content) => setNote(content), 5000) * // call stop() to cancel */ // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface TxtClientOptions { /** Base URL of your txt deployment, e.g. "https://your-app.vercel.app" */ baseUrl: string /** API key (txt_...) or a dots OAuth access token on first-party/native clients. */ apiKey: string } export interface TxtAccountIdentity { id: string provider: "dots" subject: string username: string | null status: string | null verifiedAt: string | null active: boolean groups: Array<{ id: string; name: string; role: "owner" | "writer" | "reader" }> } export interface TxtIdentityContext { account: { id: string; email: string; name: string | null } activeIdentity: TxtAccountIdentity | null identities: TxtAccountIdentity[] } export interface TxtChange { sequence: string resourceType: "file" | "folder" | "device" resourceId: string action: "upsert" | "delete" revision: number | null changedAt: string endpoint: string | null } export interface TxtChangePage { changes: TxtChange[] nextCursor: string highWaterMark: string hasMore: boolean expired?: boolean resetCursor?: string } export interface TxtBatchOperation { id?: string method: "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" path: string headers?: Record body?: unknown } export interface TxtBatchResult { id: string status: number ok: boolean body: unknown headers: Record } export interface TxtWebhook { id: string name: string url: string events: Array<"file.upsert" | "file.delete" | "folder.upsert" | "folder.delete"> enabled: boolean createdAt?: string updatedAt?: string deliveries?: { delivered: number; pending: number; failed: number } } export interface TxtWriteOptions { /** Safe retry key; repeated mutations replay the first response for 24 hours on API v1. */ idempotencyKey?: string /** ETag returned by GET/HEAD; stale writes fail with HTTP 412. */ ifMatch?: string } export interface TxtFile { lastEdit?: EditStamp | null; id: string name: string context: string tags: string[] type: string availability: "cloud" | "cloud+local" size: number folderId?: string | null createdAt: string updatedAt: string endpoint: string raw: string access: "owner" | "shared" role: "owner" | "writer" | "reader" } export interface TxtFolder { id: string name: string context: string tags: string[] parentFolderId: string | null fileCount: number createdAt: string updatedAt: string endpoint: string files: TxtFile[] } export interface TxtSkill { id: string name: string context: string tags: string[] folderId: string | null folderName: string | null type: string updatedAt: string access: "owner" | "shared" role: "owner" | "writer" | "reader" scope: "global" | "project" | "unknown" project: string | null agent: string | null skill: string endpoint: string raw: string } export interface TxtTree { user: { id: string; email: string; name: string } folders: TxtFolder[] rootFiles: TxtFile[] } export interface TxtFileDetail { id: string name: string content: string context: string tags: string[] type: string availability: "cloud" | "cloud+local" folderId: string | null size?: number createdAt: string updatedAt: string access: "owner" | "shared" role: "owner" | "writer" | "reader" ideation?: TxtIdeation } export interface TxtArtifactResult { artifact: { id: string name: string type: "html" folderId: string | null bytes: number sha256: string viewUrl: string } manifest: { id: string name: string type: "json" folderId: string | null protocol: "txt.deploy/v1" viewUrl: string } generatedBy: "deterministic" | "fx" model: string | null deployReady: true } export interface TxtVersion { id: string file_id: string content: string version_number: number created_at: string content_hash: string | null byte_size: number author_user_id: string | null author_name: string | null source: string label: string | null commit_id: string | null } export interface TxtComment { id: string file_id: string user_id: string author: string body: string selection: string | null resolved: boolean created_at: string updated_at: string } export interface TxtSuggestion { id: string file_id: string user_id: string author: string original: string suggested: string status: "pending" | "accepted" | "rejected" created_at: string updated_at: string } export interface TxtApiKey { id: string label: string token_prefix: string created_at: string last_used_at: string | null } export interface TxtConnection { id: string name: string context: string tags: string[] type: string folderId: string | null updatedAt: string score: number reasons: Array< | { kind: "explicit"; reason: string } | { kind: "shared_tags"; tags: string[] } | { kind: "same_folder" } > explicit?: boolean endpoint: string } export type TxtShareMode = "public" | "readonly" | "password" | "password-readonly" export interface TxtShareLink { shortSlug?: string shortUrl?: string namedUrl?: string | null name?: string | null linkStyle?: 'short' | 'named' canonicalUrl?: string tracking?: { domain: string; key: string; url: string } | null token: string slug: string urlPath: string publicUrl: string tokenUrl: string ownerPath: string | null live: boolean mode?: TxtShareMode createdAt?: string updatedAt?: string } export interface TxtContextSuggestion { fileId: string summary: string recommendedContext?: string | null addTags: string[] folderId?: string | null connectToFileIds: string[] reason: string confidence: number } export interface TxtIdeation { enabled: boolean seed: string status: "exploring" | "settled" passes: number lastRunAt: string | null } export interface TxtIdeationRun { fileId: string name: string content: string context: string tags: string[] folder: { id: string; name: string; created: boolean } | null connected: Array<{ fileId: string; reason: string }> openThreads: string[] ideation: TxtIdeation billing: "byok" | "subscription" provider: string model: string usage: { inputTokens: number; outputTokens: number; totalTokens: number } } export interface TxtContextAnalysis { summary: string themes: Array<{ name: string; description: string; fileIds: string[] }> suggestions: TxtContextSuggestion[] } // --------------------------------------------------------------------------- // TxtClient // --------------------------------------------------------------------------- export class TxtClient { protected base: string protected key: string constructor(options: TxtClientOptions) { this.base = options.baseUrl.replace(/\/$/, "") this.key = options.apiKey } protected headers(): Record { return { Authorization: `Bearer ${this.key}`, "Content-Type": "application/json", } } protected route(path: string) { return path } protected async request(path: string, init?: RequestInit): Promise { const res = await fetch(`${this.base}${this.route(path)}`, { ...init, headers: { ...this.headers(), ...(init?.headers ?? {}) }, }) if (!res.ok) { const text = await res.text().catch(() => "") let details: unknown = null try { details = text ? JSON.parse(text) : null } catch { details = text } throw new TxtError(res.status, text || res.statusText, path, details, res.headers.get("x-request-id")) } return res.json() as Promise } protected async requestText(path: string, init?: RequestInit): Promise { const res = await fetch(`${this.base}${this.route(path)}`, { ...init, headers: { ...this.headers(), ...(init?.headers ?? {}) }, }) if (!res.ok) { const text = await res.text().catch(() => "") let details: unknown = null try { details = text ? JSON.parse(text) : null } catch { details = text } throw new TxtError(res.status, text || res.statusText, path, details, res.headers.get("x-request-id")) } return res.text() } // ------------------------------------------------------------------------- // Files & Folders // ------------------------------------------------------------------------- async listLists(options: { q?: string; cursor?: string; limit?: number } = {}) { const params = new URLSearchParams(); for (const [key, value] of Object.entries(options)) if (value !== undefined) params.set(key, String(value)) return this.request<{ lists: ListSummary[]; nextCursor: string | null }>(`/api/me/lists${params.size ? `?${params}` : ''}`) } async getList(id: string) { return this.request<{ list: ListRecord }>(`/api/me/lists/${encodeURIComponent(id)}`) } async createList(input: TxtListInput & { requestId: string }) { return this.request<{ list: ListRecord }>('/api/me/lists', { method: 'POST', body: JSON.stringify(input) }) } async createLists(input: { requestId: string; lists: TxtListInput[] }) { return this.request<{ lists: ListSummary[] }>('/api/me/lists/bulk', { method: 'POST', body: JSON.stringify(input) }) } async editList(id: string, input: { requestId: string; revision: number; title?: string; description?: string; operations?: ListOperation[] }) { return this.request<{ list: ListRecord }>(`/api/me/lists/${encodeURIComponent(id)}`, { method: 'PATCH', body: JSON.stringify(input) }) } async forkList(id: string, input: { requestId: string; revision: number; title?: string; itemIds?: string[] }) { return this.request<{ list: ListRecord }>(`/api/me/lists/${encodeURIComponent(id)}/fork`, { method: 'POST', body: JSON.stringify(input) }) } async deleteList(id: string, input: { requestId: string; revision: number }) { return this.request<{ id: string; deleted: boolean }>(`/api/me/lists/${encodeURIComponent(id)}`, { method: 'DELETE', body: JSON.stringify(input) }) } /** List all files and folders for the authenticated user. */ async list(): Promise { return this.request("/api/me") } /** List every dots user connected to this account and the active identity for this credential. */ async listIdentities(): Promise { return this.request("/api/me/identities") } /** Switch the active dots user for this credential without affecting another device or signing out. */ async switchIdentity(identity: string): Promise<{ activeIdentity: TxtAccountIdentity }> { return this.request<{ activeIdentity: TxtAccountIdentity }>("/api/me/identities", { method: "PATCH", body: JSON.stringify({ identity }), }) } /** Flat file list (preferred by the native desktop client). */ async listFiles(): Promise { const res = await this.request<{ files: TxtFile[] }>("/api/me/files") return res.files } /** Published agent skills, including notes shared by collaborators. */ async listSkills(): Promise { const res = await this.request<{ skills: TxtSkill[]; reveal?: string }>("/api/me/skills") return res.skills } /** Realtime search across txt/groupSkills and the locally mirrored skills.sh index. */ async searchSkillCatalog(query = "", options: { view?: "trending" | "hot" | "all-time" | "official" | "txt" owner?: string limit?: number } = {}) { const params = new URLSearchParams() if (query.trim()) params.set("q", query.trim()) if (options.view) params.set("view", options.view) if (options.owner?.trim()) params.set("owner", options.owner.trim()) if (options.limit) params.set("limit", String(options.limit)) const suffix = params.size ? `?${params}` : "" return this.request<{ source: string; query: string; skills: Array<{ id: string slug: string name: string source: string publisher: string description?: string | null installs: number url: string installUrl: string | null origin: "skills.sh" | "txt" official: boolean installed: boolean txtInstalls: number rank?: number | null auditStatus?: string | null auditRisk?: string | null }> stats: { catalogSkills: number officialSkills: number upstreamInstalls: number txtInstalls: number yourInstalls: number lastSyncedAt: string | null syncStatus: string } }>(`/api/me/skills/catalog${suffix}`) } /** Read a skills.sh package, including file contents and security audits. */ async getSkillCatalogDetail(id: string, agent: "agents" | "claude" | "cursor" = "agents") { const params = new URLSearchParams({ id, agent }) return this.request<{ skill: { id: string; slug: string; source: string; installs: number; hash: string | null; url: string } files: Array<{ path: string; remoteName: string; contents: string; fileType: "md" | "txt"; bytes: number }> audit: { status: "pass" | "warn" | "unknown"; results: unknown[] } installable: boolean blockedBy: string | null }>(`/api/me/skills/catalog/detail?${params}`) } /** Cache a skills.sh package in .viewed-skills and return its main txt note. */ async viewSkill(id: string) { return this.request<{ file: { id: string; name: string; path: string; created: boolean } folder: { id: string; name: string } files: Array<{ id: string; name: string; path: string; created: boolean }> audit: { status: "pass" | "warn" | "unknown"; results: unknown[] } installable: boolean blockedBy: string | null }>("/api/me/skills/catalog/view", { method: "POST", body: JSON.stringify({ id }), }) } /** Add all or selected skills.sh files to a user-owned txt folder. */ async installSkill( id: string, agent: "agents" | "claude" | "cursor" = "agents", options: { folderId?: string; files?: string[] } = {}, ) { return this.request<{ skill: { id: string; slug: string; source: string } folder: { id: string; name: string } files: Array<{ id: string; name: string; created: boolean }> audit: { status: "pass" | "warn" | "unknown"; results: unknown[] } selection: { selected: number; available: number } destination: { stage: "txt-cloud" folder: { id: string; name: string } remoteNames: string[] syncCommand: string localHomes: string[] } reveal: string }>("/api/me/skills/install", { method: "POST", body: JSON.stringify({ id, agent, folderId: options.folderId, files: options.files }), }) } /** Create a new file. */ async createFile(input: { name: string content?: string context?: string tags?: string[] fileType?: "txt" | "md" | "pdf" availability?: "cloud" | "cloud+local" folderId?: string | null }, options: TxtWriteOptions = {}): Promise { const res = await this.request<{ file: TxtFileDetail }>("/api/me/files", { method: "POST", headers: options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined, body: JSON.stringify(input), }) return res.file } /** Import a public HTTPS resource into private dots-backed file storage. */ async importFile(input: { url: string name?: string folderId?: string | null context?: string tags?: string[] }): Promise { const res = await this.request<{ file: TxtFileDetail }>("/api/me/files/import", { method: "POST", body: JSON.stringify(input), }) return res.file } /** Render txt sources into HTML plus a deployment-agent handoff manifest. */ async renderArtifact(input: { sourceFileId: string contextFileIds?: string[] designFileIds?: string[] designFolderId?: string mode?: "html" | "site" prompt?: string model?: string outputName?: string folderId?: string | null }): Promise { return this.request("/api/me/artifacts", { method: "POST", body: JSON.stringify(input), }) } /** List deployment-approved FX models and the current default. */ async listFxModels(): Promise<{ defaultModel: string; models: string[] }> { return this.request<{ defaultModel: string; models: string[] }>("/api/me/artifacts/models") } /** Transcribe an image as returned text or a normal synced txt/Markdown file. */ async transcribeImage(input: { imageBase64?: string imageUrl?: string mediaType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp" | "image/heic" | "image/heif" mode?: "structured" | "verbatim" | "diagram" prompt?: string name?: string fileType?: "txt" | "md" folderId?: string | null output?: "text" | "file" }, options: TxtWriteOptions = {}) { return this.request<{ file?: TxtFileDetail transcription: { provider: string model: string mode: "structured" | "verbatim" | "diagram" imageSha256: string source: "upload" | "url" text?: string usage: { inputTokens: number; outputTokens: number; totalTokens: number } } }>("/api/me/transcriptions", { method: "POST", headers: options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined, body: JSON.stringify(input), }) } /** Move a file to recoverable trash. Permanent deletion is blocked for 30 days. */ async deleteFile(fileId: string): Promise { await this.request(`/api/me/files/${fileId}`, { method: "DELETE" }) } /** Get full metadata + content of a single file. */ async getFile(fileId: string): Promise { return this.request(`/api/me/files/${fileId}`) } /** Get the raw text content of a file (no JSON envelope). */ async getRaw(fileId: string): Promise { return this.requestText(`/api/me/files/${fileId}/raw`) } /** Update a file's content, metadata, type, or folder. */ async updateFile( fileId: string, patch: { content?: string name?: string context?: string tags?: string[] fileType?: "txt" | "md" | "pdf" availability?: "cloud" | "cloud+local" folderId?: string | null }, options: TxtWriteOptions = {}, ): Promise { const res = await this.request<{ file: TxtFileDetail }>(`/api/me/files/${fileId}`, { method: "PUT", headers: { ...(options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : {}), ...(options.ifMatch ? { "If-Match": options.ifMatch } : {}), }, body: JSON.stringify(patch), }) return res.file } /** Rename a file without changing its folder. */ async renameFile(fileId: string, name: string, options: TxtWriteOptions = {}): Promise { return this.updateFile(fileId, { name }, options) } /** Move a file to an owned folder, or to root with null, without renaming it. */ async moveFile(fileId: string, folderId: string | null, options: TxtWriteOptions = {}): Promise { return this.updateFile(fileId, { folderId }, options) } /** Create a revocable live link to the canonical cloud file. */ async createShareLink( fileId: string, input: { mode?: TxtShareMode; password?: string; linkStyle?: 'short' | 'named'; name?: string } = {}, ): Promise { return this.request("/api/shares", { method: "POST", body: JSON.stringify({ fileId, mode: input.mode ?? "readonly", password: input.password, linkStyle: input.linkStyle, name: input.name }), }) } /** Change the URL preference or chosen name without changing access. */ async updateShareLink(token: string, input: { linkStyle?: 'short' | 'named'; name?: string }): Promise> { return this.request>("/api/shares", { method: "PATCH", body: JSON.stringify({ token, ...input }), }) } /** Return the file's active link, if one exists. */ async getShareLink(fileId: string): Promise { const res = await this.request<{ share: TxtShareLink | null }>( `/api/shares?fileId=${encodeURIComponent(fileId)}`, ) return res.share } /** Read private lifetime views/clicks for an owned share. */ async getShareLinkStats(token: string): Promise { const result = await this.request<{ stats: PublicLinkStats }>(`/api/shares/analytics?token=${encodeURIComponent(token)}`) return result.stats } /** Revoke a share capability immediately. */ async revokeShareLink(token: string): Promise { await this.request("/api/shares", { method: "DELETE", body: JSON.stringify({ token }), }) } /** List files in a folder. Pass includeContent for bodies (heavier). */ async getFolder( folderId: string, opts?: { includeContent?: boolean }, ): Promise> }> { const qs = opts?.includeContent ? "?include=content" : "" return this.request(`/api/me/folders/${folderId}${qs}`) } /** List folders without embedding file bodies. */ async listFolders(): Promise { const res = await this.request<{ folders: TxtFolder[] }>("/api/me/folders") return res.folders } /** Create a folder with optional context and tags. */ async createFolder(input: { name: string context?: string tags?: string[] parentFolderId?: string | null }): Promise { const res = await this.request<{ folder: TxtFolder }>("/api/me/folders", { method: "POST", body: JSON.stringify(input), }) return res.folder } /** Update a folder's name, context, tags, or parent. */ async updateFolder( folderId: string, patch: { name?: string; context?: string; tags?: string[]; parentFolderId?: string | null }, ): Promise { const res = await this.request<{ folder: TxtFolder }>(`/api/me/folders/${folderId}`, { method: "PUT", body: JSON.stringify(patch), }) return res.folder } /** Delete a folder and promote its direct files and subfolders one level. */ async deleteFolder(folderId: string): Promise { await this.request(`/api/me/folders/${folderId}`, { method: "DELETE" }) } /** Discover explicit and inferred related files. */ async connections(fileId: string): Promise { const res = await this.request<{ connections: TxtConnection[] }>( `/api/me/files/${fileId}/connections`, ) return res.connections } /** Persist a direct relationship between two files. */ async connectFiles(fileId: string, targetFileId: string, reason = ""): Promise { await this.request(`/api/me/files/${fileId}/connections`, { method: "POST", body: JSON.stringify({ targetFileId, reason }), }) } /** Remove a direct relationship between two files. */ async disconnectFiles(fileId: string, targetFileId: string): Promise { await this.request( `/api/me/files/${fileId}/connections?targetFileId=${encodeURIComponent(targetFileId)}`, { method: "DELETE" }, ) } /** Ask the context engine to propose tags, folders, and explicit connections. */ async analyzeContext(prompt = ""): Promise { const res = await this.request<{ analysis: TxtContextAnalysis }>("/api/me/context", { method: "POST", body: JSON.stringify({ prompt }), }) return res.analysis } /** Start a note that explores itself from the context brain. */ async createIdeation(seed = "") { return this.request("/api/me/ideation", { method: "POST", body: JSON.stringify({ seed }), }) } /** List ideation notes and whether exploration is unlocked. */ async listIdeations() { return this.request<{ allowed: boolean billing: "byok" | "subscription" | null reason: string | null notes: Array<{ fileId: string name: string seed: string status: string passes: number lastRunAt: string | null updatedAt: string endpoint: string }> }>("/api/me/ideation") } /** Grow an existing note from the context brain. Promotes a regular note on first pass. */ async growIdeation(fileId: string, seed = "") { return this.request(`/api/me/files/${fileId}/ideate`, { method: "POST", body: JSON.stringify({ seed }), }) } async getIdeation(fileId: string) { const res = await this.request<{ ideation: TxtIdeation }>(`/api/me/files/${fileId}/ideation`) return res.ideation } // ------------------------------------------------------------------------- // Versions // ------------------------------------------------------------------------- /** List saved versions of a file (newest first). */ async listVersions(fileId: string, opts?: { includeContent?: boolean }): Promise { const query = opts?.includeContent ? "?include=content" : "" const res = await this.request<{ versions: TxtVersion[] }>( `/api/me/files/${fileId}/versions${query}`, ) return res.versions } /** Commit an immutable version. Content is optional; omitted snapshots the server working copy. */ async saveVersion( fileId: string, input: { content?: string; label?: string; source?: string; commitId?: string } = {}, ): Promise { const res = await this.request<{ version: TxtVersion }>(`/api/me/files/${fileId}/versions`, { method: "POST", body: JSON.stringify(input), }) return res.version } /** Fetch one immutable version, including its content. */ async getVersion(fileId: string, versionId: string): Promise { const res = await this.request<{ version: TxtVersion }>( `/api/me/files/${fileId}/versions/${versionId}`, ) return res.version } // ------------------------------------------------------------------------- // Comments // ------------------------------------------------------------------------- /** List all comments on a file. */ async listComments(fileId: string): Promise { const res = await this.request<{ comments: TxtComment[] }>(`/api/files/${fileId}/comments`) return res.comments } /** Post a new comment (optionally anchored to a text selection). */ async createComment(fileId: string, body: string, selection?: string): Promise { const res = await this.request<{ comment: TxtComment }>(`/api/files/${fileId}/comments`, { method: "POST", body: JSON.stringify({ body, selection }), }) return res.comment } /** Resolve or re-open a comment. */ async resolveComment(fileId: string, commentId: string, resolved: boolean): Promise { await this.request(`/api/files/${fileId}/comments`, { method: "PATCH", body: JSON.stringify({ id: commentId, resolved }), }) } /** Delete a comment. */ async deleteComment(fileId: string, commentId: string): Promise { await this.request(`/api/files/${fileId}/comments`, { method: "DELETE", body: JSON.stringify({ id: commentId }), }) } // ------------------------------------------------------------------------- // Suggestions // ------------------------------------------------------------------------- /** List all suggestions on a file. */ async listSuggestions(fileId: string): Promise { const res = await this.request<{ suggestions: TxtSuggestion[] }>(`/api/files/${fileId}/suggestions`) return res.suggestions } /** Submit a suggestion (track-change style). */ async createSuggestion(fileId: string, original: string, suggested: string): Promise { const res = await this.request<{ suggestion: TxtSuggestion }>(`/api/files/${fileId}/suggestions`, { method: "POST", body: JSON.stringify({ original, suggested }), }) return res.suggestion } /** Accept or reject a suggestion. */ async reviewSuggestion(fileId: string, suggestionId: string, status: "accepted" | "rejected"): Promise { await this.request(`/api/files/${fileId}/suggestions`, { method: "PATCH", body: JSON.stringify({ id: suggestionId, status }), }) } // ------------------------------------------------------------------------- // API Keys // ------------------------------------------------------------------------- /** List all API keys for the current user. */ async listApiKeys(): Promise { const res = await this.request<{ keys: TxtApiKey[] }>("/api/me/keys") return res.keys } /** Create a new API key with an optional label. Returns the token once — store it. */ async createApiKey(label?: string): Promise<{ key: TxtApiKey; token: string }> { return this.request("/api/me/keys", { method: "POST", body: JSON.stringify({ label: label ?? "sdk-key" }), }) } /** Revoke an API key by ID. */ async deleteApiKey(keyId: string): Promise { await this.request("/api/me/keys", { method: "DELETE", body: JSON.stringify({ id: keyId }), }) } // ------------------------------------------------------------------------- // Realtime sync (polling) // ------------------------------------------------------------------------- /** * Poll a file for changes. Uses If-None-Match so unchanged polls return 304 * (no body) — cheap revalidation for remote-filesystem sync. * * @param fileId - The file to watch. * @param onChange - Called with new content whenever it changes. * @param intervalMs - Poll interval in ms (default: 5000). * @returns - A `stop()` function to cancel the sync. * * @example * const stop = txt.sync("file_abc", (content) => setNote(content)) * // later: stop() */ sync(fileId: string, onChange: (content: string, file: TxtFileDetail) => void, intervalMs = 5000): () => void { let etag: string | undefined let handle: ReturnType | null = null let stopped = false const path = `/api/me/files/${fileId}` const poll = async () => { if (stopped) return try { const res = await fetch(`${this.base}${path}`, { headers: { ...this.headers(), ...(etag ? { "If-None-Match": etag } : {}), }, }) if (res.status === 304) return if (!res.ok) { const text = await res.text().catch(() => "") throw new TxtError(res.status, text || res.statusText, path) } const nextEtag = res.headers.get("etag") ?? undefined const file = (await res.json()) as TxtFileDetail etag = nextEtag onChange(file.content, file) } catch { // silently retry on network errors } } // immediate first fetch, then poll poll() handle = setInterval(poll, intervalMs) return () => { stopped = true if (handle !== null) clearInterval(handle) } } /** * Watch multiple files at once. * @returns A `stop()` function to cancel all watches. */ syncMany( fileIds: string[], onChange: (fileId: string, content: string, file: TxtFileDetail) => void, intervalMs = 5000, ): () => void { const stops = fileIds.map((id) => this.sync(id, (c, f) => onChange(id, c, f), intervalMs)) return () => stops.forEach((s) => s()) } } /** Stable public API client. Legacy-only extras continue to fall back transparently. */ export class TxtV1Client extends TxtClient { protected override route(path: string) { if (path === '/api/me/lists' || path.startsWith('/api/me/lists?') || path.startsWith('/api/me/lists/')) return path.replace('/api/me/lists', '/api/v1/lists') if (path === "/api/me/files/import" || path === "/api/me/files/upload") { return path.replace("/api/me/files", "/api/v1/files") } if (path === "/api/me/identities") return "/api/v1/identities" if (path === "/api/me/files" || /^\/api\/me\/files\/[^/]+(?:\/raw|\/versions(?:\/[^/]+)?|\/connections|\/plan|\/sync|\/ideate|\/ideation)?$/.test(path)) { return path.replace("/api/me/files", "/api/v1/files") } if (path === "/api/me/skills" || path.startsWith("/api/me/skills?")) return path.replace("/api/me/skills", "/api/v1/skills") if (path.startsWith("/api/me/skills/catalog")) return path.replace("/api/me/skills/catalog", "/api/v1/skills/catalog") if (path === "/api/me/skills/install") return "/api/v1/skills/install" if (/^\/api\/folders\/[^/]+\/share$/.test(path)) { return path.replace("/api/folders", "/api/v1/folders") } if (/^\/api\/files\/[^/]+\/(?:comments|suggestions)$/.test(path)) { return path.replace("/api/files", "/api/v1/files") } if (path === "/api/me/folders" || /^\/api\/me\/folders\/[^/]+(?:\/sync)?$/.test(path)) { return path.replace("/api/me/folders", "/api/v1/folders") } if (["/api/shares", "/api/shares/analytics"].includes(path.split("?")[0])) return path.replace("/api/shares", "/api/v1/shares") if (path === "/api/me/context") return "/api/v1/context" if (path === "/api/me/ideation") return "/api/v1/ideation" if (path === "/api/ai/assist") return "/api/v1/ai/assist" if (path === "/api/me/transcriptions") return "/api/v1/transcriptions" if (path === "/api/me/artifacts" || path === "/api/me/artifacts/models") { return path.replace("/api/me/artifacts", "/api/v1/artifacts") } return path } async listFilesPage(options: { limit?: number; cursor?: string } = {}) { const params = new URLSearchParams() if (options.limit) params.set("limit", String(options.limit)) if (options.cursor) params.set("cursor", options.cursor) const query = params.size ? `?${params}` : "" return this.request<{ files: TxtFile[]; nextCursor: string | null }>(`/api/me/files${query}`) } /** Read durable file/folder changes after a cursor, including delete tombstones. */ async changes(cursor: string | number = 0, limit = 200) { const params = new URLSearchParams({ cursor: String(cursor), limit: String(limit) }) return this.request(`/api/v1/changes?${params}`) } /** Execute up to 25 ordered operations. Retry the complete batch with one idempotency key. */ async batch(operations: TxtBatchOperation[], options: { stopOnError?: boolean; idempotencyKey?: string } = {}) { const { idempotencyKey, ...bodyOptions } = options return this.request<{ results: TxtBatchResult[]; complete: boolean }>("/api/v1/batch", { method: "POST", headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : undefined, body: JSON.stringify({ operations, ...bodyOptions }), }) } /** Push local mutations and pull changes since cursor in one deterministic round trip. */ async syncRound(input: { cursor: string | number mutations?: TxtBatchOperation[] changeLimit?: number stopOnError?: boolean idempotencyKey?: string }) { const { idempotencyKey, ...body } = input return this.request<{ results: TxtBatchResult[]; pull: TxtChangePage }>("/api/v1/sync", { method: "POST", headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : undefined, body: JSON.stringify(body), }) } async session() { return this.request<{ user: { id: string; email: string; name: string }; auth: { type: "api_key" | "dots_oauth"; scopes?: string[]; keyId?: string } }>("/api/v1/session") } async listWebhooks() { return this.request<{ webhooks: TxtWebhook[] }>("/api/v1/webhooks") } /** The signing secret is returned once. Store it in a secret manager. */ async createWebhook(input: { name?: string; url: string; events?: TxtWebhook["events"] }) { return this.request<{ webhook: TxtWebhook; signingSecret: string }>("/api/v1/webhooks", { method: "POST", body: JSON.stringify(input), }) } async updateWebhook(id: string, patch: { name?: string; url?: string; events?: TxtWebhook["events"]; enabled?: boolean }) { return this.request<{ webhook: TxtWebhook }>(`/api/v1/webhooks/${id}`, { method: "PATCH", body: JSON.stringify(patch), }) } async deleteWebhook(id: string) { return this.request<{ deleted: true }>(`/api/v1/webhooks/${id}`, { method: "DELETE" }) } } // --------------------------------------------------------------------------- // TxtError // --------------------------------------------------------------------------- export class TxtError extends Error { constructor( public readonly status: number, message: string, public readonly path: string, public readonly details: unknown = null, public readonly requestId: string | null = null, ) { super(`[txt] ${status} ${message} (${path})`) this.name = "TxtError" } } // --------------------------------------------------------------------------- // React hook (web / React Native) // --------------------------------------------------------------------------- /** * useTxtFile — React hook for live-synced file content. * * Compatible with React Native and web (no DOM dependency). * * @example * const { content, file, loading, error, update } = useTxtFile(client, "file_id") */ export function useTxtFile( client: TxtClient, fileId: string | null | undefined, intervalMs = 5000, ) { // Lazy import React so this file stays importable in non-React environments // eslint-disable-next-line @typescript-eslint/no-require-imports const { useState, useEffect, useCallback } = require("react") as typeof import("react") const [content, setContent] = useState("") const [file, setFile] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { if (!fileId) return setLoading(true) setError(null) const stop = client.sync( fileId, (c, f) => { setContent(c) setFile(f) setLoading(false) }, intervalMs, ) return stop }, [client, fileId, intervalMs]) const update = useCallback( async (newContent: string) => { if (!fileId) return const updated = await client.updateFile(fileId, { content: newContent }) setContent(updated.content) setFile(updated) }, [client, fileId], ) return { content, file, loading, error, update } }