@lotics/cli 0.198.0 → 0.205.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.
@@ -92,10 +92,27 @@ export interface ReportFrame {
92
92
  tried?: string;
93
93
  wanted?: string;
94
94
  }
95
+ /** One promise a refused write would break, as the tool transport states it. */
96
+ export interface ToolBreakingChange {
97
+ kind: string;
98
+ alias: string;
99
+ path: string;
100
+ rule?: string;
101
+ message?: string;
102
+ app_id?: string;
103
+ }
95
104
  export interface ToolExecuteResult {
96
105
  result: unknown;
97
106
  model_output?: string;
98
107
  error?: string;
108
+ /** The refusal's `code` — the same discriminator the HTTP envelope carries,
109
+ * and the only part of a failure a caller may branch on. Absent when the
110
+ * failure was not a typed error, and from any server older than it. */
111
+ error_code?: string;
112
+ /** A refused input addressed per field: one sentence from a typed alias, a
113
+ * list from a rejecting table workflow. */
114
+ field_errors?: Record<string, string> | Record<string, string[]>;
115
+ breaking_changes?: readonly ToolBreakingChange[];
99
116
  }
100
117
  /**
101
118
  * A settled (or in-flight) app-agent run, the transcript-excluded projection
@@ -352,6 +369,14 @@ export interface ScaffoldWorkspaceResult {
352
369
  table_id: string;
353
370
  /** False when a table of that label already existed and was adopted. */
354
371
  created: boolean;
372
+ /**
373
+ * What this run put ON that table — the delta a schema write is confirmed by.
374
+ * Absent from an instance too old to state it, so the line omits the counts
375
+ * rather than reporting a run that added nothing.
376
+ */
377
+ created_fields?: number;
378
+ created_options?: number;
379
+ created_views?: number;
355
380
  }>;
356
381
  /**
357
382
  * Every role the model declares. `created` is false when a GROUP of that name
@@ -396,6 +421,12 @@ export interface WorkspaceModelExport {
396
421
  area: string;
397
422
  message: string;
398
423
  }>;
424
+ /**
425
+ * Labels of the entities the link closure added — the tables the caller did not
426
+ * name and a link pulled in. Absent from an instance too old to state it, which
427
+ * is why the printout says nothing rather than "0 pulled in".
428
+ */
429
+ pulled_in?: string[];
399
430
  }
400
431
  /**
401
432
  * A request the API refused. The message carries the status and the server's
@@ -618,12 +649,28 @@ export declare class LoticsClient {
618
649
  */
619
650
  private buildHeaders;
620
651
  private request;
652
+ /** `email` is null for a credential no person signs in as; `account_type` is absent from an older server. */
653
+ /** `credential_origin` describes the CREDENTIAL this call carried, not the
654
+ * member above: `sign_in` for a terminal somebody signed in from, `api_key`
655
+ * for one an admin issued. Null for a row minted before it was recorded, and
656
+ * absent from an older server — both mean not told. */
621
657
  whoami(): Promise<{
622
658
  member_id: string;
623
- email: string;
659
+ email: string | null;
624
660
  name: string;
661
+ account_type?: string;
625
662
  organization_id: string;
626
663
  organization_name: string;
664
+ credential_origin?: "api_key" | "sign_in" | null;
665
+ }>;
666
+ /**
667
+ * Hand this client's own credential back — what `lotics auth logout` calls
668
+ * before it forgets the key, so signing out ends the credential instead of
669
+ * leaving a live one on a server nobody can see it from. Takes no id: the
670
+ * only thing it can revoke is the key these requests carry.
671
+ */
672
+ revokeSelfCredential(): Promise<{
673
+ id: string;
627
674
  }>;
628
675
  /**
629
676
  * File a hand-written report, or a list of them in one call. Unlike a
@@ -639,6 +686,7 @@ export declare class LoticsClient {
639
686
  app_id?: string;
640
687
  }): Promise<{
641
688
  accepted: number;
689
+ report_ids?: string[];
642
690
  }>;
643
691
  setWorkspaceId(id: string): void;
644
692
  /** The workspace id the client targets (the `x-workspace-id` header), if resolved. */
@@ -937,7 +985,12 @@ export declare class LoticsClient {
937
985
  * provenance is a 400, and one already on the latest release a 409 — both
938
986
  * refusals, both before any write. Admin-only.
939
987
  */
940
- upgradeApp(app_id: string): Promise<AppUpgradeResult>;
988
+ upgradeApp(app_id: string,
989
+ /** Apply the new release even though it breaks what this app's published API
990
+ * promises, snapshotting the broken contract as a new version. */
991
+ opts?: {
992
+ acknowledge_breaking_api_change?: boolean;
993
+ }): Promise<AppUpgradeResult>;
941
994
  /**
942
995
  * Capture live records from this workspace as a starter's sample data.
943
996
  *
@@ -1063,6 +1116,20 @@ export declare class LoticsClient {
1063
1116
  name: string;
1064
1117
  private_filters: unknown;
1065
1118
  }>>;
1119
+ /**
1120
+ * The organization's member groups — the directory `lotics app codegen` turns
1121
+ * into the `GRP` alias map.
1122
+ *
1123
+ * Over the HTTP route rather than `query_member_groups`, because the tool is
1124
+ * admin-only and the route is member-visible by design (`docs/iam.md` § Groups:
1125
+ * reading the directory is not administration). Codegen runs for every author,
1126
+ * so an admin-only read here would leave a non-admin's `GRP` empty and their
1127
+ * app unbuildable.
1128
+ */
1129
+ getMemberGroups(): Promise<Array<{
1130
+ id: string;
1131
+ name: string;
1132
+ }>>;
1066
1133
  /**
1067
1134
  * Resolve the display name + fields (incl. select options) of the given tables
1068
1135
  * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
@@ -1094,6 +1161,40 @@ export declare class LoticsClient {
1094
1161
  public_subdomain: string;
1095
1162
  origin: string;
1096
1163
  }>;
1164
+ /**
1165
+ * Publish the app's API — the owner's promise about what its declared queries
1166
+ * and workflows return to a caller outside the app.
1167
+ *
1168
+ * Snapshots the contract and answers the version it took, plus what the
1169
+ * published surface exposes that the owner may not have intended. A query
1170
+ * that does not name its columns is refused (400) before anything is written:
1171
+ * the app cannot promise field names it never stated.
1172
+ */
1173
+ publishAppApi(app_id: string): Promise<{
1174
+ app_id: string;
1175
+ contract_version: number;
1176
+ published_at: string;
1177
+ warnings: string[];
1178
+ }>;
1179
+ /** End the promise. `unpublished: false` means the app was publishing nothing,
1180
+ * which this leaves unchanged. The superseded snapshot stays, so a later
1181
+ * publish continues the numbering rather than reusing a version. */
1182
+ unpublishAppApi(app_id: string): Promise<{
1183
+ app_id: string;
1184
+ unpublished: boolean;
1185
+ }>;
1186
+ /** Whether the app is publishing an API, and which contract version. */
1187
+ getAppApiPublication(app_id: string): Promise<{
1188
+ published: boolean;
1189
+ contract_version: number | null;
1190
+ published_at: string | null;
1191
+ }>;
1192
+ /**
1193
+ * The published API as an OpenAPI 3.1 document — what a consumer's own
1194
+ * generator reads. Rendered from the live SNAPSHOT rather than the manifest,
1195
+ * so it describes what the app has promised; 404 while nothing is published.
1196
+ */
1197
+ getAppOpenApiDocument(app_id: string): Promise<Record<string, unknown>>;
1097
1198
  getAppVersion(app_id: string, version_id: string): Promise<{
1098
1199
  id: string;
1099
1200
  app_id: string;
@@ -1173,6 +1274,9 @@ export declare class LoticsClient {
1173
1274
  * server refuses it when the live body has moved since, rather than
1174
1275
  * letting a stale copy overwrite an edit its author never saw. */
1175
1276
  expected_body_sha?: string;
1277
+ /** Carry out this write even though it breaks what the app's published API
1278
+ * promises, snapshotting the broken contract as a new version. */
1279
+ acknowledge_breaking_api_change?: boolean;
1176
1280
  }): Promise<ToolExecuteResult>;
1177
1281
  /**
1178
1282
  * Bind (create or replace) an app query by alias via the `set_app_query` tool
@@ -1188,7 +1292,10 @@ export declare class LoticsClient {
1188
1292
  description?: string;
1189
1293
  },
1190
1294
  /** The fingerprint this edit was based on — makes the write conditional. */
1191
- expected_sha?: string): Promise<ToolExecuteResult>;
1295
+ expected_sha?: string,
1296
+ /** Carry out this write even though it breaks what the app's published API
1297
+ * promises, snapshotting the broken contract as a new version. */
1298
+ acknowledge_breaking_api_change?: boolean): Promise<ToolExecuteResult>;
1192
1299
  /**
1193
1300
  * Bind (create or replace) an app agent by alias via the `set_app_agent` tool
1194
1301
  * — the deploy-free authoring path for `apps.agents`, parallel to
@@ -1366,6 +1473,13 @@ export declare class LoticsClient {
1366
1473
  workflow_aliases?: string[];
1367
1474
  agent_aliases?: string[];
1368
1475
  query_aliases?: string[];
1476
+ /**
1477
+ * Ship this version even though its manifest breaks what the app's published
1478
+ * API promises, snapshotting the broken contract as a new version. A
1479
+ * multipart field is text, so it goes over as the two spellings the server's
1480
+ * own schema admits.
1481
+ */
1482
+ acknowledge_breaking_api_change?: boolean;
1369
1483
  }): Promise<{
1370
1484
  version_id: string;
1371
1485
  version_number: number;
@@ -266,10 +266,12 @@ var LoticsClient = class {
266
266
  body = {};
267
267
  }
268
268
  const message = typeof body.message === "string" ? body.message : text;
269
+ const hint = typeof body.hint === "string" ? `
270
+ ${body.hint}` : "";
269
271
  const trace = requestId === void 0 ? "" : `
270
272
 
271
273
  Request id: ${requestId} \u2014 quote this to Lotics support.`;
272
- throw new LoticsRequestError(`${response.status}: ${message}${trace}`, response.status, body);
274
+ throw new LoticsRequestError(`${response.status}: ${message}${hint}${trace}`, response.status, body);
273
275
  }
274
276
  /**
275
277
  * The backend's `log()` middleware registers `user-agent`,
@@ -319,9 +321,23 @@ var LoticsClient = class {
319
321
  if (!response.ok) await this.throwResponseError(response, headers["x-request-id"]);
320
322
  return response.json();
321
323
  }
324
+ /** `email` is null for a credential no person signs in as; `account_type` is absent from an older server. */
325
+ /** `credential_origin` describes the CREDENTIAL this call carried, not the
326
+ * member above: `sign_in` for a terminal somebody signed in from, `api_key`
327
+ * for one an admin issued. Null for a row minted before it was recorded, and
328
+ * absent from an older server — both mean not told. */
322
329
  async whoami() {
323
330
  return this.request("GET", "/v1/cli/whoami");
324
331
  }
332
+ /**
333
+ * Hand this client's own credential back — what `lotics auth logout` calls
334
+ * before it forgets the key, so signing out ends the credential instead of
335
+ * leaving a live one on a server nobody can see it from. Takes no id: the
336
+ * only thing it can revoke is the key these requests carry.
337
+ */
338
+ async revokeSelfCredential() {
339
+ return this.request("DELETE", "/v1/api_keys/self");
340
+ }
325
341
  /**
326
342
  * File a hand-written report, or a list of them in one call. Unlike a
327
343
  * telemetry flush, the headers this stamps are CORRECT: the invocation making
@@ -517,8 +533,12 @@ var LoticsClient = class {
517
533
  * provenance is a 400, and one already on the latest release a 409 — both
518
534
  * refusals, both before any write. Admin-only.
519
535
  */
520
- async upgradeApp(app_id) {
521
- return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/upgrade`, {});
536
+ async upgradeApp(app_id, opts = {}) {
537
+ return this.request(
538
+ "POST",
539
+ `/v1/apps/${encodeURIComponent(app_id)}/upgrade`,
540
+ opts.acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
541
+ );
522
542
  }
523
543
  /**
524
544
  * Capture live records from this workspace as a starter's sample data.
@@ -650,6 +670,23 @@ var LoticsClient = class {
650
670
  );
651
671
  return tables.filter((table) => table !== null);
652
672
  }
673
+ /**
674
+ * The organization's member groups — the directory `lotics app codegen` turns
675
+ * into the `GRP` alias map.
676
+ *
677
+ * Over the HTTP route rather than `query_member_groups`, because the tool is
678
+ * admin-only and the route is member-visible by design (`docs/iam.md` § Groups:
679
+ * reading the directory is not administration). Codegen runs for every author,
680
+ * so an admin-only read here would leave a non-admin's `GRP` empty and their
681
+ * app unbuildable.
682
+ */
683
+ async getMemberGroups() {
684
+ const { groups } = await this.request(
685
+ "GET",
686
+ "/v1/iam/member_groups"
687
+ );
688
+ return groups.map((group) => ({ id: group.id, name: group.name }));
689
+ }
653
690
  /**
654
691
  * Resolve the display name + fields (incl. select options) of the given tables
655
692
  * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
@@ -695,6 +732,36 @@ var LoticsClient = class {
695
732
  { public_subdomain }
696
733
  );
697
734
  }
735
+ /**
736
+ * Publish the app's API — the owner's promise about what its declared queries
737
+ * and workflows return to a caller outside the app.
738
+ *
739
+ * Snapshots the contract and answers the version it took, plus what the
740
+ * published surface exposes that the owner may not have intended. A query
741
+ * that does not name its columns is refused (400) before anything is written:
742
+ * the app cannot promise field names it never stated.
743
+ */
744
+ async publishAppApi(app_id) {
745
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/api/publish`, {});
746
+ }
747
+ /** End the promise. `unpublished: false` means the app was publishing nothing,
748
+ * which this leaves unchanged. The superseded snapshot stays, so a later
749
+ * publish continues the numbering rather than reusing a version. */
750
+ async unpublishAppApi(app_id) {
751
+ return this.request("DELETE", `/v1/apps/${encodeURIComponent(app_id)}/api/publish`);
752
+ }
753
+ /** Whether the app is publishing an API, and which contract version. */
754
+ async getAppApiPublication(app_id) {
755
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/api/publication`);
756
+ }
757
+ /**
758
+ * The published API as an OpenAPI 3.1 document — what a consumer's own
759
+ * generator reads. Rendered from the live SNAPSHOT rather than the manifest,
760
+ * so it describes what the app has promised; 404 while nothing is published.
761
+ */
762
+ async getAppOpenApiDocument(app_id) {
763
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/openapi.json`);
764
+ }
698
765
  async getAppVersion(app_id, version_id) {
699
766
  return this.request(
700
767
  "GET",
@@ -784,7 +851,10 @@ var LoticsClient = class {
784
851
  ...body.outputs ? { outputs: body.outputs } : {},
785
852
  ...body.name ? { name: body.name } : {},
786
853
  ...body.description ? { description: body.description } : {},
787
- ...body.expected_body_sha ? { expected_body_sha: body.expected_body_sha } : {}
854
+ ...body.expected_body_sha ? { expected_body_sha: body.expected_body_sha } : {},
855
+ // Sent only when the caller asked for it: absent means "refuse a break",
856
+ // which is the answer a caller who said nothing gave.
857
+ ...body.acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
788
858
  });
789
859
  }
790
860
  /**
@@ -795,12 +865,13 @@ var LoticsClient = class {
795
865
  * deploy validates the manifest. Note: `apps.queries` is manifest-authoritative,
796
866
  * so the next `lotics app deploy` overwrites this from the manifest.
797
867
  */
798
- async setAppQuery(app_id, alias, declaration, expected_sha) {
868
+ async setAppQuery(app_id, alias, declaration, expected_sha, acknowledge_breaking_api_change) {
799
869
  return this.execute("set_app_query", {
800
870
  app_id,
801
871
  alias,
802
872
  declaration,
803
- ...expected_sha ? { expected_sha } : {}
873
+ ...expected_sha ? { expected_sha } : {},
874
+ ...acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
804
875
  });
805
876
  }
806
877
  /**
@@ -1003,16 +1074,12 @@ var LoticsClient = class {
1003
1074
  formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
1004
1075
  formData.append("agent_aliases", JSON.stringify(args.agent_aliases ?? []));
1005
1076
  formData.append("query_aliases", JSON.stringify(args.query_aliases ?? []));
1077
+ if (args.acknowledge_breaking_api_change) {
1078
+ formData.append("acknowledge_breaking_api_change", "true");
1079
+ }
1006
1080
  const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
1007
1081
  const headers = this.buildHeaders();
1008
1082
  const response = await fetch(url, { method: "POST", headers, body: formData });
1009
- if (response.status === 409) {
1010
- const conflict = await response.json();
1011
- const err = new Error(conflict.message ?? "Deploy conflict \u2014 pull required");
1012
- err.code = "VERSION_CONFLICT";
1013
- err.current_version_id = conflict.current_version_id ?? null;
1014
- throw err;
1015
- }
1016
1083
  if (!response.ok) {
1017
1084
  await this.throwResponseError(response, headers["x-request-id"]);
1018
1085
  }
@@ -99,13 +99,16 @@ lotics app codegen # no deploy, no version bump
99
99
  ```
100
100
 
101
101
  Regenerates `.lotics/`: the three `.d.ts` companions that type `useQuery` / `useWorkflow` /
102
- `useAgentRun`, and — when credentials resolve — `app_fields.ts`, exporting `F` (table → field →
103
- `"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_…"`).
104
105
 
105
- 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:
106
107
 
107
108
  ```tsx
108
109
  row.opt(r[F.SHIPMENT.direction]) === OPT.SHIPMENT.direction.export
110
+ useCommentCounts({ table_id: TBL.SHIPMENT });
111
+ useMembers({ group: GRP.sale });
109
112
  ```
110
113
 
111
114
  An alias is slugified from the display name, so a rename on the platform MOVES it: re-run `codegen`
@@ -119,6 +122,11 @@ Author them as `kind: "project"` with a `filter`. A bare `from_table` over-expos
119
122
  degrades at scale. Scope per-user reads with `is_current_member` **inside the template** — a
120
123
  `member_id` passed from the client is an IDOR, since the caller chooses it.
121
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
+
122
130
  Decode cells with the `row.*` helpers (`row.text`, `row.opt`, `readSelect`, `readLinks`), never by
123
131
  reaching into the raw shape: a select cell is `[{key,label}]`, and a hand-rolled reader silently
124
132
  returns the wrong half.
@@ -169,7 +177,9 @@ writes the schema back into the manifest and refreshes that alias's types in pla
169
177
  The alias's `description` rides along from the manifest. It is the one line an agent reads when
170
178
  choosing between your workflows, so write it rather than leaving the generated placeholder — and
171
179
  keep it inside 300 characters, the cap a push holds both a workflow's and a query's `description`
172
- 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.
173
183
 
174
184
  **Every workflow you declare is also the chat agent's write surface.** So the alias's *shape* is an
175
185
  agent-facing decision, not only a screen-facing one — take a list where one job covers many
@@ -286,17 +296,21 @@ actually carries, not what the source says it should.
286
296
 
287
297
  ```
288
298
  npm run typecheck && npm run lint && npm test
289
- 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,
290
301
  # plus the portability gate a library publish applies, plus
291
- # 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
292
304
  lotics app deploy -m "<what changed + why>"
293
305
  ```
294
306
 
295
307
  **A green suite says nothing about how the screen LOOKS**, and that half starts with
296
308
  `--screens`: it renders the app over its real data (Chrome needed), walks every tab at both
297
309
  widths, and refuses what a review would; what it measures is in `lotics docs cli_reference`. What
298
- it cannot measure is in `lotics docs reviewing` — render it, look at it, then measure what the look
299
- 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.
300
314
 
301
315
  Every check above reads the SOURCE; none of them renders it. So the entire class of defect that
302
316
  lives in the pixels — wrong form for the subject, a treatment that contradicts what an element
@@ -336,7 +350,7 @@ lotics app codegen # after any schema change
336
350
  npm run typecheck # honest, because codegen is current
337
351
  lotics run run_app_workflow '{"app_id":…,"alias":…}' # prove the mutation path
338
352
  lotics app dev # prove the screen
339
- lotics app check --screens # measure it, before anyone looks
353
+ lotics app check --screens --shots shots/ # measure it AND photograph it, before anyone looks
340
354
  …
341
355
  lotics app workflow set <alias> # push the body; the server verifies
342
356
  lotics app deploy -m "…" # pushes pending bindings, then ships