@lotics/cli 0.196.0 → 0.197.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/dist/src/cli.js CHANGED
@@ -15464,6 +15464,15 @@ var LoticsClient = class {
15464
15464
  async getAppWorkflow(app_id, alias) {
15465
15465
  return this.execute("get_app_workflow", { app_id, alias });
15466
15466
  }
15467
+ /**
15468
+ * The app's capability catalog exactly as a chat or MCP caller reads it —
15469
+ * every query, workflow and agent alias the caller's scope reaches, with the
15470
+ * description each is chosen BY. One read for the whole app, so a check over
15471
+ * what those readers see costs one request rather than one per alias.
15472
+ */
15473
+ async getAppCapabilities(app_id) {
15474
+ return this.execute("get_app_capabilities", { app_id });
15475
+ }
15467
15476
  /**
15468
15477
  * Fetch the server-generated workspace `.d.ts` + the wrapper envelope that
15469
15478
  * make a `src/workflows/<alias>.ts` body locally typecheckable (GAP-59).
@@ -15657,6 +15666,21 @@ var LoticsClient = class {
15657
15666
  await fs2.promises.writeFile(absolutePath, buffer);
15658
15667
  return absolutePath;
15659
15668
  }
15669
+ /**
15670
+ * One page of this workspace's stored files, newest first — `GET /v1/files`.
15671
+ *
15672
+ * The only enumeration there is: every other file surface takes an id, so
15673
+ * without this "what is in the store" is answerable from Postgres alone, which
15674
+ * is exactly the escape hatch a CLI-first workflow exists to remove. Paged by a
15675
+ * keyset cursor the server mints; a caller walks until `next_cursor` is null.
15676
+ */
15677
+ async listFiles(opts) {
15678
+ const params = new URLSearchParams();
15679
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
15680
+ if (opts?.cursor !== void 0) params.set("cursor", opts.cursor);
15681
+ const query = params.toString();
15682
+ return this.request("GET", `/v1/files${query ? `?${query}` : ""}`);
15683
+ }
15660
15684
  async downloadFileById(fileId, destination, options) {
15661
15685
  const signedHeaders = this.buildHeaders();
15662
15686
  const signedUrlRes = await fetch(
@@ -33628,19 +33652,7 @@ var queryWindowFrameSchema = zod_default.object({
33628
33652
  following: zod_default.number().int().nonnegative().optional().describe("Rows following current. Omit for unbounded.")
33629
33653
  });
33630
33654
  var queryNodeSchema = zod_default.lazy(
33631
- () => zod_default.discriminatedUnion("kind", [
33632
- queryFromTableNodeSchema,
33633
- queryProjectNodeSchema,
33634
- queryFilterNodeSchema,
33635
- queryJoinNodeSchema,
33636
- queryUnionNodeSchema,
33637
- queryGroupNodeSchema,
33638
- queryWindowNodeSchema,
33639
- querySortNodeSchema,
33640
- queryLimitNodeSchema,
33641
- queryUnpivotNodeSchema,
33642
- queryUnnestNodeSchema
33643
- ])
33655
+ () => zod_default.discriminatedUnion("kind", queryNodeSchemas)
33644
33656
  );
33645
33657
  var queryFromTableNodeSchema = zod_default.object({
33646
33658
  kind: zod_default.literal("from_table"),
@@ -33770,6 +33782,25 @@ var queryUnnestNodeSchema = zod_default.object({
33770
33782
  "Keep source rows whose cell is NULL / empty as one output row with NULL element columns. Defaults to false (such rows are dropped)."
33771
33783
  )
33772
33784
  });
33785
+ var queryNodeSchemas = [
33786
+ queryFromTableNodeSchema,
33787
+ queryProjectNodeSchema,
33788
+ queryFilterNodeSchema,
33789
+ queryJoinNodeSchema,
33790
+ queryUnionNodeSchema,
33791
+ queryGroupNodeSchema,
33792
+ queryWindowNodeSchema,
33793
+ querySortNodeSchema,
33794
+ queryLimitNodeSchema,
33795
+ queryUnpivotNodeSchema,
33796
+ queryUnnestNodeSchema
33797
+ ];
33798
+ var queryNodeKeysByKind = new Map(
33799
+ queryNodeSchemas.map((schema) => [
33800
+ schema.shape.kind.value,
33801
+ new Set(Object.keys(schema.shape))
33802
+ ])
33803
+ );
33773
33804
  var appWorkflowInputBaseSchema = zod_default.object({
33774
33805
  description: zod_default.string().optional().describe("Human-readable description of this input \u2014 surfaces in agent + CLI tooling"),
33775
33806
  required: zod_default.boolean().optional().describe("Whether the input must be provided. Defaults to true.")
@@ -36684,7 +36715,7 @@ function resultSideEffects(result) {
36684
36715
  }
36685
36716
 
36686
36717
  // src/version.ts
36687
- var VERSION = "0.196.0";
36718
+ var VERSION = "0.197.0";
36688
36719
 
36689
36720
  // src/timezone.ts
36690
36721
  function machineTimezone() {
@@ -37113,8 +37144,15 @@ var COMMANDS = [
37113
37144
  help: [
37114
37145
  " lotics workspace List workspaces in the active org (marks current)",
37115
37146
  " lotics workspace select <id> Switch to a different workspace",
37116
- " lotics workspace create <name> Create a new workspace (admin only)",
37117
- " lotics workspace rename <name> Rename the current workspace (admin only)",
37147
+ " lotics workspace create <name> [--timezone <Area/City>] [--currency <ISO>]",
37148
+ " Create a new workspace (admin only). Without them it",
37149
+ " takes the org's oldest workspace's zone and currency \u2014",
37150
+ " which decide how every date buckets and every money",
37151
+ " field renders, and are invisible once wrong",
37152
+ " lotics workspace settings [--name <n>] [--currency <ISO>] [--timezone <Area/City>]",
37153
+ " Change the current workspace's name, currency or zone",
37154
+ " (admin only). Only what you name changes",
37155
+ " lotics workspace rename <name> Rename the current workspace \u2014 settings, by name alone",
37118
37156
  " lotics workspace delete <id> --yes Delete a workspace (admin only; soft delete, recoverable)",
37119
37157
  " lotics workspace doctor Report dangling schema references (admin only)"
37120
37158
  ]
@@ -37194,7 +37232,9 @@ var COMMANDS = [
37194
37232
  help: [
37195
37233
  " lotics run <tool> '<json>' Execute a tool",
37196
37234
  " lotics run <tool> @args.json Read JSON args from a file (large payloads)",
37197
- " cat args.json | lotics run <tool> Read JSON args from stdin (large payloads)",
37235
+ " cat args.json | lotics run <tool> - Read JSON args from stdin (large payloads).",
37236
+ " The trailing - is required: without it the tool",
37237
+ " runs with no arguments and never waits.",
37198
37238
  " Exits non-zero when the RESULT reports the work",
37199
37239
  " failed (a refused workflow, a failed agent run) \u2014",
37200
37240
  " not only when the call did.",
@@ -37215,9 +37255,14 @@ var COMMANDS = [
37215
37255
  " (-m is required \u2014 it's the version's audit trail;",
37216
37256
  " carries code + queries only \u2014 workflow bindings are",
37217
37257
  " managed by set_app_workflow / remove_app_workflow.",
37218
- " --prune also UNBINDS aliases this bundle no longer",
37219
- " names; off by default because `run_app_workflow` and",
37220
- " chat reach a binding the bundle never calls)",
37258
+ " --prune also UNBINDS what this project retired \u2014 an",
37259
+ " alias this bundle stopped calling, and one whose",
37260
+ " declaration you deleted; off by default because",
37261
+ " `run_app_workflow` and chat reach a binding the",
37262
+ " bundle never calls.",
37263
+ " --prune-invoked <alias> unbinds a workflow the",
37264
+ " workspace has already RUN \u2014 named per alias, only",
37265
+ " when you know that caller is gone)",
37221
37266
  " lotics app versions [app_id] Show deploy history newest-first (version,",
37222
37267
  " timestamp, deployer, build status, -m message;",
37223
37268
  " * marks the currently served version)",
@@ -37249,8 +37294,13 @@ var COMMANDS = [
37249
37294
  " lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body",
37250
37295
  " through set_app_workflow (server verifies)",
37251
37296
  " lotics app workflow pull Rewrite src/workflows/*.ts from the server",
37252
- " lotics app workflow check [alias] Typecheck src/workflows bodies locally (one",
37253
- " isolated program per alias; the app's own tsc)",
37297
+ " lotics app workflow check [alias...] Typecheck src/workflows bodies locally (one",
37298
+ " isolated program per alias; the app's own tsc).",
37299
+ " Name several to check several; none checks all",
37300
+ " lotics app workflow diff [alias...] Show how src/workflows/<alias>.ts differs from",
37301
+ " the body the server is running, line by line.",
37302
+ " None named diffs every alias that reads as",
37303
+ " drifted. Exits 1 when anything differs",
37254
37304
  " lotics app query set <alias> Push package.json#lotics.queries.<alias> to",
37255
37305
  " apps.queries via set_app_query and regenerate",
37256
37306
  " .lotics/app_queries.d.ts (no deploy; re-synced by",
@@ -37262,7 +37312,12 @@ var COMMANDS = [
37262
37312
  " those with set_app_agent, then pull.",
37263
37313
  " lotics app subdomain <new-subdomain> Rename the app's public address (its subdomain)",
37264
37314
  ` lotics app rename "<new name>" Rename the app's display name (launcher title)`,
37265
- " lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)"
37315
+ " lotics app dev [path] [--port=<n>] [--vite-port=<n>]",
37316
+ " Run the app locally with HMR (RPC forwarded to prod).",
37317
+ " --port is the wrapper you open, --vite-port the module",
37318
+ " server. Pin both to run several apps at once; with no",
37319
+ " --vite-port, vite.config's server.port is used",
37320
+ " lotics app dev --view-as <member_id> \u2026 as that member sees it (admin only, reads only)"
37266
37321
  ]
37267
37322
  },
37268
37323
  {
@@ -37294,6 +37349,13 @@ var COMMANDS = [
37294
37349
  verbs: ["file"],
37295
37350
  aliases: ["upload", "download", "preview"],
37296
37351
  help: [
37352
+ " lotics file list [--limit <n>] [--cursor <token>]",
37353
+ " List this workspace's files newest-first \u2014",
37354
+ " id, date, bytes, type, name. One page; the last",
37355
+ " line prints the command for the next one",
37356
+ " lotics file delete <file_id> Archive a stored file. Refused while a record,",
37357
+ " comment, knowledge doc, document template or",
37358
+ " voice session still references it \u2014 it names them",
37297
37359
  " lotics file upload <file|dir...> Upload files (alias: lotics upload)",
37298
37360
  " lotics file upload --stdin --as <name>",
37299
37361
  " Upload bytes you already hold, piped on stdin",
@@ -37315,9 +37377,33 @@ var COMMANDS = [
37315
37377
  function renderCommandsHelp() {
37316
37378
  return COMMANDS.flatMap((group) => group.help).join("\n");
37317
37379
  }
37380
+ function helpEntries(help) {
37381
+ const entries2 = [];
37382
+ for (const line of help) {
37383
+ if (line.startsWith(" lotics ") || entries2.length === 0) entries2.push([line]);
37384
+ else entries2[entries2.length - 1].push(line);
37385
+ }
37386
+ return entries2;
37387
+ }
37388
+ function commandHelp(path15) {
37389
+ const words = path15.filter((word) => word !== "");
37390
+ if (words.length === 0) return null;
37391
+ const group = COMMANDS.find(
37392
+ (g) => g.verbs.includes(words[0]) || (g.aliases ?? []).includes(words[0])
37393
+ );
37394
+ if (!group) return null;
37395
+ if (words.length === 1) return group.help.join("\n");
37396
+ const prefix = ` lotics ${words.join(" ")}`;
37397
+ const matched = helpEntries(group.help).filter((entry) => entry[0].startsWith(prefix));
37398
+ return (matched.length > 0 ? matched.flat() : group.help).join("\n");
37399
+ }
37318
37400
  function knownVerbs() {
37319
37401
  return new Set(COMMANDS.flatMap((group) => [...group.verbs, ...group.aliases ?? []]));
37320
37402
  }
37403
+ function commandAliases(verb) {
37404
+ const group = COMMANDS.find((g) => g.verbs.includes(verb));
37405
+ return new Set(group?.aliases ?? []);
37406
+ }
37321
37407
 
37322
37408
  // src/inputs.ts
37323
37409
  function shellQuotingHint(raw, source) {
@@ -37336,7 +37422,10 @@ function readStdin() {
37336
37422
  async function ingestJsonArgs(opts) {
37337
37423
  let raw = opts.rawArg;
37338
37424
  let source = "inline";
37339
- if (raw && raw.startsWith("@")) {
37425
+ if (raw === "-") {
37426
+ source = "stdin";
37427
+ raw = await opts.readStdin();
37428
+ } else if (raw && raw.startsWith("@")) {
37340
37429
  source = "file";
37341
37430
  const argsPath = raw.slice(1);
37342
37431
  try {
@@ -37347,9 +37436,6 @@ async function ingestJsonArgs(opts) {
37347
37436
  message: `Cannot read args file "${argsPath}": ${err instanceof Error ? err.message : String(err)}`
37348
37437
  };
37349
37438
  }
37350
- } else if (!raw && !opts.stdinIsTTY) {
37351
- source = "stdin";
37352
- raw = await opts.readStdin();
37353
37439
  }
37354
37440
  if (!raw) return { kind: "ok", args: {} };
37355
37441
  try {
@@ -38521,6 +38607,13 @@ function queryOutputNames(node) {
38521
38607
  }
38522
38608
 
38523
38609
  // ../shared/src/app_dts.ts
38610
+ function selectOptionValues(decl, optionsByFieldKey) {
38611
+ const inline = Array.isArray(decl.options) ? decl.options : null;
38612
+ const source = inline ?? (typeof decl.field === "string" ? optionsByFieldKey?.get(decl.field) : void 0) ?? [];
38613
+ return [...source].map(
38614
+ (o) => o !== null && typeof o === "object" && "value" in o && typeof o.value === "string" ? o.value : null
38615
+ ).filter((v) => v !== null);
38616
+ }
38524
38617
  var IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
38525
38618
  function isValidIdentifier(name) {
38526
38619
  return IDENTIFIER_REGEX.test(name);
@@ -38531,7 +38624,7 @@ function inputsToType(inputs, opts) {
38531
38624
  for (const [key, decl] of Object.entries(inputs)) {
38532
38625
  if (decl === null || typeof decl !== "object") continue;
38533
38626
  const d = decl;
38534
- const tsType = inputDeclToTsType(d);
38627
+ const tsType = inputDeclToTsType(d, opts?.optionsByFieldKey);
38535
38628
  const isOptional = d.required === false;
38536
38629
  const fieldType = isOptional && nullableOptional ? `${tsType} | null` : tsType;
38537
38630
  const optional2 = isOptional ? "?" : "";
@@ -38543,7 +38636,7 @@ function inputsToType(inputs, opts) {
38543
38636
  ${fields.join("\n")}
38544
38637
  }`;
38545
38638
  }
38546
- function inputDeclToTsType(decl) {
38639
+ function inputDeclToTsType(decl, optionsByFieldKey) {
38547
38640
  const type = decl.type;
38548
38641
  switch (type) {
38549
38642
  case "text":
@@ -38561,13 +38654,7 @@ function inputDeclToTsType(decl) {
38561
38654
  return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
38562
38655
  }
38563
38656
  case "select": {
38564
- const options = Array.isArray(decl.options) ? decl.options : [];
38565
- const literals = options.map((o) => {
38566
- if (o !== null && typeof o === "object" && "value" in o && typeof o.value === "string") {
38567
- return JSON.stringify(o.value);
38568
- }
38569
- return null;
38570
- }).filter((v) => v !== null);
38657
+ const literals = selectOptionValues(decl, optionsByFieldKey).map((v) => JSON.stringify(v));
38571
38658
  const inner = literals.length > 0 ? `${literals.join(" | ")} | (string & {})` : "string";
38572
38659
  return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
38573
38660
  }
@@ -38577,11 +38664,11 @@ function inputDeclToTsType(decl) {
38577
38664
  return decl.multi === true ? "ReadonlyArray<string>" : "string";
38578
38665
  case "object": {
38579
38666
  const fields = decl.fields !== null && typeof decl.fields === "object" ? decl.fields : {};
38580
- return inputsToType(fields);
38667
+ return inputsToType(fields, { optionsByFieldKey });
38581
38668
  }
38582
38669
  case "array": {
38583
38670
  const items = decl.items !== null && typeof decl.items === "object" ? decl.items : null;
38584
- return items ? `ReadonlyArray<${inputDeclToTsType(items)}>` : "ReadonlyArray<unknown>";
38671
+ return items ? `ReadonlyArray<${inputDeclToTsType(items, optionsByFieldKey)}>` : "ReadonlyArray<unknown>";
38585
38672
  }
38586
38673
  case "json":
38587
38674
  return "unknown";
@@ -38589,19 +38676,20 @@ function inputDeclToTsType(decl) {
38589
38676
  return "unknown";
38590
38677
  }
38591
38678
  }
38592
- function objectFieldsToType(fields) {
38679
+ function objectFieldsToType(fields, optionsByFieldKey) {
38593
38680
  const parts = [];
38594
38681
  for (const [key, decl] of Object.entries(fields)) {
38595
38682
  if (decl === null || typeof decl !== "object") continue;
38596
38683
  const d = decl;
38597
- const optional2 = d.required === false ? "?" : "";
38684
+ const isOptional = d.required === false;
38685
+ const tsType = outputDeclToTsType(d, optionsByFieldKey);
38598
38686
  const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
38599
- parts.push(`${fieldKey}${optional2}: ${outputDeclToTsType(d)}`);
38687
+ parts.push(`${fieldKey}${isOptional ? "?" : ""}: ${isOptional ? `${tsType} | null` : tsType}`);
38600
38688
  }
38601
38689
  if (parts.length === 0) return "Record<string, never>";
38602
38690
  return `{ ${parts.join("; ")} }`;
38603
38691
  }
38604
- function outputDeclToTsType(decl) {
38692
+ function outputDeclToTsType(decl, optionsByFieldKey) {
38605
38693
  const type = decl.type;
38606
38694
  switch (type) {
38607
38695
  case "text":
@@ -38616,20 +38704,17 @@ function outputDeclToTsType(decl) {
38616
38704
  case "record_link":
38617
38705
  return decl.multi === true ? "ReadonlyArray<string>" : "string";
38618
38706
  case "select": {
38619
- const options = Array.isArray(decl.options) ? decl.options : [];
38620
- const literals = options.map(
38621
- (o) => o !== null && typeof o === "object" && "value" in o && typeof o.value === "string" ? JSON.stringify(o.value) : null
38622
- ).filter((v) => v !== null);
38707
+ const literals = selectOptionValues(decl, optionsByFieldKey).map((v) => JSON.stringify(v));
38623
38708
  const inner = literals.length > 0 ? `${literals.join(" | ")} | (string & {})` : "string";
38624
38709
  return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
38625
38710
  }
38626
38711
  case "object": {
38627
38712
  const fields = decl.fields !== null && typeof decl.fields === "object" ? decl.fields : {};
38628
- return objectFieldsToType(fields);
38713
+ return objectFieldsToType(fields, optionsByFieldKey);
38629
38714
  }
38630
38715
  case "array": {
38631
38716
  const items = decl.items !== null && typeof decl.items === "object" ? decl.items : null;
38632
- return items ? `ReadonlyArray<${outputDeclToTsType(items)}>` : "ReadonlyArray<unknown>";
38717
+ return items ? `ReadonlyArray<${outputDeclToTsType(items, optionsByFieldKey)}>` : "ReadonlyArray<unknown>";
38633
38718
  }
38634
38719
  case "json":
38635
38720
  return "unknown";
@@ -38645,7 +38730,7 @@ var QUERIES_HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
38645
38730
 
38646
38731
  import "@lotics/app-sdk";
38647
38732
  `;
38648
- function generateAppQueriesDts(queries) {
38733
+ function generateAppQueriesDts(queries, optionsByFieldKey) {
38649
38734
  const entries2 = Object.entries(queries ?? {});
38650
38735
  if (entries2.length === 0) {
38651
38736
  return `${QUERIES_HEADER}
@@ -38660,7 +38745,7 @@ declare module "@lotics/app-sdk" {
38660
38745
  const lines = [];
38661
38746
  const columnLines = [];
38662
38747
  for (const [alias, declaration] of entries2) {
38663
- const valueType = inputsToType(declaration.params ?? {});
38748
+ const valueType = inputsToType(declaration.params ?? {}, { optionsByFieldKey });
38664
38749
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
38665
38750
  lines.push(` ${aliasKey}: ${valueType};`);
38666
38751
  const parsed = declaration.ast === void 0 ? void 0 : queryNodeSchema.safeParse(declaration.ast);
@@ -38692,7 +38777,7 @@ var WORKFLOWS_HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
38692
38777
 
38693
38778
  import "@lotics/app-sdk";
38694
38779
  `;
38695
- function generateAppWorkflowsDts(workflows) {
38780
+ function generateAppWorkflowsDts(workflows, optionsByFieldKey) {
38696
38781
  const entries2 = Object.entries(workflows ?? {});
38697
38782
  if (entries2.length === 0) {
38698
38783
  return `${WORKFLOWS_HEADER}
@@ -38707,11 +38792,13 @@ declare module "@lotics/app-sdk" {
38707
38792
  const inputLines = [];
38708
38793
  const resultLines = [];
38709
38794
  for (const [alias, declaration] of entries2) {
38710
- const valueType = declaration.inputs ? inputsToType(declaration.inputs, { nullableOptional: true }) : "Record<string, unknown>";
38795
+ const valueType = declaration.inputs ? inputsToType(declaration.inputs, { nullableOptional: true, optionsByFieldKey }) : "Record<string, unknown>";
38711
38796
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
38712
38797
  inputLines.push(` ${aliasKey}: ${valueType};`);
38713
38798
  if (declaration.outputs) {
38714
- resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
38799
+ resultLines.push(
38800
+ ` ${aliasKey}: ${objectFieldsToType(declaration.outputs, optionsByFieldKey)};`
38801
+ );
38715
38802
  }
38716
38803
  }
38717
38804
  const resultsBlock = resultLines.length > 0 ? `
@@ -38734,7 +38821,7 @@ var AGENTS_HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
38734
38821
 
38735
38822
  import "@lotics/app-sdk";
38736
38823
  `;
38737
- function generateAppAgentsDts(agents) {
38824
+ function generateAppAgentsDts(agents, optionsByFieldKey) {
38738
38825
  const entries2 = Object.entries(agents ?? {});
38739
38826
  if (entries2.length === 0) {
38740
38827
  return `${AGENTS_HEADER}
@@ -38749,11 +38836,13 @@ declare module "@lotics/app-sdk" {
38749
38836
  const inputLines = [];
38750
38837
  const resultLines = [];
38751
38838
  for (const [alias, declaration] of entries2) {
38752
- const valueType = declaration.inputs ? inputsToType(declaration.inputs) : "Record<string, unknown>";
38839
+ const valueType = declaration.inputs ? inputsToType(declaration.inputs, { optionsByFieldKey }) : "Record<string, unknown>";
38753
38840
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
38754
38841
  inputLines.push(` ${aliasKey}: ${valueType};`);
38755
38842
  if (declaration.outputs) {
38756
- resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
38843
+ resultLines.push(
38844
+ ` ${aliasKey}: ${objectFieldsToType(declaration.outputs, optionsByFieldKey)};`
38845
+ );
38757
38846
  }
38758
38847
  }
38759
38848
  const resultsBlock = resultLines.length > 0 ? `
@@ -39074,6 +39163,20 @@ export type AppFields = typeof F;
39074
39163
  export type AppOptions = typeof OPT;
39075
39164
  `;
39076
39165
  }
39166
+ function selectOptionsByFieldKey(tables) {
39167
+ const byKey = /* @__PURE__ */ new Map();
39168
+ for (const table of tables) {
39169
+ for (const field of table.fields) {
39170
+ if (field.options && field.options.length > 0) {
39171
+ byKey.set(
39172
+ field.id,
39173
+ field.options.map((option) => ({ value: option.id }))
39174
+ );
39175
+ }
39176
+ }
39177
+ }
39178
+ return byKey;
39179
+ }
39077
39180
  function codegenTableIds(queries, allowlist) {
39078
39181
  const ids = new Set(allowlist);
39079
39182
  for (const declaration of Object.values(queries)) {
@@ -41123,15 +41226,7 @@ async function dispatchRpc(client, body, opts) {
41123
41226
  if (!p || typeof p.alias !== "string") {
41124
41227
  throw new Error("query payload must include `alias`");
41125
41228
  }
41126
- return client.appQuery(body.app_id, {
41127
- alias: p.alias,
41128
- params: p.params,
41129
- limit: p.limit,
41130
- offset: p.offset,
41131
- sort: p.sort,
41132
- filter: p.filter,
41133
- count: p.count
41134
- });
41229
+ return client.appQuery(body.app_id, { ...p, alias: p.alias });
41135
41230
  }
41136
41231
  case "field_options": {
41137
41232
  const p = body.payload;
@@ -41265,6 +41360,12 @@ function openAppTarget(payload) {
41265
41360
  }
41266
41361
 
41267
41362
  // src/dev/wrapper_page.ts
41363
+ function deepLinkLoc(pathname, search) {
41364
+ var explicit = new URLSearchParams(search).get("_loc");
41365
+ if (explicit) return explicit;
41366
+ if (pathname === "/" || pathname === "/index.html") return null;
41367
+ return pathname + search;
41368
+ }
41268
41369
  function buildWrapperPage(args) {
41269
41370
  const { app_name, app_id, workspace_id, vite_url, api_url, web_app_url } = args;
41270
41371
  return `<!doctype html>
@@ -41322,7 +41423,8 @@ function buildWrapperPage(args) {
41322
41423
  // server (a url param flowing into an iframe src is a redirect vector);
41323
41424
  // anything else falls back to the app root.
41324
41425
  const appSrc = new URL(VITE_URL);
41325
- const savedLoc = new URLSearchParams(window.location.search).get("_loc");
41426
+ var deepLinkLoc = (${deepLinkLoc.toString()});
41427
+ const savedLoc = deepLinkLoc(window.location.pathname, window.location.search);
41326
41428
  if (savedLoc) {
41327
41429
  try {
41328
41430
  const resolved = new URL(savedLoc, VITE_ORIGIN);
@@ -41682,6 +41784,18 @@ var STOP_DEADLINE_MS = 2e3;
41682
41784
  function serializeRpcResult(result) {
41683
41785
  return JSON.stringify(result ?? null);
41684
41786
  }
41787
+ function servesWrapperPage(method, pathname) {
41788
+ return method === "GET" && !pathname.startsWith("/_");
41789
+ }
41790
+ function viteChildArgs(viteEntry, vitePort, uiSrc) {
41791
+ return [
41792
+ viteEntry,
41793
+ "--port",
41794
+ String(vitePort),
41795
+ "--strictPort",
41796
+ ...uiSrc ? ["--force"] : []
41797
+ ];
41798
+ }
41685
41799
  function nodeForChildProcess() {
41686
41800
  return process.versions.bun === void 0 ? process.execPath : "node";
41687
41801
  }
@@ -41705,7 +41819,7 @@ async function startDevServer(args) {
41705
41819
  }
41706
41820
  const viteChild = spawn(
41707
41821
  nodeForChildProcess(),
41708
- [viteEntry, "--port", String(vitePort), "--strictPort"],
41822
+ viteChildArgs(viteEntry, vitePort, process.env.LOTICS_UI_SRC),
41709
41823
  {
41710
41824
  cwd: args.projectDir,
41711
41825
  stdio: [process.stdin.isTTY ? "inherit" : "ignore", "inherit", "inherit"],
@@ -41754,7 +41868,7 @@ async function startDevServer(args) {
41754
41868
  const server = http.createServer(async (req, res) => {
41755
41869
  const url2 = req.url ?? "/";
41756
41870
  const pathname = url2.split("?")[0];
41757
- if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
41871
+ if (servesWrapperPage(req.method, pathname)) {
41758
41872
  res.writeHead(200, {
41759
41873
  "Content-Type": "text/html; charset=utf-8",
41760
41874
  "Cache-Control": "no-cache, no-store, must-revalidate"
@@ -43411,6 +43525,7 @@ __export(expression_string_exports, {
43411
43525
  capitalize: () => capitalize,
43412
43526
  contains: () => contains,
43413
43527
  endsWith: () => endsWith,
43528
+ formatDecimal: () => formatDecimal,
43414
43529
  formatNumber: () => formatNumber,
43415
43530
  join: () => join3,
43416
43531
  length: () => length,
@@ -43426,6 +43541,18 @@ __export(expression_string_exports, {
43426
43541
  trim: () => trim,
43427
43542
  upper: () => upper
43428
43543
  });
43544
+
43545
+ // ../shared/src/expression_locale.ts
43546
+ function isUsableLocale(locale) {
43547
+ try {
43548
+ Intl.NumberFormat.supportedLocalesOf(locale);
43549
+ return true;
43550
+ } catch {
43551
+ return false;
43552
+ }
43553
+ }
43554
+
43555
+ // ../shared/src/expression_string.ts
43429
43556
  function join3(arr, separator) {
43430
43557
  if (arr == null) return "";
43431
43558
  if (!Array.isArray(arr)) {
@@ -43658,10 +43785,29 @@ function formatNumber(value, decimals) {
43658
43785
  if (typeof value !== "number" || Number.isNaN(value)) {
43659
43786
  throw new Error("formatNumber: expected number, got " + typeof value);
43660
43787
  }
43788
+ assertDecimals(decimals, "formatNumber");
43789
+ return value.toFixed(decimals);
43790
+ }
43791
+ function formatDecimal(value, decimals, locale) {
43792
+ if (value == null) return "";
43793
+ if (typeof value !== "number" || Number.isNaN(value)) {
43794
+ throw new Error("formatDecimal: expected number, got " + typeof value);
43795
+ }
43796
+ assertDecimals(decimals, "formatDecimal");
43797
+ if (typeof locale !== "string" || locale === "" || !isUsableLocale(locale)) {
43798
+ throw new Error(
43799
+ `formatDecimal(value, decimals, locale) requires a locale tag. Got ${JSON.stringify(locale)}. Example: formatDecimal(151000, 0, "vi-VN").`
43800
+ );
43801
+ }
43802
+ return new Intl.NumberFormat(locale, {
43803
+ minimumFractionDigits: decimals,
43804
+ maximumFractionDigits: decimals
43805
+ }).format(value);
43806
+ }
43807
+ function assertDecimals(decimals, funcName) {
43661
43808
  if (!Number.isInteger(decimals) || decimals < 0 || decimals > 100) {
43662
- throw new Error("formatNumber: decimals must be an integer between 0 and 100");
43809
+ throw new Error(`${funcName}: decimals must be an integer between 0 and 100`);
43663
43810
  }
43664
- return value.toFixed(decimals);
43665
43811
  }
43666
43812
 
43667
43813
  // ../shared/src/expression_math.ts
@@ -44074,7 +44220,7 @@ function coalesce(...values2) {
44074
44220
  for (const val of values2) {
44075
44221
  if (val != null) return val;
44076
44222
  }
44077
- return void 0;
44223
+ return null;
44078
44224
  }
44079
44225
  function toNumber(val) {
44080
44226
  if (val == null) return null;
@@ -44143,6 +44289,15 @@ function parseDateStringInTimezone(dateStr, timezone) {
44143
44289
  const zonedDate = new TZDate(year, month, day, hour, minute, second, ms, timezone);
44144
44290
  return new Date(zonedDate.getTime());
44145
44291
  }
44292
+ function isDayOnly(date5) {
44293
+ return typeof date5 === "string" && /^\d{4}(?:-\d{2}(?:-\d{2})?)?$/.test(date5);
44294
+ }
44295
+ function compareCalendarDays(left, right, timezone) {
44296
+ if (timezone) {
44297
+ return differenceInCalendarDays(new TZDate(left, timezone), new TZDate(right, timezone));
44298
+ }
44299
+ return differenceInCalendarDays(left, right);
44300
+ }
44146
44301
  function normalizeDate(date5, timezone) {
44147
44302
  if (date5 instanceof Date) {
44148
44303
  return date5;
@@ -44424,6 +44579,13 @@ function getDateExpressionFunctions(defaultTimezone) {
44424
44579
  // ========================================================================
44425
44580
  /**
44426
44581
  * Check if dateLeft is before dateRight. Returns false if either is null.
44582
+ *
44583
+ * A DAY-precision operand (a `date` field, which holds no time) compares at
44584
+ * DAY granularity in the workspace timezone. Resolving the day to midnight
44585
+ * and comparing instants made `isBefore({due}, now())` answer by the
44586
+ * reader's offset: the deadline "had passed" from the first hour of its own
44587
+ * day, and a countdown rendered from the same cell disagreed with the flag
44588
+ * beside it. Two instants still compare as instants.
44427
44589
  */
44428
44590
  isBefore: (dateLeft, dateRight, timezone) => {
44429
44591
  if (dateLeft == null || dateLeft === "" || dateRight == null || dateRight === "") return false;
@@ -44433,10 +44595,14 @@ function getDateExpressionFunctions(defaultTimezone) {
44433
44595
  const tz = getTimezone(timezone);
44434
44596
  const left = normalizeDateWithValidation(dateLeft, tz, "isBefore");
44435
44597
  const right = normalizeDateWithValidation(dateRight, tz, "isBefore");
44598
+ if (isDayOnly(dateLeft) || isDayOnly(dateRight)) {
44599
+ return compareCalendarDays(left, right, tz) < 0;
44600
+ }
44436
44601
  return isBefore(left, right);
44437
44602
  },
44438
44603
  /**
44439
44604
  * Check if dateLeft is after dateRight. Returns false if either is null.
44605
+ * Day-precision operands compare at day granularity — see isBefore.
44440
44606
  */
44441
44607
  isAfter: (dateLeft, dateRight, timezone) => {
44442
44608
  if (dateLeft == null || dateLeft === "" || dateRight == null || dateRight === "") return false;
@@ -44446,6 +44612,9 @@ function getDateExpressionFunctions(defaultTimezone) {
44446
44612
  const tz = getTimezone(timezone);
44447
44613
  const left = normalizeDateWithValidation(dateLeft, tz, "isAfter");
44448
44614
  const right = normalizeDateWithValidation(dateRight, tz, "isAfter");
44615
+ if (isDayOnly(dateLeft) || isDayOnly(dateRight)) {
44616
+ return compareCalendarDays(left, right, tz) > 0;
44617
+ }
44449
44618
  return isAfter(left, right);
44450
44619
  },
44451
44620
  /**
@@ -44486,6 +44655,9 @@ function getDateExpressionFunctions(defaultTimezone) {
44486
44655
  },
44487
44656
  /**
44488
44657
  * Check if a date is within a range (inclusive). Returns false if any input is null.
44658
+ * It is the two comparisons above, so a day-precision operand anywhere in it
44659
+ * puts the whole check at day granularity — otherwise `isWithinRange(d, a, b)`
44660
+ * and `!isBefore(d, a) && !isAfter(d, b)` would answer differently.
44489
44661
  */
44490
44662
  isWithinRange: (date5, startDate, endDate, timezone) => {
44491
44663
  if (date5 == null || date5 === "" || startDate == null || startDate === "" || endDate == null || endDate === "") return false;
@@ -44497,6 +44669,9 @@ function getDateExpressionFunctions(defaultTimezone) {
44497
44669
  const normalizedDate = normalizeDateWithValidation(date5, tz, "isWithinRange");
44498
44670
  const normalizedStart = normalizeDateWithValidation(startDate, tz, "isWithinRange");
44499
44671
  const normalizedEnd = normalizeDateWithValidation(endDate, tz, "isWithinRange");
44672
+ if (isDayOnly(date5) || isDayOnly(startDate) || isDayOnly(endDate)) {
44673
+ return compareCalendarDays(normalizedDate, normalizedStart, tz) >= 0 && compareCalendarDays(normalizedDate, normalizedEnd, tz) <= 0;
44674
+ }
44500
44675
  return isWithinInterval(normalizedDate, {
44501
44676
  start: normalizedStart,
44502
44677
  end: normalizedEnd
@@ -46988,9 +47163,12 @@ function walkToolInvocation(call, scope, opts, allowWaits) {
46988
47163
  const input = walkToolInputObject(arg, scope);
46989
47164
  return { tool_name: name, input };
46990
47165
  }
47166
+ var TOOL_INPUT_ARRAY_SPREAD = "Spread is not supported inside a tool input. Write concat(<array>, <array>) here, or bind the merged array first (const xs = [...a, ...b];) and pass xs.";
47167
+ var TOOL_INPUT_OBJECT_SPREAD = "Spread is not supported inside a tool input. Bind the merged object first (const x = { ...a, b: 1 };) and pass x.";
46991
47168
  function walkToolInputObject(obj, scope) {
46992
47169
  const out = {};
46993
47170
  for (const p of obj.properties) {
47171
+ if (p.type === "SpreadElement") fail2(p, TOOL_INPUT_OBJECT_SPREAD, "spread_element");
46994
47172
  if (p.type !== "ObjectProperty") fail2(p, "Tool kwargs must be plain key: value pairs.");
46995
47173
  if (p.computed) fail2(p, "Computed property keys are not supported.", "computed_key");
46996
47174
  const key = p.key.type === "StringLiteral" ? p.key.value : p.key.type === "Identifier" ? p.key.name : null;
@@ -47003,6 +47181,7 @@ function walkToolInputValue(node, scope) {
47003
47181
  if (node.type === "ObjectExpression") {
47004
47182
  const inner = {};
47005
47183
  for (const p of node.properties) {
47184
+ if (p.type === "SpreadElement") fail2(p, TOOL_INPUT_OBJECT_SPREAD, "spread_element");
47006
47185
  if (p.type !== "ObjectProperty") fail2(p, "Tool kwargs must be plain key: value pairs.");
47007
47186
  if (p.computed) fail2(p, "Computed property keys are not supported.");
47008
47187
  const key = p.key.type === "StringLiteral" ? p.key.value : p.key.type === "Identifier" ? p.key.name : null;
@@ -47015,7 +47194,7 @@ function walkToolInputValue(node, scope) {
47015
47194
  const arr = [];
47016
47195
  for (const elem of node.elements) {
47017
47196
  if (elem === null) fail2(node, "Sparse arrays are not supported.", "sparse_array");
47018
- if (elem.type === "SpreadElement") fail2(elem, "Spread/rest is not supported.", "spread_element");
47197
+ if (elem.type === "SpreadElement") fail2(elem, TOOL_INPUT_ARRAY_SPREAD, "spread_element");
47019
47198
  arr.push(walkToolInputValue(elem, scope));
47020
47199
  }
47021
47200
  return arr;
@@ -47525,6 +47704,7 @@ function generatePrefixedId(prefix) {
47525
47704
  return `${prefix}_${nanoid3()}`;
47526
47705
  }
47527
47706
  var RECORD_ID_PATTERN = new RegExp(`^rec_[${ID_ALPHABET}]{${ID_LENGTH}}$`);
47707
+ var FILE_ID_PATTERN = new RegExp(`^fil_[${ID_ALPHABET}]{${ID_LENGTH}}$`);
47528
47708
  function generateWorkspaceId() {
47529
47709
  return generatePrefixedId("wsp");
47530
47710
  }
@@ -47697,6 +47877,137 @@ async function requireLatestNpmVersion(packageName, { timeoutMs = 1e4 } = {}) {
47697
47877
  return version2;
47698
47878
  }
47699
47879
 
47880
+ // src/line_diff.ts
47881
+ function lineDiff(before, after) {
47882
+ const rows = before.length;
47883
+ const cols = after.length;
47884
+ const lcs = Array.from(
47885
+ { length: rows + 1 },
47886
+ () => Array.from({ length: cols + 1 }, () => 0)
47887
+ );
47888
+ for (let i2 = rows - 1; i2 >= 0; i2--) {
47889
+ for (let j2 = cols - 1; j2 >= 0; j2--) {
47890
+ lcs[i2][j2] = before[i2] === after[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
47891
+ }
47892
+ }
47893
+ const out = [];
47894
+ let i = 0;
47895
+ let j = 0;
47896
+ while (i < rows && j < cols) {
47897
+ if (before[i] === after[j]) {
47898
+ out.push({ kind: "same", text: before[i] });
47899
+ i++;
47900
+ j++;
47901
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
47902
+ out.push({ kind: "removed", text: before[i] });
47903
+ i++;
47904
+ } else {
47905
+ out.push({ kind: "added", text: after[j] });
47906
+ j++;
47907
+ }
47908
+ }
47909
+ for (; i < rows; i++) out.push({ kind: "removed", text: before[i] });
47910
+ for (; j < cols; j++) out.push({ kind: "added", text: after[j] });
47911
+ return out;
47912
+ }
47913
+ var MARK = { same: " ", removed: "-", added: "+" };
47914
+ function renderLineDiff(before, after, opts = {}) {
47915
+ const context = opts.context ?? 3;
47916
+ const lines = lineDiff(before.split("\n"), after.split("\n"));
47917
+ const changed = lines.map((line) => line.kind !== "same");
47918
+ const keep = lines.map(
47919
+ (_, index) => changed.slice(Math.max(0, index - context), index + context + 1).some(Boolean)
47920
+ );
47921
+ const out = [];
47922
+ let elided = false;
47923
+ for (const [index, line] of lines.entries()) {
47924
+ if (!keep[index]) {
47925
+ elided = true;
47926
+ continue;
47927
+ }
47928
+ if (elided && out.length > 0) out.push(" \u2026");
47929
+ elided = false;
47930
+ out.push(`${MARK[line.kind]} ${line.text}`);
47931
+ }
47932
+ return out.join("\n");
47933
+ }
47934
+
47935
+ // src/vite_config_text.ts
47936
+ var VITE_CONFIG_NAMES = [
47937
+ "vite.config.ts",
47938
+ "vite.config.mts",
47939
+ "vite.config.cts",
47940
+ "vite.config.js",
47941
+ "vite.config.mjs",
47942
+ "vite.config.cjs"
47943
+ ];
47944
+ function codeOnly(source) {
47945
+ let out = "";
47946
+ let i = 0;
47947
+ while (i < source.length) {
47948
+ const two = source.slice(i, i + 2);
47949
+ if (two === "//") {
47950
+ while (i < source.length && source[i] !== "\n") {
47951
+ out += " ";
47952
+ i++;
47953
+ }
47954
+ continue;
47955
+ }
47956
+ if (two === "/*") {
47957
+ while (i < source.length && source.slice(i, i + 2) !== "*/") {
47958
+ out += source[i] === "\n" ? "\n" : " ";
47959
+ i++;
47960
+ }
47961
+ out += " ";
47962
+ i += 2;
47963
+ continue;
47964
+ }
47965
+ const quote2 = source[i];
47966
+ if (quote2 === '"' || quote2 === "'" || quote2 === "`") {
47967
+ out += " ";
47968
+ i++;
47969
+ while (i < source.length && source[i] !== quote2) {
47970
+ if (source[i] === "\\") {
47971
+ out += " ";
47972
+ i += 2;
47973
+ continue;
47974
+ }
47975
+ out += source[i] === "\n" ? "\n" : " ";
47976
+ i++;
47977
+ }
47978
+ out += " ";
47979
+ i++;
47980
+ continue;
47981
+ }
47982
+ out += source[i];
47983
+ i++;
47984
+ }
47985
+ return out;
47986
+ }
47987
+ function objectBlocks(code, key) {
47988
+ const blocks = [];
47989
+ const literal2 = key.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
47990
+ for (const opener of code.matchAll(new RegExp(String.raw`\b${literal2}\s*:\s*\{`, "g"))) {
47991
+ let depth = 1;
47992
+ let i = opener.index + opener[0].length;
47993
+ const start = i;
47994
+ while (i < code.length && depth > 0) {
47995
+ if (code[i] === "{") depth++;
47996
+ else if (code[i] === "}") depth--;
47997
+ i++;
47998
+ }
47999
+ blocks.push(code.slice(start, i));
48000
+ }
48001
+ return blocks;
48002
+ }
48003
+ function declaredVitePort(config2) {
48004
+ for (const block of objectBlocks(codeOnly(config2), "server")) {
48005
+ const found = block.match(/(?:^|[{,;])\s*port\s*:\s*(\d+)\s*(?:[,}]|$)/);
48006
+ if (found) return Number(found[1]);
48007
+ }
48008
+ return null;
48009
+ }
48010
+
47700
48011
  // ../shared/src/schemas/workflow_issues.ts
47701
48012
  var issueSchema = external_exports.object({
47702
48013
  source: external_exports.enum(["parse", "typecheck", "resolve", "lint", "structural"]),
@@ -48321,16 +48632,16 @@ function writeAppMeta(projectDir, meta3) {
48321
48632
  pkg.lotics = { ...meta3, ...preserved };
48322
48633
  fs11.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
48323
48634
  }
48324
- function writeWorkflowOutputs(projectDir, alias, outputs) {
48635
+ function writeWorkflowDeclarationFields(projectDir, alias, fields) {
48325
48636
  const pkgPath = path12.join(projectDir, "package.json");
48326
48637
  const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
48327
48638
  const declaration = pkg.lotics?.workflows?.[alias];
48328
48639
  if (!declaration) {
48329
48640
  throw new Error(
48330
- `package.json#lotics.workflows.${alias} disappeared before the derived-outputs write-back.`
48641
+ `package.json#lotics.workflows.${alias} disappeared before the post-set write-back.`
48331
48642
  );
48332
48643
  }
48333
- declaration.outputs = outputs;
48644
+ Object.assign(declaration, fields);
48334
48645
  fs11.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
48335
48646
  }
48336
48647
  function workflowBodyDrift(projectDir, workflows, synced = {}) {
@@ -48440,13 +48751,22 @@ function writeAppDtsKitLinks(projectDir) {
48440
48751
  writeDevLinkTsconfig(projectDir);
48441
48752
  writeKitTypeAugmentation(projectDir);
48442
48753
  }
48443
- function writeAppDts(projectDir, manifest) {
48754
+ function writeAppDts(projectDir, manifest, optionsByFieldKey) {
48444
48755
  const dotLotics = path12.join(projectDir, ".lotics");
48445
48756
  fs11.mkdirSync(dotLotics, { recursive: true });
48446
48757
  const written = [
48447
- [path12.join(dotLotics, "app_workflows.d.ts"), generateAppWorkflowsDts(manifest.workflows)],
48448
- [path12.join(dotLotics, "app_queries.d.ts"), generateAppQueriesDts(manifest.queries)],
48449
- [path12.join(dotLotics, "app_agents.d.ts"), generateAppAgentsDts(manifest.agents)]
48758
+ [
48759
+ path12.join(dotLotics, "app_workflows.d.ts"),
48760
+ generateAppWorkflowsDts(manifest.workflows, optionsByFieldKey)
48761
+ ],
48762
+ [
48763
+ path12.join(dotLotics, "app_queries.d.ts"),
48764
+ generateAppQueriesDts(manifest.queries, optionsByFieldKey)
48765
+ ],
48766
+ [
48767
+ path12.join(dotLotics, "app_agents.d.ts"),
48768
+ generateAppAgentsDts(manifest.agents, optionsByFieldKey)
48769
+ ]
48450
48770
  ];
48451
48771
  for (const [file2, content] of written) fs11.writeFileSync(file2, content);
48452
48772
  writeAppDtsKitLinks(projectDir);
@@ -48469,34 +48789,36 @@ function writeAppFields(projectDir, tables) {
48469
48789
  fs11.writeFileSync(file2, generateAppFields(tables));
48470
48790
  return file2;
48471
48791
  }
48472
- async function writeGeneratedAppFields(client, projectDir, app, queries) {
48792
+ async function writeGeneratedAppFields(client, projectDir, app, manifest) {
48793
+ const queries = manifest.queries ?? {};
48473
48794
  if (client.viewAsMemberId) {
48474
48795
  console.error(
48475
48796
  `\u26A0 Skipped .lotics/app_fields.ts \u2014 running with --view-as ${client.viewAsMemberId}. The schema is read as that member, so any table they cannot see would be silently missing from F/OPT. Re-run without --view-as to regenerate it.`
48476
48797
  );
48477
48798
  return;
48478
48799
  }
48800
+ let tables;
48479
48801
  try {
48480
- {
48481
- const tableIds = resolveCodegenTableIds(projectDir, queries);
48482
- const tables = await client.getWorkspaceSchema(tableIds);
48483
- const missing = tableIds.filter((id) => !tables.some((t) => t.id === id));
48484
- if (missing.length > 0) {
48485
- console.error(
48486
- `\u26A0 Skipped .lotics/app_fields.ts \u2014 ${missing.length} table(s) the app queries could not be read: ${missing.join(", ")}.
48802
+ const tableIds = resolveCodegenTableIds(projectDir, queries);
48803
+ tables = await client.getWorkspaceSchema(tableIds);
48804
+ const missing = tableIds.filter((id) => !tables.some((t) => t.id === id));
48805
+ if (missing.length > 0) {
48806
+ console.error(
48807
+ `\u26A0 Skipped .lotics/app_fields.ts \u2014 ${missing.length} table(s) the app queries could not be read: ${missing.join(", ")}.
48487
48808
  Writing anyway would bake a narrowed F/OPT map that compiles and then throws at runtime. Check this key's access to those tables.
48488
48809
  An existing app_fields.ts is kept and still works; a project that has none yet will fail to BUILD until this resolves \u2014 which is the loud failure, not a new problem.`
48489
- );
48490
- return;
48491
- }
48492
- const fieldsPath = writeAppFields(projectDir, tables);
48493
- note(
48494
- `Wrote ${fieldsPath} (baked form \u2014 bespoke; ${tables.length} table${tables.length === 1 ? "" : "s"})`
48495
48810
  );
48811
+ return;
48496
48812
  }
48813
+ const fieldsPath = writeAppFields(projectDir, tables);
48814
+ note(
48815
+ `Wrote ${fieldsPath} (baked form \u2014 bespoke; ${tables.length} table${tables.length === 1 ? "" : "s"})`
48816
+ );
48497
48817
  } catch (err) {
48498
48818
  warnAppFieldsUnwritten(projectDir, err);
48819
+ return;
48499
48820
  }
48821
+ writeAppDts(projectDir, manifest, selectOptionsByFieldKey(tables));
48500
48822
  }
48501
48823
  function warnAppFieldsUnwritten(projectDir, err) {
48502
48824
  const reason = err instanceof Error ? err.message : String(err);
@@ -48540,6 +48862,13 @@ function installedKitVersion(projectDir, pkg) {
48540
48862
  return null;
48541
48863
  }
48542
48864
  }
48865
+ function installedKitMajorAtLeast(projectDir, pkg, major) {
48866
+ const version2 = installedKitVersion(projectDir, pkg);
48867
+ if (version2 === null) return false;
48868
+ const installedMajor = parseInt(version2.split(".")[0], 10);
48869
+ if (Number.isNaN(installedMajor)) return false;
48870
+ return installedMajor >= major;
48871
+ }
48543
48872
  async function publishedKitVersion(pkg) {
48544
48873
  const controller = new AbortController();
48545
48874
  const timeout = setTimeout(() => controller.abort(), 3e3);
@@ -48695,6 +49024,7 @@ async function appCodegen(args) {
48695
49024
  for (const alias of pruneOrphanWorkflowGlobals(projectDir, declared)) {
48696
49025
  console.error(`Removed stale workflow types for ${alias} \u2014 no longer in package.json#lotics.workflows`);
48697
49026
  }
49027
+ reportUndeclaredCalledAliases(calledAppAliases(readAppSourceText(projectDir)), meta3);
48698
49028
  for (const alias of orphanWorkflowBodies(projectDir, declared)) {
48699
49029
  console.error(
48700
49030
  `\u26A0 ${path12.join(WORKFLOWS_DIR, `${alias}.ts`)} is not declared in package.json#lotics.workflows \u2014 'workflow check' and 'workflow set' both skip it. Declare the alias or delete the file.`
@@ -48709,12 +49039,11 @@ async function appCodegen(args) {
48709
49039
  let app = null;
48710
49040
  try {
48711
49041
  app = await args.client.getApp(meta3.app_id);
48712
- await writeGeneratedAppFields(
48713
- args.client,
48714
- projectDir,
48715
- { app_id: meta3.app_id },
48716
- meta3.queries ?? {}
48717
- );
49042
+ await writeGeneratedAppFields(args.client, projectDir, { app_id: meta3.app_id }, {
49043
+ workflows: meta3.workflows,
49044
+ queries: meta3.queries,
49045
+ agents: meta3.agents
49046
+ });
48718
49047
  } catch (err) {
48719
49048
  warnAppFieldsUnwritten(projectDir, err);
48720
49049
  }
@@ -48743,17 +49072,24 @@ async function refreshWorkflowGlobals(client, projectDir, app_id, workflows) {
48743
49072
  alias,
48744
49073
  toWorkflowDtsDeclaration(declaration)
48745
49074
  );
48746
- if (refreshed) console.error(`Refreshed workflow types for ${alias}`);
49075
+ if (refreshed.kind === "refreshed") {
49076
+ console.error(`Refreshed workflow types for ${alias}`);
49077
+ continue;
49078
+ }
49079
+ console.error(
49080
+ `Wrote workflow types for ${alias} \u2014 ${refreshed.hasFile ? "its body file is empty" : `no body at ${path12.join(WORKFLOWS_DIR, `${alias}.ts`)}`}. Write the body there, then 'lotics app workflow check ${alias}'.`
49081
+ );
48747
49082
  }
48748
49083
  }
48749
49084
  async function refreshWorkflowTypes(client, projectDir, app_id, alias, declaration) {
48750
49085
  const file2 = workflowFilePath(projectDir, alias);
48751
- if (!fs11.existsSync(file2)) return false;
48752
- const body = stripWorkflowHeader(fs11.readFileSync(file2, "utf-8"));
48753
- if (body.trim() === "") return false;
49086
+ const body = fs11.existsSync(file2) ? stripWorkflowHeader(fs11.readFileSync(file2, "utf-8")) : "";
48754
49087
  const envelope2 = await fetchWorkflowGlobals(client, projectDir, app_id, alias, declaration);
49088
+ if (body.trim() === "") {
49089
+ return { kind: "types_only", hasFile: fs11.existsSync(file2) };
49090
+ }
48755
49091
  writeWorkflowFile(projectDir, alias, body, envelope2);
48756
- return true;
49092
+ return { kind: "refreshed" };
48757
49093
  }
48758
49094
  function readPriorStamp(projectDir) {
48759
49095
  const pkgPath = path12.join(projectDir, "package.json");
@@ -48861,7 +49197,7 @@ async function appCreate(client, args) {
48861
49197
  await materializeScaffold(targetPath, files);
48862
49198
  if (plan !== null) {
48863
49199
  writeAppDts(targetPath, { queries: plan.queries });
48864
- await writeGeneratedAppFields(client, targetPath, { app_id: app.id }, plan.queries);
49200
+ await writeGeneratedAppFields(client, targetPath, { app_id: app.id }, { queries: plan.queries });
48865
49201
  if (!fs11.existsSync(path12.join(targetPath, ".lotics", "app_fields.ts"))) {
48866
49202
  throw new Error(
48867
49203
  `.lotics/app_fields.ts could not be written (the warning above says why), and every generated screen reads it \u2014 resolve that, then \`cd ${path12.relative(process.cwd(), targetPath) || "."} && lotics app codegen && lotics app deploy\`.`
@@ -49080,12 +49416,11 @@ async function hydrateAppProject(client, targetPath, app, opts) {
49080
49416
  const sha = declarationSha(declaration);
49081
49417
  writeSynced(targetPath, "queries", alias, { content: sha, live: sha });
49082
49418
  }
49083
- await writeGeneratedAppFields(
49084
- client,
49085
- targetPath,
49086
- { app_id: app.id },
49087
- app.queries ?? {}
49088
- );
49419
+ await writeGeneratedAppFields(client, targetPath, { app_id: app.id }, {
49420
+ workflows: toManifestWorkflows(app.workflows ?? {}),
49421
+ queries: app.queries ?? {},
49422
+ agents: toManifestAgents(app.agents ?? {})
49423
+ });
49089
49424
  const workflows = app.workflows ?? {};
49090
49425
  if (Object.keys(workflows).length > 0) {
49091
49426
  const { written, kept, unseenLive } = await writeWorkflowFiles(
@@ -49278,12 +49613,11 @@ async function appDeploy(client, args) {
49278
49613
  warnIfProjectFootguns(projectDir, sourceText);
49279
49614
  writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
49280
49615
  try {
49281
- await writeGeneratedAppFields(
49282
- client,
49283
- projectDir,
49284
- { app_id: meta3.app_id },
49285
- meta3.queries ?? {}
49286
- );
49616
+ await writeGeneratedAppFields(client, projectDir, { app_id: meta3.app_id }, {
49617
+ workflows: meta3.workflows,
49618
+ queries: meta3.queries,
49619
+ agents: meta3.agents
49620
+ });
49287
49621
  } catch (err) {
49288
49622
  warn(
49289
49623
  `\u26A0 Could not regenerate .lotics/app_fields.ts (${err.message}).
@@ -49357,7 +49691,8 @@ async function appDeploy(client, args) {
49357
49691
  try {
49358
49692
  const drifted = staleWorkflowGlobals(projectDir, meta3.workflows ?? {});
49359
49693
  for (const { alias, declaration } of drifted) {
49360
- if (await refreshWorkflowTypes(client, projectDir, meta3.app_id, alias, declaration)) {
49694
+ const refreshed = await refreshWorkflowTypes(client, projectDir, meta3.app_id, alias, declaration);
49695
+ if (refreshed.kind === "refreshed") {
49361
49696
  note(`Refreshed workflow types for ${alias} \u2014 this deploy moved its declaration.`);
49362
49697
  }
49363
49698
  }
@@ -49377,10 +49712,16 @@ async function appDeploy(client, args) {
49377
49712
  warnIfInertAgentTools(liveAfter);
49378
49713
  }
49379
49714
  if (liveAfter) {
49380
- const orphans = orphanedBindings(called, liveAfter, meta3.bundle_calls);
49715
+ const undeclared = undeclaredBindings({
49716
+ meta: meta3,
49717
+ synced: readSynced(projectDir),
49718
+ live: liveAfter
49719
+ });
49720
+ const lostCallSite = orphanedBindings(called, liveAfter, meta3.bundle_calls);
49721
+ const orphans = allOrphans(lostCallSite, undeclared);
49381
49722
  let outstanding = orphans;
49382
49723
  if (args.prune) {
49383
- if (!meta3.bundle_calls) {
49724
+ if (!meta3.bundle_calls && nothingOrphaned(undeclared)) {
49384
49725
  note(
49385
49726
  "Nothing to prune yet: this project has no record of what the previous bundle called, so there is no way to tell a retired binding from an agent-facing one. This deploy records it; the next can compare."
49386
49727
  );
@@ -49390,12 +49731,14 @@ async function appDeploy(client, args) {
49390
49731
  projectDir,
49391
49732
  { app_id: meta3.app_id },
49392
49733
  orphans,
49393
- called.dynamic
49734
+ undeclared,
49735
+ called.dynamic,
49736
+ args.pruneInvoked ?? []
49394
49737
  );
49395
49738
  } else {
49396
- warnAboutOrphanedBindings(called, liveAfter, meta3.bundle_calls);
49739
+ warnAboutOrphanedBindings(called, liveAfter, meta3.bundle_calls, undeclared);
49397
49740
  }
49398
- const settled = outstanding.queries.length === 0 && outstanding.workflows.length === 0 && outstanding.agents.length === 0;
49741
+ const settled = nothingOrphaned(bothOf(outstanding, lostCallSite));
49399
49742
  if (settled) {
49400
49743
  writeAppMeta(projectDir, {
49401
49744
  ...readAppMeta(projectDir),
@@ -49463,8 +49806,14 @@ async function appCheck(client, args = {}) {
49463
49806
  writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
49464
49807
  const sourceText = readAppSourceText(projectDir);
49465
49808
  const called = calledAppAliases(sourceText);
49809
+ const undeclaredCalls = reportUndeclaredCalledAliases(called, meta3);
49466
49810
  warnAboutDevLink(projectDir, "deploy");
49467
- warnAboutOrphanedBindings(called, app, meta3.bundle_calls);
49811
+ warnAboutOrphanedBindings(
49812
+ called,
49813
+ app,
49814
+ meta3.bundle_calls,
49815
+ undeclaredBindings({ meta: meta3, synced: readSynced(projectDir), live: app })
49816
+ );
49468
49817
  const pending = pendingBindings({
49469
49818
  projectDir,
49470
49819
  meta: meta3,
@@ -49480,6 +49829,7 @@ async function appCheck(client, args = {}) {
49480
49829
  warnIfProjectFootguns(projectDir, sourceText);
49481
49830
  warnIfUnbranded(app);
49482
49831
  warnIfUndescribed(app);
49832
+ await warnIfWorkflowsUndescribed(client, meta3);
49483
49833
  const unportable = scanProjectForIds(projectDir);
49484
49834
  reportPortabilityIds(unportable);
49485
49835
  const bodiesFailing = await countFailingWorkflowBodies(client, projectDir, meta3);
@@ -49506,13 +49856,43 @@ async function appCheck(client, args = {}) {
49506
49856
  });
49507
49857
  console.error(formatScreenFindings(report));
49508
49858
  }
49509
- if (!nothingPending(pending) || unportable.length > 0 || bodiesFailing > 0 || typesFailing || screenFindings > 0) {
49859
+ if (!nothingPending(pending) || unportable.length > 0 || undeclaredCalls > 0 || bodiesFailing > 0 || typesFailing || screenFindings > 0) {
49510
49860
  process.exit(1);
49511
49861
  }
49512
49862
  console.error(
49513
49863
  "Checked the app's types, bindings, capabilities, agent schemas, workflow bodies and id portability" + (args.screens === true ? ", and every screen at 1280 and 375" : "") + " \u2014 nothing blocking."
49514
49864
  );
49515
49865
  }
49866
+ var capabilityAliasesSchema = zod_default.object({
49867
+ aliases: zod_default.array(
49868
+ zod_default.object({
49869
+ alias: zod_default.string(),
49870
+ kind: zod_default.string(),
49871
+ description: zod_default.string().nullish()
49872
+ })
49873
+ ).optional()
49874
+ });
49875
+ async function warnIfWorkflowsUndescribed(client, meta3) {
49876
+ let res;
49877
+ try {
49878
+ res = await client.getAppCapabilities(meta3.app_id);
49879
+ } catch {
49880
+ return;
49881
+ }
49882
+ if (res.error) return;
49883
+ const parsed = capabilityAliasesSchema.safeParse(res.result);
49884
+ if (!parsed.success || parsed.data.aliases === void 0) return;
49885
+ const workflows = parsed.data.aliases.filter((entry) => entry.kind === "workflow");
49886
+ const undescribed = workflows.filter((entry) => entry.description == null || entry.description === "").map((entry) => entry.alias);
49887
+ if (undescribed.length === 0) return;
49888
+ console.error(
49889
+ `
49890
+ \u26A0 ${undescribed.length} of ${workflows.length} workflow${workflows.length === 1 ? "" : "s"} are offered with no description: ${undescribed.join(", ")}.
49891
+ That text is what chat and MCP read to choose between aliases; without it they choose by
49892
+ the alias. Write one line per workflow in
49893
+ package.json#lotics.workflows.<alias>.description, then 'lotics app workflow set <alias>'.`
49894
+ );
49895
+ }
49516
49896
  async function countFailingWorkflowBodies(client, projectDir, meta3) {
49517
49897
  const bound = Object.keys(meta3.workflows ?? {});
49518
49898
  const typed = bound.filter((alias) => fs11.existsSync(workflowGlobalsPath(projectDir, alias)));
@@ -49532,8 +49912,29 @@ async function countFailingWorkflowBodies(client, projectDir, meta3) {
49532
49912
  }
49533
49913
  const results = checkWorkflowBodies(await loadProjectTypescript(projectDir), toCheck);
49534
49914
  printWorkflowCheckResults(projectDir, results);
49915
+ reportStoredBodiesOwedMigration(
49916
+ projectDir,
49917
+ results.filter((r) => r.issues.length > 0).map((r) => r.alias)
49918
+ );
49535
49919
  return results.filter((r) => r.issues.length > 0).length;
49536
49920
  }
49921
+ function reportStoredBodiesOwedMigration(projectDir, failing) {
49922
+ const baselines = readSynced(projectDir).workflows;
49923
+ const unedited = failing.filter((alias) => {
49924
+ const baseline = baselines[alias]?.content;
49925
+ if (baseline === void 0) return false;
49926
+ const file2 = workflowFilePath(projectDir, alias);
49927
+ if (!fs11.existsSync(file2)) return false;
49928
+ return contentSha(stripWorkflowHeader(fs11.readFileSync(file2, "utf-8"))) === baseline;
49929
+ });
49930
+ if (unedited.length === 0) return;
49931
+ console.error(
49932
+ `
49933
+ \u26A0 ${unedited.length} of those bod${unedited.length === 1 ? "y is" : "ies are"} unchanged since this checkout pushed or pulled ${unedited.length === 1 ? "it" : "them"} \u2014 ${unedited.join(", ")}. That is the body the server is RUNNING, so this is a migration it is owed, not a stale tree.
49934
+ The usual cause is a grammar move: a link field reads as an id array, so drop '.id' or descend with linked(\u2026).
49935
+ Edit each body, then 'lotics app workflow set <alias>'. Until then the stored body keeps running as it always has.`
49936
+ );
49937
+ }
49537
49938
  async function pushPendingBindings(client, projectDir, pending) {
49538
49939
  const plan = [
49539
49940
  ...pending.queries.map((alias) => `query ${alias}`),
@@ -49662,36 +50063,64 @@ function orphanedBindings(called, live, previouslyCalled) {
49662
50063
  referenced
49663
50064
  );
49664
50065
  }
49665
- function warnAboutOrphanedBindings(called, live, previouslyCalled) {
49666
- const orphans = orphanedBindings(called, live, previouslyCalled);
50066
+ function undeclaredBindings(args) {
50067
+ const { meta: meta3, synced, live } = args;
50068
+ const removed = (kind, declared, bound) => Object.keys(bound ?? {}).filter((alias) => synced[kind][alias] !== void 0 && (declared ?? {})[alias] === void 0).sort();
50069
+ return {
50070
+ queries: removed("queries", meta3.queries, live.queries),
50071
+ workflows: removed("workflows", meta3.workflows, live.workflows),
50072
+ agents: removed("agents", meta3.agents, live.agents)
50073
+ };
50074
+ }
50075
+ function nothingOrphaned(orphans) {
50076
+ return orphans.queries.length === 0 && orphans.workflows.length === 0 && orphans.agents.length === 0;
50077
+ }
50078
+ function allOrphans(lostCallSite, undeclared) {
50079
+ const merge3 = (a, b) => [.../* @__PURE__ */ new Set([...a, ...b])].sort();
50080
+ return {
50081
+ queries: merge3(lostCallSite.queries, undeclared.queries),
50082
+ workflows: merge3(lostCallSite.workflows, undeclared.workflows),
50083
+ agents: merge3(lostCallSite.agents, undeclared.agents)
50084
+ };
50085
+ }
50086
+ function bothOf(a, b) {
50087
+ const both = (x, y) => x.filter((alias) => y.includes(alias));
50088
+ return {
50089
+ queries: both(a.queries, b.queries),
50090
+ workflows: both(a.workflows, b.workflows),
50091
+ agents: both(a.agents, b.agents)
50092
+ };
50093
+ }
50094
+ function warnAboutOrphanedBindings(called, live, previouslyCalled, undeclared) {
50095
+ const lostCallSite = orphanedBindings(called, live, previouslyCalled);
50096
+ const label = (kind, alias, undeclaredOfKind) => ` \u2022 ${kind} ${alias}${undeclaredOfKind.includes(alias) ? " (no longer declared in package.json)" : ""}`;
50097
+ const merged = allOrphans(lostCallSite, undeclared);
49667
50098
  const lines = [
49668
- ...orphans.queries.map((a) => ` \u2022 query ${a}`),
49669
- ...orphans.workflows.map((a) => ` \u2022 workflow ${a}`),
49670
- ...orphans.agents.map((a) => ` \u2022 agent ${a}`)
50099
+ ...merged.queries.map((a) => label("query", a, undeclared.queries)),
50100
+ ...merged.workflows.map((a) => label("workflow", a, undeclared.workflows)),
50101
+ ...merged.agents.map((a) => label("agent", a, undeclared.agents))
49671
50102
  ];
49672
50103
  if (lines.length === 0) return 0;
50104
+ const dynamicCaveat = called.dynamic.length > 0 && !nothingOrphaned(lostCallSite) ? `
50105
+
50106
+ This app computes an alias at runtime, so one of the bindings above still
50107
+ declared in package.json may be called after all \u2014 \`--prune\` leaves those in
50108
+ place. Make those call sites literal to prune them.` + (nothingOrphaned(undeclared) ? `` : ` The ones marked "no longer
50109
+ declared" are unbound anyway.`) : ``;
49673
50110
  console.error(
49674
50111
  `
49675
- \u26A0 ${lines.length} binding(s) still live that this bundle STOPPED calling:
49676
- ` + lines.join("\n") + (called.dynamic.length > 0 ? `
49677
-
49678
- This app computes an alias at runtime, so one of these may be called after
49679
- all \u2014 and \`--prune\` therefore leaves them ALL in place. Make those call
49680
- sites literal to prune them.` : `
50112
+ \u26A0 ${lines.length} binding(s) still live that this project has retired:
50113
+ ` + lines.join("\n") + `
49681
50114
 
49682
- Removing the call site does not unbind it, so it keeps serving. It also
49683
- stays published to chat and to MCP, which reach an alias with no call
49684
- site \u2014 so an agent can still invoke what you meant to retire.
49685
- To unbind: lotics app deploy --prune`)
50115
+ Removing the call site or the declaration does not unbind it, so it keeps
50116
+ serving. It also stays published to chat and to MCP, which reach an alias
50117
+ with no call site \u2014 so an agent can still invoke what you meant to retire.
50118
+ To unbind: lotics app deploy --prune` + dynamicCaveat
49686
50119
  );
49687
50120
  return lines.length;
49688
50121
  }
49689
- async function pruneOrphanedBindings(client, projectDir, app, orphans, dynamic) {
49690
- const stillBound = {
49691
- queries: [],
49692
- workflows: [],
49693
- agents: []
49694
- };
50122
+ async function pruneOrphanedBindings(client, projectDir, app, orphans, undeclared, dynamic, pruneInvoked) {
50123
+ const stillBound = { queries: [], workflows: [], agents: [] };
49695
50124
  const targets = [
49696
50125
  ...orphans.queries.map((alias) => ({
49697
50126
  tool: "remove_app_query",
@@ -49716,22 +50145,45 @@ async function pruneOrphanedBindings(client, projectDir, app, orphans, dynamic)
49716
50145
  }))
49717
50146
  ];
49718
50147
  if (targets.length === 0) return stillBound;
49719
- if (dynamic.length > 0) {
50148
+ const deferred = dynamic.length > 0 ? targets.filter((t) => !undeclared[t.kind].includes(t.alias)) : [];
50149
+ if (deferred.length > 0) {
49720
50150
  console.error(
49721
50151
  `
49722
- \u26A0 Left ${targets.length} unused binding(s) in place: this app calls ${dynamic.join(" / ")} with an alias it computes at runtime, and a static scan cannot tell which binding that reaches \u2014 so any of these may be live:
49723
- ` + targets.map((t) => ` \u2022 ${t.label}`).join("\n") + `
50152
+ \u26A0 Left ${deferred.length} unused binding(s) in place: this app calls ${dynamic.join(" / ")} with an alias it computes at runtime, and a static scan cannot tell which binding that reaches \u2014 so any of these may be live:
50153
+ ` + deferred.map((t) => ` \u2022 ${t.label}`).join("\n") + `
49724
50154
  Make those call sites literal and the next deploy will clean them up, or remove the ones you know are dead by hand.`
49725
50155
  );
49726
- for (const t of targets) stillBound[t.kind].push(t.alias);
49727
- return stillBound;
50156
+ for (const t of deferred) stillBound[t.kind].push(t.alias);
49728
50157
  }
50158
+ const prunable = targets.filter((t) => !deferred.includes(t));
50159
+ if (prunable.length === 0) return stillBound;
49729
50160
  const tablesBefore = new Set(resolveCodegenTableIds(projectDir, readAppMeta(projectDir).queries ?? {}));
50161
+ const notTargeted = pruneInvoked.filter(
50162
+ (alias) => !prunable.some((t) => t.kind === "workflows" && t.alias === alias)
50163
+ );
50164
+ if (notTargeted.length > 0) {
50165
+ console.error(
50166
+ ` \u26A0 --prune-invoked named ${notTargeted.map((a) => `"${a}"`).join(", ")}, which this prune is not removing. The flag applies to a workflow alias this prune is unbinding.`
50167
+ );
50168
+ }
49730
50169
  let removedLocally = false;
49731
- for (const target of targets) {
49732
- const res = await client.execute(target.tool, { app_id: app.app_id, alias: target.alias });
50170
+ for (const target of prunable) {
50171
+ const overrideInvocationGuard = target.kind === "workflows" && pruneInvoked.includes(target.alias);
50172
+ const res = await client.execute(target.tool, {
50173
+ app_id: app.app_id,
50174
+ alias: target.alias,
50175
+ // Sent only for the aliases the operator named, so the default stays the
50176
+ // guard: a run recorded against an alias means something outside the
50177
+ // bundle reaches it.
50178
+ ...overrideInvocationGuard ? { even_if_invoked: true } : {}
50179
+ });
49733
50180
  if (res.error) {
49734
50181
  console.error(` \u2717 could not unbind ${target.label} \u2014 ${res.error}`);
50182
+ if (target.kind === "workflows" && !overrideInvocationGuard) {
50183
+ console.error(
50184
+ ` If nothing outside this bundle calls it: lotics app deploy --prune --prune-invoked ${target.alias} -m "<message>"`
50185
+ );
50186
+ }
49735
50187
  stillBound[target.kind].push(target.alias);
49736
50188
  continue;
49737
50189
  }
@@ -49774,7 +50226,11 @@ async function pruneOrphanedBindings(client, projectDir, app, orphans, dynamic)
49774
50226
  ` \u26A0 .lotics/app_fields.ts no longer covers ${dropped.join(", ")} \u2014 ${dropped.length === 1 ? "that table was" : "those tables were"} named only by the pruned quer${orphans.queries.length === 1 ? "y" : "ies"}. If your source still uses F/OPT for ${dropped.length === 1 ? "it" : "them"}, add ${dropped.map((id) => `"${id}"`).join(", ")} to package.json#lotics.codegen.tables and re-run lotics app codegen.`
49775
50227
  );
49776
50228
  }
49777
- await writeGeneratedAppFields(client, projectDir, app, meta3.queries ?? {});
50229
+ await writeGeneratedAppFields(client, projectDir, app, {
50230
+ workflows: meta3.workflows,
50231
+ queries: meta3.queries,
50232
+ agents: meta3.agents
50233
+ });
49778
50234
  }
49779
50235
  } catch (err) {
49780
50236
  console.error(
@@ -49880,72 +50336,10 @@ function warnIfUndeclaredCapabilities(sourceText, capabilities) {
49880
50336
  Add to package.json#lotics.capabilities: ${block}`
49881
50337
  );
49882
50338
  }
49883
- var VITE_CONFIG_NAMES = [
49884
- "vite.config.ts",
49885
- "vite.config.mts",
49886
- "vite.config.cts",
49887
- "vite.config.js",
49888
- "vite.config.mjs",
49889
- "vite.config.cjs"
49890
- ];
49891
- function codeOnly(source) {
49892
- let out = "";
49893
- let i = 0;
49894
- while (i < source.length) {
49895
- const two = source.slice(i, i + 2);
49896
- if (two === "//") {
49897
- while (i < source.length && source[i] !== "\n") {
49898
- out += " ";
49899
- i++;
49900
- }
49901
- continue;
49902
- }
49903
- if (two === "/*") {
49904
- while (i < source.length && source.slice(i, i + 2) !== "*/") {
49905
- out += source[i] === "\n" ? "\n" : " ";
49906
- i++;
49907
- }
49908
- out += " ";
49909
- i += 2;
49910
- continue;
49911
- }
49912
- const quote2 = source[i];
49913
- if (quote2 === '"' || quote2 === "'" || quote2 === "`") {
49914
- out += " ";
49915
- i++;
49916
- while (i < source.length && source[i] !== quote2) {
49917
- if (source[i] === "\\") {
49918
- out += " ";
49919
- i += 2;
49920
- continue;
49921
- }
49922
- out += source[i] === "\n" ? "\n" : " ";
49923
- i++;
49924
- }
49925
- out += " ";
49926
- i++;
49927
- continue;
49928
- }
49929
- out += source[i];
49930
- i++;
49931
- }
49932
- return out;
49933
- }
49934
50339
  function defineBlockKeys(config2) {
49935
- const code = codeOnly(config2);
49936
50340
  const keys2 = /* @__PURE__ */ new Set();
49937
- for (const opener of code.matchAll(/\bdefine\s*:\s*\{/g)) {
49938
- let depth = 1;
49939
- let i = opener.index + opener[0].length;
49940
- const start = i;
49941
- while (i < code.length && depth > 0) {
49942
- if (code[i] === "{") depth++;
49943
- else if (code[i] === "}") depth--;
49944
- i++;
49945
- }
49946
- for (const key of code.slice(start, i).matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*:/g)) {
49947
- keys2.add(key[1]);
49948
- }
50341
+ for (const block of objectBlocks(codeOnly(config2), "define")) {
50342
+ for (const key of block.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*:/g)) keys2.add(key[1]);
49949
50343
  }
49950
50344
  return keys2;
49951
50345
  }
@@ -49995,6 +50389,22 @@ function warnIfProjectFootguns(projectDir, sourceText) {
49995
50389
  );
49996
50390
  }
49997
50391
  }
50392
+ if (configName !== void 0 && installedKitMajorAtLeast(projectDir, "@lotics/ui", 48)) {
50393
+ const config2 = fs11.readFileSync(path12.join(projectDir, configName), "utf-8");
50394
+ const handWritten = objectBlocks(codeOnly(config2), "optimizeDeps").some(
50395
+ (block) => /(?:^|[{,;])\s*include\s*:/.test(block)
50396
+ );
50397
+ if (handWritten) {
50398
+ lines.push(
50399
+ [
50400
+ ` \u2022 ${configName} sets optimizeDeps.include \u2014 @lotics/ui 48 ships built ESM, so Vite`,
50401
+ " pre-bundles it unaided. The list cannot help and a stale entry still fails the",
50402
+ " dev boot ('Failed to resolve dependency: \u2026, present in optimizeDeps.include').",
50403
+ " delete the optimizeDeps block (and any loticsOptimizeDeps import \u2014 48 has none)"
50404
+ ].join("\n")
50405
+ );
50406
+ }
50407
+ }
49998
50408
  if (/\bwindow\.open\s*\(/.test(sourceText)) {
49999
50409
  lines.push(
50000
50410
  [
@@ -50053,6 +50463,35 @@ function warnIfInertAgentTools(app) {
50053
50463
  function anyToolReaches(tools, field) {
50054
50464
  return DECLARATION_BOUND_TOOLS.some((g) => g.field === field && tools.includes(g.tool));
50055
50465
  }
50466
+ function undeclaredCalledAliases(called, meta3) {
50467
+ const declared = {
50468
+ queries: new Set(Object.keys(meta3.queries ?? {})),
50469
+ workflows: new Set(Object.keys(meta3.workflows ?? {}))
50470
+ };
50471
+ return {
50472
+ queries: [...new Set(called.queries)].filter((a) => !declared.queries.has(a)).sort(),
50473
+ workflows: [...new Set(called.workflows)].filter((a) => !declared.workflows.has(a)).sort()
50474
+ };
50475
+ }
50476
+ function reportUndeclaredCalledAliases(called, meta3) {
50477
+ const undeclared = undeclaredCalledAliases(called, meta3);
50478
+ const lines = [
50479
+ ...undeclared.queries.map((a) => ` \u2022 useQuery("${a}") \u2014 no package.json#lotics.queries.${a}`),
50480
+ ...undeclared.workflows.map(
50481
+ (a) => ` \u2022 useWorkflow("${a}") \u2014 no package.json#lotics.workflows.${a}`
50482
+ )
50483
+ ];
50484
+ if (lines.length === 0) return 0;
50485
+ console.error(
50486
+ `
50487
+ \u2717 ${lines.length} call site${lines.length === 1 ? "" : "s"} name${lines.length === 1 ? "s" : ""} an alias this project does not declare:
50488
+ ` + lines.join("\n") + `
50489
+
50490
+ These COMPILE \u2014 the hooks keep a bare-string overload for a computed alias \u2014 and fail
50491
+ when the screen renders. Declare the alias, or fix the call site.`
50492
+ );
50493
+ return lines.length;
50494
+ }
50056
50495
  function warnIfUnboundAliases(app, called) {
50057
50496
  const {
50058
50497
  queries: unboundQueries,
@@ -50076,10 +50515,23 @@ function warnIfUnboundAliases(app, called) {
50076
50515
  for (const alias of unboundAgents) lines.push(` \u2022 agent "${alias}" \u2192 bind with set_app_agent (lotics run set_app_agent \u2026)`);
50077
50516
  console.error(lines.join("\n"));
50078
50517
  }
50518
+ function resolveDeclaredVitePort(projectDir, flagPort) {
50519
+ const configName = VITE_CONFIG_NAMES.find((name) => fs11.existsSync(path12.join(projectDir, name)));
50520
+ const declared = configName === void 0 ? null : declaredVitePort(fs11.readFileSync(path12.join(projectDir, configName), "utf-8"));
50521
+ if (declared === null) return flagPort;
50522
+ if (flagPort === void 0) return declared;
50523
+ if (flagPort !== declared) {
50524
+ console.error(
50525
+ `\u26A0 --vite-port=${flagPort} overrides ${configName}'s server.port ${declared}. Vite is bound to ${flagPort}; drop the flag to use the config's port.`
50526
+ );
50527
+ }
50528
+ return flagPort;
50529
+ }
50079
50530
  async function appDev(client, args) {
50080
50531
  const projectDir = path12.resolve(args.projectDir ?? process.cwd());
50081
50532
  const meta3 = readAppMeta(projectDir);
50082
50533
  warnAboutDevLink(projectDir, "dev");
50534
+ const vitePort = resolveDeclaredVitePort(projectDir, args.vitePort);
50083
50535
  writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
50084
50536
  const app = await client.getApp(meta3.app_id);
50085
50537
  const handle = await startDevServer({
@@ -50089,7 +50541,7 @@ async function appDev(client, args) {
50089
50541
  workspace_id: meta3.workspace_id,
50090
50542
  api_url: client.baseUrl,
50091
50543
  port: args.port,
50092
- vitePort: args.vitePort,
50544
+ vitePort,
50093
50545
  client,
50094
50546
  commentsEnabled: meta3.capabilities?.comments
50095
50547
  });
@@ -50350,13 +50802,16 @@ async function appWorkflowSet(client, args) {
50350
50802
  ...typeof declaration.description === "string" ? { description: declaration.description } : {}
50351
50803
  });
50352
50804
  console.error(`Set workflow "${args.alias}" \u2192 ${result.workflow_id}`);
50805
+ if (declaration.workflow_id !== result.workflow_id) {
50806
+ writeWorkflowDeclarationFields(projectDir, args.alias, { workflow_id: result.workflow_id });
50807
+ }
50353
50808
  for (const warning of result.warnings) {
50354
50809
  const issue2 = issueSchema.safeParse(warning);
50355
50810
  console.error(issue2.success ? renderWorkflowIssue(issue2.data) : ` ${JSON.stringify(warning)}`);
50356
50811
  }
50357
50812
  const derivedOutputs = result.outputs;
50358
50813
  if (!declaration.outputs && derivedOutputs && Object.keys(derivedOutputs).length > 0) {
50359
- writeWorkflowOutputs(projectDir, args.alias, derivedOutputs);
50814
+ writeWorkflowDeclarationFields(projectDir, args.alias, { outputs: derivedOutputs });
50360
50815
  console.error(
50361
50816
  ` Wrote the derived result.data schema into package.json#lotics.workflows.${args.alias}.outputs.`
50362
50817
  );
@@ -50364,7 +50819,7 @@ async function appWorkflowSet(client, args) {
50364
50819
  inputs: declaration.inputs,
50365
50820
  outputs: derivedOutputs
50366
50821
  });
50367
- if (refreshed) console.error(` Refreshed workflow types for ${args.alias}.`);
50822
+ if (refreshed.kind === "refreshed") console.error(` Refreshed workflow types for ${args.alias}.`);
50368
50823
  } else if (derivedOutputs) {
50369
50824
  console.error(` result.data schema: ${JSON.stringify(derivedOutputs)}`);
50370
50825
  }
@@ -50432,22 +50887,79 @@ async function appWorkflowPull(client, args = {}) {
50432
50887
  reportUnseenLive("workflow body", unseenLive, meta3.app_id);
50433
50888
  ensureAppTsconfig(projectDir);
50434
50889
  }
50435
- async function appWorkflowCheck(args) {
50890
+ async function appWorkflowDiff(client, args = {}) {
50436
50891
  const projectDir = process.cwd();
50437
50892
  const meta3 = readAppMeta(projectDir);
50438
50893
  const bound = Object.keys(meta3.workflows ?? {});
50439
- let aliases;
50440
- if (args.alias) {
50441
- if (!bound.includes(args.alias)) {
50442
- console.error(
50443
- `No workflow "${args.alias}" in package.json#lotics.workflows. Bound aliases: ${bound.length > 0 ? bound.join(", ") : "(none)"}.`
50444
- );
50445
- process.exit(1);
50894
+ const requested = args.aliases ?? [];
50895
+ const unbound = requested.filter((alias) => !bound.includes(alias));
50896
+ if (unbound.length > 0) {
50897
+ fail(
50898
+ `No workflow ${unbound.map((a) => `"${a}"`).join(", ")} in package.json#lotics.workflows. Bound aliases: ${bound.length > 0 ? bound.join(", ") : "(none)"}.`
50899
+ );
50900
+ }
50901
+ const aliases = requested.length > 0 ? [...new Set(requested)] : workflowBodyDrift(projectDir, meta3.workflows, readSynced(projectDir).workflows);
50902
+ if (aliases.length === 0) {
50903
+ console.error("No workflow body differs from the baseline this checkout last pushed or pulled.");
50904
+ return;
50905
+ }
50906
+ let differing = 0;
50907
+ for (const alias of aliases) {
50908
+ const file2 = workflowFilePath(projectDir, alias);
50909
+ if (!fs11.existsSync(file2)) {
50910
+ console.error(`\u26A0 Skipped "${alias}" \u2014 no body at ${path12.relative(projectDir, file2)}.`);
50911
+ continue;
50446
50912
  }
50447
- aliases = [args.alias];
50448
- } else {
50449
- aliases = bound;
50913
+ const res = await client.getAppWorkflow(meta3.app_id, alias);
50914
+ if (res.error) {
50915
+ if (isMissingWorkflowBodyError(res.error)) {
50916
+ console.error(
50917
+ `\u26A0 Skipped "${alias}" \u2014 the server has no rendered body for it (never set, or legacy).`
50918
+ );
50919
+ continue;
50920
+ }
50921
+ fail(`Could not read workflow "${alias}" of ${meta3.app_id}: ${res.error}`);
50922
+ }
50923
+ const parsed = zod_default.object({ source: zod_default.string() }).safeParse(res.result);
50924
+ if (!parsed.success) {
50925
+ console.error(`\u26A0 Skipped "${alias}" \u2014 the server returned no body source.`);
50926
+ continue;
50927
+ }
50928
+ const live = normalizeWorkflowBody(parsed.data.source);
50929
+ const local = stripWorkflowHeader(fs11.readFileSync(file2, "utf-8"));
50930
+ if (live === local) {
50931
+ console.error(`\u2713 ${alias} \u2014 identical to the body the server is running.`);
50932
+ continue;
50933
+ }
50934
+ differing++;
50935
+ console.error(`
50936
+ ${alias} (- live, + ${path12.join(WORKFLOWS_DIR, `${alias}.ts`)})`);
50937
+ console.log(renderLineDiff(live, local));
50938
+ }
50939
+ if (differing > 0) {
50940
+ console.error(
50941
+ `
50942
+ ${differing} workflow ${differing === 1 ? "body differs" : "bodies differ"} from the server. 'lotics app workflow set <alias>' pushes one; 'lotics app workflow pull --force' takes the server's.`
50943
+ );
50944
+ process.exitCode = 1;
50450
50945
  }
50946
+ }
50947
+ function resolveCheckAliases(requested, bound) {
50948
+ const unbound = [...new Set(requested)].filter((alias) => !bound.includes(alias));
50949
+ if (unbound.length > 0) return { kind: "unbound", unbound };
50950
+ return { kind: "ok", aliases: requested.length > 0 ? [...new Set(requested)] : [...bound] };
50951
+ }
50952
+ async function appWorkflowCheck(args) {
50953
+ const projectDir = process.cwd();
50954
+ const meta3 = readAppMeta(projectDir);
50955
+ const bound = Object.keys(meta3.workflows ?? {});
50956
+ const resolved = resolveCheckAliases(args.aliases ?? [], bound);
50957
+ if (resolved.kind === "unbound") {
50958
+ fail(
50959
+ `No workflow ${resolved.unbound.map((a) => `"${a}"`).join(", ")} in package.json#lotics.workflows. Bound aliases: ${bound.length > 0 ? bound.join(", ") : "(none)"}.`
50960
+ );
50961
+ }
50962
+ const aliases = resolved.aliases;
50451
50963
  if (aliases.length === 0) {
50452
50964
  console.error(`App ${meta3.app_id} has no bound workflows to check.`);
50453
50965
  return;
@@ -50460,8 +50972,7 @@ async function appWorkflowCheck(args) {
50460
50972
  const tsApi = await loadProjectTypescript(projectDir);
50461
50973
  const results = checkWorkflowBodies(tsApi, toCheck);
50462
50974
  printWorkflowCheckResults(projectDir, results);
50463
- const failed = results.filter((r) => r.issues.length > 0);
50464
- if (failed.length > 0) process.exit(1);
50975
+ if (results.some((r) => r.issues.length > 0)) process.exitCode = 1;
50465
50976
  }
50466
50977
  async function collectWorkflowBodyChecks(projectDir, meta3, aliases, client) {
50467
50978
  const toCheck = [];
@@ -50470,15 +50981,14 @@ async function collectWorkflowBodyChecks(projectDir, meta3, aliases, client) {
50470
50981
  const globalsPath = workflowGlobalsPath(projectDir, alias);
50471
50982
  if (!fs11.existsSync(bodyPath)) {
50472
50983
  console.error(
50473
- `\u26A0 Skipped "${alias}" \u2014 no body at ${path12.relative(projectDir, bodyPath)}. Run 'lotics app workflow pull' to write it.`
50984
+ `\u26A0 Skipped "${alias}" \u2014 no body at ${path12.relative(projectDir, bodyPath)}. Write it there (its types are in ${path12.relative(projectDir, globalsPath)}), or 'lotics app workflow pull' if the server already has one.`
50474
50985
  );
50475
50986
  continue;
50476
50987
  }
50477
50988
  if (!fs11.existsSync(globalsPath)) {
50478
- console.error(
50989
+ fail(
50479
50990
  `Cannot check "${alias}" \u2014 missing types at ${path12.relative(projectDir, globalsPath)}. Run 'lotics app codegen' to fetch them (it refreshes types WITHOUT touching your body; a pull would overwrite it).`
50480
50991
  );
50481
- process.exit(1);
50482
50992
  }
50483
50993
  toCheck.push({ alias, input: { bodyPath, globalsPath } });
50484
50994
  }
@@ -50556,6 +51066,7 @@ function parseArgs(argv) {
50556
51066
  remove: void 0,
50557
51067
  includeHidden: false,
50558
51068
  timezone: void 0,
51069
+ currency: void 0,
50559
51070
  email: void 0,
50560
51071
  message: void 0,
50561
51072
  session: void 0,
@@ -50568,11 +51079,13 @@ function parseArgs(argv) {
50568
51079
  printCreated: false,
50569
51080
  cleanup: false,
50570
51081
  prune: false,
51082
+ pruneInvoked: [],
50571
51083
  screens: false,
50572
51084
  noSampleData: false,
50573
51085
  adopt: false,
50574
51086
  entity: [],
50575
51087
  limit: void 0,
51088
+ cursor: void 0,
50576
51089
  tables: [],
50577
51090
  bind: [],
50578
51091
  version: false,
@@ -50670,6 +51183,9 @@ function parseArgs(argv) {
50670
51183
  case "--timezone":
50671
51184
  flags.timezone = takeValue(arg, "<Area/City>");
50672
51185
  break;
51186
+ case "--currency":
51187
+ flags.currency = takeValue(arg, "<ISO code>");
51188
+ break;
50673
51189
  case "-m":
50674
51190
  case "--message":
50675
51191
  flags.message = takeValue(arg, "<message>");
@@ -50711,6 +51227,9 @@ function parseArgs(argv) {
50711
51227
  case "--limit":
50712
51228
  flags.limit = takeValue(arg, "<n>");
50713
51229
  break;
51230
+ case "--cursor":
51231
+ flags.cursor = takeValue(arg, "<token>");
51232
+ break;
50714
51233
  case "--tables":
50715
51234
  flags.tables = [...flags.tables, ...takeList(arg, "<tbl_id,tbl_id>")];
50716
51235
  break;
@@ -50719,6 +51238,9 @@ function parseArgs(argv) {
50719
51238
  case "--bind":
50720
51239
  flags.bind = [...flags.bind, takeValue(arg, "<entity>=<Label>")];
50721
51240
  break;
51241
+ case "--prune-invoked":
51242
+ flags.pruneInvoked = [...flags.pruneInvoked, ...takeList(arg, "<alias>")];
51243
+ break;
50722
51244
  case "--prune":
50723
51245
  flags.prune = true;
50724
51246
  break;
@@ -51175,7 +51697,14 @@ async function runPreviewCommand(filePath, flags) {
51175
51697
  if (!existsSync4(abs2)) fail(`File not found: ${abs2}`);
51176
51698
  const ext = extname(abs2).toLowerCase();
51177
51699
  const type = ext === ".docx" ? "docx" : ext === ".xlsx" || ext === ".xls" || ext === ".csv" ? "xlsx" : ext === ".html" || ext === ".htm" ? "html" : null;
51178
- if (!type) fail(`Unsupported file type "${ext}" \u2014 preview supports .docx, .xlsx/.csv and .html. (PDFs open directly \u2014 no preview needed.)`);
51700
+ if (!type) {
51701
+ const route = ext === ".pdf" ? `
51702
+ A PDF is rasterised outside this CLI: lotics file download <fil_id>, then
51703
+ 'pdftoppm -png -r 150 <file.pdf> <out-prefix>' (poppler-utils) for one PNG per page.` : "";
51704
+ fail(
51705
+ `Unsupported file type "${ext}" \u2014 preview supports .docx, .xlsx/.csv and .html.${route}`
51706
+ );
51707
+ }
51179
51708
  let server;
51180
51709
  let pageUrl;
51181
51710
  if (type === "html") {
@@ -51787,23 +52316,32 @@ async function main() {
51787
52316
  console.error(`${flag} needs a value \u2014 write it as: ${flag} ${expects}`);
51788
52317
  process.exit(1);
51789
52318
  }
51790
- if (command === "file") {
51791
- if (!subcommand) {
51792
- printHelp();
51793
- return;
51794
- }
51795
- command = subcommand;
51796
- subcommand = toolArgs;
51797
- toolArgs = restArgs.shift();
51798
- }
51799
52319
  if (flags.help && command === "report") {
51800
52320
  console.error(REPORT_USAGE);
51801
52321
  return;
51802
52322
  }
51803
52323
  if (flags.help || !command && !flags.version) {
52324
+ const scoped = flags.help ? commandHelp([parsed.command, parsed.subcommand, parsed.toolArgs].filter((w) => w !== void 0)) : null;
52325
+ if (scoped !== null) {
52326
+ console.log(scoped);
52327
+ return;
52328
+ }
51804
52329
  printHelp();
51805
52330
  return;
51806
52331
  }
52332
+ if (command === "file") {
52333
+ if (!subcommand) {
52334
+ const help = commandHelp(["file"]);
52335
+ if (help !== null) console.log(help);
52336
+ else printHelp();
52337
+ return;
52338
+ }
52339
+ if (commandAliases("file").has(subcommand)) {
52340
+ command = subcommand;
52341
+ subcommand = toolArgs;
52342
+ toolArgs = restArgs.shift();
52343
+ }
52344
+ }
51807
52345
  if (flags.version && command !== void 0) {
51808
52346
  console.error(
51809
52347
  `--version prints this CLI's version and takes no argument; it cannot be combined with a command.
@@ -52307,7 +52845,7 @@ async function main() {
52307
52845
  );
52308
52846
  client2 = void 0;
52309
52847
  }
52310
- await appWorkflowCheck({ alias: restArgs[0], client: client2 });
52848
+ await appWorkflowCheck({ aliases: restArgs, client: client2 });
52311
52849
  return;
52312
52850
  }
52313
52851
  if (command === "app" && subcommand === "codegen") {
@@ -52476,6 +53014,59 @@ async function main() {
52476
53014
  }
52477
53015
  return;
52478
53016
  }
53017
+ if (command === "file") {
53018
+ if (subcommand !== "list" && subcommand !== "delete") {
53019
+ console.error(`Unknown file subcommand: ${subcommand}
53020
+ `);
53021
+ console.error(commandHelp(["file"]));
53022
+ process.exit(1);
53023
+ }
53024
+ const { client: client2, ctx: ctx2 } = await requireClient(flags);
53025
+ await resolveWorkspace(client2, ctx2);
53026
+ if (subcommand === "list") {
53027
+ let limit;
53028
+ if (flags.limit !== void 0) {
53029
+ limit = Number(flags.limit);
53030
+ if (!Number.isInteger(limit) || limit < 1) {
53031
+ console.error(`Invalid --limit "${flags.limit}" \u2014 expected a positive integer.`);
53032
+ process.exit(1);
53033
+ }
53034
+ }
53035
+ const page = await client2.listFiles({ limit, cursor: flags.cursor });
53036
+ if (flags.json) {
53037
+ console.log(JSON.stringify(page, null, 2));
53038
+ } else {
53039
+ for (const file2 of page.files) {
53040
+ const size2 = file2.size == null ? "-" : String(file2.size);
53041
+ console.log(`${file2.id} ${file2.created_at} ${size2} ${file2.mime_type} ${file2.filename}`);
53042
+ }
53043
+ console.error(
53044
+ page.next_cursor === null ? `${page.files.length} file${page.files.length === 1 ? "" : "s"} \u2014 end of the store.` : `${page.files.length} files. Next page: lotics file list --cursor ${page.next_cursor}`
53045
+ );
53046
+ }
53047
+ return;
53048
+ }
53049
+ const fileId = toolArgs;
53050
+ if (!fileId || !isStoredFileId(fileId)) {
53051
+ console.error("Usage: lotics file delete <file_id>");
53052
+ console.error(
53053
+ "Archives a stored file. Refused while a record, comment, knowledge doc, document"
53054
+ );
53055
+ console.error("template or voice session still references it \u2014 those are named back.");
53056
+ process.exit(1);
53057
+ }
53058
+ const deleted = await client2.execute("delete_file", { file_id: fileId }, { format: "text" });
53059
+ if (deleted.error) {
53060
+ console.error(deleted.error);
53061
+ process.exit(1);
53062
+ }
53063
+ if (flags.json) {
53064
+ console.log(JSON.stringify(deleted.result, null, 2));
53065
+ } else {
53066
+ console.log(deleted.model_output ?? JSON.stringify(deleted.result));
53067
+ }
53068
+ return;
53069
+ }
52479
53070
  const appPathArg = subcommand === "dev" ? toolArgs : void 0;
52480
53071
  const { client, ctx } = await requireClient(
52481
53072
  flags,
@@ -52501,6 +53092,22 @@ async function main() {
52501
53092
  process.exitCode = 1;
52502
53093
  return;
52503
53094
  }
53095
+ const settingsName = subcommand === "rename" ? toolArgs : flags.name;
53096
+ if (subcommand === "rename" && !settingsName) {
53097
+ console.error("Usage: lotics workspace rename <new name>");
53098
+ console.error('Renames the CURRENT workspace. Switch first with "lotics workspace select <id>".');
53099
+ process.exit(1);
53100
+ }
53101
+ if (subcommand === "settings" && settingsName === void 0 && flags.currency === void 0 && flags.timezone === void 0) {
53102
+ console.error(
53103
+ "Usage: lotics workspace settings [--name <name>] [--currency <ISO>] [--timezone <Area/City>]"
53104
+ );
53105
+ console.error(
53106
+ "Changes the CURRENT workspace. The currency decides how every money field renders and the"
53107
+ );
53108
+ console.error("zone decides how every date buckets; neither is visible once it is wrong.");
53109
+ process.exit(1);
53110
+ }
52504
53111
  const workspaces = await client.listWorkspaces();
52505
53112
  const currentWorkspaceId = ctx.workspaceId;
52506
53113
  if (subcommand === "select") {
@@ -52526,45 +53133,47 @@ Available workspaces:`);
52526
53133
  console.error(`Switched to workspace: ${target.name} (${target.id})${where}`);
52527
53134
  return;
52528
53135
  }
52529
- if (subcommand === "create") {
52530
- const name = toolArgs;
52531
- if (!name) {
52532
- console.error("Usage: lotics workspace create <name> [--timezone <tz>]");
53136
+ if (subcommand === "settings" || subcommand === "rename") {
53137
+ const current = workspaces.find((ws) => ws.id === currentWorkspaceId);
53138
+ if (!current) {
53139
+ console.error('No workspace selected. Choose one with "lotics workspace select <id>".');
52533
53140
  process.exit(1);
52534
53141
  }
52535
- const timezone = flags.timezone;
52536
- const created = await client.createWorkspace({ name, timezone });
52537
- setSelectedWorkspace(created.id, ctx.orgId);
52538
- client.setWorkspaceId(created.id);
53142
+ const updated = await client.updateWorkspace({
53143
+ name: settingsName ?? current.name,
53144
+ default_currency: flags.currency ?? current.default_currency,
53145
+ timezone: flags.timezone ?? current.timezone
53146
+ });
52539
53147
  if (flags.json) {
52540
- console.log(JSON.stringify(created, null, 2));
53148
+ console.log(JSON.stringify(updated, null, 2));
52541
53149
  } else {
52542
- console.error(`Created workspace: ${created.name} (${created.id})`);
52543
- console.error(`Switched to ${created.id}`);
53150
+ console.error(
53151
+ `Workspace ${updated.id}: ${updated.name} \xB7 ${updated.default_currency} \xB7 ${updated.timezone}`
53152
+ );
52544
53153
  }
52545
53154
  return;
52546
53155
  }
52547
- if (subcommand === "rename") {
53156
+ if (subcommand === "create") {
52548
53157
  const name = toolArgs;
52549
53158
  if (!name) {
52550
- console.error("Usage: lotics workspace rename <new name>");
52551
- console.error('Renames the CURRENT workspace. Switch first with "lotics workspace select <id>".');
53159
+ console.error("Usage: lotics workspace create <name> [--timezone <tz>] [--currency <ISO>]");
52552
53160
  process.exit(1);
52553
53161
  }
52554
- const current = workspaces.find((ws) => ws.id === currentWorkspaceId);
52555
- if (!current) {
52556
- console.error('No workspace selected. Choose one with "lotics workspace select <id>".');
52557
- process.exit(1);
52558
- }
52559
- const renamed = await client.updateWorkspace({
53162
+ const timezone = flags.timezone;
53163
+ const created = await client.createWorkspace({
52560
53164
  name,
52561
- default_currency: current.default_currency,
52562
- timezone: current.timezone
53165
+ timezone,
53166
+ default_currency: flags.currency
52563
53167
  });
53168
+ setSelectedWorkspace(created.id, ctx.orgId);
53169
+ client.setWorkspaceId(created.id);
52564
53170
  if (flags.json) {
52565
- console.log(JSON.stringify(renamed, null, 2));
53171
+ console.log(JSON.stringify(created, null, 2));
52566
53172
  } else {
52567
- console.error(`Renamed "${current.name}" to "${renamed.name}" (${renamed.id})`);
53173
+ console.error(
53174
+ `Created workspace: ${created.name} (${created.id}) \xB7 ${created.default_currency} \xB7 ${created.timezone}`
53175
+ );
53176
+ console.error(`Switched to ${created.id}`);
52568
53177
  }
52569
53178
  return;
52570
53179
  }
@@ -52653,9 +53262,16 @@ Available workspaces:`);
52653
53262
  return;
52654
53263
  }
52655
53264
  if (subcommand === "deploy") {
53265
+ if (flags.pruneInvoked.length > 0 && !flags.prune) {
53266
+ console.error(
53267
+ "--prune-invoked lifts one guard inside --prune; it does nothing on its own. Add --prune."
53268
+ );
53269
+ process.exit(1);
53270
+ }
52656
53271
  await appDeploy(client, {
52657
53272
  message: (flags.message ?? toolArgs)?.trim() || void 0,
52658
- prune: flags.prune
53273
+ prune: flags.prune,
53274
+ pruneInvoked: flags.pruneInvoked
52659
53275
  });
52660
53276
  return;
52661
53277
  }
@@ -52698,7 +53314,8 @@ Available workspaces:`);
52698
53314
  console.error("Usage:");
52699
53315
  console.error(" lotics app workflow set <alias> Push src/workflows/<alias>.ts");
52700
53316
  console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
52701
- console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
53317
+ console.error(" lotics app workflow check [alias...] Typecheck src/workflows bodies locally");
53318
+ console.error(" lotics app workflow diff [alias...] Show how a body differs from the server's");
52702
53319
  process.exit(1);
52703
53320
  };
52704
53321
  if (action === "set") {
@@ -52715,6 +53332,10 @@ Available workspaces:`);
52715
53332
  await appWorkflowPull(client, { force: flags.force === true });
52716
53333
  return;
52717
53334
  }
53335
+ if (action === "diff") {
53336
+ await appWorkflowDiff(client, { aliases: restArgs });
53337
+ return;
53338
+ }
52718
53339
  workflowUsage();
52719
53340
  }
52720
53341
  if (subcommand === "agent") {
@@ -52859,7 +53480,6 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
52859
53480
  const toolName = subcommand;
52860
53481
  const ingested = await ingestJsonArgs({
52861
53482
  rawArg: toolArgs,
52862
- stdinIsTTY: process.stdin.isTTY ?? false,
52863
53483
  readFile: (p) => fs14.readFileSync(p, "utf-8"),
52864
53484
  readStdin
52865
53485
  });