@lotics/cli 0.284.0 → 0.284.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,6 +58,10 @@ async function uploadParts(options) {
58
58
  return done.sort((a, b) => a.part_number - b.part_number);
59
59
  }
60
60
 
61
+ // ../shared/src/cli_capabilities.ts
62
+ var CLI_CAPABILITIES_HEADER = "x-lotics-cli-capabilities";
63
+ var CLI_CAPABILITIES = ["docs-fallback"];
64
+
61
65
  // src/client.ts
62
66
  import crypto from "node:crypto";
63
67
  import fs from "node:fs";
@@ -244,6 +248,7 @@ var LoticsClient = class {
244
248
  }
245
249
  if (invocation2) {
246
250
  headers["x-lotics-cli-command"] = invocation2.command;
251
+ headers[CLI_CAPABILITIES_HEADER] = CLI_CAPABILITIES.join(",");
247
252
  if (invocation2.session !== null) {
248
253
  headers["x-posthog-session-id"] = invocation2.session;
249
254
  }
@@ -5,7 +5,7 @@ under an alias — a JS identifier (`/^[a-zA-Z_$][a-zA-Z0-9_$]*$/`):
5
5
 
6
6
  | Binding | Declared with | The app calls it with | An agent calls it with |
7
7
  |---|---|---|---|
8
- | Query | `set_app_query`, `set_app_queries` | `useQuery("<alias>", params?)` | `run_app_query` |
8
+ | Query | `set_app_queries` | `useQuery("<alias>", params?)` | `run_app_query` |
9
9
  | Workflow | `set_app_workflow` | `useWorkflow("<alias>")({ ...inputs })` | `run_app_workflow` |
10
10
  | Agent | `set_app_agent` | `useAgentRun("<alias>")({ ...inputs })` | — |
11
11
 
@@ -13,6 +13,12 @@ Each runs under the app's authority (`{ type: "app", app_id }`), the app owner's
13
13
  calling member's. Declaring one needs the app's owner or an admin. A write is live: the app's next
14
14
  call uses it. `get_app_capabilities` lists an app's aliases with their params and inputs.
15
15
 
16
+ Each reader returns a fingerprint of what it read — `get_app_query`'s `sha`, `get_app_workflow`'s
17
+ `body_sha`, `get_app_agent`'s `instructions_sha` — and a write built on that read passes it back:
18
+ `set_app_queries`' `expected_shas` (alias → `sha`), `set_app_workflow`'s `expected_body_sha`,
19
+ `set_app_agent`'s `expected_instructions_sha`, `remove_app_binding`'s `expected_sha`. The write is
20
+ refused, and writes nothing, when the binding changed since; omit it for an unconditional write.
21
+
16
22
  ## Queries
17
23
 
18
24
  A query declaration is `{ ast, params?, description?, templates? }`:
@@ -32,10 +38,10 @@ A save checks what a deploy checks: every table is in this workspace and within
32
38
  every projected, filtered and sorted field resolves, every param token is declared, and a key a
33
39
  node's kind does not take (a stray `sort`, `limit`, `filter`, `search`) is refused by name.
34
40
 
35
- `set_app_query` edits ONE alias and merges: send only the fields you change, `null` clears
36
- `params`, `description` or `templates`, and a new alias needs an `ast`. `set_app_queries` writes
37
- several at once: an alias it names is replaced whole, an alias it omits is kept.
38
- `remove_app_query` deletes one; `get_app_query` reads one back.
41
+ `set_app_queries` takes `{ <alias>: <declaration> }`, one alias or several, and merges each: send
42
+ only the fields you change, `null` clears `params`, `description` or `templates` (never `ast`), and a
43
+ new alias needs an `ast`. Any other field is refused by name. An alias it omits is kept.
44
+ `remove_app_binding` with `kind: "query"` deletes one; `get_app_query` reads one back.
39
45
 
40
46
  ### The query tree
41
47
 
@@ -135,7 +141,8 @@ name?, description? }`.
135
141
  - A call whose payload does not match `inputs` is refused before the body runs.
136
142
 
137
143
  `get_app_workflow` reads the body back; `dry_run_workflow` with `trigger_type: "app_workflow"`
138
- runs it against sample inputs before binding; `remove_app_workflow` unbinds it.
144
+ runs it against sample inputs before binding; `remove_app_binding` with `kind: "workflow"` unbinds
145
+ it.
139
146
 
140
147
  ## Agents
141
148
 
@@ -146,8 +153,7 @@ returns a typed result and keeps a run history per session. The declaration:
146
153
  - `tool_names` — the tools it may call, from these and no other: `analyze_pdf_template`,
147
154
  `code_edit_file`, `code_exec`, `code_read_file`, `code_write_file`, `excel_create_file`,
148
155
  `excel_find_cells`, `excel_format_range`, `excel_get_range`, `excel_update_range`,
149
- `generate_bank_qr_code`, `generate_excel_from_template`, `generate_image`,
150
- `generate_pdf_from_template`, `generate_qr_code`, `generate_word_from_template`, `get_template`,
156
+ `generate_bank_qr_code`, `generate_document`, `generate_image`, `generate_qr_code`, `get_template`,
151
157
  `grep_knowledge`, `list_banks`, `list_knowledge`, `lookup_business`, `query_templates`,
152
158
  `read_knowledge`, `run_app_query`, `run_app_workflow`, `validate_excel_template`,
153
159
  `vietcombank_convert_currency`, `view_files`, `word_create_document`, `word_find_text`,
@@ -178,4 +184,4 @@ returns a typed result and keeps a run history per session. The declaration:
178
184
  table. A result that validates is written to that row under the app's authority.
179
185
 
180
186
  Send only what you change: an omitted field keeps its stored value, `null` clears an optional one.
181
- `get_app_agent` reads one back; `remove_app_agent` deletes it.
187
+ `get_app_agent` reads one back; `remove_app_binding` with `kind: "agent"` deletes it.
@@ -19,7 +19,8 @@ workflow wrote while you tried it, stay where they are. Try a write on a throwaw
19
19
  - **An app stated in a model** — the default, and the right one for almost every job. `model.json`
20
20
  says how a row of each entity is recognised (`records`) and, in `apps`, one register over one
21
21
  entity and the record its rows open: its columns and filters, its sections in the order its work
22
- reaches them, its acts and the checks that guard them (`lotics docs model/apps`). The platform
22
+ reaches them, its acts and the checks that guard them (`lotics docs model/register`,
23
+ `model/record-page`, `model/acts`, `model/checks`). The platform
23
24
  compiles each app and draws it with the runtime every such app shares, so how each piece looks is
24
25
  the platform's, one way per concept, and no key changes it. Every write the app makes is a
25
26
  generated workflow that re-checks on the server what the model states.
@@ -77,6 +78,7 @@ distinction. Read what a field MEANS before you remove it.
77
78
 
78
79
  ```
79
80
  lotics docs model # how to write model.json, with a worked example
81
+ lotics docs design # how to design each app: the method, a treatment per kind of row
80
82
  lotics model apply model.json # check it, apply the tables, mint a version of every app
81
83
  lotics model apply model.json --app orders # only the apps named; the tables are applied whole
82
84
  lotics model apply model.json --plan # what the apply would change, writing nothing
@@ -91,6 +93,14 @@ table is empty, and mints one version per app, printing each app's id, the versi
91
93
  A table change is not undone by rolling an app back, so `--plan` first says what the apply would
92
94
  create or change in the tables and which apps it would create, update or refuse — writing nothing.
93
95
 
96
+ **An app leaving out a treatment its rows call for is refused** — readings over its rows, a record's
97
+ sections, an act per status move, a picture where its rows hold photos (`lotics docs design`). Each
98
+ refusal names the rule, and beside them comes each refused app's patch adopting its decisions: merge
99
+ the patches into the file in the order given, or state why the app stays as it is under the
100
+ `declines` key the refusal names (`lotics docs model/declines`), then apply again. `--plan` reads the
101
+ decisions beside the workspace's rows, and with `--json` gives each app as the patches leave it, its
102
+ `draft`.
103
+
94
104
  **The model changes after the app exists, and applying it again is how it lands.** Edit the file,
95
105
  apply it. An act whose write the model cannot say names its own `workflow`; that workflow's body is
96
106
  the live one, and `lotics run set_app_workflow` changes it. An act that reads papers runs an agent
@@ -108,7 +118,7 @@ lotics app deploy -m "<what changed>" # build, upload, a new version live
108
118
  ```
109
119
 
110
120
  What the app reads and writes is bound on the app, never in the project: `lotics run
111
- set_app_query` binds a named query, `lotics run set_app_workflow` a workflow body, and each mints a
121
+ set_app_queries` binds named queries, `lotics run set_app_workflow` a workflow body, and each mints a
112
122
  version. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build
113
123
  and carries every binding forward unchanged.
114
124
 
@@ -142,8 +152,8 @@ lotics run run_app_workflow '{"app_id":"app_…","alias":"…","inputs":{…}}'
142
152
  # exits non-zero when the run failed, so it is assertable
143
153
  ```
144
154
 
145
- A workflow is also how an app **produces a document** — the `generate_*_from_template` tools fill a
146
- template you registered once (`lotics docs document_templates`).
155
+ A workflow is also how an app **produces a document** — `generate_document` fills a template
156
+ you registered once (`lotics docs document_templates`).
147
157
 
148
158
  ## 7 — Look at it, then name it
149
159
 
@@ -164,6 +174,11 @@ The app is drawn live, as you, read-only: a screen that writes when it opens sho
164
174
  refused, and `errors` lists what failed on the page. A version that looks wrong is one
165
175
  `lotics run rollback_app` away from the one before.
166
176
 
177
+ For an app a model applied, `verdict` comes beside the images: what a `--plan` of the workspace's
178
+ model finds of that app now, beside its rows — what the next apply would refuse it for and the notes
179
+ on it, the `patch` adopting the decisions among them, and the app's body as the patch
180
+ leaves it (`draft`). `verdict_unread` says why none was read.
181
+
167
182
  Then set the icon, the colour and the app's own `description` through `lotics run update_app`. The
168
183
  `description` heads the capability listing the member's chat agent reads on **every** turn, so a
169
184
  standing process the app expects that agent to carry out belongs there and nowhere else.
@@ -27,7 +27,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
27
27
  | `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or stdin behind the `-` sentinel (`cat args.json \| lotics run <tool> -`) — both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). **Stdin is asked for, never guessed.** `lotics run <tool>` with no payload runs the tool with no arguments and returns at once. `lotics report` takes the same sentinel. In PowerShell use `@file`: quotes inside an inline argument are consumed by the shell, and the CLI reports the JSON it received with its quotes gone — the error names both escapes. |
28
28
  | `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |
29
29
  | `lotics run <tool>` — **file cells** | A file in a tool's result carries its `fil_…` id and metadata and **no `url`**, on every tool and in both output modes. That is not a broken file — this surface resolves no URL for a cell. Reach the bytes with `lotics file download <file_id>`, which takes the id straight from the cell; the text output says so whenever a result carries one. |
30
- | — | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_agent`, `run_app_query`) and every one that changes an app (`set_app_query`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command exists only for work that touches a local file: `model apply`, `model pull`, `app create --custom`, `app pull`, `app deploy`. |
30
+ | — | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_agent`, `run_app_query`) and every one that changes an app (`set_app_queries`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command exists only for work that touches a local file: `model apply`, `model pull`, `app create --custom`, `app pull`, `app deploy`. |
31
31
  | — | **The exit code reports the WORK, not just the call — for the two tools that RUN one.** `run_app_workflow` and `run_app_agent` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`) exit non-zero and print `<tool> → <status>: <message>` to stderr, so `lotics run … && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE — an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` is data, and exits 0. |
32
32
  | `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |
33
33
  | `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes — harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |
@@ -42,14 +42,14 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
42
42
  | `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` — one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |
43
43
  | `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** — the Library's list, `list_knowledge`, and the corpus `grep_knowledge` searches — while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as "everything". |
44
44
  | `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |
45
- | `lotics setup <model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes — `--name` and `--timezone` apply), then applies the model to its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** — it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as — that address IS the account it holds, not a second one. The file is a workspace MODEL, and it is read and checked before an account is created, because a file with a typo in it must not leave an organization behind. Then it is `lotics model apply` run on the new workspace: its tables, rows and apps; the sign-in link lands on its app when it has one, else on the workspace's app list. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently writes into an org the caller did not name — the message says how to do each thing on purpose. Without it, `setup` applies the model to the account you already have. A path positional after the file is accepted and IGNORED with a warning — `setup` writes nothing to disk — so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** — what `lotics model apply --json` does (`tables`; `apps`, each with `alias`, `app_id`, `version_id`, `origin`, `address` and `findings`; and `findings`) plus `organization_id`, `workspace_id` and `signin_url`, and a `warnings` array carrying everything the prose form would have said out of band, such as a sign-in link that could not be minted. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup …`. |
46
- | `lotics model apply <model.json> [--app <alias> ...] [--plan] [--json]` | **The model, applied to this workspace, through the `apply_model` tool.** The file is read and checked with the validator the server runs, every problem in one run, before anything is uploaded. Documents a row attaches by a path beside the file are uploaded first and the rows sent with their `fil_` ids; a path this workspace already recorded keeps its id, so a re-apply uploads nothing twice. Then the tool adopts or creates every table (the table this workspace bound the entity to, else an existing table of the same label, is ADOPTED and given the fields, options and views it lacks; no stored value changes), writes first rows only where every bound table is empty, and mints a new version of each app the model declares — `--app` (repeatable, comma-separated) narrows which apps, while the tables are applied whole. Prints one line per app — alias, `app_id`, `created`/`updated` with the version minted or `unchanged`, and the address it is served at — then the model's notes once, as the server states them. **`--plan` writes nothing and uploads nothing**: it prints what the apply would do to the tables (what it would create, what it leaves as the workspace has it, what the two disagree about), which apps it would create or update, and what it would refuse an app for, exiting 1 when it would refuse one; workflow bodies are checked only at apply, since they name fields a plan has not created. **A rollback restores an app's earlier version** (`lotics run rollback_app`); table changes and data writes stay. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, tables, apps, findings}` on stdout (with `--plan`, each table and app is what the apply would do), or `{ok: false, findings}` when the file does not check. |
47
- | `lotics model pull [-o <model.json>]` | **This workspace's model, rebuilt from what owns each part** — the tables, fields, options, templates and roles the workspace holds, how rows are recognised, and each app's body from its current version — through the `get_model` tool, as the file `model apply` reads: to stdout, or to the file `-o` names. What the workspace holds that a model cannot state is printed on stderr, never written into the file. Applying what it wrote changes nothing. |
45
+ | `lotics setup <model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes — `--name` and `--timezone` apply), then applies the model to its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** — it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as — that address IS the account it holds, not a second one. The file is a workspace MODEL, and it is read and checked before an account is created — the design decisions an apply would refuse it for too (`lotics docs design`), since a new workspace holds no rows to change them — because a file with a typo in it must not leave an organization behind. Then it is `lotics model apply` run on the new workspace: its tables, rows and apps; the sign-in link lands on its app when it has one, else on the workspace's app list. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently writes into an org the caller did not name — the message says how to do each thing on purpose. Without it, `setup` applies the model to the account you already have. A path positional after the file is accepted and IGNORED with a warning — `setup` writes nothing to disk — so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** — what `lotics model apply --json` does (`tables`; `apps`, each with `alias`, `app_id`, `version_id`, `origin`, `address` and `findings`; and `findings`) plus `organization_id`, `workspace_id` and `signin_url`, and a `warnings` array carrying everything the prose form would have said out of band, such as a sign-in link that could not be minted. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup …`. |
46
+ | `lotics model apply <model.json> [--app <alias> ...] [--plan] [--json]` | **The model, applied to this workspace, through the `apply_model` tool.** The file is read and checked against the model's own rules with the validator the server runs, every problem in one run, before anything is uploaded. **The apply refuses an app leaving out a treatment its rows call for** (`lotics docs design`) — judged by the server beside the workspace's rows, before it writes anything — until the app adopts it or states why not under the `declines` key the refusal names; the refusal carries each refused app's patch adopting its decisions, to merge in the order printed. Documents a row attaches by a path beside the file are uploaded first and the rows sent with their `fil_` ids; a path this workspace already recorded keeps its id, so a re-apply uploads nothing twice. **Where the file was pulled (`-o`) or applied in this workspace before, only what it changed since is sent**, as a `patch`: what changed in the workspace since and the file does not touch stays. What the file takes out is sent as a removal: it goes where an apply rebuilds it (an app's acts and columns, a write rule) and is refused, naming the tool that deletes it, where an apply never deletes it (a table, a field, an option, an app). A file that reorders items named by their `alias` sends the whole model, said on stderr. The copy each change is read against is kept per workspace and file under `~/.lotics/model_bases`: an apply narrowed by `--app` leaves in it the other apps as they were, so the next apply sends their edits again, and an apply that fails — refused, or cut off — leaves none, so the next sends the whole model. Then the tool adopts or creates every table (the table this workspace bound the entity to, else an existing table of the same label, is ADOPTED and given the fields, options and views it lacks; no stored value changes), writes first rows only where every bound table is empty, and mints a new version of each app the model declares — `--app` (repeatable, comma-separated) narrows which apps, while the tables are applied whole. Prints one line per app — alias, `app_id`, `created`/`updated` with the version minted or `unchanged`, and the address it is served at — then the model's notes once, as the server states them. **`--plan` writes nothing and uploads nothing**: it prints what the apply would do to the tables (what it would create, what it leaves as the workspace has it, what the two disagree about), which apps it would create or update, and what it would refuse an app for — the decisions among its findings, read beside the workspace's rows, with each refused app's patch — exiting 1 when it would refuse one; workflow bodies are checked only at apply, since they name fields a plan has not created. **A rollback restores an app's earlier version** (`lotics run rollback_app`); table changes and data writes stay. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, tables, apps, findings}` on stdout (with `--plan`, each table and app is what the apply would do, an app the patches change carrying its body as they leave it as `draft`, and `designs` each refused app's patch in merge order), or `{ok: false, findings}` when the file does not check. |
47
+ | `lotics model pull [-o <model.json>]` | **This workspace's model, rebuilt from what owns each part** — the tables, fields, options, templates and roles the workspace holds, how rows are recognised, and each app's body from its current version — through the `get_model` tool, as the file `model apply` reads: to stdout, or to the file `-o` names. What the workspace holds that a model cannot state is printed on stderr, never written into the file. Applying what it wrote changes nothing. With `-o`, what it wrote is the copy the next `model apply` of that file reads its changes against. |
48
48
  | `lotics app create <name> --custom [path]` | **A custom-code app**: creates the app (`POST /v1/apps`), scaffolds a Vite + React + TypeScript project into `[path]` (default `./<name>`, refused when not empty — before the app row exists) that depends on `@lotics/app-sdk` alone and draws with plain React, and installs it (`npm install --ignore-scripts`), then writes the declarations of the app's live bindings (`get_app_types`) into `.lotics/`, which the project's `tsconfig.json` includes. `package.json#lotics` names the app and its workspace, which is how `app deploy` in that directory finds both. The app has no version until the first `lotics app deploy`. `--custom` is required: an app the runtime draws from a model is made by `lotics model apply`. The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |
49
49
  | `lotics app pull [app_id] [path]` | **A custom-code app's live source, as a project ready to deploy on it.** The target is `[path]`, else this directory when it is the app's project (or no app is named), else `./<name>`. **The app's own project is brought up to date in place** — but only when it holds no edit since the version `package.json#lotics.current_version_id` names: its source (packed as a deploy packs it) is compared with that version's archive, ignoring `.lotics/` and `package.json#lotics`, and any difference refuses the pull, naming the changed files; a pull never merges, so local work is never lost. Already at the live version is a no-op that says so. **An empty or new directory receives the source whole**; any other directory is refused. Downloads come from `GET /v1/apps/{id}/versions/{version_id}/source`. `package.json#lotics` is then set to exactly the app, its workspace and the pulled version, dropping every other key an older CLI wrote there, so the next `lotics app deploy` builds on the live version; then `.lotics/` is written (`get_app_types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). A JSON app has no source tree: the server's refusal names `get_model`, and `lotics model pull` is its pull. |
50
50
  | `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`get_app_types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged — those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live, and names `lotics app pull`: run in this directory, it brings an unedited project up to date, and lists the files a project with edits changed, to carry over into a fresh pull. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |
51
- | `lotics docs` \| `lotics docs <area>[/<section>]` | **This CLI's own references, carried inside the binary** — the model reference, this index, and every doc under `docs/` — so the doc a reader opens always describes the binary answering. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |
51
+ | `lotics docs` \| `lotics docs <area>[/<section>]` \| `lotics docs [<area>[/<section>]] --grep <text>` | **This CLI's own references, carried inside the binary** — the model reference, this index, and every doc under `docs/` — so the doc a reader opens describes the binary answering, listed by the job a reader comes to do. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. **A reference the binary's copy lacks, or a part of one the server serves, is read from the server's `docs` tool** with this machine's credential, after the copy's refusal — a page the server added since this binary was built, which a server refusal can cite. A name matching more than one reference, or a part missing from a guide to this CLI, is answered by the copy alone. **`--grep` searches the references the server serves** (not this CLI's own guides, which it refuses), with this machine's credential, narrowed to the area or section named: literal text unless `--regex`, with `--case-sensitive`, `--diacritic-insensitive`, `--context-lines <n>` and `--limit <n>` — the dialect `grep_knowledge` reads — each hit under the address that opens its page; any of those options without `--grep` is refused. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |
52
52
  | `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script — the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe — a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |
53
- | `lotics docs model` \| `lotics docs model/<section>[/…]` | **The model reference, from inside the binary** — the one doc this CLI carries rather than resolves, because it describes this CLI's own model checker; listed first by `lotics docs`, at this CLI's version. Its first page is what a model composes with, the working order (jobs → entities and fields → `records` → one app per job → `model apply`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `"<entity-alias>:<ref>"`), the rules, and one complete worked example; it points at `https://lotics.ai/presets/index.json` for complete example models of several trades. **Offline, no account.** |
53
+ | `lotics docs model` \| `lotics docs model/<section>[/…]` | **The model reference, from inside the binary** — it describes this CLI's own model checker, at this CLI's version; `lotics docs` lists it under "Build an app", after `design`. Its first page is what a model composes with, the working order (jobs → entities and fields → `records` → one app per job → `model apply`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `"<entity-alias>:<ref>"`), the rules, and one complete worked example; it points at `https://lotics.ai/presets/index.json` for complete example models of several trades. **Offline, no account.** |
54
54
  | `lotics report '<json>'` \| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** — `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` — invoking it IS the consent that passive collection needs an opt-in for — but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. **Prints the id of each frame filed** — a filing nobody can cite cannot be answered about. The ids come from the server, so an instance that only logs the frames prints the count alone; the CLI never mints one of its own, which would hand back a token that resolves to nothing. |
55
55
 
@@ -121,7 +121,7 @@ spelling silently zeroes every row written before the field existed.
121
121
 
122
122
  ## Fields
123
123
 
124
- What `create_table` and `update_table` take in `add_fields`, and `update_table` in `update_fields`.
124
+ What `create_table` (on the CLI or in chat) and `update_table` take in `add_fields`, and `update_table` in `update_fields`.
125
125
 
126
126
  ### Types and formats
127
127
 
@@ -242,7 +242,7 @@ lookup — `source_field_key`, `lookup_field_key`, `order_by?`
242
242
  ### Changing a field
243
243
 
244
244
  A field added by `update_table` shows in a view only when `add_to_views` names it, at the view's far
245
- right; `update_view` with `move_field` places it beside the columns it belongs with.
245
+ right; on the CLI or in chat, `update_view` with `move_field` places it beside the columns it belongs with.
246
246
 
247
247
  An `update_fields` entry is `{ field_key, name?, description?, convert_to?, … }` with the same flat
248
248
  properties as `add_fields`, plus what only an update does:
package/docs/design.md ADDED
@@ -0,0 +1,225 @@
1
+ # Designing an app
2
+
3
+ Start here to build an app. This page is the method and the choice of treatment for each thing a job
4
+ reads; every key it names is a key of `model.json`, and the page of the `model` reference stating a
5
+ treatment's keys stands beside it in § Treatments. A treatment worked through in an app of a complete model
6
+ is a page at `examples/<treatment>` — `lotics docs`, or the `docs` tool.
7
+
8
+ A model states COMPOSITION — which fields, lists and acts each job's screen holds. How each piece looks is
9
+ the platform's, one way per concept, and no key changes it. What the model leaves unnamed is never drawn,
10
+ and a list the model leaves as a table is drawn as one, even where a calendar, a face or a picture would
11
+ answer the reader at a glance. That is the most common failure of a model, so the apply refuses an app leaving
12
+ out a treatment it requires — a register's readings, a record's sections and children, its status moves, a picture,
13
+ rows in time, a template's act — until the model states it or declines it.
14
+
15
+ ## The method
16
+
17
+ 1. **People and jobs, before any table.** Name each person by the work they do, then each JOB in one
18
+ sentence: who does it, what exists when it is done, the record it moves. The sentence is the app's
19
+ `name` and `description`. One app per job, each one register over one entity — an app per table, or one
20
+ app holding every table, is no job's screen.
21
+ 2. **The entities the jobs touch.** A status is a state the work sits in, six options or fewer: a touch
22
+ ("called") is a row of a log, a money fact ("paid in full") a formula the figure shows. A quantity is a
23
+ number with its `unit`, never words in a text field; a quantity of several kinds is child rows, one per
24
+ kind, totalled at the foot of their block.
25
+ 3. **How each row is recognised** — a `records` entry for every entity an app lists, opens or picks: its
26
+ `title` (who or what it is, never a code — the code is a `subtitle`), up to two `subtitle` fields the job
27
+ reads at first glance, its `image`, its `party`, its `status` and which options are `closed`, its
28
+ `figure`, its `due` dates.
29
+ 4. **Each app's composition** — the register's `columns` in the order the job reads them, as many as fit a
30
+ desk (one that does not fit is drawn nowhere in the row; what is read only on the way deeper is the
31
+ record's), the `filters` it narrows by every day (at most 3, each a field the rows show), its `layout`
32
+ and `readings`; the record's `sections` in the order the work reaches them, each one's `fields`, `blocks`
33
+ and `acts`; the `checks` that warn or refuse. A rule the business states — a field needed before an act,
34
+ a quantity bounded, a picker narrowed, a duplicate reused — is the model's (an act's `requires`, a check's
35
+ `blocks`, `write_rules`), and every write re-checks it; a check without `blocks` only warns.
36
+ 5. **A treatment for every list and every entity** — the next section. Choose it from what the rows ARE,
37
+ before writing any register.
38
+ 6. **Plan, apply, look.** The `apply_model` tool (`lotics model apply` on the CLI) with `plan` checks the
39
+ whole model beside the workspace's rows and writes nothing — the patch adopting each app's decisions in
40
+ `designs`, and the app's body as they leave it its `draft`; without it, it mints each app live. Then look at each app —
41
+ § Look at it — and judge it against § The visual bar. A correction is a change to the model, applied again.
42
+
43
+ ## Treatments
44
+
45
+ A finding naming a section below is a treatment the model left out. A decided one refuses the apply, and the
46
+ check gives each app it refuses a patch stating its decided treatments: merge the patches in the order the check gives
47
+ them, or state why the app stays as it is in its `declines` (`model/declines`), under the key the finding names —
48
+ then check again. A note refuses nothing.
49
+
50
+ | The rows are | The treatment | Its keys |
51
+ |---|---|---|
52
+ | People or organisations | `records.<entity>.party`, with a photo or logo as its `image` | `model/records` |
53
+ | Things with a photo, a scan or a paper | `records.<entity>.image`; `layout: "cards"` where the picture is how a row is found | `model/records`, `model/cards` |
54
+ | Read by their day | `layout: "calendar"`; on a record, an `agenda` block | `model/calendar`, `model/blocks` |
55
+ | Booked on a resource | `layout: "lanes"` with `lanes`, and `write_rules` `no_overlap` | `model/lanes`, `model/write-rules` |
56
+ | A plan of spans | `layout: "gantt"` with `gantt` | `model/gantt` |
57
+ | Who did what on which day | `layout: "roster"` with `roster` | `model/roster` |
58
+ | A log of what happened | a `timeline` block | `model/blocks` |
59
+ | Opened to be worked on | the record's `sections`, in the order the work reaches them — `at` where it moves in steps | `model/record-page` |
60
+ | Rows that each belong to another row | a `rows` block on that row's record | `model/blocks` |
61
+ | Moving through a status | an act per move, `status.history`, and the register's `readings` | `model/acts`, `model/records`, `model/readings` |
62
+ | Counted over a period | the register's `tabs` over one `period`; an `open` tab's `where` reads only what was true at the period's end, never a status the row moves on from | `model/tabs` |
63
+ | Papers a case gathers | a rows block with `expect`; the papers made, an act's `templates` | `model/blocks`, `model/acts` |
64
+ | Papers to read | an act's `intake` with `fills` | `model/acts` |
65
+ | Calls and visits | an act's `record` | `model/acts` |
66
+ | Documents covering lines | a rows block with `under` | `model/blocks` |
67
+ | One owner's question across jobs | a dashboard app | `model/dashboards` |
68
+
69
+ ### Faces and pictures
70
+
71
+ #### Party
72
+
73
+ `records.<entity>.party` is `"person"` where the rows are people (a contact, a patient, a driver) and
74
+ `"organization"` where they are companies (a customer, a supplier, a carrier). Every surface drawing one of
75
+ its rows — the register's row, a link's chip, a picker's option, the record's header — then draws a face:
76
+ its `image` (a photo, a logo), else its initials. Without it, a person is drawn as a thing. A workspace
77
+ member is a `select_member` field, and is drawn by their face already. Worked: `examples/party`.
78
+
79
+ #### Image
80
+
81
+ `records.<entity>.image` names the files field that pictures a row — a product's photo, a damage photo, a
82
+ scan, a receipt. Every row of it then leads with its picture, or its paper's preview, wherever it is drawn;
83
+ a link to it previews the file. Never a field an act keeps the paper it makes in (`into`). Register
84
+ `layout: "cards"` where the picture is how a row is found. A files field the record keeps as a folder of
85
+ papers is a `files` block, read whole. Worked: `examples/cards`.
86
+
87
+ ### Rows in time
88
+
89
+ Rows that each stand on a day, or hold a run of days or hours, are read where they stand in time — never
90
+ as two date columns. A row holds a span when its `records` line (title and subtitle) holds two dates, its
91
+ `records.frees` names the day it ends, or `write_rules.<entity>.no_overlap` books it on a row its `by` links to (a
92
+ chair, a machine). A `no_overlap` by a person or a value alone keeps one row at a time each — a member's periods in
93
+ turn — read by what they hold.
94
+
95
+ #### Calendar
96
+
97
+ `register.layout: "calendar"`: the rows by their day — a month, a week, a day or a list; an entry at its
98
+ hour where its date holds one, over its span where its line holds a second date. Worked: `examples/calendar`.
99
+
100
+ #### Lanes
101
+
102
+ `register.layout: "lanes"` with `lanes` naming a one-row link whose target's rows are the lanes (a chair, a
103
+ machine, a room), each drawn even when empty: every row a block from the first date on its line to the
104
+ second, and a free stretch adds a row there. `loads` reads what a lane carries against what it holds. A
105
+ `no_overlap` `by` the same link refuses two rows on one resource at once. Worked: `examples/lanes`.
106
+
107
+ #### Gantt
108
+
109
+ `register.layout: "gantt"` with `gantt.start` (and `end`, else the entity's first `due`): one bar a row over
110
+ its run of days — a shipment, a hire, a task of a project — read against today. `gantt.milestones` marks
111
+ dates on the bar, `planned` the plan under it, `progress` how far it is done, `after` the rows it waits on;
112
+ the register's `group` is its lanes. Worked: `examples/gantt`.
113
+
114
+ #### Roster
115
+
116
+ `register.layout: "roster"` with `roster` naming the child whose rows fill the days — one link back to the
117
+ register's entity and a date on its line: what each row did on each day (shifts, runs, attendance). With
118
+ `expect` (and `of`) the columns are the options of a select instead of days: which papers each row holds.
119
+ Worked: `examples/roster`.
120
+
121
+ #### Agenda
122
+
123
+ An `agenda` block on a record: a child's planned entries by day, soonest first, today marked, each with its
124
+ picture; within a day by the hour, or by a single select of the part of the day. `start` counts Day 1 from a
125
+ date of the record (a trip, a course).
126
+
127
+ #### Timeline
128
+
129
+ A `timeline` block on a record: a child read as a log of dated entries, newest first, with a composer — the
130
+ calls, visits and notes about the record, each with who and when, and its files where the child's `image`
131
+ names them. Entries planned ahead are an `agenda`; the status's moves are its history. Worked:
132
+ `examples/party`.
133
+
134
+ ### Moves and numbers
135
+
136
+ #### Moves
137
+
138
+ Rows a status stages towards its `closed` options move by acts: each moves one option on to the next (`when` the
139
+ status holds it, `set` to the next), at the foot of the section whose work it ends. A status with nothing closed is a
140
+ condition a person sets. A value decided as a row reaches an outcome — why it was lost, refused, voided — is that
141
+ act's `asks`, never a field the reader must remember to fill.
142
+
143
+ #### History
144
+
145
+ `records.<entity>.status.history` names a child the app's own writes append one row to per status move: its
146
+ link to the row, the new status, the moment and who. It keeps when each row entered each status and who
147
+ moved it; `record.history: true` draws it beside the record where the owner asks for that trail. Worked: `examples/approval`.
148
+
149
+ #### Readings
150
+
151
+ `register.readings` stand above the rows and read the rows in view. Lead with the job's headline number —
152
+ its exception as a formula's yes/no in a `metric`'s `where` (what is late, what is short), never a stage the
153
+ status chips already count — then at most the mix (`breakdown`) and the movement (`trend`), varied by job.
154
+ A `pivot` reads on a dashboard. A dashboard app (`dashboard`) answers one owner's question across jobs, each
155
+ reading windowed by the period the reader picks, and a `list` reading hands over the rows to act on. A
156
+ record's `metric`, `breakdown` and `trend` blocks read its own child rows. Worked: `examples/approval`, `examples/dashboard`.
157
+
158
+ ### The record
159
+
160
+ #### Sections
161
+
162
+ `record.sections`, in the order the work reaches them: the fields people write, a `files` block per paper the record
163
+ keeps, a block per child, the acts at each one's foot. A register laid out in time (calendar, lanes, roster) opens the
164
+ default record: every field a person writes.
165
+
166
+ #### Children
167
+
168
+ Rows that each belong to one row of another entity (a one-row link to it) are drawn under that row: a `rows` block on
169
+ its record, a `timeline` where they are a log, an `agenda` where they are planned on a day and within it, a roster of
170
+ them, or lanes of them by that link. Which children a record lists is the author's — one of them at least.
171
+
172
+ ### Papers
173
+
174
+ #### Expect
175
+
176
+ A rows block with `expect` naming the child's title — a single select of kinds, or a one-row link to a
177
+ catalog: one line per kind, the ones not yet filled standing empty, filled in place. `of` narrows the kinds
178
+ to the record's own multi-select of those it needs. The papers a case gathers, the fees a record knows it
179
+ owes. Worked: `examples/case`.
180
+
181
+ #### Templates
182
+
183
+ A paper the business makes is an act's `template`, kept in the record's files field `into` — or several at
184
+ once, `templates`: listed where the act stands, each made one as its file and each not yet made as a
185
+ placeholder, the reader ticking which to make. A template no act names is made by nothing. Worked: `examples/case`.
186
+
187
+ #### Intake
188
+
189
+ A paper the business is brought and reads is an act's `intake`: the press takes the papers, an agent reads
190
+ them, and each is filed as the line of its kind in the child the record `expect`s, while `fills` writes what
191
+ they state into the record's fields — shown before it is saved. Worked: `examples/case`.
192
+
193
+ #### Under
194
+
195
+ A rows block with `under`: the documents that cover lines (an invoice over its fee lines) stand as headings
196
+ over the lines they cover, and a line no document covers offers to make one.
197
+
198
+ ### Calls
199
+
200
+ #### Record act
201
+
202
+ An act's `record`: the press records a call, visit or meeting — its audio, its transcript, its screen where
203
+ captured — and files it as a new row of the record's `timeline` child (`into`), stamped `at` and `by`;
204
+ `fills` lets an agent fill that row's fields from the transcript. Worked: `examples/calls`.
205
+
206
+ ## The visual bar
207
+
208
+ - A person shows a face, a thing its picture, a file its preview, a plan its days on a time axis, a
209
+ quantity a meter (`records.limits`) or a chart, a register its `readings` under its title, and long text
210
+ reads whole (a `text` block).
211
+ - Every category the job reads is coloured: each select option's `color`, and a `mark` where a brand or a
212
+ glyph names it — on every option of a select read down a column (how it was paid, the channel, the mode).
213
+ - Every files field is drawn somewhere — a section's `fields`, a `files` block, the `image`, or where the
214
+ act making it stands.
215
+ - A screen with none of these is bland, and bland is a finding: text drawn where a treatment above exists
216
+ is a change to the model.
217
+
218
+ ## Look at it
219
+
220
+ After an apply, the `screenshot_app` tool draws each app as its people see it — the register, and a record
221
+ by its `path` — at a desk's width and a phone's. Beside the shots, its `verdict` is what a plan of the workspace's
222
+ model finds of the app now, beside its rows: what the next apply would refuse it for and the notes on it, the
223
+ `patch` adopting the decisions among them, and the app's body as the patch leaves it (`draft`). Judge each shot
224
+ against § The visual bar, change the model, and apply again. The `rollback_app` tool returns an app to an earlier
225
+ version; the tables and rows stay as they are.
@@ -12,11 +12,11 @@ run `lotics tools <tool_name>` — that is always the source of truth for argume
12
12
 
13
13
  | Type | What it fills | Output | Create with |
14
14
  |---|---|---|---|
15
- | `pdf-form` | fields overlaid on an **uploaded PDF** at fixed positions | PDF | `create_pdf_template` (mode `form`) |
16
- | `html` (a PDF) | an **HTML + Handlebars** layout you write from scratch | PDF | `create_pdf_template` (mode `html`) |
15
+ | `pdf-form` | fields overlaid on an **uploaded PDF** at fixed positions | PDF | `create_pdf_template` (mode `form`), on the CLI or in chat |
16
+ | `html` (a PDF) | an **HTML + Handlebars** layout you write from scratch | PDF | `create_pdf_template` (mode `html`), on the CLI or in chat |
17
17
  | `excel` | an **uploaded `.xlsx`** with `{{marker}}` cells | `.xlsx` | `create_excel_template` |
18
18
  | `word` | an **uploaded `.docx`** with `{{marker}}`s | `.docx` | `create_word_template` |
19
- | `email` | an **inline HTML + Handlebars** body, rendered when the email is sent | email | `create_email_template` |
19
+ | `email` | an **inline HTML + Handlebars** body, rendered when the email is sent | email | `create_email_template`, on the CLI or in chat |
20
20
 
21
21
  Two shapes underneath: **file-backed** (`pdf-form`, `excel`, `word`) clone an uploaded
22
22
  office file and mark it up; **inline** (`html`, `email`) store the markup you author directly.
@@ -25,31 +25,31 @@ office file and mark it up; **inline** (`html`, `email`) store the markup you au
25
25
 
26
26
  1. **Create** a template — register the file (or inline markup) and declare a `variables`
27
27
  map (the named slots the template fills). This returns a template id (`dtl_…`).
28
- 2. **Generate** a filled file — call the matching `generate_*_from_template` with the
28
+ 2. **Generate** a filled file — call `generate_document` with the
29
29
  template id, a `filename` (no extension), and a `data` map keyed by your variable names.
30
30
  It substitutes the markers and returns a **generated file** (with a `file_id`).
31
31
  3. **Chain** the result — generation only *produces* the file. Attaching it to a record,
32
32
  sending it, or saving it locally is a separate step: inside a workflow, pass the returned
33
33
  `file_id` to the next step; from the CLI, `lotics download <file_id>` fetches it.
34
34
 
35
- Email is the exception: there is **no** `generate_email_from_template`. An email template is
35
+ Email is the exception: `generate_document` refuses an email template. An email template is
36
36
  rendered to a subject + body **at send time** from the template and the data the send step
37
- supplies — so you author it with `create_email_template` and a workflow/app send step (or the
37
+ supplies — so you author it with `create_email_template` (on the CLI or in chat) and a workflow/app send step (or the
38
38
  web composer) does the rendering and delivery. External agents send their own email directly.
39
39
 
40
40
  ## Creating each type
41
41
 
42
- ### PDF from HTML (`create_pdf_template`, mode `html`)
42
+ ### PDF from HTML
43
43
 
44
- Write a full HTML document with inline CSS and `{{variable}}` placeholders; it renders to PDF
45
- via a headless browser. Good for invoices, reports, letters, certificates — anything whose
44
+ Write a full HTML document with inline CSS and `{{variable}}` placeholders, as `create_pdf_template`
45
+ with mode `html` on the CLI or in chat; it renders to PDF via a headless browser. Good for invoices, reports, letters, certificates — anything whose
46
46
  layout you control. Declare `variables` (plain text, rich `html`, an auto-rendered `table`,
47
47
  or a `list`), and optionally `format` (page size) and `orientation`. See
48
48
  `lotics tools create_pdf_template` for the exact variable shapes.
49
49
 
50
- ### PDF form (`create_pdf_template`, mode `form`)
50
+ ### PDF form
51
51
 
52
- For an existing PDF you must fill in place (a government form, a printed contract):
52
+ For an existing PDF you must fill in place (a government form, a printed contract), on the CLI or in chat:
53
53
 
54
54
  1. `lotics upload ./form.pdf` → a `file_id`.
55
55
  2. `create_pdf_template` with mode `form` and that `file_id`.
@@ -59,7 +59,7 @@ For an existing PDF you must fill in place (a government form, a printed contrac
59
59
 
60
60
  Form-mode data is scalar-only (text, numbers, checkboxes) — one value per positioned field.
61
61
 
62
- ### Excel (`create_excel_template`)
62
+ ### Excel
63
63
 
64
64
  1. Build a `.xlsx` in Excel (or with any spreadsheet library) and put `{{marker}}`s in the cells that
65
65
  should be filled. `lotics upload ./template.xlsx` → a `file_id`.
@@ -68,10 +68,10 @@ Form-mode data is scalar-only (text, numbers, checkboxes) — one value per posi
68
68
  placement) blocks the save, and declared variables with no matching marker come back in
69
69
  `unmarked_variables` (skipped, not filled).
70
70
 
71
- `validate_excel_template` checks an uploaded file's markers without creating a template. Pass
71
+ On the CLI, `validate_excel_template` checks an uploaded file's markers without creating a template. Pass
72
72
  raw numbers / ISO dates / booleans at generate time — the cell's number format handles display.
73
73
 
74
- ### Word (`create_word_template`)
74
+ ### Word
75
75
 
76
76
  1. Build a `.docx` with `{{marker}}`s where values go. `lotics upload ./template.docx`.
77
77
  2. `create_word_template` with the `template_file_id`. `variables` is optional — every
@@ -84,9 +84,9 @@ raw numbers / ISO dates / booleans at generate time — the cell's number format
84
84
  At generate time, `data` must provide a key for **every** marker — pass `""` for fields
85
85
  that should render blank; a missing key fails with the full list of missing markers.
86
86
 
87
- ### Email (`create_email_template`)
87
+ ### Email
88
88
 
89
- Inline HTML + Handlebars, no uploaded file. Declare `variables`, and optionally a default
89
+ Inline HTML + Handlebars, no uploaded file — `create_email_template`, on the CLI or in chat. Declare `variables`, and optionally a default
90
90
  `subject` (which itself supports `{{variable}}` expressions) and default to/cc/bcc. The body
91
91
  and subject render from the data at send time.
92
92
 
@@ -120,15 +120,16 @@ Run `lotics tools create_excel_template`, `create_word_template`, `create_pdf_te
120
120
 
121
121
  ```bash
122
122
  # See what a template expects, then fill it
123
- lotics tools generate_excel_from_template
123
+ lotics tools generate_document
124
124
  lotics run get_template '{"template_id":"dtl_..."}' # its declared variables
125
- lotics run generate_excel_from_template '{"document_template_id":"dtl_...","filename":"invoice-1042","data":{"company_name":"Acme","total":1042,"items":[...]}}'
125
+ lotics run generate_document '{"document_template_id":"dtl_...","filename":"invoice-1042","data":{"company_name":"Acme","total":1042,"items":[...]}}'
126
126
  lotics download <file_id> -o ./out/
127
127
  ```
128
128
 
129
129
  - `data` is a map keyed by your variable names. Scalars for scalar/form fields; arrays for
130
130
  `table`/`list`/loop variables. `filename` excludes the extension (the type sets it).
131
- - Each `generate_*_from_template` returns a generated file object; take its `file_id` onward.
131
+ - `generate_document` renders the template's own type and returns a generated file object; take
132
+ its `file_id` onward.
132
133
  - In a workflow, a **generate step** calls the same tools and hands the `file_id` to the next
133
134
  step (attach to a record, send as an attachment, etc.).
134
135
 
@@ -140,8 +141,8 @@ Four unified tools work across all five types:
140
141
  |---|---|
141
142
  | `query_templates` | list templates (optionally filter by type: excel/word/pdf/email) |
142
143
  | `get_template` | one template's full definition, incl. the `variables` it expects |
143
- | `clone_template` | copy a template to iterate on |
144
- | `delete_template` | remove a template |
144
+ | `clone_template` | copy a template to iterate on, on the CLI or in chat |
145
+ | `delete_template` | remove a template, on the CLI or in chat |
145
146
 
146
147
  Call `get_template` before generating when you don't already know a template's variable names.
147
148