@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.
- package/README.md +20 -4
- package/dist/probe_page.js +217 -27
- package/dist/src/cli.js +7450 -967
- package/dist/src/client.d.ts +94 -12
- package/dist/src/client.js +57 -3
- package/docs/building_an_app.md +47 -13
- package/docs/cli_reference.md +16 -12
- package/package.json +1 -1
package/dist/src/client.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
608
|
-
* stamps are CORRECT: the invocation making
|
|
609
|
-
* is about, so the courier and the cargo are
|
|
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:
|
|
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`
|
package/dist/src/client.js
CHANGED
|
@@ -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
|
|
327
|
-
* stamps are CORRECT: the invocation making
|
|
328
|
-
* is about, so the courier and the cargo are
|
|
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`
|
package/docs/building_an_app.md
CHANGED
|
@@ -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
|
|
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
|
|
100
|
-
`"fld_…"`)
|
|
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
|
|
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
|
-
|
|
109
|
-
fails `tsc`
|
|
110
|
-
only honest if the generated ids are current, and you never
|
|
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
|
|
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
|
|
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` —
|
|
279
|
-
|
|
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
|
|
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
|