@lotics/app-sdk 0.62.2 → 0.62.4

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/AGENTS.md CHANGED
@@ -15,7 +15,7 @@ signature; open the file.**
15
15
  | Doc | Read it for |
16
16
  |---|---|
17
17
  | [docs/queries.md](./docs/queries.md) | **The query engine authoring reference** — AST node kinds, per-field-type operator support, filters/params/pruning, free-text search, combining tables (join/union/link/`unnest`/`record_id`), shaping (aggregates, date buckets, windows), runtime refinement bounds, limits & the efficiency playbook. |
18
- | [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
18
+ | [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
19
19
  | [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`, read-after-write ordering (a re-read must not overtake an in-flight write). |
20
20
  | [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY authoring reference** — the JS subset a `src/workflows/<alias>.ts` body may use: the parse-at-save/never-execute model, opaque `fld_*`/`opt_*` keys, expression sources + link descent, every step form (tool call, `agent`, waits, `validate`, `return`), the accepted sugar and its canonical lowering, helpers + callback rules, record-write surfaces, the traps, the bright line, and the verify loop — `check` (the only local gate: the app's own `npm run typecheck` never sees a body) → `dry_run_workflow` (static green is not a run) → `set`. |
21
21
  | [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
@@ -38,6 +38,9 @@ signature; open the file.**
38
38
  column, including files. → [queries](./docs/queries.md)
39
39
  - **Never hand-roll the serialization contract** — decode cells with the typed readers.
40
40
  → [data_fetching](./docs/data_fetching.md)
41
+ - **Narrow `__source_record_id` / `__source_table_id` before use** — a grouped query emits neither,
42
+ so an unchecked read hands a workflow (or an AI record ref) `undefined`.
43
+ → [data_fetching](./docs/data_fetching.md)
41
44
  - **Errors fail loud** — no swallowed catches, no silent fallbacks.
42
45
 
43
46
  ## Keeping this reference current
@@ -62,10 +62,18 @@ export interface UseCommentsArgs {
62
62
  record_id: string;
63
63
  }
64
64
  /**
65
+ * `record_id` must be a REAL id. The fetch keys on `available` alone, so an
66
+ * empty string is not skipped — it fetches comments for nothing. When the id
67
+ * comes off a row's `__source_record_id` addressing column, narrow it first: a
68
+ * grouped query carries none, and a hook cannot be called conditionally, so the
69
+ * guard belongs at the component that renders the panel.
70
+ *
65
71
  * ```tsx
66
- * const { comments, available, createComment } = useComments({
67
- * record_id: row.__source_record_id,
68
- * });
72
+ * // In a record screen, where an id definitionally exists:
73
+ * const { comments, available, createComment } = useComments({ record_id: recordId });
74
+ *
75
+ * // From a row, gate the whole panel rather than passing a placeholder:
76
+ * {row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
69
77
  * ```
70
78
  */
71
79
  export declare function useComments(args: UseCommentsArgs): CommentsState;
@@ -38,10 +38,18 @@ function toStorageFiles(files) {
38
38
  }
39
39
  let optimisticCounter = 0;
40
40
  /**
41
+ * `record_id` must be a REAL id. The fetch keys on `available` alone, so an
42
+ * empty string is not skipped — it fetches comments for nothing. When the id
43
+ * comes off a row's `__source_record_id` addressing column, narrow it first: a
44
+ * grouped query carries none, and a hook cannot be called conditionally, so the
45
+ * guard belongs at the component that renders the panel.
46
+ *
41
47
  * ```tsx
42
- * const { comments, available, createComment } = useComments({
43
- * record_id: row.__source_record_id,
44
- * });
48
+ * // In a record screen, where an id definitionally exists:
49
+ * const { comments, available, createComment } = useComments({ record_id: recordId });
50
+ *
51
+ * // From a row, gate the whole panel rather than passing a placeholder:
52
+ * {row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
45
53
  * ```
46
54
  */
47
55
  export function useComments(args) {
@@ -6,6 +6,41 @@ import type { ResolvedMember } from "./members.js";
6
6
  import type { ResolvedOption } from "./select.js";
7
7
  export type { AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, AgentRunLanding } from "./agent_stream.js";
8
8
  export { buildChoiceOutput } from "./agent_stream.js";
9
+ /**
10
+ * One row of a query result: the projected columns, plus the platform's
11
+ * ADDRESSING columns.
12
+ *
13
+ * The projected values stay `unknown` — which columns a row carries is fixed by
14
+ * the query's manifest declaration, not by this type, so the cell readers
15
+ * (`row.text`, `readLinks`, `readFiles`, …) are what narrow them.
16
+ *
17
+ * The `__source_*` columns are different in kind: the compiler injects them at
18
+ * every layer, and they are the only way to address the RECORD a row came from
19
+ * — what `useComments`, a workflow's `record_id` input, and `useAiContext`'s
20
+ * record refs all need. Typed as `unknown` they were unusable without a cast,
21
+ * which is how this SDK's own examples came to show code that does not compile.
22
+ *
23
+ * They are OPTIONAL, and that is the honest shape rather than a hedge: a
24
+ * grouped query collapses rows, so its output has no originating record and the
25
+ * compiler emits no addressing columns for it (the platform's own `record_id`
26
+ * filter refuses such a query for exactly this reason). Narrow before use —
27
+ * on an aggregate row these are genuinely absent, not merely unproven.
28
+ *
29
+ * The line for what belongs here is "consumed RAW", not "starts with
30
+ * `__source_`". A row also carries `__source_locked`, `__created_at` /
31
+ * `__updated_at` and per-projection `__src_field_*`, and each of those has a
32
+ * reader that owns its decoding (`readLocked`, the `row.*` helpers) — a reader
33
+ * IS the narrowing, so a type here would duplicate it. Only these two are
34
+ * handed straight to a `record_id` / `table_id` parameter with nothing in
35
+ * between, which is why only these two needed a type.
36
+ */
37
+ export interface QueryRow {
38
+ [column: string]: unknown;
39
+ /** The record this row came from. Absent on a grouped/aggregated row. */
40
+ __source_record_id?: string;
41
+ /** The table that record lives in. Absent on a grouped/aggregated row. */
42
+ __source_table_id?: string;
43
+ }
9
44
  /** Fields shared by every query hook's return value. */
10
45
  interface QueryStateBase {
11
46
  /**
@@ -152,7 +187,9 @@ export interface PaginatedQueryOptions extends BaseQueryOptions {
152
187
  *
153
188
  * ```tsx
154
189
  * const issue = useWorkflow("issueInvoiceStorageDrop");
155
- * await issue({ record_id: row.__source_record_id });
190
+ * // Guard the addressing column: a grouped query's rows carry none, and the
191
+ * // workflow would be handed `undefined` where it declares a record.
192
+ * if (row.__source_record_id) await issue({ record_id: row.__source_record_id });
156
193
  * ```
157
194
  */
158
195
  export declare function useWorkflow<K extends keyof AppWorkflows & string>(alias: K): UseWorkflowFn<K>;
@@ -199,8 +236,8 @@ type QueryArgs<K extends keyof AppQueries & string, O> = AppQueries[K] extends R
199
236
  * with the declared alias → param-type map, so an undeclared alias is a
200
237
  * compile-time error and params are typed per the manifest.
201
238
  */
202
- export declare function useQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, QueryOptions>): QueryState<Record<string, unknown>>;
203
- export declare function useQuery(alias: string, params?: Record<string, unknown>, opts?: QueryOptions): QueryState<Record<string, unknown>>;
239
+ export declare function useQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, QueryOptions>): QueryState<QueryRow>;
240
+ export declare function useQuery(alias: string, params?: Record<string, unknown>, opts?: QueryOptions): QueryState<QueryRow>;
204
241
  /** The resolved option set of one select column, plus an index for value
205
242
  * rendering. The companion to a query row, for select fields. */
206
243
  export interface FieldOptions {
@@ -272,8 +309,8 @@ export declare function useFieldOptions(alias: string, opts?: FieldOptionsOption
272
309
  * const { rows, loadMore, hasMore } = useInfiniteQuery("feed", {}, { pageSize: 30 });
273
310
  * ```
274
311
  */
275
- export declare function useInfiniteQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, InfiniteQueryOptions>): InfiniteQueryState<Record<string, unknown>>;
276
- export declare function useInfiniteQuery(alias: string, params?: Record<string, unknown>, opts?: InfiniteQueryOptions): InfiniteQueryState<Record<string, unknown>>;
312
+ export declare function useInfiniteQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, InfiniteQueryOptions>): InfiniteQueryState<QueryRow>;
313
+ export declare function useInfiniteQuery(alias: string, params?: Record<string, unknown>, opts?: InfiniteQueryOptions): InfiniteQueryState<QueryRow>;
277
314
  /**
278
315
  * Page-model query with a total — the data hook behind a numbered, jumpable
279
316
  * table (pairs with `@lotics/ui/pagination`). It owns the page
@@ -288,8 +325,8 @@ export declare function useInfiniteQuery(alias: string, params?: Record<string,
288
325
  * usePaginatedQuery("orders", { q }, { pageSize: 25, sort, filter });
289
326
  * ```
290
327
  */
291
- export declare function usePaginatedQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, PaginatedQueryOptions>): PaginatedQueryState<Record<string, unknown>>;
292
- export declare function usePaginatedQuery(alias: string, params?: Record<string, unknown>, opts?: PaginatedQueryOptions): PaginatedQueryState<Record<string, unknown>>;
328
+ export declare function usePaginatedQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, PaginatedQueryOptions>): PaginatedQueryState<QueryRow>;
329
+ export declare function usePaginatedQuery(alias: string, params?: Record<string, unknown>, opts?: PaginatedQueryOptions): PaginatedQueryState<QueryRow>;
293
330
  /** A file the host has stored and resolved serving URLs for. */
294
331
  export interface UploadedFile {
295
332
  id: string;
@@ -412,7 +449,14 @@ export declare function useAttachments(): AttachmentsState;
412
449
  * ```tsx
413
450
  * useAiContext("orders_list", {
414
451
  * description: `Viewing ${rows.length} orders filtered to status=open, sorted by due date.`,
415
- * records: rows.map((r) => ({ table_id: r.__source_table_id, record_id: r.__source_record_id })),
452
+ * // `flatMap` + the guard, not `map`: the addressing columns are absent on a
453
+ * // GROUPED query's rows, so a ref built without checking carries `undefined`
454
+ * // and points the agent at nothing.
455
+ * records: rows.flatMap((r) =>
456
+ * r.__source_table_id && r.__source_record_id
457
+ * ? [{ table_id: r.__source_table_id, record_id: r.__source_record_id }]
458
+ * : [],
459
+ * ),
416
460
  * data: { filter: "status=open", sort: "due_date desc" },
417
461
  * });
418
462
  * ```
package/dist/src/hooks.js CHANGED
@@ -373,7 +373,14 @@ function serializeAiContext(context) {
373
373
  * ```tsx
374
374
  * useAiContext("orders_list", {
375
375
  * description: `Viewing ${rows.length} orders filtered to status=open, sorted by due date.`,
376
- * records: rows.map((r) => ({ table_id: r.__source_table_id, record_id: r.__source_record_id })),
376
+ * // `flatMap` + the guard, not `map`: the addressing columns are absent on a
377
+ * // GROUPED query's rows, so a ref built without checking carries `undefined`
378
+ * // and points the agent at nothing.
379
+ * records: rows.flatMap((r) =>
380
+ * r.__source_table_id && r.__source_record_id
381
+ * ? [{ table_id: r.__source_table_id, record_id: r.__source_record_id }]
382
+ * : [],
383
+ * ),
377
384
  * data: { filter: "status=open", sort: "due_date desc" },
378
385
  * });
379
386
  * ```
@@ -17,7 +17,7 @@
17
17
  export { mount } from "./mount.js";
18
18
  export type { MountOptions } from "./mount.js";
19
19
  export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, useAiContext, buildChoiceOutput, } from "./hooks.js";
20
- export type { UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunLanding, AgentRunRecord, AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
20
+ export type { QueryRow, UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunLanding, AgentRunRecord, AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
21
21
  export { useComments, useCommentCounts } from "./comments.js";
22
22
  export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
23
23
  export { useViewer } from "./viewer.js";
package/docs/ai.md CHANGED
@@ -22,7 +22,7 @@ A declaration carries:
22
22
  | `instructions` | System instructions — the task the agent performs per run |
23
23
  | `tool_names` | The tools the agent may call, resolved against the platform's automation tool registry. The capability boundary for everything EXCEPT workspace data — the run can use nothing else. May be empty — including for an agent that reads documents, since a [`file` input carries its own content](#file-inputs--what-the-agent-can-actually-see) |
24
24
  | `knowledge_doc_ids` | The knowledge docs the agent may read, and the whole set it can reach — a doc absent from this list is unreadable even if the agent names its id. Declare `grep_knowledge` / `read_knowledge` in `tool_names` to read them. There is no size limit and nothing is inlined, so a multi-megabyte reference corpus (a full tariff, a regulation set) is a normal declaration. Reach for `code_exec` only to COMPUTE across the corpus — counting, cross-referencing — never merely to read it |
25
- | `query_aliases` | The app's own named queries the agent may run via `run_app_query` — its **entire read surface** over records |
25
+ | `query_aliases` | The app's own named queries the agent may run via `run_app_query` — its **entire read surface** over records. You list them; there is no "all of them" shorthand, and adding a query to the app never widens an existing agent. Omitted means the agent reads nothing |
26
26
  | `workflow_aliases` | The app's own workflows the agent may invoke via `run_app_workflow` — its **entire write surface** |
27
27
  | `model_id` | Optional chat model pin. Omit (preferred) to follow the platform default chat model, resolved at run time — the agent tracks model generations with no rewrite. Pin only a deliberate, tested choice |
28
28
  | `effort_level` | Optional reasoning depth for adaptive-thinking models. Requires an explicit `model_id` pin — effort is tuned per model |
@@ -277,7 +277,13 @@ import { useAiContext } from "@lotics/app-sdk";
277
277
  // A list screen publishes what it rendered:
278
278
  useAiContext("orders_list", {
279
279
  description: `Viewing ${rows.length} orders filtered to status=open, sorted by due date.`,
280
- records: rows.map((r) => ({ table_id: r.__source_table_id, record_id: r.__source_record_id })),
280
+ // Guarded, not `map`: the addressing columns are absent on a GROUPED query's
281
+ // rows, so an unchecked ref carries `undefined` and points the agent at nothing.
282
+ records: rows.flatMap((r) =>
283
+ r.__source_table_id && r.__source_record_id
284
+ ? [{ table_id: r.__source_table_id, record_id: r.__source_record_id }]
285
+ : [],
286
+ ),
281
287
  data: { filter: "status=open", sort: "due_date desc" },
282
288
  });
283
289
 
@@ -337,6 +343,13 @@ gated per turn on `app:use` for the app in the member's workspace, and runs
337
343
  under the app's OWNER authority with `is_current_member` bound to the MEMBER
338
344
  (exactly as the app's own UI does).
339
345
 
346
+ The agent is handed a **catalog** of those aliases — each one's params and its
347
+ `description` (a query's own, [declared in the manifest](./queries.md); a
348
+ workflow's, off its workflow row) — so it picks the right one instead of
349
+ guessing an alias and learning the list from a refusal. Your descriptions are
350
+ what it reads, on this surface and inside your own agents' runs. They enter the
351
+ prompt as a capability listing, never as instructions.
352
+
340
353
  **Writes carry a second gate that reads do not: `run_app_workflow` asks the
341
354
  member to approve every call from chat.** A wrong read is fixed by asking
342
355
  again; a wrong write is not. The alias list bounds what an agent *could* reach;
@@ -246,6 +246,11 @@ workflows), `__created_at` / `__updated_at` (record timestamps), and per-project
246
246
  metadata. A grouped query collapses rows and emits none of these. Details:
247
247
  [./queries.md](./queries.md).
248
248
 
249
+ A row is typed `QueryRow`: projected columns are `unknown` (the manifest declares them, so the
250
+ readers below are what narrow them), while `__source_record_id` / `__source_table_id` are typed
251
+ `string | undefined` — reachable without a cast, but only after narrowing, because "a grouped query
252
+ emits none of these" is a fact the type states rather than one you have to remember.
253
+
249
254
  ### The readers
250
255
 
251
256
  | Reader | In → out | Semantics |
package/docs/files.md CHANGED
@@ -265,6 +265,9 @@ with `files` absent.
265
265
 
266
266
  ```tsx
267
267
  const generate = useWorkflow("generateInvoice");
268
+ // `__source_record_id` is optional — a grouped query carries none — so narrow it
269
+ // rather than handing the workflow `undefined` where it declares a record.
270
+ if (!row.__source_record_id) return;
268
271
  const result = await generate({ record_id: row.__source_record_id });
269
272
  if (result.status === "success" && result.files?.length) {
270
273
  await openExternal(result.files[0].url);
@@ -241,13 +241,18 @@ client-side up front, and re-checked server-side.
241
241
  ### Reading & writing
242
242
 
243
243
  ```tsx
244
- const { comments, available, createComment } = useComments({
245
- record_id: row.__source_record_id,
246
- });
244
+ // In a record screen, where an id definitionally exists:
245
+ const { comments, available, createComment } = useComments({ record_id: recordId });
246
+
247
+ // From a row, gate the whole panel rather than passing a placeholder:
248
+ {row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
247
249
  ```
248
250
 
249
- `record_id` comes from the row's `__source_record_id` addressing column (see
250
- [queries](./queries.md)). State: `{ comments, loading, error, available, createComment,
251
+ `record_id` must be a **real** id — the fetch keys on `available` alone, so an empty string is not
252
+ skipped, it fetches comments for nothing. When the id comes from a row's `__source_record_id`
253
+ addressing column (see [queries](./queries.md)) narrow it first: a grouped query emits no addressing
254
+ columns, and a hook cannot be called conditionally, so the guard belongs at the component that
255
+ renders the panel. State: `{ comments, loading, error, available, createComment,
251
256
  updateComment, deleteComment, refetch }`.
252
257
 
253
258
  - `comments` — newest first on the wire (server order). Pass the array as-is to `@lotics/ui`'s
package/docs/queries.md CHANGED
@@ -35,7 +35,8 @@ Queries live in the app's `package.json` under `lotics.queries` — an alias →
35
35
  },
36
36
  "orderByCode": {
37
37
  "ast": { /* … a filter with "{{params.code}}" … */ },
38
- "params": { "code": { "type": "text" } }
38
+ "params": { "code": { "type": "text" } },
39
+ "description": "One order by its code, with its customer and line total."
39
40
  }
40
41
  }
41
42
  }
@@ -48,9 +49,15 @@ Queries live in the app's `package.json` under `lotics.queries` — an alias →
48
49
  (`text`, `number`, `boolean`, `date`, `datetime`, `email`, `select`, `member`, `record_link`,
49
50
  `date_range`, `file`, `json`, `object`, `array`); each param may set `required: false`
50
51
  (default is required) and `description`. Nesting is capped at depth 8.
52
+ - **`description`** — one line saying what the query returns, capped at 300 characters. No app
53
+ code reads it. It is for the **agents** that reach this app's data — the app's own declared
54
+ agents, and a member's chat while they have the app open — which otherwise see only an alias,
55
+ a JS identifier that names a query without saying what it covers. Write it for whoever has to
56
+ choose between your aliases; longer guidance belongs in an agent's own `instructions`.
51
57
  - Aliases must be valid JS identifiers (`useQuery("openOrders")` and codegen depend on it).
52
58
 
53
- `lotics app deploy` syncs this map to the server. The **server holds the canonical template**;
59
+ `lotics app query set <alias>` (or `--all`) pushes this map to the server a **deploy does
60
+ not**, it echoes the live row back unchanged. The **server holds the canonical template**;
54
61
  the app never sends a raw AST over the wire. This is the exposure model: a public app can read
55
62
  exactly what its author's queries project — params fill declared value holes and can never
56
63
  widen the query's reach (a token in a `table_id` or field-key position fails deploy validation).
@@ -59,7 +66,8 @@ widen the query's reach (a token in a `table_id` or field-key position fails dep
59
66
 
60
67
  Deploy fails — with the compiler's own message, never raw SQL/Postgres text — when any alias:
61
68
 
62
- 1. isn't a valid identifier, or the declaration isn't `{ ast, params? }`;
69
+ 1. isn't a valid identifier, or the declaration isn't `{ ast, params?, description? }` (a
70
+ description over 300 characters is refused here too);
63
71
  2. references `{{params.x}}` without declaring param `x` (typos can't silently widen);
64
72
  3. doesn't parse as a `QueryNode` **as the raw template** (the runtime parses the stored
65
73
  template before substitution, so a template that only parses after substitution would fail
@@ -99,7 +107,7 @@ executes it inside a bounded transaction.
99
107
  (`private_filters`) are excluded on every reach — base scans, link extraction, and
100
108
  non-self-scoped traversals alike. (Row scopes don't apply when the app owner is an admin.)
101
109
  - **Dev loop**: `lotics app dev` forwards query RPCs to the **deployed** manifest. Editing
102
- `lotics.queries` locally does nothing until you `lotics app deploy`.
110
+ `lotics.queries` locally does nothing until you `lotics app query set`.
103
111
 
104
112
  ### Results
105
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.62.2",
3
+ "version": "0.62.4",
4
4
  "description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,7 +20,7 @@
20
20
  "docs"
21
21
  ],
22
22
  "scripts": {
23
- "build": "tsgo",
23
+ "build": "tsgo -p tsconfig.build.json",
24
24
  "typecheck": "tsgo --noEmit",
25
25
  "test": "vitest run",
26
26
  "prepublishOnly": "npm run build"