@lotics/cli 0.196.0 → 0.198.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.
@@ -19,6 +19,34 @@ export interface AppQueryFilterGroup {
19
19
  }
20
20
  /** Runtime filter/sort the app query RPC applies AFTER the named query, bounded to its output columns. */
21
21
  export type AppQueryFilter = AppQueryFilterCondition | AppQueryFilterGroup;
22
+ /**
23
+ * Body of the app query RPC — the one wire shape every transport carries, so
24
+ * the dev bridge forwards it whole rather than naming fields that then drift
25
+ * from what the deployed SDK sends.
26
+ */
27
+ export interface AppQueryBody {
28
+ alias: string;
29
+ params?: Record<string, unknown>;
30
+ limit?: number;
31
+ offset?: number;
32
+ sort?: AppQuerySortKey[];
33
+ filter?: AppQueryFilter;
34
+ /** Return only `total` over the filtered set — no rows. */
35
+ count?: boolean;
36
+ /** Opt into keyset (seek) pagination, which answers with `next_cursor`. */
37
+ keyset?: boolean;
38
+ /** The previous page's `next_cursor`, in keyset mode. */
39
+ cursor?: string;
40
+ }
41
+ /** Result of the app query RPC. `total` answers a `count` read; `truncated` and
42
+ * `next_cursor` a rows read — `next_cursor` in keyset mode only, null on the
43
+ * last page. */
44
+ export interface AppQueryResult {
45
+ rows: unknown[];
46
+ total?: number;
47
+ truncated?: boolean;
48
+ next_cursor?: string | null;
49
+ }
22
50
  /**
23
51
  * Where a download's bytes go: the FILE to write, or the DIRECTORY to write the
24
52
  * stored name into. Stated by the caller, never inferred from the path's shape —
@@ -52,6 +80,18 @@ export interface WorkspaceInfo {
52
80
  organization_id: string;
53
81
  created_at: string;
54
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
+ }
55
95
  export interface ToolExecuteResult {
56
96
  result: unknown;
57
97
  model_output?: string;
@@ -313,11 +353,21 @@ export interface ScaffoldWorkspaceResult {
313
353
  /** False when a table of that label already existed and was adopted. */
314
354
  created: boolean;
315
355
  }>;
356
+ /**
357
+ * Every role the model declares. `created` is false when a GROUP of that name
358
+ * was already here and the role bound to it — which is how a role silently
359
+ * inherits another workspace's membership, and why the line prints it the way
360
+ * an entity's does.
361
+ */
316
362
  roles: Array<{
317
363
  alias: string;
318
364
  group_id: string;
365
+ created: boolean;
319
366
  }>;
320
- /** Ids of the rows written, per entity alias — the handle for deleting them again. */
367
+ /**
368
+ * Ids of the rows written, per entity alias — the handle for deleting them
369
+ * again, and what `scaffold apply --documents` joins its rows to.
370
+ */
321
371
  record_ids: Record<string, string[]>;
322
372
  /** Rows were sent and none were written, because the run adopted a table. */
323
373
  rows_skipped: boolean;
@@ -576,24 +626,19 @@ export declare class LoticsClient {
576
626
  organization_name: string;
577
627
  }>;
578
628
  /**
579
- * File a hand-written report. Unlike a telemetry flush, the headers this
580
- * stamps are CORRECT: the invocation making the request is the one the report
581
- * is about, so the courier and the cargo are the same session.
629
+ * File a hand-written report, or a list of them in one call. Unlike a
630
+ * telemetry flush, the headers this stamps are CORRECT: the invocation making
631
+ * the request is the one the report is about, so the courier and the cargo are
632
+ * the same session.
582
633
  */
583
634
  sendReport(body: {
584
- report: {
585
- goal: string;
586
- actual: string;
587
- expected?: string;
588
- tried?: string;
589
- wanted?: string;
590
- };
635
+ report: ReportFrame | ReportFrame[];
591
636
  cli_session_id: string | null;
592
637
  cli_version: string;
593
638
  workspace_id?: string;
594
639
  app_id?: string;
595
640
  }): Promise<{
596
- accepted: true;
641
+ accepted: number;
597
642
  }>;
598
643
  setWorkspaceId(id: string): void;
599
644
  /** The workspace id the client targets (the `x-workspace-id` header), if resolved. */
@@ -602,6 +647,7 @@ export declare class LoticsClient {
602
647
  createWorkspace(body: {
603
648
  name: string;
604
649
  timezone?: string;
650
+ default_currency?: string;
605
651
  }): Promise<WorkspaceInfo>;
606
652
  /**
607
653
  * Apply a workspace MODEL to the current workspace — the from-scratch half of
@@ -708,10 +754,19 @@ export declare class LoticsClient {
708
754
  * source archive so agent-authored bindings (via `set_app_workflow`)
709
755
  * survive the pull/edit/deploy loop. Null/undefined on apps that have
710
756
  * never had a workflow declared.
757
+ *
758
+ * `table_ids` is the set of tables the bound BODY touches, recorded when the
759
+ * body was verified. It is what makes a table an app only ever WRITES to
760
+ * addressable: codegen covers every table a query reads, and a workflow
761
+ * writing a table no screen reads had no `F`/`OPT` entry, so its body
762
+ * carried pasted `fld_`/`opt_` literals that a rename breaks silently
763
+ * instead of failing `tsc`. Absent on a binding written before the server
764
+ * recorded it — the set widens what codegen covers, it does not define it.
711
765
  */
712
766
  workflows?: Record<string, {
713
767
  workflow_id: string;
714
768
  inputs?: Record<string, unknown>;
769
+ table_ids?: string[];
715
770
  }> | null;
716
771
  /**
717
772
  * Live alias → query declaration map from `apps.queries`. Source of truth
@@ -748,6 +803,10 @@ export declare class LoticsClient {
748
803
  name: string;
749
804
  description?: string;
750
805
  icon?: string;
806
+ /** The launcher tile's colour — the app's own, carried from whatever declared it. */
807
+ theme?: {
808
+ color?: string;
809
+ };
751
810
  }): Promise<{
752
811
  id: string;
753
812
  name: string;
@@ -981,6 +1040,29 @@ export declare class LoticsClient {
981
1040
  }): Promise<StarterPublish>;
982
1041
  /** The state of a publish: which app is building, and the version once every dist is in. */
983
1042
  getStarterPublish(publish_id: string): Promise<StarterPublish>;
1043
+ /**
1044
+ * The ROW RULE each of these tables declares — `private_filters` as stored, or
1045
+ * `null` where the table has none.
1046
+ *
1047
+ * `GET /v1/tables/{id}` rather than the `get_table` tool beside it: the rule is
1048
+ * an IAM fact about who may read a row, and the tool's output is the schema an
1049
+ * agent writes records against, which is why it carries fields and not this.
1050
+ * One call per id. A table this credential may not read — 403, or 404 for one
1051
+ * that is gone — is DROPPED: the only caller warns about rules it can see, and
1052
+ * a table it cannot read is not evidence of one. EVERY other failure throws.
1053
+ * An expired key, a 500, a timeout and an offline host all mean the scan has
1054
+ * no answer, and swallowing them would render as "this table declares no
1055
+ * rule" — the guard at its quietest exactly where it knows least.
1056
+ *
1057
+ * The filter is carried untyped: a published `.d.ts` cannot name
1058
+ * `@lotics/shared`'s filter schema, and the one reader asks a single question
1059
+ * of the tree rather than interpreting it.
1060
+ */
1061
+ getTableRowRules(tableIds: string[]): Promise<Array<{
1062
+ id: string;
1063
+ name: string;
1064
+ private_filters: unknown;
1065
+ }>>;
984
1066
  /**
985
1067
  * Resolve the display name + fields (incl. select options) of the given tables
986
1068
  * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
@@ -1042,18 +1124,7 @@ export declare class LoticsClient {
1042
1124
  * Run a named query declared in the app's manifest, scoped to the app's IAM
1043
1125
  * principal. Mirrors POST /v1/apps/{app_id}/query.
1044
1126
  */
1045
- appQuery(app_id: string, body: {
1046
- alias: string;
1047
- params?: Record<string, unknown>;
1048
- limit?: number;
1049
- offset?: number;
1050
- sort?: AppQuerySortKey[];
1051
- filter?: AppQueryFilter;
1052
- count?: boolean;
1053
- }): Promise<{
1054
- rows: unknown[];
1055
- total?: number;
1056
- }>;
1127
+ appQuery(app_id: string, body: AppQueryBody): Promise<AppQueryResult>;
1057
1128
  appMembers(app_id: string, group_id?: string): Promise<{
1058
1129
  members: Array<{
1059
1130
  id: string;
@@ -1142,6 +1213,13 @@ export declare class LoticsClient {
1142
1213
  * `lotics app pull`, which writes it to `src/workflows/<alias>.ts`.
1143
1214
  */
1144
1215
  getAppWorkflow(app_id: string, alias: string): Promise<ToolExecuteResult>;
1216
+ /**
1217
+ * The app's capability catalog exactly as a chat or MCP caller reads it —
1218
+ * every query, workflow and agent alias the caller's scope reaches, with the
1219
+ * description each is chosen BY. One read for the whole app, so a check over
1220
+ * what those readers see costs one request rather than one per alias.
1221
+ */
1222
+ getAppCapabilities(app_id: string): Promise<ToolExecuteResult>;
1145
1223
  /**
1146
1224
  * Fetch the server-generated workspace `.d.ts` + the wrapper envelope that
1147
1225
  * make a `src/workflows/<alias>.ts` body locally typecheckable (GAP-59).
@@ -1294,6 +1372,27 @@ export declare class LoticsClient {
1294
1372
  bundle_size_bytes: number;
1295
1373
  }>;
1296
1374
  downloadFile(url: string, outputPath: string): Promise<string>;
1375
+ /**
1376
+ * One page of this workspace's stored files, newest first — `GET /v1/files`.
1377
+ *
1378
+ * The only enumeration there is: every other file surface takes an id, so
1379
+ * without this "what is in the store" is answerable from Postgres alone, which
1380
+ * is exactly the escape hatch a CLI-first workflow exists to remove. Paged by a
1381
+ * keyset cursor the server mints; a caller walks until `next_cursor` is null.
1382
+ */
1383
+ listFiles(opts?: {
1384
+ limit?: number;
1385
+ cursor?: string;
1386
+ }): Promise<{
1387
+ files: Array<{
1388
+ id: string;
1389
+ filename: string;
1390
+ mime_type: string;
1391
+ size?: number | null;
1392
+ created_at: string;
1393
+ }>;
1394
+ next_cursor: string | null;
1395
+ }>;
1297
1396
  downloadFileById(fileId: string, destination?: DownloadDestination, options?: {
1298
1397
  reserved?: Set<string>;
1299
1398
  }): Promise<{
@@ -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,42 @@ 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
+ }
616
653
  /**
617
654
  * Resolve the display name + fields (incl. select options) of the given tables
618
655
  * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
@@ -790,6 +827,15 @@ var LoticsClient = class {
790
827
  async getAppWorkflow(app_id, alias) {
791
828
  return this.execute("get_app_workflow", { app_id, alias });
792
829
  }
830
+ /**
831
+ * The app's capability catalog exactly as a chat or MCP caller reads it —
832
+ * every query, workflow and agent alias the caller's scope reaches, with the
833
+ * description each is chosen BY. One read for the whole app, so a check over
834
+ * what those readers see costs one request rather than one per alias.
835
+ */
836
+ async getAppCapabilities(app_id) {
837
+ return this.execute("get_app_capabilities", { app_id });
838
+ }
793
839
  /**
794
840
  * Fetch the server-generated workspace `.d.ts` + the wrapper envelope that
795
841
  * make a `src/workflows/<alias>.ts` body locally typecheckable (GAP-59).
@@ -983,6 +1029,21 @@ var LoticsClient = class {
983
1029
  await fs.promises.writeFile(absolutePath, buffer);
984
1030
  return absolutePath;
985
1031
  }
1032
+ /**
1033
+ * One page of this workspace's stored files, newest first — `GET /v1/files`.
1034
+ *
1035
+ * The only enumeration there is: every other file surface takes an id, so
1036
+ * without this "what is in the store" is answerable from Postgres alone, which
1037
+ * is exactly the escape hatch a CLI-first workflow exists to remove. Paged by a
1038
+ * keyset cursor the server mints; a caller walks until `next_cursor` is null.
1039
+ */
1040
+ async listFiles(opts) {
1041
+ const params = new URLSearchParams();
1042
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
1043
+ if (opts?.cursor !== void 0) params.set("cursor", opts.cursor);
1044
+ const query = params.toString();
1045
+ return this.request("GET", `/v1/files${query ? `?${query}` : ""}`);
1046
+ }
986
1047
  async downloadFileById(fileId, destination, options) {
987
1048
  const signedHeaders = this.buildHeaders();
988
1049
  const signedUrlRes = await fetch(
@@ -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
@@ -105,9 +108,10 @@ Address fields by alias, never by a pasted id:
105
108
  row.opt(r[F.SHIPMENT.direction]) === OPT.SHIPMENT.direction.export
106
109
  ```
107
110
 
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.
111
+ An alias is slugified from the display name, so a rename on the platform MOVES it: re-run `codegen`
112
+ and every call site on the old alias fails `tsc` until it names the new one. **Re-run after any
113
+ schema change** — local typecheck is only honest if the generated ids are current, and you never
114
+ deploy to refresh types.
111
115
 
112
116
  ## 5 — Named queries
113
117
 
@@ -129,6 +133,22 @@ Details: `lotics docs queries`.
129
133
 
130
134
  ## 6 — Workflows, the only way an app writes
131
135
 
136
+ **A workflow binding is SOURCE, and it deploys with the app.** Its two halves are
137
+ `package.json#lotics.workflows.<alias>` and `src/workflows/<alias>.ts`; `lotics app pull` writes
138
+ both, and `lotics app deploy` pushes whatever differs from what it last saw live — through
139
+ `set_app_workflow`, before the bundle ships, so a version that shipped always reproduces what runs.
140
+ Nothing is bound by hand after a deploy, and nothing needs to be: an alias whose body and
141
+ declaration are in the repo is an alias the next clone can build. Three consequences worth knowing
142
+ before you write one:
143
+
144
+ - **A push refuses when the live body moved past the copy you edited.** The baseline rides as a
145
+ precondition, so two people editing one alias is a refusal naming it, never a silent clobber.
146
+ - **An unchanged alias is not pushed.** An update is a diff, so a deploy that changes one screen
147
+ does not re-push nine workflows.
148
+ - **Deleting the declaration and the file does not unbind it.** The binding keeps serving, and
149
+ keeps being published to chat and to MCP. `lotics app deploy --prune` unbinds what the project no
150
+ longer names, and it is opt-in because an alias can be invoked from outside the bundle.
151
+
132
152
  A workflow body is a file you open and edit. The new-alias path is typed from the first line:
133
153
 
134
154
  1. Declare it in `package.json#lotics.workflows.<alias>` — its `inputs`, and `outputs` only to
@@ -232,6 +252,12 @@ Vite plus an RPC-forwarding server, in a sandboxed iframe matching production, w
232
252
  auth and HMR. File flows work too — the dev server relays the bytes, so upload, preview and
233
253
  download are all exercisable locally.
234
254
 
255
+ **Navigate with `waitUntil: "domcontentloaded"`.** A dev app never goes network-quiet — the Vite
256
+ client and the app's own cross-origin frame each keep a connection open — so a driver that opts
257
+ into `networkidle` waits out its timeout on an app that rendered fine, and the timeout reads as the
258
+ app being broken. Any in-app path opens directly (`http://localhost:<port>/lo/rec_…`); the wrapper
259
+ serves every path that is not one of its own `/_…` routes.
260
+
235
261
  **Drive it with a browser, in this order** — each step's failure means something different:
236
262
 
237
263
  1. **Does it render at all?** A blank iframe is almost always a bundling problem, not your code.