/** One sort key forwarded to the app query RPC (wire shape of a `TableRecordSort` entry). */ export interface AppQuerySortKey { field_key: string; order: "asc" | "desc"; } /** A filter condition over one output column (wire shape of a `TableRecordFilters` node). */ export interface AppQueryFilterCondition { node_type: "condition"; field_key: string; type?: string; operator: string; value?: unknown; } /** A boolean group of filter nodes (wire shape — recursive). */ export interface AppQueryFilterGroup { node_type: "group"; logic: "and" | "or"; children: Array; } /** Runtime filter/sort the app query RPC applies AFTER the named query, bounded to its output columns. */ export type AppQueryFilter = AppQueryFilterCondition | AppQueryFilterGroup; /** * Body of the app query RPC — the one wire shape every transport carries, so * the dev bridge forwards it whole rather than naming fields that then drift * from what the deployed SDK sends. */ export interface AppQueryBody { alias: string; params?: Record; limit?: number; offset?: number; sort?: AppQuerySortKey[]; filter?: AppQueryFilter; /** Return only `total` over the filtered set — no rows. */ count?: boolean; /** Opt into keyset (seek) pagination, which answers with `next_cursor`. */ keyset?: boolean; /** The previous page's `next_cursor`, in keyset mode. */ cursor?: string; } /** Result of the app query RPC. `total` answers a `count` read; `truncated` and * `next_cursor` a rows read — `next_cursor` in keyset mode only, null on the * last page. */ export interface AppQueryResult { rows: unknown[]; total?: number; truncated?: boolean; next_cursor?: string | null; } /** * Where a download's bytes go: the FILE to write, or the DIRECTORY to write the * stored name into. Stated by the caller, never inferred from the path's shape — * `lotics file download` gives the positional path one meaning and `-o` the * other, so there is nothing here to guess. */ export type DownloadDestination = { file: string; } | { dir: string; }; export interface LoticsClientOptions { apiKey: string; /** The Lotics this key belongs to — `ResolvedContext.apiUrl`, which is the * registration's own `api_url`. Required and never defaulted: a client that * can fall back to an ambient host is a client that can send a private * instance's key to production (`docs/on_premise.md` section 8). */ apiUrl: string; workspaceId?: string; /** Admin "View as": when set, every request carries `x-view-as-member-id`, so * the backend evaluates IAM scoping (and `is_current_member`) as this member. * Admin-only — the server rejects a non-admin key. Writes stay attributed to * the key's owner. */ viewAsMemberId?: string; } export interface WorkspaceInfo { id: string; name: string; timezone: string; default_currency: string; organization_id: string; created_at: string; } /** * One hand-written report. Declared here rather than imported: this entry's * published `.d.ts` must resolve for a consumer who has none of this package's * other modules. */ export interface ReportFrame { goal: string; actual: string; expected?: string; tried?: string; wanted?: string; } /** One promise a refused write would break, as the tool transport states it. */ export interface ToolBreakingChange { kind: string; alias: string; path: string; rule?: string; message?: string; app_id?: string; } export interface ToolExecuteResult { result: unknown; model_output?: string; error?: string; /** The refusal's `code` — the same discriminator the HTTP envelope carries, * and the only part of a failure a caller may branch on. Absent when the * failure was not a typed error, and from any server older than it. */ error_code?: string; /** A refused input addressed per field: one sentence from a typed alias, a * list from a rejecting table workflow. */ field_errors?: Record | Record; breaking_changes?: readonly ToolBreakingChange[]; } /** * A settled (or in-flight) app-agent run, the transcript-excluded projection * `GET /v1/apps/{app_id}/agent-runs` returns. `output` is the STRUCTURED result * for a typed agent (an object) or the final text for a free-text agent (a * string); `status` is `running` until the run settles to `completed` / `error` * / `aborted`. The authoritative record `lotics app agent run` reports from * (never the stream). */ export interface AppAgentRunSummary { id: string; app_id: string; agent_alias: string; session_id: string; status: string; input: Record | null; output: string | Record | null; usage: { input_tokens: number; output_tokens: number; } | null; error_message: string | null; triggered_by_member_id: string | null; started_at: string; completed_at: string | null; /** Single-run GET only, while `status` is "awaiting_input": the pending ask * derived from the transcript — what /continue answers. */ pending_interactive?: { tool_call_id: string; tool_name: string; input: Record; }; } export interface ToolInfo { name: string; description: string; input_schema: unknown; } /** * What `POST /v1/apps/{app_id}/upgrade` reports it did — an offer applied in * part, and every part it declined to touch named back. * * Both `skipped_*` lists are aliases the owner has EDITED since the copy, so * the new version's body was left alone; `dropped_fields` are columns the new * version stopped declaring and that still hold the owner's data. Neither is an * error, and both are the reason this result is printed rather than counted. */ export interface AppUpgradeResult { app_id: string; from_version: number; to_version: number; deployed: { version_id: string; version_number: number; }; created_fields: Array<{ entity: string; field: string; }>; skipped_workflows: string[]; skipped_agents: string[]; dropped_fields: Array<{ entity: string; field: string; }>; } /** * A single knowledge doc with its HYDRATED body — the shape of * `GET /v1/knowledge_docs/{id}`, and the one content-read path a non-sandbox * client has. `content_sha` is the concurrency token the REST PATCH echoes back * as `expected_content_sha`; the `update_knowledge` TOOL CASes internally, so a * CLI caller never needs to pass it. */ /** A doc's metadata, as every listing serves it — no body. */ export interface KnowledgeDocSummary { id: string; workspace_id: string; name: string; description: string; /** How the doc is classified. Order-insensitive; empty means untagged. */ tags: string[]; /** When the doc was taken out of circulation, or null while it is in use. */ hidden_at: string | null; content_sha: string | null; files: unknown[]; created_at: string; updated_at: string; } export interface KnowledgeDocDetail extends KnowledgeDocSummary { content: string; } export interface FileUploadResult { files: Array<{ id: string; filename: string; mime_type: string; }>; errors: Array<{ filename: string; error: string; }>; } /** One finding from the extract behind a package publish. */ export interface ExtractFinding { severity: "error" | "warning" | "info"; area: string; message: string; } export interface StarterPublishRequest { /** The package to release into. Omitted, the origin apps resolve it. */ starter_id?: string; /** The origin apps that ship, in alias-minting order. */ app_ids: string[]; /** Live knowledge doc ids to bundle. Omitted keeps the previous version's set; [] drops them all. */ knowledge_doc_ids?: string[]; /** First publish only: alias fixes before v1 freezes them. */ renames?: Array<{ from: string; to: string; }>; /** First publish only: the listing name; defaults to the workspace's. */ name?: string; /** First publish only: the listing description; defaults to the first app's. */ description?: string; /** First publish only: the listing icon; defaults to the first app's. */ icon?: string; /** First publish only: the listing accent colour; defaults to the first app's. */ color?: string; } export interface StarterPublishPreview { /** The starter this would publish into, or null when it would mint one. */ starter_id: string | null; starter_name: string; version: number; /** The entities it would carry. */ entities: Array<{ alias: string; label: string; }>; apps: Array<{ alias: string; app_id: string; name: string; }>; /** Empty after v1 — the aliases froze there. */ renamable_aliases: { entities: string[]; fields: string[]; options: string[]; roles: string[]; templates: string[]; }; added_aliases: string[]; changed_artifacts: string[]; knowledge: { added: string[]; removed: string[]; changed: string[]; }; findings: ExtractFinding[]; } /** A publish is a job; this is the row the requester polls. */ export interface StarterPublish { id: string; status: "pending" | "building" | "completed" | "failed"; /** Null until a first publish completes. */ starter_id: string | null; version_id: string | null; version: number | null; app_ids: string[]; building_app_alias: string | null; apps_built: number; error: string | null; created_at: string; started_at: string | null; finished_at: string | null; } /** * A published version's contract — the artifact set a copy of it creates. * * Narrowed to what a caller CHECKING a copy reads back. The authoritative shape * is `packageContractSchema`, which lives in `@lotics/shared` — a specifier a * published `.d.ts` cannot resolve, so it is declared here rather than imported, * the same trade the publish shapes above make. */ export interface StarterVersionContract { /** * One table per entity, scaffolded under `label`, with the columns it * declares — a copy whose table landed with the right NAME and the wrong * columns is the failure a table-name check cannot see. */ entities: Array<{ alias: string; label: string; description?: string; fields: Array<{ alias: string; label: string; type: string; }>; }>; templates: Array<{ alias: string; label: string; }>; apps: Array<{ alias: string; name: string; /** The named queries the app's shipped source invokes. */ queries: Array<{ alias: string; /** Param name → declaration. `required` defaults to TRUE when absent. */ params?: Record; }>; }>; /** The knowledge docs the starter ships, keyed by alias. */ knowledge: Record; /** Sample rows per entity alias — `row_count` is the contract's own count of them. */ fixtures: Record; } /** Advisory knowledge warnings surfaced by a copy (never block). */ export interface KnowledgeWarnings { /** `knowledge_expects` doc names with no matching workspace doc. */ missing_expected_docs: string[]; } /** * A workspace MODEL on the wire — a package contract with no apps, plus the * first rows that travel beside it keyed by entity alias. * * The contract's authoritative shape is `packageContractSchema` in * `@lotics/shared`, a specifier a published `.d.ts` cannot resolve — so the wire * body is typed structurally here, the same trade `StarterVersionContract` * makes. The CLI passes a value it has already parsed through that schema. */ export interface ScaffoldWorkspaceRequest { contract: Record; rows?: Record; }>>; /** Bind an entity whose label already names a table here. Absent, a colliding label is refused. */ adopt?: boolean; } export interface ScaffoldWorkspaceResult { /** Every entity the model declares, in contract order. */ entities: Array<{ alias: string; table_id: string; /** False when a table of that label already existed and was adopted. */ created: boolean; /** * What this run put ON that table — the delta a schema write is confirmed by. * Absent from an instance too old to state it, so the line omits the counts * rather than reporting a run that added nothing. */ created_fields?: number; created_options?: number; created_views?: number; }>; /** * Every role the model declares. `created` is false when a GROUP of that name * was already here and the role bound to it — which is how a role silently * inherits another workspace's membership, and why the line prints it the way * an entity's does. */ roles: Array<{ alias: string; group_id: string; created: boolean; }>; /** * Ids of the rows written, per entity alias — the handle for deleting them * again, and what `scaffold apply --documents` joins its rows to. */ record_ids: Record; /** Rows were sent and none were written, because the run adopted a table. */ rows_skipped: boolean; } /** * A workspace read BACK as a model — the inverse of the scaffold above. * * `entities` and `roles` are the model file's own two keys, whose authoritative * shapes are `contractEntitySchema` / `contractRoleSchema` in `@lotics/shared` — * specifiers a published `.d.ts` cannot resolve, so they are typed here as what * this client does with them, which is hand them on whole. The same trade * `ScaffoldWorkspaceRequest` makes in the other direction. * * `findings` are about the export rather than part of it: a workspace holds * things a model file cannot express, and a file that dropped them silently * would be read as the whole workspace. */ export interface WorkspaceModelExport { contract: { entities: Array>; roles: Array>; templates: Array>; }; findings: Array<{ severity: "error" | "warning" | "info"; area: string; message: string; }>; /** * Labels of the entities the link closure added — the tables the caller did not * name and a link pulled in. Absent from an instance too old to state it, which * is why the printout says nothing rather than "0 pulled in". */ pulled_in?: string[]; } /** * A request the API refused. The message carries the status and the server's * sentence, which is what reaches a person; `status` and `body` are for the * few callers that branch on a refusal — a 409 whose `body.reason` says the * run had nothing to do, rather than that it failed. */ export declare class LoticsRequestError extends Error { readonly status: number; readonly body: Record; constructor(message: string, status: number, body: Record); } /** * The website: where a person goes when the CLI cannot finish the job, and where * the presets are SERVED FROM. Held beside the API base so the pair is read * together, named once rather than inlined at each message that points * somewhere, and overridable for the same reason the API base is — a run against * a local site must read that site's presets, not production's. */ export declare const WEB_APP_URL: string; /** One package on the public shelf — what a chooser decides with, nothing else. */ export interface OfficialStarter { id: string; name: string; description: string | null; icon: string | null; latest_version: number; /** * The apps the copy creates. A name and a sentence leave the chooser * guessing; these are what someone's own words get matched against when * deciding between a package and a model. */ apps: Array<{ alias: string; name: string; description?: string; }>; /** How many tables the copy creates — the other half of "what is in this?". */ table_count: number; } /** * The starters Lotics publishes, fetched with no credential. * * A plain function rather than a `LoticsClient` method because there is no key * to build a client around — and that is the point of the endpoint. "Is there a * starter for what I do, or should I build?" is decided before an account * exists, so needing one to ask means signing up to find out the answer was no. */ export declare function fetchOfficialStarters(apiUrl: string): Promise; /** * One keyless GET of JSON, bounded — the shape every read that predates an * account takes. * * Bounded because these are the FIRST commands a new user runs and a hang with * nothing on screen is the worst version of the failure; `config.ts` bounds its * own unattended fetch for the same reason, and `docs/network_reliability.md` is * a log of connections from our users' networks degrading rather than refusing, * which is the shape that hangs. * * A transport failure THROWS and a status is RETURNED, because only the first is * "check your connection": a status means the server replied and the network is * demonstrably fine, and a caller that answered both the same way would tell an * owner their package is missing every time the link drops. */ export declare function getPublicJson(url: string, what: string): Promise<{ ok: true; body: unknown; } | { ok: false; status: number; }>; /** * One official package READ WHOLE, with no credential — or the status that says * the shelf does not serve it. * * The shelf row says what a package is; this says what is in it — entities with * their fields, the roles, each template's metadata and each app's name. Never a * workflow body: this is a read for deciding and for writing a model from, not a * copy. * * Unauthenticated for the same reason the shelf is: a preset is READ rather than * instantiated, and the whole point of reading one is that it happens before an * account exists. Only official packages are served — the `is_official` filter, * exactly as on the shelf — so a caller's OWN package answers a status here and * is read through the authenticated pair instead. A transport failure still * throws: "unreachable" is not "not on the shelf", and answering both the same * way would tell an owner their package is missing every time the link drops. */ export declare function readOfficialStarter(apiUrl: string, starter_id: string): Promise<{ ok: true; package: OfficialStarterRead; } | { ok: false; status: number; }>; /** The same read for a caller with nothing to fall back to — a refusal is the end. */ export declare function getOfficialStarter(apiUrl: string, starter_id: string): Promise; /** A column as the public read shows it: enough to write a model field from. */ export interface OfficialStarterField { alias: string; label: string; type: string; } /** A table as the public read shows it: its columns. */ export interface OfficialStarterEntity { alias: string; label: string; description?: string; fields: OfficialStarterField[]; } /** * What the public read of one package carries. * * Every namespace is declared structurally, NARROWED to what a reader turning * this into a model needs — fields carry their type, apps only their name — the * same trade `StarterVersionContract` makes: a published `.d.ts` cannot resolve * `@lotics/shared`. `packageContractSchema` is the definition, and the CLI * parses a preset's block through `modelPresetSchema` before resolving a `from` * file against it. */ export interface OfficialStarterRead { id: string; name: string; description: string | null; latest_version: number; contract: { entities: OfficialStarterEntity[]; roles: Array<{ alias: string; label: string; }>; templates: Array<{ alias: string; label: string; type: string; }>; /** The apps a copy creates. */ apps: Array<{ alias: string; name: string; description?: string; }>; /** Sample rows per entity alias — how many, never the rows. */ fixtures: Record; knowledge: Record; knowledge_expects: string[]; }; } /** * What a terminal holds while it waits to be let in: the handle the server knows * the request by, the `secret` that proves this is the same terminal that asked, * and the `code` the person matches against the confirm page before pressing it. * * `secret` is a credential and is never printed — the `code` is what a person * reads, and it proves nothing on its own. */ export interface CliLoginRequest { request_id: string; secret: string; code: string; expires_at: string; } /** Where a login request stands. `approved` carries the key, and only once. */ export type CliLoginState = { status: "pending"; } | { status: "expired"; } | { status: "claimed"; } | { status: "approved"; api_key: string; organization_id: string; organization_name: string; workspace_id: string; }; /** * Ask Lotics to mail a sign-in link, and read back whether it was confirmed. * * Plain functions rather than `LoticsClient` methods for the same reason * `fetchOfficialStarters` is one: there is no key to build a client around, and * that is the whole point — this is the pair a terminal holding NO credential * uses to obtain one. Sending an `Authorization` header would make the endpoint * answerable only to callers who no longer need it. * * The same answer whether or not the address has an account: it would * otherwise tell any stranger which emails are registered here. */ export declare function startCliLogin(apiUrl: string, email: string): Promise; export declare function pollCliLogin(apiUrl: string, request_id: string, secret: string): Promise; export declare class LoticsClient { private apiKey; private workspaceId; /** The active "View as" target member id, if any. Read-only after * construction — surfaced so `lotics app dev` can show it in the banner. */ readonly viewAsMemberId: string | undefined; /** API URL the client is configured against. Read-only after construction. * Surfaced for callers that need to display it (`lotics app dev`'s banner) or * to hand it on (the wrapper page's RPC target, the scaffold's font proxy). */ readonly baseUrl: string; constructor(options: LoticsClientOptions); private throwResponseError; /** * The backend's `log()` middleware registers `user-agent`, * `x-posthog-session-id` and `x-request-id` onto the per-request Logger, so * they ride EVERY log line that request emits — the validation 400, the tool * error, the timing. Sending them is therefore the whole of the correlation * work: it turns an anonymous API-key request into "`app workflow set`, from * cli 0.117.0, the fourth command of this session". * * The request id is minted HERE, with the other headers, so it cannot reach * some paths and not others: several commands build their own transport around * these headers, and an id threaded only through `request` would leave * `app deploy`, `uploadFiles` and every workflow call unfindable. */ private buildHeaders; private request; /** `email` is null for a credential no person signs in as; `account_type` is absent from an older server. */ /** `credential_origin` describes the CREDENTIAL this call carried, not the * member above: `sign_in` for a terminal somebody signed in from, `api_key` * for one an admin issued. Null for a row minted before it was recorded, and * absent from an older server — both mean not told. */ whoami(): Promise<{ member_id: string; email: string | null; name: string; account_type?: string; organization_id: string; organization_name: string; credential_origin?: "api_key" | "sign_in" | null; }>; /** * Hand this client's own credential back — what `lotics auth logout` calls * before it forgets the key, so signing out ends the credential instead of * leaving a live one on a server nobody can see it from. Takes no id: the * only thing it can revoke is the key these requests carry. */ revokeSelfCredential(): Promise<{ id: string; }>; /** * File a hand-written report, or a list of them in one call. Unlike a * telemetry flush, the headers this stamps are CORRECT: the invocation making * the request is the one the report is about, so the courier and the cargo are * the same session. */ sendReport(body: { report: ReportFrame | ReportFrame[]; cli_session_id: string | null; cli_version: string; workspace_id?: string; app_id?: string; }): Promise<{ accepted: number; report_ids?: string[]; }>; setWorkspaceId(id: string): void; /** The workspace id the client targets (the `x-workspace-id` header), if resolved. */ getWorkspaceId(): string | undefined; listWorkspaces(): Promise; createWorkspace(body: { name: string; timezone?: string; default_currency?: string; }): Promise; /** * Apply a workspace MODEL to the current workspace — the from-scratch half of * the starter pipeline, on a contract nobody published. * * Additive: with `adopt`, an entity whose label already names a table here * binds to it and gains the fields, options and views it is missing; without * `adopt`, that label is refused. Nothing is ever modified or deleted, and * rows land only where every bound table is empty. Admin-only. */ scaffoldWorkspace(body: ScaffoldWorkspaceRequest): Promise; /** * Read this workspace's schema back as a model — the tables it has (or only * the ones named), their fields, options and views, plus its roles. * * A pure read, and admin-only for the same reason the scaffold is: the whole * schema is what comes back. */ exportWorkspaceModel(opts?: { tables?: string[]; }): Promise; /** Renames (and re-settings) the CURRENT workspace — the endpoint reads the * target from the request's workspace, never a path id. */ updateWorkspace(body: { name: string; default_currency: string; timezone: string; }): Promise; deleteWorkspace(id: string): Promise<{ id: string; deleted: boolean; }>; login(body?: { /** * Return the sign-in link instead of mailing it — the browser handoff. * * Grants nothing new: an API key is member-bound, so the link mints a * session for exactly the identity this client already holds. The returned * URL IS a credential though: one-time, short-lived, and not for anywhere it * persists. */ return_link?: boolean; /** Relative path to land on after signing in; server-validated as relative. */ redirect_path?: string; }): Promise<{ email: string; url?: string; }>; listTools(): Promise<{ tools: string[]; categories: Record; }>; getTool(name: string): Promise; execute(tool: string, args: Record, options?: { format?: "json" | "text"; timeoutMs?: number; }): Promise; /** * Fetch one knowledge doc with its hydrated `content` — the single content-read * path for a non-sandbox client (the `list_knowledge` tool returns metadata * only, and the sandbox-staging read path is unavailable here). Works for both * the file-model and legacy parked-column rows. Mirrors GET /v1/knowledge_docs/{id}. */ getKnowledgeDoc(knowledge_doc_id: string): Promise; /** * Every doc's metadata — REST rather than the `list_knowledge` tool, because * this is the surface a PERSON manages the corpus from and the tool is bound * to what the assistant may browse. That distinction is the whole point of * `include_hidden`: a hidden doc is out of the assistant's corpus by design, * and someone still has to be able to find it in order to put it back. */ listKnowledgeDocs(opts?: { includeHidden?: boolean; }): Promise; /** Apply one metadata change to a set of docs. Tags are an add/remove DIFF — * see the endpoint's own note on why a replacement is the wrong shape here. */ bulkUpdateKnowledgeDocs(body: { knowledge_doc_ids: string[]; add_tags?: string[]; remove_tags?: string[]; hidden?: boolean; }): Promise; getApp(app_id: string): Promise<{ id: string; name: string; /** What the app is for. Heads the capability catalog the member's chat agent * reads every turn the app is open, so it is where a standing process is * stated. Null when unset. */ description?: string | null; workspace_id: string; current_version_id: string | null; /** Launcher icon — a Lucide name, or an image ref. Null when unset. */ icon?: string | null; /** Launcher theme — `{ color }` is a palette token. Null when unset. */ theme?: { color?: string | null; } | null; /** * Live alias → workflow declaration map from `apps.workflows`. Source of * truth for `lotics app pull` — supersedes the manifest embedded in the * source archive so agent-authored bindings (via `set_app_workflow`) * survive the pull/edit/deploy loop. Null/undefined on apps that have * never had a workflow declared. * * `table_ids` is the set of tables the bound BODY touches, recorded when the * body was verified. It is what makes a table an app only ever WRITES to * addressable: codegen covers every table a query reads, and a workflow * writing a table no screen reads had no `F`/`OPT` entry, so its body * carried pasted `fld_`/`opt_` literals that a rename breaks silently * instead of failing `tsc`. Absent on a binding written before the server * recorded it — the set widens what codegen covers, it does not define it. */ workflows?: Record; outputs?: Record; description?: string; table_ids?: string[]; }> | null; /** * Live alias → query declaration map from `apps.queries`. Source of truth * for `lotics app pull`. Null/undefined on apps with no declared queries. */ queries?: Record; description?: string; }> | null; /** * Live alias → agent declaration map from `apps.agents`. Source of truth for * `lotics app pull` — supersedes the source archive so `set_app_agent` * authoring survives the pull. Null/undefined on apps with no declared * agents. Drives `useAgentRun` typing. */ agents?: Record; outputs?: Record; /** * The app's own queries/workflows this agent may call through * `run_app_query` / `run_app_workflow`. These are REFERENCES that never * appear in the client bundle, so anything asking "is this alias still * used" must read them or it will answer no for an agent-driven app. */ query_aliases?: string[]; workflow_aliases?: string[]; }> | null; }>; createApp(body: { name: string; description?: string; icon?: string; /** The launcher tile's colour — the app's own, carried from whatever declared it. */ theme?: { color?: string; }; }): Promise<{ id: string; name: string; workspace_id: string; current_version_id: string | null; }>; /** * Take a starter off the shelf, or `undo` to put it back (backs `opctl * library unpublish`). It hides from non-owning orgs and can no longer be * copied; copies already made are unaffected — they never linked back. * Owner-org admin-only. */ unpublishStarter(starter_id: string, body: { undo: boolean; }): Promise<{ id: string; name: string; retired_at: string | null; }>; /** * Edit a starter's registry listing — the name and description a stranger * reads before copying, and what every copy's app row is created from. * * A version is an immutable snapshot; the listing is not. Omit a field to * leave it, pass `description: null` to clear it. Owner-org admin-only. */ editStarterListing(starter_id: string, body: { name?: string; description?: string | null; icon?: string | null; theme?: { color?: string | null; } | null; }): Promise<{ id: string; name: string; description: string | null; icon: string | null; theme: Record | null; }>; /** * The starters this organization can copy — Lotics-reviewed ones plus its own, * never a catalogue of everything published. The server returns exactly what * instantiate would accept, so the list cannot offer a refusal. Admin-only. */ listStarters(): Promise; /** How many tables the copy creates. */ table_count: number; }>>; /** * Copy a starter into the current workspace. * * Server-side this scaffolds the schema, creates the templates, docs and * sample records, creates every app the starter carries and deploys each * from its prebuilt dist — no build anywhere. `apps` reports each deploy; * one that failed carries its `error` and the copy is complete around it. * Admin-only. */ instantiateStarter(starter_id: string, body: { version?: number; no_sample_data?: boolean; adopt?: boolean; /** * The labels THIS workspace calls the package's entities and fields. * * Scaffold adopts by label, so binding renames the contract's labels * before it runs and the copy lands on the tables the caller already has. * A bound field whose type differs from the one the contract declares is * refused — the type is the contract, only the naming moves. Structural * for the same reason the rest of this file's wire shapes are; * `packageBindSchema` in `@lotics/shared` is the definition. */ bind?: Record; }>; }): Promise<{ /** Each app's deploy, in contract order. `error` set and `deployed` null when one did not land. */ apps: Array<{ alias: string; app_id: string; name: string; deployed: { version_id: string; version_number: number; } | null; error: string | null; }>; starter_id: string; version: number; binding: Record>; sample_record_ids: Record; knowledge_warnings: KnowledgeWarnings; }>; /** * Which starter this app is the origin of. 404 when it has published none. * * The app row carries no pin, so this is the only app→starter direction there * is — provenance lives on the published version. Admin-only. */ getAppOriginStarter(app_id: string): Promise<{ starter_id: string; latest_version: number; }>; /** * Apply the latest version of the package this app was copied from. * * The other provenance direction from `getAppOriginStarter`, and the only one * that writes: that one answers which package this app PUBLISHED, this one * reads what a copy recorded and re-derives the app against a later release. * * The offer is partial by design and the result says how partial: schema is * additive (`created_fields`, and a `dropped_fields` the new version stopped * declaring is left standing), and an artifact the owner has edited is kept * and named (`skipped_workflows` / `skipped_agents`). An app with no * provenance is a 400, and one already on the latest release a 409 — both * refusals, both before any write. Admin-only. */ upgradeApp(app_id: string, /** Apply the new release even though it breaks what this app's published API * promises, snapshotting the broken contract as a new version. */ opts?: { acknowledge_breaking_api_change?: boolean; }): Promise; /** * Capture live records from this workspace as a starter's sample data. * * The alias-keyed shape is produced SERVER-side, because the contract alias * space is minted by extract and exists nowhere a project can read it. Pure * read — the caller writes the returned files into the project and reviews * them, which matters: these rows are copied verbatim into every workspace * that takes the starter. Admin-only. */ captureStarterFixtures(app_id: string, opts?: { entities?: string[]; limit?: number; }): Promise<{ app_id: string; available_entity_aliases: string[]; captured: Array<{ entity_alias: string; content_ref: string; file: { rows: Array<{ ref: string; fields: Record; }>; }; notes: string[]; }>; }>; /** * Fetch a registry starter's metadata (`latest_version` and the Lotics-backed * `is_official` trust badge). Admin-only; cross-tenant by id. */ getStarter(starter_id: string): Promise<{ id: string; name: string; description: string | null; latest_version: number; is_official: boolean; retired_at: string | null; /** Whether the CALLING org owns it — the copy-time trust badge, without exposing the owner's org id. */ owned_by_caller: boolean; /** The shelf tile. Null = unset; a copy's app tiles come from the contract. */ icon: string | null; theme: Record | null; created_at: string; updated_at: string; }>; /** Version history newest-first (no contract payloads) — backs `opctl library show`. Admin-only. */ listStarterVersions(starter_id: string): Promise<{ versions: Array<{ version: number; changelog: string | null; created_at: string; }>; }>; /** * One version's contract — what a copy of it is supposed to produce. The * history above omits it (heavy per row); this is the read that carries it, * so a check compares a copy against the declaration itself rather than * against a description of it. Admin-only; cross-tenant by id like * `getStarter`. */ getStarterVersion(starter_id: string, version: number): Promise<{ version: number; contract: StarterVersionContract; }>; /** * Workspace-wide dangling-reference sweep — active app/workflow artifacts * whose prefixed schema ids no longer resolve. Backs * `lotics workspace doctor`. Admin-only. */ getWorkspaceDanglingReferences(): Promise>; /** * Preview publishing a set of this workspace's apps as one starter version — * the GET behind `opctl library publish` (no `--yes`). The server runs the * same extraction the publish runs and reports which starter it would * release into (null: it would mint one), the next version, the aliases a * first publish can still rename, the diff against the current version, the * knowledge delta, and the findings (an `error` blocks the publish). No * writes. Admin-only. */ previewStarterPublish(opts: StarterPublishRequest): Promise; /** * Publish this workspace's apps as a starter version — a JOB, because every * app is built once against sentinel field keys and eleven builds outlast a * request. Everything a request can refuse is refused here with nothing * written: a blocking finding or another publish still running for this * org (409), a missing deploy or a bad declaration (400). The response is * the job to poll with `getStarterPublish`. Admin-only. */ requestStarterPublish(body: StarterPublishRequest & { changelog?: string | null; }): Promise; /** The state of a publish: which app is building, and the version once every dist is in. */ getStarterPublish(publish_id: string): Promise; /** * The ROW RULE each of these tables declares — `private_filters` as stored, or * `null` where the table has none. * * `GET /v1/tables/{id}` rather than the `get_table` tool beside it: the rule is * an IAM fact about who may read a row, and the tool's output is the schema an * agent writes records against, which is why it carries fields and not this. * One call per id. A table this credential may not read — 403, or 404 for one * that is gone — is DROPPED: the only caller warns about rules it can see, and * a table it cannot read is not evidence of one. EVERY other failure throws. * An expired key, a 500, a timeout and an offline host all mean the scan has * no answer, and swallowing them would render as "this table declares no * rule" — the guard at its quietest exactly where it knows least. * * The filter is carried untyped: a published `.d.ts` cannot name * `@lotics/shared`'s filter schema, and the one reader asks a single question * of the tree rather than interpreting it. */ getTableRowRules(tableIds: string[]): Promise>; /** * The organization's member groups — the directory `lotics app codegen` turns * into the `GRP` alias map. * * Over the HTTP route rather than `query_member_groups`, because the tool is * admin-only and the route is member-visible by design (`docs/iam.md` § Groups: * reading the directory is not administration). Codegen runs for every author, * so an admin-only read here would leave a non-admin's `GRP` empty and their * app unbuildable. */ getMemberGroups(): Promise>; /** * Resolve the display name + fields (incl. select options) of the given tables * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts` * alias maps. One `get_table` call per id (the tool surface has no batch * variant); a missing/inaccessible table is dropped rather than throwing, so a * stale id in the scope set never fails codegen. */ getWorkspaceSchema(tableIds: string[]): Promise; }>; }>>; /** * Rename an app's public subdomain — the label its origin is built on. * Mirrors PUT /v1/apps/{app_id}/subdomain, and returns the finished `origin` * because only the instance knows the domain and scheme it serves apps on. * The old subdomain stops resolving once the change lands. */ setAppSubdomain(app_id: string, public_subdomain: string): Promise<{ app_id: string; public_subdomain: string; origin: string; }>; /** * Publish the app's API — the owner's promise about what its declared queries * and workflows return to a caller outside the app. * * Snapshots the contract and answers the version it took, plus what the * published surface exposes that the owner may not have intended. A query * that does not name its columns is refused (400) before anything is written: * the app cannot promise field names it never stated. */ publishAppApi(app_id: string): Promise<{ app_id: string; contract_version: number; published_at: string; warnings: string[]; }>; /** End the promise. `unpublished: false` means the app was publishing nothing, * which this leaves unchanged. The superseded snapshot stays, so a later * publish continues the numbering rather than reusing a version. */ unpublishAppApi(app_id: string): Promise<{ app_id: string; unpublished: boolean; }>; /** Whether the app is publishing an API, and which contract version. */ getAppApiPublication(app_id: string): Promise<{ published: boolean; contract_version: number | null; published_at: string | null; }>; /** * The published API as an OpenAPI 3.1 document — what a consumer's own * generator reads. Rendered from the live SNAPSHOT rather than the manifest, * so it describes what the app has promised; 404 while nothing is published. */ getAppOpenApiDocument(app_id: string): Promise>; getAppVersion(app_id: string, version_id: string): Promise<{ id: string; app_id: string; version: number; r2_prefix: string; entry_html_path: string; build_status: string; }>; getAppVersionSourceUrl(app_id: string, version_id: string): Promise; /** Deploy history for an app — newest first. Backs `lotics app versions`. */ listAppVersions(app_id: string, opts?: { limit?: number; offset?: number; }): Promise<{ current_version_id: string | null; versions: Array<{ id: string; version: number; message: string | null; build_status: string; bundle_size_bytes: number | null; created_at: string; created_by: string | null; created_by_name: string | null; }>; }>; /** * Run a named query declared in the app's manifest, scoped to the app's IAM * principal. Mirrors POST /v1/apps/{app_id}/query. */ appQuery(app_id: string, body: AppQueryBody): Promise; appMembers(app_id: string, group_id?: string): Promise<{ members: Array<{ id: string; name: string | null; email: string | null; image: string | null; }>; }>; /** * Resolve the full option set (key, label, color) of a named query's select * columns — the picker companion to `appQuery`. Mirrors * POST /v1/apps/{app_id}/field-options. */ appFieldOptions(app_id: string, alias: string): Promise<{ fields: Record; }>; }>; /** * Execute a workflow by alias declared in package.json#lotics.workflows. * Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute. */ appWorkflow(app_id: string, alias: string, inputs: unknown): Promise; /** * Bind (create or replace) an app workflow by alias via the `set_app_workflow` * tool — the SINGLE author of `apps.workflows` + the workflow row. `source` is * the verbatim JS-subset body (no `on({...})` trigger). `inputs`/`outputs` are * the typed schemas declared in `package.json#lotics.workflows.`. The * server re-verifies the body and echoes the bound `outputs` (declared, else * DERIVED from `return({ data })`), so the CLI can show the author what shape * `result.data` will carry. Wraps the tool rather than a bespoke endpoint so * the file flow stays a convenience over the existing single-author contract. */ setAppWorkflow(app_id: string, alias: string, body: { source: string; inputs?: Record; outputs?: Record; name?: string; description?: string; /** The `body_sha` this push was built on. Makes the write conditional: the * server refuses it when the live body has moved since, rather than * letting a stale copy overwrite an edit its author never saw. */ expected_body_sha?: string; /** Carry out this write even though it breaks what the app's published API * promises, snapshotting the broken contract as a new version. */ acknowledge_breaking_api_change?: boolean; }): Promise; /** * Bind (create or replace) an app query by alias via the `set_app_query` tool * — the deploy-free authoring path for `apps.queries`, parallel to * `setAppWorkflow`. `declaration` is the `{ ast, params? }` from * `package.json#lotics.queries.`. The server validates it exactly as a * deploy validates the manifest. Note: `apps.queries` is manifest-authoritative, * so the next `lotics app deploy` overwrites this from the manifest. */ setAppQuery(app_id: string, alias: string, declaration: { ast: unknown; params?: Record | null; description?: string | null; }, /** The fingerprint this edit was based on — makes the write conditional. */ expected_sha?: string, /** Carry out this write even though it breaks what the app's published API * promises, snapshotting the broken contract as a new version. */ acknowledge_breaking_api_change?: boolean): Promise; /** * Bind (create or replace) an app agent by alias via the `set_app_agent` tool * — the deploy-free authoring path for `apps.agents`, parallel to * `setAppWorkflow`/`setAppQuery`. * * `set_app_agent` REPLACES the whole declaration, so this takes the whole * declaration. `lotics app agent set` is the caller that assembles it (prose * from `src/agents/.md`, typed fields from the manifest) precisely so * no caller has to remember that a partial payload silently drops * `instructions`, `outputs` and the model pin. */ setAppAgent(app_id: string, alias: string, /** ONLY the fields being changed. The server merges against the stored * declaration, so nothing a caller omits is lost and a field the server adds * later needs no change here. Pass `null` to clear an optional field. */ patch: Record, /** Carry out this write even though it breaks what the app's published API * promises, snapshotting the broken contract as a new version. An agent's * alias and its declared `inputs`/`outputs` are in the published contract, * so the same answer `setAppQuery` takes is owed here. */ acknowledge_breaking_api_change?: boolean): Promise; /** * Fetch one app workflow's faithful source + bound input/output schemas via * `get_app_workflow`. `source` is the JS-subset body re-rendered from the * persisted step tree (incl. the `return({ data })` clause, opaque field/option * keys) — the exact text `lotics app workflow set` would push back. Feeds * `lotics app pull`, which writes it to `src/workflows/.ts`. */ getAppWorkflow(app_id: string, alias: string): Promise; /** * The app's capability catalog exactly as a chat or MCP caller reads it — * every query, workflow and agent alias the caller's scope reaches, with the * description each is chosen BY. One read for the whole app, so a check over * what those readers see costs one request rather than one per alias. */ getAppCapabilities(app_id: string): Promise; /** * Fetch the server-generated workspace `.d.ts` + the wrapper envelope that * make a `src/workflows/.ts` body locally typecheckable (GAP-59). * The server is the single source of the type model — the CLI never * re-implements it. `envelope_prefix`/`envelope_suffix` are the exact * `async function __workflow(): …` wrapper the server compiles inside, so the * local typecheck mirrors the set-time verdict. Mirrors * POST /v1/apps/{app_id}/workflows/{alias}/dts. * * `declaration` (the manifest's `{ inputs?, outputs? }`) is posted as the body * `{ declaration }` ONLY when the alias isn't `set` on the server yet — the * server then synthesizes the dts from the declared schemas instead of 400ing * "no workflow alias". A registered alias needs no declaration (the server's * own bound contract wins), so the field is omitted in that case. */ getAppWorkflowDts(app_id: string, alias: string, declaration?: { inputs?: Record; outputs?: Record; }): Promise<{ dts: string; envelope_prefix: string; envelope_suffix: string; }>; /** * Open a streaming agent run and return the RAW streamed `Response` (the * caller reads `res.body`). Unlike `request`, this does not buffer/parse the * body — it's the SSE stream the `lotics app dev` harness proxies to the * iframe. Mirrors POST /v1/apps/{app_id}/agents/{alias}/runs. */ appAgentRunStream(app_id: string, alias: string, body: { session_id: string; input: Record; }, signal?: AbortSignal): Promise; /** * Continue a PARKED (`awaiting_input`) agent run with the user's answer to its * pending `ask_user_choice` — returns the RAW streamed continuation `Response`, * exactly like `appAgentRunStream`. Mirrors * POST /v1/apps/{app_id}/agent-runs/{run_id}/continue. */ appAgentRunContinueStream(app_id: string, run_id: string, body: { tool_call_id: string; output: Record; }, signal?: AbortSignal): Promise; /** * A session's app-agent run history, oldest-first (the run just started is the * last, and its exact id is on the stream response's `x-app-agent-run-id` * header). Transcript excluded; structured `output`/`input` included. Mirrors * GET /v1/apps/{app_id}/agent-runs. */ listAgentRuns(app_id: string, session_id: string): Promise<{ runs: AppAgentRunSummary[]; }>; /** * A single run by id — the poll read a client follows after its stream drops * (a parked `awaiting_input` row carries `pending_interactive` so the question * survives reconnection). Mirrors GET /v1/apps/{app_id}/agent-runs/{run_id}. */ getAgentRun(app_id: string, run_id: string): Promise<{ run: AppAgentRunSummary; }>; /** * Request cancellation of an in-flight (or parked) run. Mirrors * POST /v1/apps/{app_id}/agent-runs/{run_id}/cancel. */ cancelAgentRun(app_id: string, run_id: string): Promise<{ ok: true; }>; /** * Mint a presigned URL for uploading a file into an app. Mirrors * POST /v1/apps/{app_id}/files/upload-url. */ appRequestFileUpload(app_id: string, body: { filename: string; mime_type: string; file_size: number; }): Promise<{ file_id: string; file_storage_key: string; upload_url: string; }>; /** * Finalize a presigned upload once the bytes are in storage. Mirrors * POST /v1/apps/{app_id}/files/complete. */ appCompleteFileUpload(app_id: string, body: { file_id: string; file_storage_key: string; filename: string; }): Promise<{ file: { id: string; filename: string; mime_type: string; url?: string; thumbnail_url?: string; }; }>; appGetRecordComments(app_id: string, record_id: string): Promise; appCreateRecordComment(app_id: string, record_id: string, body: { content: string; file_ids?: string[]; }): Promise; appUpdateRecordComment(app_id: string, record_id: string, comment_id: string, body: { content: string; file_ids?: string[]; files?: unknown[]; }): Promise; appDeleteRecordComment(app_id: string, record_id: string, comment_id: string): Promise; appGetTableCommentCounts(app_id: string, table_id: string): Promise>; deployAppVersion(args: { app_id: string; source_archive: Buffer; dist_archive: Buffer; prev_version_id?: string | null; message?: string | null; /** * Alias → query declaration map ECHOED BACK from the live row (see * `readLiveQueries`), not read from the manifest. Every key the row holds * must survive the round trip — `description` included — because a server * that treats a present map as authoritative writes exactly what it is * sent, so a lossy echo silently strips whatever it forgot. */ queries?: Record; description?: string; }>; /** * Opt-in app capabilities from `package.json#lotics.capabilities`. The CLI * sends this on every deploy (defaulting to `{}`): the manifest is * authoritative, so an empty/absent block turns every capability off. * `comments: true` enables the members-only `useComments` primitive. */ capabilities?: { comments?: boolean; }; /** * The manifest's `lotics.workflows` KEYS — the workflow aliases the deployed * code declares (NOT the bindings; `set_app_workflow` / `remove_app_workflow` * own `apps.workflows`). Recorded on the new version so `remove_app_workflow` * refuses to unbind an alias the served version still calls. Always sent * (empty array when none declared). */ workflow_aliases?: string[]; agent_aliases?: string[]; query_aliases?: string[]; /** * Ship this version even though its manifest breaks what the app's published * API promises, snapshotting the broken contract as a new version. A * multipart field is text, so it goes over as the two spellings the server's * own schema admits. */ acknowledge_breaking_api_change?: boolean; }): Promise<{ version_id: string; version_number: number; bundle_size_bytes: number; }>; downloadFile(url: string, outputPath: string): Promise; /** * One page of this workspace's stored files, newest first — `GET /v1/files`. * * The only enumeration there is: every other file surface takes an id, so * without this "what is in the store" is answerable from Postgres alone, which * is exactly the escape hatch a CLI-first workflow exists to remove. Paged by a * keyset cursor the server mints; a caller walks until `next_cursor` is null. */ listFiles(opts?: { limit?: number; cursor?: string; }): Promise<{ files: Array<{ id: string; filename: string; mime_type: string; size?: number | null; created_at: string; }>; next_cursor: string | null; }>; downloadFileById(fileId: string, destination?: DownloadDestination, options?: { reserved?: Set; }): Promise<{ path: string; filename: string; stored_filename: string; }>; downloadRecordFiles(recordId: string, fieldKey: string, outputDir?: string): Promise>; uploadFiles(filePaths: string[], options?: { filenames?: string[]; }): Promise; private uploadLargeFile; /** * Store files from bytes the caller already holds — the path for a caller that * never had them on disk (an email attachment decoded in memory, a generated * document, a fetched URL). `uploadFiles` is this with a read in front, so * both routes hit one endpoint and one mime-derivation rule. */ uploadFileBytes(items: { bytes: Uint8Array; filename: string; mimeType?: string; }[]): Promise; }