@lotics/cli 0.76.1 → 0.86.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.
@@ -62,22 +62,3 @@ export declare function buildStarterTemplate(args: {
62
62
  /** @lotics/app-sdk version range, e.g. "^0.7.0". Defaults to the fallback pin. */
63
63
  sdk_version?: string;
64
64
  }): StarterFile[];
65
- /**
66
- * Files the PACKAGE scaffold (`lotics package new`) writes IN PLACE OF the shared
67
- * app starter's versions. The app starter demonstrates in-app routing over static
68
- * data — the wrong first screen for a package, which is a workspace-agnostic
69
- * blueprint installed into many workspaces. A package starter must instead exercise
70
- * the package machinery: the contract-derived `F`/`OPT` surface (from the generated
71
- * `.lotics/app_fields.ts`, which resolves each alias to THIS installation's concrete
72
- * id at module load), a named query, and a config knob. So an author begins from a
73
- * real screen that shows how a binding is read.
74
- *
75
- * `packageNew` overlays these onto the `buildStarterTemplate` output by path — the
76
- * same per-file swap it does for `package.json`. Kept here (next to the app starter)
77
- * so both stay editable + unit-testable in one place. Scenario-generic: entity
78
- * `item`, fields name/notes/status(open|done), query `items`, config knob `heading`
79
- * — matching `starterContract` in `package_commands.ts`.
80
- */
81
- export declare function buildPackageStarterOverrides(args: {
82
- app_name: string;
83
- }): StarterFile[];
@@ -748,395 +748,6 @@ See https://lotics.ai/docs/app-sdk for the SDK reference.
748
748
  },
749
749
  ];
750
750
  }
751
- /**
752
- * Files the PACKAGE scaffold (`lotics package new`) writes IN PLACE OF the shared
753
- * app starter's versions. The app starter demonstrates in-app routing over static
754
- * data — the wrong first screen for a package, which is a workspace-agnostic
755
- * blueprint installed into many workspaces. A package starter must instead exercise
756
- * the package machinery: the contract-derived `F`/`OPT` surface (from the generated
757
- * `.lotics/app_fields.ts`, which resolves each alias to THIS installation's concrete
758
- * id at module load), a named query, and a config knob. So an author begins from a
759
- * real screen that shows how a binding is read.
760
- *
761
- * `packageNew` overlays these onto the `buildStarterTemplate` output by path — the
762
- * same per-file swap it does for `package.json`. Kept here (next to the app starter)
763
- * so both stay editable + unit-testable in one place. Scenario-generic: entity
764
- * `item`, fields name/notes/status(open|done), query `items`, config knob `heading`
765
- * — matching `starterContract` in `package_commands.ts`.
766
- */
767
- export function buildPackageStarterOverrides(args) {
768
- const nameLit = JSON.stringify(args.app_name);
769
- return [
770
- {
771
- // The package App.tsx imports `../.lotics/app_fields` — a DETERMINISTIC
772
- // pure function of contract.json (generate_package_fields.ts) — so unlike
773
- // the app starter's fully-ignored `.lotics`, it must be committed or the
774
- // scaffold's own shipped CI (npm ci → typecheck/test/build on a clean
775
- // clone) fails on the missing module. The sync-written `.d.ts` companions
776
- // stay ignored: they need a live workspace and their absence only degrades
777
- // hooks to untyped overloads, never breaks the build.
778
- path: ".gitignore",
779
- content: `node_modules
780
- dist
781
- *.tsbuildinfo
782
- .DS_Store
783
- .lotics/*
784
- !.lotics/app_fields.ts
785
- coverage
786
- `,
787
- },
788
- {
789
- path: "src/App.tsx",
790
- content: `import { useMemo, type ReactNode } from "react";
791
- import { ScrollView, View } from "react-native";
792
- import { useConfig, useQuery, readSelect, row } from "@lotics/app-sdk";
793
- import { AppRouter } from "@lotics/app-sdk/router";
794
- import { Text } from "@lotics/ui/text";
795
- import { Card } from "@lotics/ui/card";
796
- import { Button } from "@lotics/ui/button";
797
- import { OptionBadge } from "@lotics/ui/option_badge";
798
- import { Skeleton } from "@lotics/ui/skeleton";
799
- import { EmptyState } from "@lotics/ui/empty_state";
800
- import { Callout, CalloutTitle, CalloutText, CalloutActions } from "@lotics/ui/callout";
801
- import { F, OPT } from "../.lotics/app_fields";
802
-
803
- // This is a PACKAGE starter — a workspace-agnostic blueprint installed into many
804
- // workspaces, each binding the contract's aliases to DIFFERENT concrete ids. So a
805
- // screen never hardcodes a \`fld_…\`/\`opt_…\` id: it addresses a field by contract
806
- // alias through \`F\` and a select option through \`OPT\`, both from the generated
807
- // \`.lotics/app_fields.ts\` (which resolves every alias to THIS installation's id at
808
- // module load from the binding). The aliases come from contract.json — entity
809
- // \`item\`, fields name/notes/status, options open/done, query \`items\`, config knob
810
- // \`heading\`. Edit contract.json, then \`lotics package sync\` to regenerate F/OPT.
811
-
812
- interface Item {
813
- id: string;
814
- name: string;
815
- notes: string;
816
- status: ReturnType<typeof readSelect>[number] | null;
817
- done: boolean;
818
- }
819
-
820
- // The \`items\` query is a bare from_entity, so a row is keyed by FIELD id: read a
821
- // cell as \`r[F.ITEM.<field>]\` and decode it with the SDK's pure readers (\`row.text\`,
822
- // \`readSelect\`). \`OPT.ITEM.status.done\` is this install's \`opt_…\` id, so comparing
823
- // the cell's stored option key to it marks a done row.
824
- function decode(r: Record<string, unknown>): Item {
825
- const status = readSelect(r[F.ITEM.status])[0] ?? null;
826
- return {
827
- id: row.text(r.__source_record_id),
828
- name: row.text(r[F.ITEM.name]),
829
- notes: row.text(r[F.ITEM.notes]),
830
- status,
831
- done: status?.key === OPT.ITEM.status.done,
832
- };
833
- }
834
-
835
- // Outer <View flex:1> claims the iframe height (index.html sets html/body/#root to
836
- // 100% + #root is a flex column); the list scrolls beneath the heading. Keep this
837
- // flex chain plain (not @lotics/ui/stack) so a fill-remaining-space child gets height.
838
- function Screen({ children }: { children: ReactNode }) {
839
- return (
840
- <View style={{ flex: 1 }}>
841
- <ScrollView contentContainerStyle={{ padding: 24, alignItems: "center" }}>
842
- <View style={{ maxWidth: 640, width: "100%", gap: 16 }}>{children}</View>
843
- </ScrollView>
844
- </View>
845
- );
846
- }
847
-
848
- function ItemsScreen() {
849
- // \`heading\` is a contract config knob — an installation overrides it and
850
- // useConfig() renders the customized value; the literal here is only the fallback.
851
- const { config } = useConfig({ heading: ${nameLit} });
852
- const itemsQ = useQuery("items");
853
- const items = useMemo(() => itemsQ.rows.map(decode), [itemsQ.rows]);
854
- // First-load only: a skeleton while the very first fetch is in flight with no rows
855
- // yet — never blank already-loaded rows to a spinner on a background refetch.
856
- const firstLoad = itemsQ.loading && items.length === 0;
857
-
858
- return (
859
- <Screen>
860
- <Text size="xxl" weight="semibold" level={1}>
861
- {config.heading}
862
- </Text>
863
-
864
- {itemsQ.error ? (
865
- <Callout tone="error">
866
- <CalloutTitle>Couldn't load items</CalloutTitle>
867
- <CalloutText>{itemsQ.error}</CalloutText>
868
- <CalloutActions>
869
- <Button title="Try again" onPress={() => itemsQ.refetch()} />
870
- </CalloutActions>
871
- </Callout>
872
- ) : firstLoad ? (
873
- <View style={{ gap: 10 }}>
874
- {[0, 1, 2].map((i) => (
875
- <Skeleton key={i} height={64} radius={12} />
876
- ))}
877
- </View>
878
- ) : items.length > 0 ? (
879
- <View style={{ gap: 10 }}>
880
- {items.map((item) => (
881
- <Card key={item.id}>
882
- <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
883
- <View style={{ flex: 1, gap: 2 }}>
884
- <Text weight="medium" color={item.done ? "muted" : undefined}>
885
- {item.name}
886
- </Text>
887
- {item.notes.length > 0 ? (
888
- <Text size="sm" color="muted" numberOfLines={1}>
889
- {item.notes}
890
- </Text>
891
- ) : null}
892
- </View>
893
- <OptionBadge value={item.status} />
894
- </View>
895
- </Card>
896
- ))}
897
- </View>
898
- ) : (
899
- <EmptyState
900
- icon="list-checks"
901
- message="No items yet"
902
- hint="Rows in this package's table appear here."
903
- />
904
- )}
905
- </Screen>
906
- );
907
- }
908
-
909
- const routes = [{ path: "/", element: <ItemsScreen /> }];
910
-
911
- export default function App() {
912
- return <AppRouter routes={routes} />;
913
- }
914
- `,
915
- },
916
- {
917
- path: "src/App.test.tsx",
918
- content: `import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
919
- import { render, screen, cleanup } from "@testing-library/react";
920
- import App from "./App";
921
-
922
- // Mock @lotics/app-sdk — the app's one external boundary (data + RPC + the package
923
- // binding). \`importOriginal\` keeps the PURE cell readers (\`row\`, \`readSelect\`) real,
924
- // so mock rows decode exactly like wire rows; only the hooks and getAppBinding are
925
- // stubbed. The binding ids below are what F.ITEM.* / OPT.ITEM.status.* resolve to;
926
- // the query rows are keyed by the SAME ids.
927
- const h = vi.hoisted(() => ({
928
- state: {
929
- rows: [] as Array<Record<string, unknown>>,
930
- loading: false,
931
- error: null as string | null,
932
- },
933
- binding: {
934
- fields: { "item.name": "fld_name", "item.notes": "fld_notes", "item.status": "fld_status" },
935
- options: { "item.status:open": "opt_open", "item.status:done": "opt_done" },
936
- roles: {},
937
- },
938
- }));
939
-
940
- vi.mock("@lotics/app-sdk", async (importOriginal) => {
941
- const actual = await importOriginal<typeof import("@lotics/app-sdk")>();
942
- return {
943
- ...actual,
944
- getAppBinding: async () => h.binding,
945
- useConfig: (defaults: Record<string, unknown>) => ({ config: defaults, loading: false }),
946
- useQuery: () => ({
947
- rows: h.state.rows,
948
- loading: h.state.loading,
949
- isValidating: false,
950
- error: h.state.error,
951
- refetch: () => {},
952
- }),
953
- };
954
- });
955
-
956
- const rowOf = (id: string, name: string, notes: string, statusKey: string) => ({
957
- __source_record_id: id,
958
- fld_name: name,
959
- fld_notes: notes,
960
- fld_status: [{ key: statusKey, label: statusKey === "opt_done" ? "Done" : "Open" }],
961
- });
962
-
963
- beforeEach(() => {
964
- h.state.rows = [];
965
- h.state.loading = false;
966
- h.state.error = null;
967
- });
968
-
969
- // vitest globals are off, so @testing-library/react's automatic afterEach cleanup is
970
- // never registered — clean up explicitly or renders accumulate across tests and a
971
- // second render's duplicate matches fail getByText.
972
- afterEach(cleanup);
973
-
974
- describe("App", () => {
975
- test("renders the heading and the loaded items with status badges", () => {
976
- h.state.rows = [
977
- rowOf("1", "First item", "with a note", "opt_open"),
978
- rowOf("2", "Second item", "", "opt_done"),
979
- ];
980
- render(<App />);
981
-
982
- expect(screen.getByText(${nameLit})).toBeTruthy();
983
- expect(screen.getByText("First item")).toBeTruthy();
984
- expect(screen.getByText("Second item")).toBeTruthy();
985
- expect(screen.getByText("with a note")).toBeTruthy();
986
- expect(screen.getByText("Open")).toBeTruthy();
987
- expect(screen.getByText("Done")).toBeTruthy();
988
- expect(screen.queryByText("No items yet")).toBeNull();
989
- });
990
-
991
- test("shows the empty state when there are no items", () => {
992
- render(<App />);
993
- expect(screen.getByText("No items yet")).toBeTruthy();
994
- });
995
-
996
- test("does not flash the list or empty state on first load", () => {
997
- h.state.loading = true;
998
- render(<App />);
999
- expect(screen.queryByText("No items yet")).toBeNull();
1000
- });
1001
-
1002
- test("surfaces a load error loudly", () => {
1003
- h.state.error = "Network unreachable";
1004
- render(<App />);
1005
- expect(screen.getByText("Couldn't load items")).toBeTruthy();
1006
- expect(screen.getByText("Network unreachable")).toBeTruthy();
1007
- });
1008
- });
1009
- `,
1010
- },
1011
- {
1012
- path: "README.md",
1013
- content: `# ${escapeHtml(args.app_name)}
1014
-
1015
- A Lotics **app package** — a versioned, installable blueprint (a \`contract.json\`
1016
- data model + app source) that installs into many workspaces. Authored locally,
1017
- published and run through the \`lotics package\` CLI. See \`docs/app_packages.md\`.
1018
-
1019
- ## Dev loop
1020
-
1021
- \`\`\`bash
1022
- lotics workspace create "${escapeHtml(args.app_name)} dev" --dev # a throwaway dev workspace
1023
- lotics package dev --workspace <dev_ws> # sync into it + run the dev server
1024
- # edit contract.json → re-run \`lotics package sync\` to migrate + re-materialize
1025
- # edit src/* → hot reload
1026
- \`\`\`
1027
-
1028
- \`sync\` / \`dev\` regenerate \`.lotics/app_fields.ts\` (the runtime \`F\` / \`OPT\` / \`ROLE\`
1029
- surface) from \`contract.json\`, plus the typed \`.lotics/app_{queries,workflows,agents}.d.ts\`
1030
- companions from the live installation — so \`useQuery\` / \`useWorkflow\` stay typed in the
1031
- dev loop.
1032
-
1033
- ## Quality
1034
-
1035
- \`\`\`bash
1036
- npm run typecheck
1037
- npm run lint
1038
- npm test
1039
- \`\`\`
1040
-
1041
- ## Publish
1042
-
1043
- \`\`\`bash
1044
- lotics package publish -m "v1" # build + publish an immutable version
1045
- lotics package install <package_id> # install into any workspace
1046
- \`\`\`
1047
-
1048
- ## The app screen
1049
-
1050
- \`src/App.tsx\` reads the contract BY ALIAS through \`F\` / \`OPT\` (from the generated
1051
- \`.lotics/app_fields.ts\`) — never a raw \`fld_…\` / \`opt_…\` id, since every install binds
1052
- different concrete ids. It lists the \`items\` query and renders the \`heading\` config knob.
1053
- \`@lotics/ui\` primitives render via react-native-web (aliased in \`vite.config.ts\`).
1054
-
1055
- ## Contract reference
1056
-
1057
- \`contract.json\` is the package's alias-keyed data model
1058
- (schema: \`@lotics/shared/schemas/app_packages\`). Every cross-reference is by **alias**;
1059
- the materializer resolves alias → this workspace's concrete id at install.
1060
-
1061
- ### Aliases
1062
-
1063
- - Entity / field / option / role / template / config aliases are lowercase slugs
1064
- matching \`^[a-z][a-z0-9_]*$\`. A field's fully-qualified key is \`<entity>.<field>\`
1065
- (e.g. \`item.status\`); a select option's is \`<entity>.<field>:<option>\`
1066
- (e.g. \`item.status:done\`).
1067
- - Query / workflow / agent aliases are runtime lookup keys (1–200 chars) — the app
1068
- source invokes them verbatim (\`useQuery("items")\`), so renaming one severs the call.
1069
-
1070
- ### Entities & fields
1071
-
1072
- \`\`\`jsonc
1073
- { "alias": "item", "label": "Item", "fields": [
1074
- { "alias": "name", "label": "Name", "type": "text", "required": true },
1075
- { "alias": "status", "label": "Status", "type": "select",
1076
- "options": [ { "alias": "open", "label": "Open", "color": "blue" },
1077
- { "alias": "done", "label": "Done", "color": "green" } ] } ] }
1078
- \`\`\`
1079
-
1080
- Field \`type\`: \`text\`, \`number\`, \`date\`, \`boolean\`, \`select\` (+ \`options\`),
1081
- \`select_member\`, \`select_record_link\` (\`target_entity\` = an entity alias), \`files\`,
1082
- \`formula\` (expression references same-entity fields as \`{alias}\`), \`rollup\`, \`lookup\`,
1083
- \`autonumber\`. \`required\` is advisory (app / workflow-layer UX only — the table model
1084
- has no required constraint). Select \`options\` are \`{ alias, label, color }\`.
1085
-
1086
- ### Queries
1087
-
1088
- Alias-form AST — the same node kinds as the runtime query engine, except a
1089
- \`from_table\` node carries \`from_entity\` (an entity alias) instead of a \`table_id\`:
1090
-
1091
- \`\`\`jsonc
1092
- { "alias": "items", "ast": {
1093
- "kind": "from_table", "from_entity": "item",
1094
- "sort": [ { "field_key": "name", "order": "asc" } ] } }
1095
- \`\`\`
1096
-
1097
- ### Workflows
1098
-
1099
- The sole app-side mutation path. Each workflow has a \`trigger\`, typed \`inputs\` /
1100
- \`outputs\`, and a JS-subset \`body\`.
1101
-
1102
- - Trigger — app-invoked or table-lifecycle:
1103
- - \`{ "type": "app" }\` — called by alias from the app (\`useWorkflow("<alias>")\`).
1104
- - \`{ "type": "entity_lifecycle", "entity": "<entity>", "event": "<event>" }\` — fires
1105
- on the bound table. \`event\` ∈ \`before_create\`, \`after_create\`, \`before_update\`,
1106
- \`after_update\`, \`before_delete\`, \`after_delete\`.
1107
- - Typed \`inputs\` (\`text\` / \`number\` / \`date\` / \`member\` / \`select\` / \`record_link\`,
1108
- plus \`object\` / \`array\`):
1109
- - \`record_link\` — its \`table_id\` names an **entity alias**.
1110
- - \`member\` — its \`group\` names a **role alias**.
1111
- - \`select\` — an option \`value\` in \`entity.field:option\` form binds to that option's
1112
- id (a plain value is a literal, left as-is).
1113
- - Body **sentinel tokens** — a body addresses package objects by reserved tokens the
1114
- materializer rewrites to concrete ids at install. Always inside a **string literal**:
1115
- - \`@@entity:<entity>@@\`
1116
- - \`@@field:<entity>.<field>@@\`
1117
- - \`@@option:<entity>.<field>:<option>@@\`
1118
- - \`@@role:<role>@@\`
1119
- - \`@@template:<template>@@\`
1120
-
1121
- ### Config knobs
1122
-
1123
- Typed customization knobs the app reads via \`useConfig()\`. Each has an \`alias\`,
1124
- \`label\`, \`type\`, and \`default\`:
1125
-
1126
- - \`text\` → a string default; \`boolean\` → a boolean; \`number\` → a number;
1127
- \`color\` → a palette color token; \`select\` → \`options: [{ value, label }]\` with the
1128
- \`default\` being one of those \`value\`s.
1129
-
1130
- \`\`\`jsonc
1131
- { "alias": "heading", "label": "List heading", "type": "text", "default": "Items" }
1132
- \`\`\`
1133
-
1134
- Publish validates the whole contract (alias uniqueness, every cross-reference
1135
- resolves, sentinel tokens name declared objects) before storing the version.
1136
- `,
1137
- },
1138
- ];
1139
- }
1140
751
  function escapeHtml(s) {
1141
752
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1142
753
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "vitest";
2
- import { buildStarterTemplate, buildPackageStarterOverrides, STARTER_FALLBACK_SDK_VERSION, STARTER_FALLBACK_UI_VERSION, STARTER_REACT_NATIVE_VERSION, } from "./starter_template.js";
2
+ import { buildStarterTemplate, STARTER_FALLBACK_SDK_VERSION, STARTER_FALLBACK_UI_VERSION, STARTER_REACT_NATIVE_VERSION, } from "./starter_template.js";
3
3
  function fileNamed(files, path) {
4
4
  const f = files.find((f) => f.path === path);
5
5
  if (!f)
@@ -113,71 +113,3 @@ describe("buildStarterTemplate", () => {
113
113
  expect(files.find((f) => f.path === "tsconfig.workflows.json")).toBeUndefined();
114
114
  });
115
115
  });
116
- describe("buildPackageStarterOverrides", () => {
117
- const overrides = buildPackageStarterOverrides({ app_name: "Widget Tracker" });
118
- test("overrides exactly the three files packageNew swaps by path", () => {
119
- // These paths must line up with buildStarterTemplate's, or the swap is a no-op
120
- // (a stray file, or the app version shipped instead of the package one).
121
- expect(overrides.map((f) => f.path).sort()).toEqual([".gitignore", "README.md", "src/App.test.tsx", "src/App.tsx"]);
122
- const starterPaths = new Set(buildStarterTemplate({ app_name: "Widget Tracker", app_id: "", workspace_id: "" }).map((f) => f.path));
123
- for (const f of overrides)
124
- expect(starterPaths.has(f.path)).toBe(true);
125
- });
126
- test("App.tsx exercises the binding: F/OPT, the items query, and the heading config knob", () => {
127
- // The whole point of the package starter — a screen that reads the contract by
128
- // ALIAS through the generated app_fields, not a static hardcoded demo.
129
- const app = fileNamed(overrides, "src/App.tsx");
130
- expect(app).toContain('from "../.lotics/app_fields"');
131
- expect(app).toContain("F.ITEM.name");
132
- expect(app).toContain("OPT.ITEM.status.done");
133
- expect(app).toContain('useQuery("items")');
134
- expect(app).toContain("useConfig({ heading:");
135
- expect(app).toContain("config.heading");
136
- // read via the SDK cell readers + rendered through the kit's OptionBadge
137
- expect(app).toContain("readSelect(");
138
- expect(app).toContain("OptionBadge");
139
- // first-load skeleton gated on loading && no rows; a designed empty state
140
- expect(app).toContain("itemsQ.loading && items.length === 0");
141
- expect(app).toContain("EmptyState");
142
- // routed via AppRouter (the shell is kept)
143
- expect(app).toContain("<AppRouter routes={routes}");
144
- // the config default is the package name
145
- expect(app).toContain('useConfig({ heading: "Widget Tracker" })');
146
- });
147
- test("App.test.tsx registers afterEach(cleanup) and mocks the SDK boundary incl. getAppBinding", () => {
148
- // Finding: vitest globals are off, so @testing-library's auto-cleanup never runs.
149
- // A multi-render test needs an explicit afterEach(cleanup) or renders accumulate
150
- // and a second getByText fails on duplicate matches.
151
- const testFile = fileNamed(overrides, "src/App.test.tsx");
152
- expect(testFile).toContain("afterEach(cleanup)");
153
- expect(testFile).toContain('vi.mock("@lotics/app-sdk"');
154
- expect(testFile).toContain("getAppBinding");
155
- // the pure readers stay real (importOriginal), so mock rows decode like wire rows
156
- expect(testFile).toContain("importOriginal");
157
- // asserts outcomes, not calls
158
- expect(testFile).toContain("No items yet");
159
- });
160
- test("README carries a contract reference grounded in the schema", () => {
161
- const readme = fileNamed(overrides, "README.md");
162
- expect(readme).toContain("## Contract reference");
163
- // the two workflow trigger forms
164
- expect(readme).toContain('{ "type": "app" }');
165
- expect(readme).toContain('"type": "entity_lifecycle"');
166
- // the six lifecycle events
167
- expect(readme).toContain("before_create");
168
- expect(readme).toContain("after_delete");
169
- // typed-input alias rules
170
- expect(readme).toContain("`table_id` names an **entity alias**");
171
- expect(readme).toContain("entity.field:option");
172
- // the sentinel-token grammar
173
- expect(readme).toContain("@@entity:<entity>@@");
174
- expect(readme).toContain("@@field:<entity>.<field>@@");
175
- expect(readme).toContain("@@option:<entity>.<field>:<option>@@");
176
- expect(readme).toContain("@@role:<role>@@");
177
- expect(readme).toContain("@@template:<template>@@");
178
- // config knob kinds, and query alias-form AST
179
- expect(readme).toContain("from_entity");
180
- // fences are single-escaped backticks, not doubled — no stray backslash before ```
181
- expect(readme).not.toContain("\\```");
182
- });
183
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.76.1",
3
+ "version": "0.86.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {