@lotics/cli 0.316.19 → 0.316.21

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.
@@ -525,9 +525,9 @@ var LoticsClient = class {
525
525
  return { path: absolutePath, filename, stored_filename: originalFilename };
526
526
  }
527
527
  async downloadRecordFiles(recordId, fieldAlias, outputDir) {
528
- const result = await this.execute("get_record", { record_id: recordId }, { format: "text" });
528
+ const result = await this.execute("query_records", { record_ids: [recordId] }, { format: "text" });
529
529
  if (result.error) throw new Error(result.error);
530
- const record = result.result;
530
+ const record = result.result?.records?.[0];
531
531
  const files = record?.data?.[fieldAlias];
532
532
  if (!Array.isArray(files)) {
533
533
  throw new Error(`Field ${fieldAlias} on ${recordId} is not a file field or has no value`);
@@ -18,7 +18,7 @@ are by its own — `get_app` lists them in the one roster, and nothing of a log
18
18
 
19
19
  ## Finding what an app offers
20
20
 
21
- `get_workspace` maps the workspace. Its `search` matches the keys and descriptions of the Query and Workflow nodes an app declares, and the app's line names the matching keys; it never matches the generated `act_`, `create_` or `update_` aliases. `get_app` then gives the contracts. An agent runs a declared node before it falls back to `aggregate_records`.
21
+ `get_workspace` maps the workspace. Its `search` matches the keys and descriptions of the Query and Workflow nodes an app declares, and the app's line names the matching keys; it never matches the generated `act_`, `create_` or `update_` aliases. `get_app` then gives the contracts. An agent runs a declared node before it falls back to `query_records`.
22
22
 
23
23
  ## What every app generates
24
24
 
@@ -182,7 +182,7 @@ run_app_workflow {"app_id":"app_…","alias":"act_hand_over","inputs":{"record_i
182
182
  |---|---|
183
183
  | `inputs` | The workflow's declared inputs. An update takes the record and only the fields that change |
184
184
  | `dry_run` | `true`: the run is made and taken back, so nothing is kept, and the answer is what the run would return, with the calls it would make — or `status: "not_previewed"` where the workflow cannot be dry-run (§ A dry run) |
185
- | `as_member` | With `dry_run`, for a workspace admin: the run is judged as that member would press it — their stage owners, row rules and app access — and writes nothing. Anyone else is refused; `dry_run` on a record tool takes it the same way, and so do `query_records`, `aggregate_records` and `get_record`, which answer what that member would read |
185
+ | `as_member` | With `dry_run`, for a workspace admin: the run is judged as that member would press it — their stage owners, row rules and app access — and writes nothing. Anyone else is refused; `dry_run` on a record tool takes it the same way, and so does `query_records`, which answers what that member would read |
186
186
  | `runs` | Several runs in one call, 50 at most, in order, each its own write, answered under `runs`. The first one refused, failed or not dry-run ends the call. An input `{"from_run": 0, "path": "data.record_id"}` is an earlier run's answer at that path. A run states its own `dry_run`, or takes the call's. Dry runs one after another share their rows, taken back together, so a run reads what the one before it made; a run reads a dry run's answer only from a run dry-run in that stretch |
187
187
 
188
188
  ```
@@ -259,7 +259,7 @@ and the runs before it stand. A refused read fails the same way, naming the para
259
259
  ## Declaring more
260
260
 
261
261
  What the business asks an agent in the app again and again, and no screen draws, is stated in the app's tree as Query and Workflow nodes, beside its
262
- Acts: every figure or list whose definition is the business's own (open orders, what a customer owes, low stock) and every recurring operation (close the month, send to the accounting service, chase overdue invoices), each with a `description` written for an agent. A question asked once stays on `aggregate_records`. A Workflow gets an Act only where a person presses it; work only an agent does has no button. Both are checked and compiled when the tree is written (`set_app`; `reference/query-node`, `reference/workflow-node`).
262
+ Acts: every figure or list whose definition is the business's own (open orders, what a customer owes, low stock) and every recurring operation (close the month, send to the accounting service, chase overdue invoices), each with a `description` written for an agent. A question asked once stays on `query_records` with `aggregate`. A Workflow gets an Act only where a person presses it; work only an agent does has no button. Both are checked and compiled when the tree is written (`set_app`; `reference/query-node`, `reference/workflow-node`).
263
263
 
264
264
  **A query** — the rows of one table, or one figure of them, in the app's own aliases:
265
265
 
@@ -301,8 +301,8 @@ a Reading's `key` take no name the app generates (`reference/app.generated-name`
301
301
  }
302
302
  ```
303
303
 
304
- `set_app_workflow` sets its code (`workflows`). A query set on an app outside
305
- the tree (`set_app_queries`, on the CLI or in chat) is removed by the next write, whose answer notes it; declared as a Query
304
+ `set_workflow` sets its code (`workflows`). A query set on an app outside
305
+ the tree (`set_app_queries`, on the CLI) is removed by the next write, whose answer notes it; declared as a Query
306
306
  node, it is the tree's. A caller sees the description and
307
307
  each input's `description` and nothing else, so each says what the value is and what leaving it out means;
308
308
  the check notes one that says nothing.
@@ -5,17 +5,16 @@ 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_queries` (CLI, chat) | `useQuery("<alias>", params?)` | `run_app_query` |
9
- | Workflow | `set_app_workflow` | `useWorkflow("<alias>")({ ...inputs })` | `run_app_workflow` |
8
+ | Query | `set_app_queries` (CLI) | `useQuery("<alias>", params?)` | `run_app_query` |
9
+ | Workflow | `set_workflow` | `useWorkflow("<alias>")({ ...inputs })` | `run_app_workflow` |
10
10
 
11
11
  Each runs under the app's authority (`{ type: "app", app_id }`), the app owner's reach — never the
12
12
  calling member's. Declaring one needs the app's owner or an admin. A write is live: the app's next
13
13
  call uses it. `get_app` lists an app's aliases with their params and inputs.
14
14
 
15
- Each reader returns a fingerprint of what it read — `get_app_workflow`'s `body_sha`, and on the CLI and in chat
15
+ Each reader returns a fingerprint of what it read — `get_workflow`'s `version`, and on the CLI
16
16
  `get_app_query`'s `sha` — and a write built on that read passes it back:
17
- `set_app_queries`' `expected_shas` (alias → `sha`), `set_app_workflow`'s `expected_body_sha`,
18
- `remove_app_binding`'s `expected_sha`. The write is
17
+ `set_app_queries`' `expected_shas` (alias → `sha`), and `set_workflow`'s `expected_version`. The write is
19
18
  refused, and writes nothing, when the binding changed since; omit it for an unconditional write.
20
19
 
21
20
  ## Queries
@@ -37,10 +36,10 @@ A save checks what a deploy checks: every table is in this workspace and within
37
36
  every projected, filtered and sorted field resolves, every param token is declared, and a key a
38
37
  node's kind does not take (a stray `sort`, `limit`, `filter`, `search`) is refused by name.
39
38
 
40
- On the CLI and in chat, `set_app_queries` takes `{ <alias>: <declaration> }`, one alias or several, and merges each: send
39
+ On the CLI, `set_app_queries` takes `{ <alias>: <declaration> }`, one alias or several, and merges each: send
41
40
  only the fields you change, `null` clears `params`, `description` or `templates` (never `ast`), and a
42
41
  new alias needs an `ast`. Any other field is refused by name. An alias it omits is kept.
43
- `remove_app_binding` with `kind: "query"` deletes one; `get_app_query` reads one back.
42
+ An alias set to `null` is unbound, under the same `expected_shas`; `get_app_query` reads one back.
44
43
 
45
44
  ### The query tree
46
45
 
@@ -128,8 +127,9 @@ keeps the type tracking the live field.
128
127
 
129
128
  ## Workflows
130
129
 
131
- `set_app_workflow` binds a workflow body to an alias: `{ app_id, alias, source, inputs?, outputs?,
132
- name?, description? }`. On an app written as a tree the alias is one the tree names — a Workflow node's
130
+ `set_workflow` binds a workflow body to an alias: `{ workflow_id?, name?, description?, source }`, the `source`
131
+ opening with `on({ type: "app", app_id, alias, inputs?, outputs? })`. Without a `workflow_id` it binds the alias; with
132
+ the one `get_app` lists for it, it replaces the body. On an app written as a tree the alias is one the tree names — a Workflow node's
133
133
  `key` or an Act's `workflow` — and the node states `inputs`, `outputs` and `description`
134
134
  (`reference/workflow-node`, whose value shapes differ from the **Inputs** below): a call stating any of
135
135
  them for it is refused, and outputs the node leaves out are derived from the body. An alias the tree
@@ -137,20 +137,21 @@ names nowhere is archived by the next `set_app`. A Workflow node runs only for s
137
137
  app: on an app shared by a public link, a visitor's call of it is refused as an alias the app does
138
138
  not have. This page's `inputs` and `outputs` are for a custom-code app.
139
139
 
140
- - `source` is the body with no `on({...})` trigger — the app is the trigger. It reads each declared
140
+ - `source` is the header, then the body; the app is the trigger. The body reads each declared
141
141
  input as `trigger.app_workflow.inputs.<name>`; no record is in scope, so it reads each one by id
142
- with `get_record`.
142
+ with `query_records` and `record_ids`.
143
143
  The body's steps are the `workflows` reference.
144
- - `inputs` is the payload the call site passes (see **Inputs**). Omitted, an existing workflow keeps
144
+ - `inputs` in the header is the payload the call site passes (see **Inputs**). Omitted, an existing workflow keeps
145
145
  its inputs; on a first bind it takes an untyped payload. `{}` clears them.
146
- - `outputs` is what `return({ data })` hands back (see **Outputs**). Usually omit it: the type is
146
+ - `outputs` in the header is what `return({ data })` hands back (see **Outputs**). Usually omit it: the type is
147
147
  derived from the body's `return({ data })`, and the result echoes the bound `outputs`. Declared,
148
148
  the body does not save unless its return matches.
149
149
  - A save checks the body — parse, type-check against the declared inputs, names, lint, structure —
150
- and answers with `[source/code] location: message` lines. `verify_only: true` makes every check
150
+ and answers with `[source/code] location: message` lines. `dry_run: true` makes every check
151
151
  and writes nothing. A call naming a live alias replaces its body in place, keeping its run history.
152
152
  - A call whose payload does not match `inputs` is refused before the body runs.
153
153
 
154
- `get_app_workflow` reads the body back; `set_app_workflow` with `verify_only` and `record_id` or `preview_inputs`
155
- rehearses a draft before binding; on the CLI and in chat, `remove_app_binding` with
156
- `kind: "workflow"` unbinds it.
154
+ `get_workflow` reads the source back; `set_workflow` with `dry_run` and `record_id` or `preview_inputs`
155
+ rehearses a draft before binding; `set_workflow` with `delete: true` and the `workflow_id` unbinds it. An unbind is refused
156
+ while the deployed version of the app calls the alias or an agent of the app declares it (`even_if_invoked` overrides it for a
157
+ workflow that has run, only when the caller that ran it is gone).
@@ -83,8 +83,7 @@ lotics app pull <app_id> # the app's tree, as ./<name>.app.json
83
83
  A new workspace is built in one order: one `set_tables` call with the tables, then every `set_app`
84
84
  call, then fixes. One call carries at most 50 tables, 200 first rows a table, and 2000 rows and 2000
85
85
  documents in all; a workspace of more tables is built in further calls, each naming the tables already made by
86
- their ids. The example files at `https://lotics.ai/presets/index.json` are read and their tables passed to
87
- `set_tables`.
86
+ their ids.
88
87
 
89
88
  A table is a document: its `fields` are keyed by alias, and a link field names the table it
90
89
  points at by id, or by the `ref` of another table of the same call, and states `multi`. A
@@ -123,10 +122,10 @@ Readings over its rows, a record's Sections, an Act per status move, a picture w
123
122
 
124
123
  **The tree changes after the app exists, and applying it again is how it lands.** Pull the tree, edit
125
124
  the file, apply it; or send `nodes` for one part. An Act whose write the tree cannot say names its own `workflow`; that workflow's code is
126
- the live one, and `lotics run set_app_workflow` changes it. A job no press starts — an agent or a
125
+ the live one, and `lotics run set_workflow` changes it. A job no press starts — an agent or a
127
126
  service handing the app a file or a list of rows — is a Workflow node the app declares
128
127
  (`lotics docs reference/workflow-node`): the node states what it takes and hands back, and `lotics run
129
- set_app_workflow` sets its code with `inputs` and `outputs` left out.
128
+ set_workflow` sets its code with `inputs` and `outputs` left out of its `on({ type: "app", ... })` header.
130
129
 
131
130
  **What a person reads and does in the app, an agent reads and does through it.** Writing the tree generates a
132
131
  query for every List, record, child List and figure the screen reads, and a workflow for every add, edit,
@@ -147,7 +146,7 @@ lotics app pull <app_id> # the live version's source, on a machin
147
146
  ```
148
147
 
149
148
  What the app reads and writes is bound on the app, never in the project: `lotics run
150
- set_app_queries` binds named queries, `lotics run set_app_workflow` a workflow body, and each mints a
149
+ set_app_queries` binds named queries, `lotics run set_workflow` a workflow body, and each mints a
151
150
  version. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build
152
151
  and carries every binding forward unchanged.
153
152
 
@@ -165,7 +164,7 @@ that caller.
165
164
  Static checks prove parse, types and names — they evaluate nothing. Rehearse a write first:
166
165
 
167
166
  ```
168
- lotics run set_app_workflow '{"app_id":"app_…","alias":"…","source":"…","verify_only":true,"record_id":"rec_…"}'
167
+ lotics run set_workflow '{"source":"on({ type: \"app\", app_id: \"app_…\", alias: \"…\" });\n…","dry_run":true,"record_id":"rec_…"}'
169
168
  ```
170
169
 
171
170
  It walks the draft through the same engine an Act's preview uses, under the app's authority: reads are real,
@@ -28,31 +28,30 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
28
28
  | `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. |
29
29
  | `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |
30
30
  | `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. |
31
- | — | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_query`) and every one that builds: `set_tables` (a list of table documents), `set_app` (one app), `set_app_queries`, `set_app_workflow`. A command exists only for work that touches a local file: `app apply`, `app create --custom`, `app pull`, `app deploy`. |
31
+ | — | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_query`) and every one that builds: `set_tables` (a list of table documents), `set_app` (one app), `set_app_queries`, `set_workflow`. A command exists only for work that touches a local file: `app apply`, `app create --custom`, `app pull`, `app deploy`. |
32
32
  | — | **An app's queries and workflows take more than their params and inputs** (`lotics docs agent_surface`). `run_app_query '{"app_id","alias","params"}'` also takes `filters` and `sort` over the query's output columns, `fields` (the columns to return), `count` with `count_by` or `count_where`, `aggregate`, `limit` (200 rows at most; 50 where none is stated) and `cursor`, and `runs` — up to 20 reads in one call, each `{"alias", …}`, a refused one answered with its `error`; one call's rows total at most 262,144 bytes. `export: {"key", …narrowing}` instead of the read arguments makes the file an app's Export makes (`lotics docs agent_surface`). `run_app_workflow '{"app_id","alias","inputs"}'` also takes `dry_run: true` — a rehearsal on the real inputs, nothing written or sent; the answer is what the run would return, with the reads it made under `dry_run.reads`, the calls it would make under `dry_run.writes` and the papers it rendered under `dry_run.papers` and the parts of the answer it could not say under `dry_run.unpredicted`; a workflow no dry run is offered of answers `status: "not_previewed"` — and `runs` — up to 50 runs in order, the first refused, failed or not previewed ending the call, an input `{"from_run": <index>, "path": "data.<key>"}` reading an earlier run's answer. |
33
33
  | — | **The exit code reports the WORK, not just the call — for the tool that RUNS one.** `run_app_workflow` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`, or `not_previewed` — a dry run asked for and not given) 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. |
34
34
  | `lotics run <tool> '<json>' -o <dir>` | Also save every **signed link** the answer carries — an object with a `url`, a `filename` and the `expires_at` its signature ends at, at any depth, such as `screenshot_app`'s images. Each is fetched into `<dir>` (created if missing) under its `filename`, taking a free spelling beside a file of that name already there, and a link's `snapshot` text is written beside it as `<name>.txt`. The written paths follow the answer on **stdout**, one per line (on stderr under `--json`, whose stdout stays the one object). A link is signed for an hour; one the store no longer serves exits non-zero. A workspace file states no `expires_at` and is left to `lotics file download`. |
35
- | `lotics run set_member '{"members": {"<email or member id>": {"role": "member", "name": "…"}}}'` | Invite, rename, remove (`null`) or restore members, admin only: the whole map is checked before an email is sent. A member's `lotics tools` does not list it, nor `set_group` or `set_workspace`. View-as is the web app's preview alone; the CLI acts as the signed-in member. |
35
+ | `lotics run set_members '{"members": {"<email or member id>": {"role": "member", "name": "…"}}}'` | Invite, rename, change a role, remove (`null`) or restore members, or give a managed member a `password` (8 to 128 characters); `groups` keeps the member groups in the same call. Admin only: the whole input is checked before an email is sent. A member's `lotics tools` does not list it, nor `set_workspace`. View-as is the web app's preview alone; the CLI acts as the signed-in member. |
36
36
  | `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. |
37
37
  | `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. |
38
38
  | `lotics file upload <file\|dir...>` (alias `lotics upload`) · `--stdin` · `--base64` · `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files`, several such files to a request up to half that route's 100 MiB cap; at or above it the CLI takes presigned part URLs and PUTs the bytes straight to object storage, so they never pass through the API. That threshold matches the AWS CLI's own `multipart_threshold`, and the number matters less than there being nothing to choose — one verb, any size, up to the 2 GiB a workspace may store. A large upload reads one part at a time, so memory stays flat regardless of file size, and a failure part-way abandons the parts already sent rather than leaving them billable and invisible. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** — an attachment decoded in memory, a generated document, a signed download link — each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY — `Buffer.from(s, "base64")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |
39
- | `lotics file download <file_id> [<path>]` · `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` → fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there — the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once — a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_alias> [-o <dir>]` (the field named by the alias `get_record` shows it under) spreads every file on a record's file field over a DIRECTORY — there is no single file for N files to be. |
39
+ | `lotics file download <file_id> [<path>]` · `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` → fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there — the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once — a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_alias> [-o <dir>]` (the field named by the alias `query_records` shows it under) spreads every file on a record's file field over a DIRECTORY — there is no single file for N files to be. |
40
40
  | `lotics file list [--limit <n>] [--cursor <token>]` | The workspace's files, newest first — id, upload time, bytes, MIME type, filename on stdout, one per line (`--json` for the object). `GET /v1/files` with no `file_ids`. A file holding the content of a knowledge doc or template you cannot use is left out. **A page, not a dump**: the store only ever grows, so the last line prints the command for the next page and `next_cursor` is null on the last one. The cursor is opaque and keyset — pass it back as given — so an upload landing mid-sweep cannot make a walk skip or repeat a row. Every other file verb takes an id, so this is the only answer to "what is in here" short of reading Postgres. |
41
- | `lotics file delete <file_id>` | Archive a stored file, over the `delete_file` tool. Deleting an original deletes every version of it; a file's current version cannot be deleted on its own, and a document template's file, or a version of it, only by a member who manages the template. **Refused while a record cell, a comment, a knowledge doc, a document template, a voice session or a pending change request still references the file or a version it takes along** — the refusal names the referents, so this is safe to try. The bytes are left in object storage; the row no longer serves them, which is what "deleted" means here. There is no `lotics delete`: the verb needs its noun. |
42
41
  | `lotics knowledge list [--include-hidden \| --deleted]` | `GET /v1/knowledge_docs` — a table of id, name, tags, description (`--json` for the docs). **REST, not `get_workspace` kind `knowledge`**: the tool answers what the ASSISTANT may browse, and a hidden doc is out of that corpus by definition, so a tool-backed listing could never show one and the person who hid it would have no way back to it. Hidden docs are left out unless `--include-hidden` asks; those rows are marked `(hidden)`. **`--deleted` is the one tool-backed form**: it calls `get_workspace` with `kind: "knowledge"` and `include_archived: true`, page by page, and prints the deleted docs you can manage (id, name, deleted time, description); it cannot be combined with `--include-hidden`. |
43
42
  | `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \| --content <str>)` | Read the body client-side (a file XOR an inline string — exactly one required), then call `set_knowledge` with `{ name, description, content, tags? }` and no `knowledge_doc_id`, which makes a doc (description defaults to `""`). `--tags` files the doc as it is made, which is the only moment a corpus reliably gets labelled. Prints the new id to stdout. Large files ride the POST body fine. |
44
43
  | `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) → the doc with its **hydrated `content`** (the one whole-content read for a non-sandbox client; the tool `read_knowledge` reads a line range). `-o` writes the body via `writeFileAtomic`; else the body goes to stdout. `--json` prints the full doc instead. |
45
44
  | `lotics knowledge update <id> [--from <file.md> \| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `set_knowledge` with a `knowledge_doc_id` and **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). `--tags` REPLACES the doc's label set — the single-doc form, where the caller is looking at one doc and can state what it should carry. At least one field required; --from and --content are mutually exclusive. |
46
45
  | `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. |
47
46
  | `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, and the corpus `get_workspace` lists and its `search` 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". |
48
- | `lotics knowledge rm <id...>` | Delete docs via `delete_knowledge` (`{ knowledge_doc_ids }`), in one call: every id must be a live doc you manage that no app or workflow names, or nothing is deleted. Sharing and history are kept. The REST execute path does not gate `needsApproval`, so this runs unattended. |
49
- | `lotics knowledge restore <id...>` | Bring deleted docs back via `restore_knowledge` (`{ knowledge_doc_ids }`), in one call: every id must be a deleted doc you manage, or nothing is restored. Refuses the no-argument form. |
47
+ | `lotics knowledge rm <id...>` | Delete docs via `set_knowledge` (`{ knowledge_doc_id, delete: true }`), one call per id: each must be a live doc you manage that no app or workflow names, and the command reports which succeeded and which failed. Sharing and history are kept. The REST execute path does not gate `needsApproval`, so this runs unattended. |
48
+ | `lotics knowledge restore <id...>` | Bring deleted docs back via `set_knowledge` (`{ knowledge_doc_id, restore: true }`), one call per id: each must be a deleted doc you manage, and the command reports which succeeded and which failed. Refuses the no-argument form. |
50
49
  | `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 /v1/apps/{id}/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: a JSON app is made by `lotics app apply` (the tool `set_app`). The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |
51
50
  | `lotics app pull [app_id] [path]` | **An app's live definition, pulled.** The app is read with `get_app`: its author is shown a body, a JSON **tree** (with the addresses of its nodes) pulled as that tree and a custom-code app (the identity alone) as its source; an app whose stored tree cannot be read is refused with the reason `get_app` gives, and so is an app that shows the caller no body (the caller does not author it, or it has no version). **A JSON app**: the tree is written as JSON to `[path]` (a directory receives `./<name>.app.json`; default `./<name>.app.json`), and the app's id and current version beside it in `<file>.lotics.json` (`{ "app_id": "…", "version_id": "…", "digest": "…" }`, the digest being the file as the CLI last left it; a record without all three is refused by `app apply`, naming `lotics app pull`) — a custom-code project records both in `package.json#lotics`, a single file has no such place — so the next `lotics app apply <file>` edits that app, and is refused if the app has since moved past that version. An existing file is overwritten only when its `.lotics.json` names this same app and the file is unchanged since it was last pulled or applied; a file with edits, or whose record names the app but no digest, is refused, naming `--force`, which overwrites it. `--force` is a flag of `app pull` alone. `--json` prints `{workspace_id, app_id, path, outcome}`. **A custom-code app**: its 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 /v1/apps/{id}/types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). |
52
51
  | `lotics app apply <file> [--app <app_id>] [--check] [--json]` | **A JSON app, one at a time, through the tool `set_app`.** The file is an app's tree (`lotics docs reference`: the App node with the nodes under it, its tables and templates named by alias). **No app known** (no `--app`, no `<file>.lotics.json` beside it): the file is sent as `{ "body": … }` to make a new app, named by the body's `name`, and the id made is printed, then recorded with its version in `<file>.lotics.json`. **An app known**: its current tree is read (`get_app`) and the **RFC 7396 merge patch** that turns it into the file is sent as `body` with the `app_id` and the version the patch was computed against as `base_version_id` — a key the file drops is `null` in the patch, a list is replaced whole, what the file leaves alone is left alone, and a tree the app already has sends nothing and says so. **A file pulled at a version the app has since left is refused** before anything is sent, naming both versions and `lotics app pull <app_id> <path>`: pull the current tree into another path, carry the edits over, apply that file. `--app` naming an app other than the sibling file's is refused too. A write that races the apply is refused by the server, which names the app's current version. A file that sets an object key to `null` is refused naming the key, since a merge patch reads `null` as a removal. `--app` names the app, else the sibling file does; an app whose `get_app` shows no body (it is not yours) or whose stored tree cannot be read is refused, and a custom-code app takes its identity (name, description, icon, colour) only, and no `<file>.lotics.json` records it, since an identity write makes no version — its source deploys with `app deploy`. **The file is never rewritten**: a node the tree leaves out draws as the rows call for it and is stored nowhere (`lotics docs design`). It prints what `set_app` answered and reports its `outcome`: its notes and findings, every finding at once, and the saves, adds and Deletes the write grants and removes. **`--check` sends `dry_run`** (a flag of `app apply` alone, as is `--app`) — what would refuse the tree, and what it would grant and remove — and writes nothing, the file and its sibling included; an `outcome` of `refused` exits 1. A tree the app already has, or one `set_app` answers `unchanged`, records the version the app stays at in the sibling. **To go back to an earlier version, apply the earlier tree again.** Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, app_id, outcome, write}`, `outcome` being `set_app`'s: `created`, `updated`, `unchanged` or `refused` (with `--check`, what it would be). |
53
52
  | `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 /v1/apps/{id}/types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent; a type error in the app's own files stops the deploy, one inside an installed package is reported and does not) 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. |
54
53
  | `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`; a literal search that finds nothing while the pattern read as an expression matches (`a|b`) returns those lines under a note saying so, and `--literal` keeps the literal zero; with `--case-sensitive`, `--diacritic-insensitive`, `--context-lines <n>` and `--limit <n>` — the dialect the knowledge lines of `get_workspace` with `search` are read in — 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`. |
55
54
  | `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. |
56
- | `lotics docs reference` \| `lotics docs reference/<section>[/…]` | **The reference for tables and apps, from inside the binary** — at this CLI's version; `lotics docs` lists it under "Build an app", after `design`. Its first page is what the documents compose with, the working order (jobs → tables and fields → `records` → one app per job → one `set_tables` call with every table, then every `set_app` call) and the section addresses; every page of it is whole. Every key of a table document and an app body, every field `type` with the config each one needs, the option / view / inline-template shapes, `records` (how a row of a table is recognised), `write_rules`, the apps (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `"<table-ref>:<row-ref>"`), the rules, the caps of one `set_tables` call, and a worked example; it points at `https://lotics.ai/presets/index.json` for example files of several trades, which an agent reads and passes to `set_tables`. **Offline, no account.** |
55
+ | `lotics docs reference` \| `lotics docs reference/<section>[/…]` | **The reference for tables and apps, from inside the binary** — at this CLI's version; `lotics docs` lists it under "Build an app", after `design`. Its first page is what the documents compose with, the working order (jobs → tables and fields → `records` → one app per job → one `set_tables` call with every table, then every `set_app` call) and the section addresses; every page of it is whole. Every key of a table document and an app body, every field `type` with the config each one needs, the option / view / inline-template shapes, `records` (how a row of a table is recognised), `write_rules`, the apps (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `"<table-ref>:<row-ref>"`), the rules, the caps of one `set_tables` call, and a worked example; **Offline, no account.** |
57
56
  | `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. |
58
57
 
@@ -175,11 +175,11 @@ A table is named by its alias wherever a call names one (`table_id`, a link's `t
175
175
  the reader first, or in the same call.
176
176
  - **`force` consents to losing values:** deleting a field rows hold, removing an option rows hold, and a change of type
177
177
  that does not convert every value.
178
- - A money formula is proven by its numbers. `aggregate_records` the affected totals before the change and after, and
178
+ - A money formula is proven by its numbers. `query_records` with `aggregate` the affected totals before the change and after, and
179
179
  compare. A new field in a formula is tested for the value you want, never for differing from an empty cell, which reads
180
180
  `""` and differs from every option.
181
181
  - A check's expressions are written in the language of `workflows/table-checks`.
182
- - `views` are stated when the table is created; afterwards they change in chat.
182
+ - Views are written with `set_table_views`, not `set_tables`: `views` is keyed by a view's id (patch; `null` deletes) or a new key (create), and `order` lists view ids or new keys. `get_table` lists each view's id and name and returns the definitions with `views: true`.
183
183
  - A refused edit writes nothing and lists everything wrong; `dry_run` is for reading the grants it would make or deciding `force`.
184
184
 
185
185
  A worked call: add the option `cancelled` to `stage` (the whole list restated), add a check, and consent to losing
@@ -68,14 +68,17 @@ this host, give the person the file and a `person` drop link.
68
68
  you control. `variables` are plain text, rich `html`, an auto-rendered `table` or a `list`; `format` (page size) and
69
69
  `orientation` are optional. A change may send `patch_ops`, exact search-and-replace edits of the stored markup, in place
70
70
  of the whole `template`.
71
- - **`pdf-form`** — an existing PDF filled in place. `set_template` with the uploaded `template_file_id`;
72
- `analyze_pdf_template` reads the labels and positions off the page; `set_template` again with `expected_version` and the
73
- positioned `variables` (`text` / `checkbox` / `image`, each with a page and x/y/size). Data is one scalar per field.
71
+ - **`pdf-form`** — an existing PDF filled in place. `set_template` with the uploaded `template_file_id`
72
+ answers `page_count` and `pages` (each page's `page_index`, unrotated MediaBox `width` and `height`, `origin_x`, `origin_y`
73
+ and its `labels`, the text with its position and font size; `get_template` returns the same); `set_template` again with
74
+ `expected_version` and the positioned `variables` (`text` / `checkbox` / `image`, each with a page and x/y/size). Data is one scalar per field. A field whose `page` is past `page_count`, or whose box lies outside that page's MediaBox,
75
+ is refused. The answer holds at most 50 pages and 400 labels a page; `pages_omitted` and each page's `labels_omitted`
76
+ count the rest, and `pages_unavailable` says why a template's pages could not be read.
74
77
  A positioned text field prints in Arimo, which covers Vietnamese; a PDF's own field keeps its font while its value is
75
78
  ASCII and switches to Arimo past it.
76
79
  - **`excel`** — a `.xlsx` with `{{marker}}`s in the cells to fill, and a `variables` map. Markers are validated at create
77
80
  time: a structural error blocks the save, and a declared variable with no marker comes back in `unmarked_variables`
78
- (skipped). Over MCP and on the CLI, `validate_excel_template` checks a file's markers without creating a template. Pass
81
+ (skipped). `set_template` with `dry_run: true` runs every check and stores nothing; a refusal lists every error with its code and cell, and `warnings[]` (`severity`, `code`, `message`, `sheet_name?`, `cell?`) comes back with the answer. Pass
79
82
  raw numbers, ISO dates and booleans at generate time; the cell's number format shows them.
80
83
  - **`word`** — a `.docx` with `{{marker}}`s. `variables` is optional: every marker is derived (`{{name}}` string,
81
84
  `{{FOR … IN name}}` array, `{{IF name}}` boolean), and a marker you did not declare is still added
@@ -133,7 +136,7 @@ generate_document {"document_template_id": "dtl_…", "filename": "invoice-104
133
136
 
134
137
  - `data` is a map keyed by your variable names. Scalars for scalar/form fields; arrays for
135
138
  `table`/`list`/loop variables. `filename` excludes the extension (the type sets it). Pass a select, a
136
- member or a link as `get_record`'s `display` words, a number and a date raw.
139
+ member or a link as `query_records`' `display` words, a number and a date raw.
137
140
  - `generate_document` renders the template's own type and returns a generated file object; take
138
141
  its `file_id` onward.
139
142
  - A PDF no template is wanted for takes `html`, a `filename` and its `data` in place of `document_template_id`: the markup is
@@ -194,8 +197,6 @@ These tools work across all five types:
194
197
  |---|---|
195
198
  | `get_workspace` | list the workspace's templates (`kind: "template"`), each with its id, type and one line |
196
199
  | `get_template` | one template's full definition, incl. the `variables` it expects and the `current_version` a change needs |
197
- | `set_template` | create a template, or change one |
198
- | `clone_template` | copy a template to iterate on, in chat |
199
- | `delete_template` | remove a template |
200
+ | `set_template` | create a template, change one, delete it (`delete: true` with `template_id`) or bring it back (`restore: true`); `dry_run: true` checks the call and stores nothing; a copy is `get_template` then `set_template` |
200
201
 
201
202
  Call `get_template` before generating when you don't already know a template's variable names.
package/docs/filters.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Filters — choosing records by their fields
2
2
 
3
- One grammar for every tool that takes a filter: `query_records`, `aggregate_records`, `update_records`
3
+ One grammar for every tool that takes a filter: `query_records` (rows and `aggregate`), `update_records`
4
4
  and `delete_records` by filter, a view's filter, a rollup's filter, and the filters a caller passes
5
5
  to an app's query.
6
6
 
@@ -151,3 +151,20 @@ before it (up to 3), and `condition` names a field of the last table reached:
151
151
  ## Sort
152
152
 
153
153
  A view's sort is a list, applied in order: `[{ "field_key": "due", "order": "asc" | "desc" }]`.
154
+
155
+ ## Counting, aggregating and reading by id
156
+
157
+ `query_records` takes `filters` in one table and answers rows, or, with `aggregate`, statistics over them: `count`, or an
158
+ operation by the field's type, optionally broken down by `group_by`. Records are read one table at a time. A question over
159
+ several tables is one number from `tables`, a list of entries that each state their own `table_id` and filter, since each
160
+ table spells its fields its own way; it combines with no top-level `table_id`, `view_id`, `search` or `filters`.
161
+
162
+ - **Values held in another table.** A filter value (a customer's name, a code) is not looked up for you. Find where it
163
+ lives with `get_workspace` and `search`, which names the tables and docs that hold the words, then state it in that
164
+ table's own filter. When `is_any_of` on a text field names values, the answer lists the ones no row of the table
165
+ matched, so a total that misses part of what you asked for says so; pass `tables` to count them across the tables
166
+ that hold them.
167
+ - **By id.** `record_ids` reads those records whole, deleted ones included, with their `display`, `links`, `archived_at`,
168
+ `created_at` and `updated_at`; `table_id` may be left out. It takes at most 50 ids on every origin (an agent, a workflow
169
+ step, an app) and combines with `field_keys` and `comments` only. `update_records` takes the same 50 ids, and with
170
+ `restore: true` brings deleted records back.
@@ -153,7 +153,7 @@ set_access {"resources": [{"resource_type": "app", "resource_id": "app_…"}], "
153
153
 
154
154
  ## 4. What the owner will ask an agent: Query and Workflow nodes
155
155
 
156
- Read the brief again for what the business asks its assistant again and again: a figure or list whose definition is its own (orders waiting for pickup), an operation (chase overdue balances). Declare each as a Query or Workflow node of the app, with a `description` for an agent that has not seen the screen (`agent_surface` § Declaring more); a connected service (MISA, a carrier) is reached by a Workflow node's step (`workflows`); a question asked once stays on `aggregate_records`.
156
+ Read the brief again for what the business asks its assistant again and again: a figure or list whose definition is its own (orders waiting for pickup), an operation (chase overdue balances). Declare each as a Query or Workflow node of the app, with a `description` for an agent that has not seen the screen (`agent_surface` § Declaring more); a connected service (MISA, a carrier) is reached by a Workflow node's step (`workflows`); a question asked once stays on `query_records` with `aggregate`.
157
157
 
158
158
  ## 5. The owner's rows: `create_records`
159
159
 
@@ -177,4 +177,4 @@ run_app_workflow {"app_id": "app_…", "alias": "act_start_baking", "inputs": {"
177
177
  screenshot_app {"app_id": "app_…"}
178
178
  ```
179
179
 
180
- Fix a failure in the table or app that causes it. Leave standing instructions with `set_workspace` (`instructions`, one entry per rule): who the business is, the words its people use, what any agent working here must keep to. As an admin, invite the people the brief names with `set_member` (`members` keyed by email, role `member` or `admin`); where the tool is missing, say whom to invite. Then show the owner what you built, job by job, each paper they sent and what makes it, with the choices you made for them to change.
180
+ Fix a failure in the table or app that causes it. Leave standing instructions with `set_workspace` (`instructions`, one entry per rule): who the business is, the words its people use, what any agent working here must keep to. As an admin, invite the people the brief names with `set_members` (`members` keyed by email, role `member` or `admin`); where the tool is missing, say whom to invite. Then show the owner what you built, job by job, each paper they sent and what makes it, with the choices you made for them to change.
@@ -99,10 +99,10 @@ whatever the docs themselves use. `tags` on `set_knowledge` states one doc's com
99
99
 
100
100
  ## Deleting and restoring
101
101
 
102
- `delete_knowledge` deletes docs: they leave every listing, search and read. **Their sharing and version history are
102
+ `set_knowledge` with a `knowledge_doc_id` and `delete: true` deletes a doc: it leaves every listing, search and read. **Its sharing and version history are
103
103
  kept**, so a deleted doc is recoverable. `get_workspace` kind `knowledge` with `include_archived: true` lists the deleted
104
- docs you manage, and `restore_knowledge` brings them back with the access they had. Both take many ids and are all or
105
- none: every id must be a doc you manage, or nothing changes. A doc with no owner is restored only by an admin, who
104
+ docs you manage, and `set_knowledge` with `restore: true` brings one back with the access it had. Each call takes one doc,
105
+ which must be one you manage. A doc with no owner is restored only by an admin, who
106
106
  becomes its owner. A doc that an app or workflow still names cannot be deleted.
107
107
 
108
108
  ## Formatting — what a doc draws
package/docs/migration.md CHANGED
@@ -20,15 +20,15 @@ roles are the workspace's own.
20
20
  | `lotics scaffold diff` | `lotics app apply <file> --check` — what would refuse the tree, writing nothing |
21
21
  | `lotics scaffold export` | `lotics app pull <app_id> [path]` — the app's tree as JSON, one app at a time |
22
22
  | `lotics app codegen`, `app check`, `app dev`, `app preview` | nothing local: read an app with `lotics run get_app`, and change it through its tools |
23
- | `lotics app workflow set` / `app query set` | `lotics run set_app_workflow` / `set_app_queries` — each mints a version |
23
+ | `lotics app workflow set` / `app query set` | `lotics run set_workflow` / `set_app_queries` — each mints a version |
24
24
  | `lotics app versions` + a redeploy of an old tree | apply the earlier tree again (`lotics app apply <file>`), or deploy the earlier source; table changes and data writes stay |
25
25
  | `lotics app rename`, `app subdomain`, `package.json#lotics.capabilities` | `lotics run set_app` with the app's id (`public_subdomain`, `capabilities`; its name is the tree's `name`) |
26
- | `lotics app api publish` / `unpublish` | `lotics run publish_app_api` / `unpublish_app_api` |
26
+ | `lotics app api publish` / `unpublish` | `lotics run set_app_api '{"app_id":"…","published":true}'` / `false` |
27
27
  | `lotics field rename` | `lotics run set_tables` with a merge patch of the table's document |
28
28
  | `lotics app kit` | nothing: a custom-code app depends on `@lotics/app-sdk` (below) |
29
29
  | `@lotics/xlsx` / `@lotics/docx` to build a template's file | any `.xlsx` / `.docx` library, then `lotics upload` (`lotics docs document_templates`) |
30
30
  | `lotics file preview` | `lotics file download <fil_id>`, then open it |
31
- | `lotics library list` / `library show <slug>`, `lotics preset list` / `preset show <slug>` | the example files at `https://lotics.ai/presets/index.json`, each read and its tables passed to `set_tables` |
31
+ | `lotics library list` / `library show <slug>`, `lotics preset list` / `preset show <slug>` | nothing: state the tables (`lotics docs reference`) and pass them to `set_tables` |
32
32
  | `lotics library init <apg_id>`, `lotics app upgrade`, `lotics library fixtures` | nothing copies a package any more: state the tables (`lotics docs reference`) and pass them to `set_tables` |
33
33
 
34
34
  `lotics tools` lists every tool, and `lotics tools <name>` prints its input.
@@ -39,7 +39,7 @@ with `plan`, `queries` or `workflows`) is no longer read by anything; keep the p
39
39
  rest.
40
40
 
41
41
  **Authored workflow bodies** an act names (`workflow` on an act) are the live workflow's own body:
42
- `lotics run set_app_workflow` changes it.
42
+ `lotics run set_workflow` changes it.
43
43
 
44
44
  **A table written before `party`** draws its people and organisations as things — a row with no
45
45
  picture shows no initials: state `party` (`person` or `organization`) in that table's `records`.