@lotics/cli 0.197.0 → 0.204.0

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.
@@ -80,6 +80,18 @@ export interface WorkspaceInfo {
80
80
  organization_id: string;
81
81
  created_at: string;
82
82
  }
83
+ /**
84
+ * One hand-written report. Declared here rather than imported: this entry's
85
+ * published `.d.ts` must resolve for a consumer who has none of this package's
86
+ * other modules.
87
+ */
88
+ export interface ReportFrame {
89
+ goal: string;
90
+ actual: string;
91
+ expected?: string;
92
+ tried?: string;
93
+ wanted?: string;
94
+ }
83
95
  export interface ToolExecuteResult {
84
96
  result: unknown;
85
97
  model_output?: string;
@@ -340,12 +352,30 @@ export interface ScaffoldWorkspaceResult {
340
352
  table_id: string;
341
353
  /** False when a table of that label already existed and was adopted. */
342
354
  created: boolean;
355
+ /**
356
+ * What this run put ON that table — the delta a schema write is confirmed by.
357
+ * Absent from an instance too old to state it, so the line omits the counts
358
+ * rather than reporting a run that added nothing.
359
+ */
360
+ created_fields?: number;
361
+ created_options?: number;
362
+ created_views?: number;
343
363
  }>;
364
+ /**
365
+ * Every role the model declares. `created` is false when a GROUP of that name
366
+ * was already here and the role bound to it — which is how a role silently
367
+ * inherits another workspace's membership, and why the line prints it the way
368
+ * an entity's does.
369
+ */
344
370
  roles: Array<{
345
371
  alias: string;
346
372
  group_id: string;
373
+ created: boolean;
347
374
  }>;
348
- /** Ids of the rows written, per entity alias — the handle for deleting them again. */
375
+ /**
376
+ * Ids of the rows written, per entity alias — the handle for deleting them
377
+ * again, and what `scaffold apply --documents` joins its rows to.
378
+ */
349
379
  record_ids: Record<string, string[]>;
350
380
  /** Rows were sent and none were written, because the run adopted a table. */
351
381
  rows_skipped: boolean;
@@ -374,6 +404,12 @@ export interface WorkspaceModelExport {
374
404
  area: string;
375
405
  message: string;
376
406
  }>;
407
+ /**
408
+ * Labels of the entities the link closure added — the tables the caller did not
409
+ * name and a link pulled in. Absent from an instance too old to state it, which
410
+ * is why the printout says nothing rather than "0 pulled in".
411
+ */
412
+ pulled_in?: string[];
377
413
  }
378
414
  /**
379
415
  * A request the API refused. The message carries the status and the server's
@@ -604,24 +640,20 @@ export declare class LoticsClient {
604
640
  organization_name: string;
605
641
  }>;
606
642
  /**
607
- * File a hand-written report. Unlike a telemetry flush, the headers this
608
- * stamps are CORRECT: the invocation making the request is the one the report
609
- * is about, so the courier and the cargo are the same session.
643
+ * File a hand-written report, or a list of them in one call. Unlike a
644
+ * telemetry flush, the headers this stamps are CORRECT: the invocation making
645
+ * the request is the one the report is about, so the courier and the cargo are
646
+ * the same session.
610
647
  */
611
648
  sendReport(body: {
612
- report: {
613
- goal: string;
614
- actual: string;
615
- expected?: string;
616
- tried?: string;
617
- wanted?: string;
618
- };
649
+ report: ReportFrame | ReportFrame[];
619
650
  cli_session_id: string | null;
620
651
  cli_version: string;
621
652
  workspace_id?: string;
622
653
  app_id?: string;
623
654
  }): Promise<{
624
- accepted: true;
655
+ accepted: number;
656
+ report_ids?: string[];
625
657
  }>;
626
658
  setWorkspaceId(id: string): void;
627
659
  /** The workspace id the client targets (the `x-workspace-id` header), if resolved. */
@@ -737,10 +769,19 @@ export declare class LoticsClient {
737
769
  * source archive so agent-authored bindings (via `set_app_workflow`)
738
770
  * survive the pull/edit/deploy loop. Null/undefined on apps that have
739
771
  * never had a workflow declared.
772
+ *
773
+ * `table_ids` is the set of tables the bound BODY touches, recorded when the
774
+ * body was verified. It is what makes a table an app only ever WRITES to
775
+ * addressable: codegen covers every table a query reads, and a workflow
776
+ * writing a table no screen reads had no `F`/`OPT` entry, so its body
777
+ * carried pasted `fld_`/`opt_` literals that a rename breaks silently
778
+ * instead of failing `tsc`. Absent on a binding written before the server
779
+ * recorded it — the set widens what codegen covers, it does not define it.
740
780
  */
741
781
  workflows?: Record<string, {
742
782
  workflow_id: string;
743
783
  inputs?: Record<string, unknown>;
784
+ table_ids?: string[];
744
785
  }> | null;
745
786
  /**
746
787
  * Live alias → query declaration map from `apps.queries`. Source of truth
@@ -777,6 +818,10 @@ export declare class LoticsClient {
777
818
  name: string;
778
819
  description?: string;
779
820
  icon?: string;
821
+ /** The launcher tile's colour — the app's own, carried from whatever declared it. */
822
+ theme?: {
823
+ color?: string;
824
+ };
780
825
  }): Promise<{
781
826
  id: string;
782
827
  name: string;
@@ -1010,6 +1055,43 @@ export declare class LoticsClient {
1010
1055
  }): Promise<StarterPublish>;
1011
1056
  /** The state of a publish: which app is building, and the version once every dist is in. */
1012
1057
  getStarterPublish(publish_id: string): Promise<StarterPublish>;
1058
+ /**
1059
+ * The ROW RULE each of these tables declares — `private_filters` as stored, or
1060
+ * `null` where the table has none.
1061
+ *
1062
+ * `GET /v1/tables/{id}` rather than the `get_table` tool beside it: the rule is
1063
+ * an IAM fact about who may read a row, and the tool's output is the schema an
1064
+ * agent writes records against, which is why it carries fields and not this.
1065
+ * One call per id. A table this credential may not read — 403, or 404 for one
1066
+ * that is gone — is DROPPED: the only caller warns about rules it can see, and
1067
+ * a table it cannot read is not evidence of one. EVERY other failure throws.
1068
+ * An expired key, a 500, a timeout and an offline host all mean the scan has
1069
+ * no answer, and swallowing them would render as "this table declares no
1070
+ * rule" — the guard at its quietest exactly where it knows least.
1071
+ *
1072
+ * The filter is carried untyped: a published `.d.ts` cannot name
1073
+ * `@lotics/shared`'s filter schema, and the one reader asks a single question
1074
+ * of the tree rather than interpreting it.
1075
+ */
1076
+ getTableRowRules(tableIds: string[]): Promise<Array<{
1077
+ id: string;
1078
+ name: string;
1079
+ private_filters: unknown;
1080
+ }>>;
1081
+ /**
1082
+ * The organization's member groups — the directory `lotics app codegen` turns
1083
+ * into the `GRP` alias map.
1084
+ *
1085
+ * Over the HTTP route rather than `query_member_groups`, because the tool is
1086
+ * admin-only and the route is member-visible by design (`docs/iam.md` § Groups:
1087
+ * reading the directory is not administration). Codegen runs for every author,
1088
+ * so an admin-only read here would leave a non-admin's `GRP` empty and their
1089
+ * app unbuildable.
1090
+ */
1091
+ getMemberGroups(): Promise<Array<{
1092
+ id: string;
1093
+ name: string;
1094
+ }>>;
1013
1095
  /**
1014
1096
  * Resolve the display name + fields (incl. select options) of the given tables
1015
1097
  * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
@@ -323,9 +323,10 @@ var LoticsClient = class {
323
323
  return this.request("GET", "/v1/cli/whoami");
324
324
  }
325
325
  /**
326
- * File a hand-written report. Unlike a telemetry flush, the headers this
327
- * stamps are CORRECT: the invocation making the request is the one the report
328
- * is about, so the courier and the cargo are the same session.
326
+ * File a hand-written report, or a list of them in one call. Unlike a
327
+ * telemetry flush, the headers this stamps are CORRECT: the invocation making
328
+ * the request is the one the report is about, so the courier and the cargo are
329
+ * the same session.
329
330
  */
330
331
  async sendReport(body) {
331
332
  return this.request("POST", "/v1/cli/report", body);
@@ -613,6 +614,59 @@ var LoticsClient = class {
613
614
  async getStarterPublish(publish_id) {
614
615
  return this.request("GET", `/v1/starters/publishes/${encodeURIComponent(publish_id)}`);
615
616
  }
617
+ /**
618
+ * The ROW RULE each of these tables declares — `private_filters` as stored, or
619
+ * `null` where the table has none.
620
+ *
621
+ * `GET /v1/tables/{id}` rather than the `get_table` tool beside it: the rule is
622
+ * an IAM fact about who may read a row, and the tool's output is the schema an
623
+ * agent writes records against, which is why it carries fields and not this.
624
+ * One call per id. A table this credential may not read — 403, or 404 for one
625
+ * that is gone — is DROPPED: the only caller warns about rules it can see, and
626
+ * a table it cannot read is not evidence of one. EVERY other failure throws.
627
+ * An expired key, a 500, a timeout and an offline host all mean the scan has
628
+ * no answer, and swallowing them would render as "this table declares no
629
+ * rule" — the guard at its quietest exactly where it knows least.
630
+ *
631
+ * The filter is carried untyped: a published `.d.ts` cannot name
632
+ * `@lotics/shared`'s filter schema, and the one reader asks a single question
633
+ * of the tree rather than interpreting it.
634
+ */
635
+ async getTableRowRules(tableIds) {
636
+ const tables = await Promise.all(
637
+ tableIds.map(async (table_id) => {
638
+ try {
639
+ const table = await this.request(
640
+ "GET",
641
+ `/v1/tables/${encodeURIComponent(table_id)}`
642
+ );
643
+ return { id: table.id, name: table.name, private_filters: table.private_filters ?? null };
644
+ } catch (error) {
645
+ const notVisible = error instanceof LoticsRequestError && (error.status === 403 || error.status === 404);
646
+ if (!notVisible) throw error;
647
+ return null;
648
+ }
649
+ })
650
+ );
651
+ return tables.filter((table) => table !== null);
652
+ }
653
+ /**
654
+ * The organization's member groups — the directory `lotics app codegen` turns
655
+ * into the `GRP` alias map.
656
+ *
657
+ * Over the HTTP route rather than `query_member_groups`, because the tool is
658
+ * admin-only and the route is member-visible by design (`docs/iam.md` § Groups:
659
+ * reading the directory is not administration). Codegen runs for every author,
660
+ * so an admin-only read here would leave a non-admin's `GRP` empty and their
661
+ * app unbuildable.
662
+ */
663
+ async getMemberGroups() {
664
+ const { groups } = await this.request(
665
+ "GET",
666
+ "/v1/iam/member_groups"
667
+ );
668
+ return groups.map((group) => ({ id: group.id, name: group.name }));
669
+ }
616
670
  /**
617
671
  * Resolve the display name + fields (incl. select options) of the given tables
618
672
  * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
@@ -22,7 +22,10 @@ cd <dir> && lotics app pull <app_id> # existing: refresh to the latest first,
22
22
  each screen as the registry shape over the live table its entity became — a `LifecycleDesk`, a
23
23
  `PartyRegister`, … from `@lotics/ui`, its slots reading the fields the plan bound. The tables
24
24
  have to exist (`lotics scaffold apply` first); a table or field the workspace lacks is refused by
25
- name. A `custom` screen arrives as its rows and the slot list, for you to compose.
25
+ name. A `custom` screen arrives as a register of the cells its roles name, cut to what still reads
26
+ at a phone's width — its slot list is a checklist of what the rows know, never a layout. The
27
+ project's `README.md` is the plan in prose: the app, its screens with their routes and shapes, and
28
+ the tables behind them.
26
29
 
27
30
  A pull writes more than source: one `src/workflows/<alias>.ts` per bound workflow, one
28
31
  `src/agents/<alias>.md` per bound agent, and the `.lotics/` type companions — so an existing app
@@ -96,18 +99,22 @@ lotics app codegen # no deploy, no version bump
96
99
  ```
97
100
 
98
101
  Regenerates `.lotics/`: the three `.d.ts` companions that type `useQuery` / `useWorkflow` /
99
- `useAgentRun`, and — when credentials resolve — `app_fields.ts`, exporting `F` (table → field →
100
- `"fld_…"`) and `OPT` (table → select field → option → `"opt_…"`) keyed by display-name aliases.
102
+ `useAgentRun`, and — when credentials resolve — `app_fields.ts`, exporting four maps keyed by
103
+ display-name aliases: `F` (table → field → `"fld_…"`), `OPT` (table → select field → option →
104
+ `"opt_…"`), `TBL` (table → `"tbl_…"`) and `GRP` (member group → `"grp_…"`).
101
105
 
102
- Address fields by alias, never by a pasted id:
106
+ Address every id by alias, never by a pasted one — a table id and a member group included:
103
107
 
104
108
  ```tsx
105
109
  row.opt(r[F.SHIPMENT.direction]) === OPT.SHIPMENT.direction.export
110
+ useCommentCounts({ table_id: TBL.SHIPMENT });
111
+ useMembers({ group: GRP.sale });
106
112
  ```
107
113
 
108
- A rename in the platform then moves your call sites when you re-run `codegen`, and a stale alias
109
- fails `tsc` instead of failing at runtime. **Re-run after any schema change** — local typecheck is
110
- only honest if the generated ids are current, and you never deploy to refresh types.
114
+ An alias is slugified from the display name, so a rename on the platform MOVES it: re-run `codegen`
115
+ and every call site on the old alias fails `tsc` until it names the new one. **Re-run after any
116
+ schema change** — local typecheck is only honest if the generated ids are current, and you never
117
+ deploy to refresh types.
111
118
 
112
119
  ## 5 — Named queries
113
120
 
@@ -115,6 +122,11 @@ Author them as `kind: "project"` with a `filter`. A bare `from_table` over-expos
115
122
  degrades at scale. Scope per-user reads with `is_current_member` **inside the template** — a
116
123
  `member_id` passed from the client is an IDOR, since the caller chooses it.
117
124
 
125
+ Name each projected column — `{ "source": "fld_…", "output": "total" }` — and the row is then read
126
+ as `r.total`, with no field map on the read path; `lotics app create --from` emits exactly that.
127
+ Write one `description` per alias too: it is the line a chat or MCP caller chooses between them by,
128
+ and `lotics app check` exits 1 naming any alias that has none.
129
+
118
130
  Decode cells with the `row.*` helpers (`row.text`, `row.opt`, `readSelect`, `readLinks`), never by
119
131
  reaching into the raw shape: a select cell is `[{key,label}]`, and a hand-rolled reader silently
120
132
  returns the wrong half.
@@ -129,6 +141,22 @@ Details: `lotics docs queries`.
129
141
 
130
142
  ## 6 — Workflows, the only way an app writes
131
143
 
144
+ **A workflow binding is SOURCE, and it deploys with the app.** Its two halves are
145
+ `package.json#lotics.workflows.<alias>` and `src/workflows/<alias>.ts`; `lotics app pull` writes
146
+ both, and `lotics app deploy` pushes whatever differs from what it last saw live — through
147
+ `set_app_workflow`, before the bundle ships, so a version that shipped always reproduces what runs.
148
+ Nothing is bound by hand after a deploy, and nothing needs to be: an alias whose body and
149
+ declaration are in the repo is an alias the next clone can build. Three consequences worth knowing
150
+ before you write one:
151
+
152
+ - **A push refuses when the live body moved past the copy you edited.** The baseline rides as a
153
+ precondition, so two people editing one alias is a refusal naming it, never a silent clobber.
154
+ - **An unchanged alias is not pushed.** An update is a diff, so a deploy that changes one screen
155
+ does not re-push nine workflows.
156
+ - **Deleting the declaration and the file does not unbind it.** The binding keeps serving, and
157
+ keeps being published to chat and to MCP. `lotics app deploy --prune` unbinds what the project no
158
+ longer names, and it is opt-in because an alias can be invoked from outside the bundle.
159
+
132
160
  A workflow body is a file you open and edit. The new-alias path is typed from the first line:
133
161
 
134
162
  1. Declare it in `package.json#lotics.workflows.<alias>` — its `inputs`, and `outputs` only to
@@ -149,7 +177,9 @@ writes the schema back into the manifest and refreshes that alias's types in pla
149
177
  The alias's `description` rides along from the manifest. It is the one line an agent reads when
150
178
  choosing between your workflows, so write it rather than leaving the generated placeholder — and
151
179
  keep it inside 300 characters, the cap a push holds both a workflow's and a query's `description`
152
- to, because both ride the capability catalog into the agent's prompt on every run.
180
+ to, because both ride the capability catalog into the agent's prompt on every run. `app check`
181
+ names every alias over it and exits 1, and `workflow set` / `query set` refuse before sending
182
+ anything, so a long line costs one edit rather than a failed push per alias.
153
183
 
154
184
  **Every workflow you declare is also the chat agent's write surface.** So the alias's *shape* is an
155
185
  agent-facing decision, not only a screen-facing one — take a list where one job covers many
@@ -266,17 +296,21 @@ actually carries, not what the source says it should.
266
296
 
267
297
  ```
268
298
  npm run typecheck && npm run lint && npm test
269
- lotics app check --screens # every pre-flight a deploy runs, without building or shipping,
299
+ lotics app check --screens --shots shots/
300
+ # every pre-flight a deploy runs, without building or shipping,
270
301
  # plus the portability gate a library publish applies, plus
271
- # every screen rendered at 1280 and 375 and measured
302
+ # every screen rendered at 1280 and 375, measured, and written
303
+ # to shots/ as a PNG per screen and per record it opened
272
304
  lotics app deploy -m "<what changed + why>"
273
305
  ```
274
306
 
275
307
  **A green suite says nothing about how the screen LOOKS**, and that half starts with
276
308
  `--screens`: it renders the app over its real data (Chrome needed), walks every tab at both
277
309
  widths, and refuses what a review would; what it measures is in `lotics docs cli_reference`. What
278
- it cannot measure is in `lotics docs reviewing` — render it, look at it, then measure what the look
279
- is telling you. Both before the deploy, not as an audit someone schedules after a complaint.
310
+ it cannot measure is in `lotics docs reviewing` — which is why `--shots <dir>` is on the same
311
+ line: it photographs the frame each probe read, so LOOKING at the app is the same run rather than
312
+ a dev server and a browser pass per screen. Open the 375 shots first. Both before the deploy, not
313
+ as an audit someone schedules after a complaint.
280
314
 
281
315
  Every check above reads the SOURCE; none of them renders it. So the entire class of defect that
282
316
  lives in the pixels — wrong form for the subject, a treatment that contradicts what an element
@@ -316,7 +350,7 @@ lotics app codegen # after any schema change
316
350
  npm run typecheck # honest, because codegen is current
317
351
  lotics run run_app_workflow '{"app_id":…,"alias":…}' # prove the mutation path
318
352
  lotics app dev # prove the screen
319
- lotics app check --screens # measure it, before anyone looks
353
+ lotics app check --screens --shots shots/ # measure it AND photograph it, before anyone looks
320
354
  …
321
355
  lotics app workflow set <alias> # push the body; the server verifies
322
356
  lotics app deploy -m "…" # pushes pending bindings, then ships