# @lotics/cli

CLI and SDK for AI agents to interact with Lotics.

Lotics is an AI-powered operations platform. Through this CLI you can:

- Manage tables, records, and views (structured data with typed fields)
- Generate documents from templates (Excel, Word, PDF, Email)
- Build and run automations — schedules, webhooks, table lifecycle workflows, button actions, app-action workflows; inspect execution and version history
- Administer the workspace — invite members, manage groups, share resources, transfer ownership, browse connected accounts (OAuth attach is web-only)
- Create and manage apps, knowledge docs, and files

## Capability guides

**`lotics docs`** lists every reference the packages installed beside your project ship — each
`@lotics/*` package carries its own, so a doc always describes the version you actually have.
`lotics docs <area>` prints one (`lotics docs ai`); `lotics docs ui` prints a package's index.
Every answer is capped at a page, so a long doc hands back its section list and
`lotics docs <area>/<section>` — or, for a doc that is one table, `lotics docs ui/catalog/Button`.
Nothing is copied here — the packages, the areas, the titles and the versions are all read at run
time, so a doc or a whole package added upstream shows up without upgrading this CLI.

**Driving this CLI from an agent? Start at [AGENTS.md](./AGENTS.md)** (`node_modules/@lotics/cli/AGENTS.md`)
— which surface answers which question, the conventions that hold across every command, and the traps.
Per-command contracts, flags, exit codes and gotchas are in
[docs/cli_reference.md](./docs/cli_reference.md); `lotics --help` is the always-current verb list.

The platform's primary surfaces have dedicated usage guides that ship inside this
package (reachable at `node_modules/@lotics/cli/docs/*.md` once installed):

- [`docs/building_an_app.md`](docs/building_an_app.md) — building a custom-code app end to end:
  the sequence the steps go in (clarify → model → types → queries → workflows → screens → ship)
  and the deploy-free inner loop. Read it once before starting an app.
- [`docs/document_templates.md`](docs/document_templates.md) — generate finished documents
  (PDF, Excel, Word, email) by filling reusable templates: the five template types, the
  create → generate → chain lifecycle, and the marker capabilities.
- [`docs/migration.md`](docs/migration.md) — what to do when a release changes the shape of a
  project this CLI owns; read once, when something on disk no longer matches what the CLI writes.
- [`docs/knowledge_docs.md`](docs/knowledge_docs.md) — the AI's rulebook layer: authoring the
  workspace facts an agent can't guess, the access-vs-activation model, and the
  catalog-then-stage retrieval model agents use to pull only the lines they need.

## Start from the library, in one command

Install the CLI, then copy a starter — it creates the account, copies the starter into its
workspace, deploys its apps, and prints a one-time sign-in link:

```bash
curl -fsSL https://lotics.ai/install.sh | bash
lotics setup <starter_id> --email you@company.com
```

The installer downloads one compiled executable — no Node.js, no npm, and copying a starter needs
neither: every app is deployed on Lotics and nothing is written here. Node 18+ comes in only when
you pull an app's source to change it (`lotics app pull <app_id>`) and build it to deploy again.

Add `--json` for one machine-readable object instead of progress — the organization, the
workspace, the app ids, what was created, the sign-in link, and any warnings. Already signed in?
Drop `--email` and `lotics setup <apg_id>` copies into the account you have.

`lotics library list` reads both shelves with no account at all. **Presets** are a trade's model,
named by a slug: read one and write a `model.json` from it — nothing is copied. **Packages** are
apps and the tables they stand on, named `apg_…`, copied in whole; each row names how many tables
and which apps a copy creates, so what you actually manage can be matched against them.
`lotics library show <slug|apg_id>` reads one, with no account either — each table as
`alias · label`, each field as `alias:type`, and a preset's variants beside the sentence that
selects each.

## Or describe your own workspace

A `model.json` names the tables, fields, options, links, views, roles and first rows, and
`lotics setup` builds them. **Start from a preset when one is your trade** — name it instead of
restating it:

```json
{
  "from": "field_service",
  "variants": ["crews"],
  "rename": { "job": { "label": "Jobs", "fields": { "code": "Job no." } } },
  "rows": { "job": [{ "ref": "j1", "fields": { "code": "J-1" } }] }
}
```

No preset is your trade? Write the full form instead, spelling the tables out. Either way:

```bash
lotics scaffold docs                 # how to write one, both forms, with a worked example. Offline
lotics scaffold check model.json     # prove it — every problem at once, offline unless it names a preset
lotics setup model.json --email you@company.com
```

It creates no apps of its own: build one in the workspace afterwards
(`lotics docs building_an_app`), or name a published package in the file's `apply` list and it is
copied in — bound onto the tables the model just made — as part of the same run.
For a model that declares apps, `lotics workspace build model.json` is the whole path in one
command — check, apply where the workspace differs, then create or regenerate each app beside the
model file and check it, deploying each under `--deploy`. An app that conflicts or checks red is
named and the rest still run; `--dry-run` writes nothing anywhere.
`setup` refuses a table name your workspace already has; `lotics scaffold apply model.json` is the
additive verb — it adopts that table, adds what the model declares beyond it, and deletes nothing.
Because it never renames, the file and the workspace drift: `lotics scaffold diff model.json`
prints exactly where and exits non-zero on any difference, and
`lotics field rename <table> <field> "<new label>"` moves a label on the platform, in the file
(`--model`) and in every app bound to it (`--apps`) at once. `lotics scaffold apply model.json
--documents` writes only the file cells of the model's rows, onto the records the first run made —
rows land only into empty tables, so it is the one way back for attachments that were missed.

The other direction, once a workspace works: `lotics scaffold export > model.json` prints its
tables (or only `--tables tbl_a,tbl_b`) as that same file, findings on stderr. It is a starting
point for the next business, never a source of truth — the labels are this one's.

## Install

```bash
curl -fsSL https://lotics.ai/install.sh | bash    # macOS, Linux, WSL — no Node.js needed
npm install -g @lotics/cli                        # or, if you already have Node 18+
```

```powershell
irm https://lotics.ai/install.ps1 | iex           # Windows PowerShell — no Node.js needed
```

## Update

`lotics upgrade` updates this CLI in place, running whichever installer this copy came from. By
hand: re-run whichever installer you used; `npm install -g @lotics/cli@latest` updates an npm
install.

The CLI checks for updates once per day and prints a note on stderr, naming the right installer, when a new version is available.

## Authentication

**Two kinds of credential, and which you hold decides what `logout` does.** `auth signup` / `auth login` give this machine a **sign-in** — it acts as you, carries whatever role you have, and is yours to see and revoke at Settings → Security → *Keys and terminals*; it lapses after 90 days of disuse, and each use pushes that out. `auth api-key` saves an **API key** an admin issued. A key an admin creates in Settings has its own name and its own access, reaching either every app and table or only the ones chosen on the key; a key created FOR a person carries that person's access and stops working when that person is removed. Either kind often lives on a server and on other people's machines too, and only an admin revokes it. `lotics auth whoami` prints which kind this machine holds — from the saved profile, or from the server when the profile does not say.

When a credential stops working the refusal says which of the three ways it is dead — "… was revoked." / "… expired." / "… belongs to a member who is no longer active in this organization." — and its `hint` names the remedy: `lotics auth login <email>` for a terminal sign-in, ask an admin for a new key for an API key. A credential whose record does not say which of the two it is gets both. A key the server does not recognize at all gets one generic answer, deliberately: an unrecognized key learns nothing from being refused.

**`lotics auth signup`** — Creates a new Lotics account, organization, workspace, and API key in one step. Sends a magic link email so you can access the web app.

```bash
lotics auth signup                                   # interactive prompts
lotics auth signup a@b.com --name "Agent"            # non-interactive
```

**`lotics auth login`** — Signs in an account that already exists, on a machine that holds no key. **It does not wait for you.** It prints the page to open (also mailed to you) and the code that page must show, then exits. Sign in there if asked, check the code, press Confirm — and run whatever you wanted to run: the next command that needs a credential collects the key before doing its own work.

```bash
lotics auth login a@b.com            # prints the page + code, exits — nothing to keep open
# …press Confirm in the browser…
lotics auth whoami                   # picks up the key, then answers

lotics auth login a@b.com --wait     # one command instead: holds the terminal until Confirm
lotics auth login a@b.com --local    # pins this directory to that org (implies --wait)
```

A command run before you press Confirm names the page and the code again and exits 1; the request is good for 15 minutes, after which the next command says to ask again. Re-running `auth login` is always safe — it replaces the request, so only the newest code is the live one.

**`lotics auth web`** — Send a magic link email to access the web app (requires prior signup or setup).

```bash
lotics auth web
```

**`lotics auth api-key`** — Saves an API key (e.g. one created in the Lotics web app). The key belongs to one org, so this **registers that org as a profile** — run it once per org. Registering a second key adds a profile; it does not overwrite the first.

```bash
lotics auth api-key                  # interactive prompt
lotics auth api-key ltk_...          # registers the key's org as a profile (now active)
```

Run `lotics auth logout [<name|id>]` to remove a profile (default: the active org), or `lotics auth logout --all` to wipe the store. Inside a pinned directory the bare form removes the **pin**, not a profile — name the org to remove its credential. A profile saved by `auth login` / `auth signup` is a **sign-in**, and logging out revokes it server-side too. One saved by `auth api-key` is an admin-issued key that other machines may also hold, so it is only removed from this machine and stays active until an admin revokes it under Settings → API keys. A profile saved before the kind was recorded states nothing, so the server is asked and the credential is revoked only if the answer is a sign-in.

## Organizations

Each saved API key belongs to one org. Register a key per org once, then switch freely — no re-pasting:

```bash
lotics org                          # list saved orgs (marks the active one)
lotics org use acme                 # switch active org by name (or org id)
lotics org use "Acme Corp"          # names are case-insensitive
```

The org **id** is the identity; the name is the label the key was saved under, and it is yours — renaming the org in the app never moves it, so the scripts scoping by it keep running. `lotics auth whoami` learns the new name and records it beside the label; both resolve, and `lotics org` prints `label (server: name)` while they differ. A name matching two saved orgs is refused rather than guessed; use the id.

### Working in parallel (worktrees)

Pin a directory to its own org/workspace so a global `org use` elsewhere never disturbs it. The pin is a pointer — the key still comes from the global store, so there's nothing to paste:

```bash
cd my-worktree
lotics org use acme --local         # writes ./.lotics/config.json { active_org }
lotics workspace select wks_...     # records the workspace in the local pin
```

Each worktree resolves independently; switching the global default in another shell leaves pinned worktrees untouched. `.lotics/` should be gitignored.

Every command that resolves a workspace names its target before it acts — `lotics → Starters / Equipment & Projects` on **stderr**, so stdout stays clean for piping. Both halves are NAMES, since the line exists to be read before a destructive write; the workspace's name is cached beside its id the first time any command lists it, and until then the line prints the id and says the name is not known yet. When nothing in the directory or environment chose the org and it came from the machine-wide default, the line says so and prints the `--local` command to pin — that default is the one another shell can move between two of your commands. `lotics auth whoami` reports the same resolution on demand, including which source won.

### Resolution precedence

```
--api-key flag  >  LOTICS_API_KEY env  >  LOTICS_ORG env (name|id)
  >  local .lotics/config.json  >  app manifest workspace → its saved profile
  >  global active profile
```

`LOTICS_WORKSPACE` (or `--workspace <id>` / `-w`) overrides the workspace at any level. For ephemeral or CI use, set `LOTICS_API_KEY` instead of saving anything.

**`LOTICS_ORG` is resolved once, before any command runs.** A value matching no saved credential refuses every verb with one sentence — a read, a write, and a check that needs no credential alike — and the refusal lists the orgs this machine does hold, so it is answerable without another command. It refuses even when a key arrives another way: a variable that scopes the command must not go unread while `LOTICS_API_KEY` sends the write somewhere else. When the variable resolves AND a key is supplied, the key decides the org and the command says so. An org keeps the name it was saved under: a server-side rename never moves it, the new name resolves as well, and `lotics org` prints both when they differ.

### Diagnostics

Every request identifies the CLI (`user-agent: lotics-cli/<version> node/<v> <platform>`) and names the command that made it (`x-lotics-cli-command: app.workflow.set`), so a failure in the server's logs can be traced to the verb and version that produced it. Neither header carries arguments: the command chain stops before any id, path, `@file`, or JSON payload.

**`LOTICS_TELEMETRY=1` additionally records the session.** Off by default. Set it in your shell profile rather than per command — each invocation is its own process. When set:

- Requests carry a session id shared by every command in the sitting — under an agent harness it adopts the harness's own session id, otherwise it rolls over after 30 minutes idle (`~/.lotics/session.json`).
- Each invocation appends one record to `~/.lotics/telemetry-<host>.ndjson` — the command, its exit code, how long it took, and, on a failure, the message that was printed. One spool per Lotics the CLI talks to, so a record made against one instance is only ever sent to that instance. Batches are sent on a later run against the same host; a failed send is retried, never dropped silently.

Arguments contribute a **hash** and a **shape** (`records[].data.name:string`) and nothing else — no values, no file contents, no record data. The hash is the point: two failures in a row with the same hash mean the error message did not tell you enough to fix the call.

Unset, nothing is stored, nothing is sent, and no spool file is created.

### Reporting a problem

```bash
lotics report '{"goal":"rename a view","actual":"no command does it","wanted":"lotics run update_view"}'
lotics report @report.json       # a long one, from a file
cat report.json | lotics report -   # or from stdin — `-` is required, see below
lotics report                    # the frame, and what is worth reporting
```

This is the channel for what the records above **cannot** show. Everything they capture hangs off a
command that failed, which leaves out the two things worth the most: a capability that does not
exist (no command ran, so nothing was recorded) and a command that exited 0 having done the wrong
thing. Also worth sending: an error whose message did not tell you how to fix it, and anything that
made authoring slower than it should have been.

**A report is a frame, not a paragraph.** A log can reconstruct what you *ran*; nothing can
reconstruct what you *wanted*, and the gap between the two is the report.

| Field | | |
|---|---|---|
| `goal` | required | what you were trying to accomplish |
| `actual` | required | what happened instead |
| `expected` | | what you expected — the gap between this and `actual` is the defect |
| `tried` | | what you already tried, and what it said |
| `wanted` | | what would have unblocked you |

There is no severity or category to pick.

Reading from stdin needs an explicit `-`. With `-` the bare form always prints the frame and a
piped one waits as long as it takes.

If `LOTICS_TELEMETRY=1` is set, the commands from your session attach themselves — don't retype
them. It runs whether or not telemetry is on (invoking it is the consent that passive recording
needs an opt-in for), posts immediately, and tells you if it did not land.

Do not paste records, file contents, or credentials.

In an app project, `lotics app *` commands derive the credential from the directory when nothing explicit chose one: the manifest names the app's workspace, and when exactly one saved profile owns that workspace, that profile is used — the machine-wide default is never consulted. An explicit flag, env var, or directory pin still wins, and an org whose profile remembers a different workspace simply falls through to the announced default.

## Workspaces

Workspaces live inside the active org. If the org has more than one, select before running tools:

```bash
lotics workspace                    # list workspaces in the active org (marks current)
lotics workspace select wks_...     # set the workspace for the active scope (pin or profile)
lotics workspace create "Sales" --timezone America/New_York --currency USD
                                    # create a new workspace (admin only); without the flags it
                                    # takes the org's oldest workspace's zone and currency
lotics workspace settings --currency USD --timezone America/New_York
                                    # change the current workspace's name/currency/zone
lotics workspace delete wks_... --yes  # delete a workspace (admin only; soft delete, recoverable)
```

Single-workspace organizations auto-select on first use. The selection is remembered per org, so switching back lands where you left off.

## CLI

```bash
# Discover tools
lotics tools                    # list tools by category with descriptions
lotics tools query_records      # show full description + input schema

# Execute
lotics run query_tables '{}'
lotics run query_records '{"table_id":"tbl_...","field_keys":["name"]}'

# Large args (a knowledge-doc `content`, a bulk update) exceed the OS arg limit —
# read them from a file or stdin instead of an inline arg:
lotics run create_knowledge @args.json
cat args.json | lotics run create_knowledge -          # the trailing - reads stdin
echo '{"table_id":"tbl_..."}' | lotics run query_records -

# Upload files (multiple files and directories supported)
lotics upload ./report.pdf ./data.csv ./documents/

# Generate a file, then download it
lotics run generate_excel_from_template '{"..."}'
lotics download <file_id> -o ./reports/

# What is in the store, newest first — one page, with the command for the next one
lotics file list --limit 50
lotics file delete <file_id>          # refused while anything still references it

# CI / non-interactive (key inline)
LOTICS_API_KEY=ltk_... lotics run query_tables '{}'

# One-off against a saved org/workspace, no switching
LOTICS_ORG=acme LOTICS_WORKSPACE=wks_... lotics run query_tables '{}'
```

## Local .xlsx and .docx files

Edit them by scripting Lotics's own engines, published as `@lotics/xlsx` and `@lotics/docx`. Prefer them over `xlsx` / `exceljs` / `docx` from npm: they round-trip faithfully with the Lotics editor, template engine, and formula engine. `npm i @lotics/xlsx @lotics/docx`, then read the API guide that ships inside the package: `lotics docs xlsx` / `lotics docs docx` prints the installed copy (`node_modules/@lotics/<pkg>/AGENTS.md` on a machine with no CLI). Both packages ship `.ts` sources as their entry, so run the script under a TypeScript-aware runner.

```ts
import { readFile, writeFile } from "node:fs/promises";
import { parseDocx, replaceText, serializeDocx } from "@lotics/docx";

const { doc, count } = replaceText(await parseDocx(await readFile("in.docx")), "{{name}}", "Acme Ltd.");
if (count === 0) throw new Error("{{name}} is not in this document — nothing written");
await writeFile("out.docx", await serializeDocx(doc));
```

`lotics preview <file.xlsx|.docx>` draws one of those files to a PNG with the same engines, so a script's output can be looked at rather than guessed at.

## Knowledge docs

A file-native surface over the workspace's knowledge docs — the AI's rulebook layer. The body is a Markdown file: `create`/`update` read it from your filesystem, `get` writes it back. See [`docs/knowledge_docs.md`](docs/knowledge_docs.md) for the model.

```bash
lotics knowledge list                                         # catalog: id, name, description (--json)
lotics knowledge create --name "Shipping tariffs" \
  --description "HS-coded rates; searchable by lane and code" \
  --from ./tariffs.md                                         # or --content '<inline>'; prints the new id
lotics knowledge get kdc_... -o ./tariffs.md                  # body → file (omit -o for stdout; --json = full doc)
lotics knowledge update kdc_... --from ./tariffs.md           # send only what changed (--name / --description too)
lotics knowledge rm kdc_...                                   # archive
```

`create` / `update` send the body inline; the server mints and version-chains the content file (and `update` diffs + resolves concurrency internally — no token to pass). `get` is the one content-read path, hydrating the body server-side.

## Custom-code apps

```bash
# Scaffold / pull / deploy a Vite+React+TS app project
lotics app create "Sales Desk"            # scaffold + deploy v1
lotics app create "Orders API" --api       # no screens: its declarations are the whole surface,
                                           # so nothing is built and nothing is deployed
lotics app pull app_...                    # bootstrap an existing app locally (incl. .lotics/*)
lotics app deploy -m "Add quote drawer"    # typecheck + build + upload a new version
lotics app versions                        # deploy history: version, when, who, -m message (* = live)
lotics app versions app_...                # ...for any app, without pulling it first

# Apply the latest version of the package this app was COPIED from. Additive on
# the schema; a workflow or agent you have edited here is kept and named, and so
# is a field the new version stopped declaring. Redeploys from the published
# dist, so nothing local is sent — pull afterwards to edit the new code.
lotics app upgrade                         # the app this directory's manifest names
lotics app upgrade app_...                 # ...for any app, without pulling it first

# The app's API: what its declared queries, workflows and agents promise to a
# caller OUTSIDE the app — a customer's own site or server. Publishing snapshots
# that promise as a numbered contract; from then on a manifest write that would
# break it is refused and every breaking change is named, unless the write carries
# --acknowledge-breaking-api (app deploy / app query set / app workflow set /
# app agent set / app upgrade). A query that does not name the columns it returns
# is refused at publish: those field names are the table's, not the app's to
# promise.
lotics app api publish                     # snapshot the contract; prints the version + warnings
lotics app api status                      # is one published, and which version callers hold
lotics app api spec -o api.openapi.json    # the OpenAPI 3.1 document, for the consumer's generator
lotics app api unpublish                   # end the promise

# Regenerate .lotics/* WITHOUT a deploy: the .d.ts type companions (always) +
# the runtime app_fields.ts (when authenticated) — F/OPT maps that address
# fields + select options by stable display-name aliases instead of opaque ids.
# `app pull` writes both too (a deploy archive can never carry app_fields.ts);
# use this after a rename, or when a pull ran offline.
lotics app codegen                         # import { F, OPT } from "../.lotics/app_fields"

# Install @lotics/ui or @lotics/app-sdk from your CHECKOUT, to prove a kit change
# on a real app before it is published: build, pack into .lotics/kit/, install by
# file specifier, then hash one built file on both sides — a repack under the same
# name is otherwise served from the lockfile's first tarball. `app check` warns and
# `app deploy` refuses (--allow-local-kit ships it anyway), because that tarball is
# not in the source archive. --published puts the registry version back.
lotics app kit ../lotics/packages/ui
lotics app kit ../lotics/packages/ui --published

# Every deploy pre-flight, WITHOUT the build or the version row: the app's own
# typecheck over regenerated .lotics types, agent schemas vs the live app, aliases
# the code calls that nothing bound, undeclared capabilities, query drift. Exits 1 on what a deploy refuses, so CI can gate on it.
lotics app check

# --screens adds the rendered surface: each screen the app's navigation reaches,
# and the first record each one opens onto a PAGE, headless in Chrome at 1280
# and 375, measured against @lotics/ui docs/reviewing.md. --screen and --width
# narrow it while you iterate on one screen.
lotics app check --screens
lotics app check --screens --screen "Đơn hàng" --width 375

# Running an app's alias is a TOOL call, like every other tool. Exits non-zero
# when the RUN failed, not only when the call did.
lotics run run_app_workflow '{"app_id":"app_...","alias":"issueInvoice","inputs":{"record_id":"rec_..."}}'
cat args.json | lotics run run_app_workflow -  # bulk inputs bypass ARG_MAX
# Honest post-run harvest: created records + a paste-ready cleanup plan + the
# caveat (external/notification calls can't be auto-undone; sub-workflows may run).
lotics run run_app_workflow '{...}' --print-created
lotics run run_app_workflow '{...}' --cleanup   # also deletes created records (NOT a rollback)

# Edit workflow bodies as files. `app pull` writes src/workflows/<alias>.ts (the
# faithful server source, wrapped + referencing its .lotics/workflows/<alias>.globals.d.ts);
# edit the body, then push it back through set_app_workflow — the server verifies it
# (a deploy calls the same verb for every alias the project holds ahead of the app, so
# this is the one-alias spelling of it). `app workflow check` runs the server's own
# JS-subset parser + type-check locally, so a body that passes is one `set` will accept
# (`set` still adds name resolution + lint + structural checks, which need the workspace):
lotics app workflow pull                    # rewrite src/workflows/*.ts + globals from the server
lotics app workflow check                   # parse + typecheck every body locally ([alias...] for some)
lotics app workflow diff issueInvoice       # how the local body differs from the one the server runs
lotics app workflow set issueInvoice        # push the edited src/workflows/issueInvoice.ts

# Iterate on a named query WITHOUT a deploy: push package.json#lotics.queries.<alias>
# to apps.queries (server-validated like a deploy). apps.queries is manifest-
# authoritative, so the next `app deploy` re-syncs it — keep the manifest current.
lotics app query set openInvoices           # push package.json#lotics.queries.openInvoices

# Move one alias out of this app and into another of the same workspace — the
# declaration (and a workflow's body) land in the target project, the target is
# bound to its OWN row, and the source is unbound and undeclared here. No release
# either side. Run it in the source project; refused while anything here still
# calls the alias, unless you say --even-if-invoked.
lotics app query move openInvoices --to ../ke-toan
lotics app workflow move issueInvoice --to ../ke-toan

# Run a bound app agent end-to-end (no deployed UI needed — app row + declaration
# + member auth). Streams progress to stderr; reports the SETTLED run (structured
# output / final text) to stdout; exits 0 only when the run completed.
lotics run run_app_agent '{"app_id":"app_abc","alias":"recognize","input":{"image_file_id":"fil_..."}}'
cat input.json | lotics run run_app_agent -          # inputs via stdin/@file
lotics run run_app_agent '{...}' --json              # full run summary to stdout
# A run that outlives the call's bounded wait keeps going server-side; read it with
lotics run get_app_agent_run '{"app_id":"app_abc","run_id":"run_..."}'

# Dev-link @lotics/ui to a monorepo checkout for ONE command — nothing hand-written is touched
# (the matching tsc `paths` land in the CLI's own .lotics/tsconfig.link.json)
LOTICS_UI_SRC=/abs/monorepo/packages/ui/src lotics app dev
LOTICS_UI_SRC=/abs/monorepo/packages/ui/src lotics app deploy -m "..."   # warns: bundles YOUR kit copy
lotics app dev                             # unset ⇒ @lotics/ui resolves from node_modules again
```

`app codegen` puts a table in `app_fields.ts` when a declared query reads it or a bound workflow writes it — the queries come from `package.json#lotics.queries`, the written tables from each binding's own `table_ids`, so a table only a workflow body touches is addressable by alias with nothing to declare.

## SDK

```typescript
import { LoticsClient } from "@lotics/cli";

const client = new LoticsClient({ apiKey: "ltk_..." });

const { result } = await client.execute("query_tables", {});
const upload = await client.uploadFiles(["./report.pdf", "./data.csv"]);
await client.downloadFile(url, "./output.xlsx");
const { tools, categories } = await client.listTools();
const info = await client.getTool("query_records");
```
