@lotics/cli 0.101.0 → 0.102.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.
@@ -28695,6 +28695,73 @@ ${e.toString()}`);
28695
28695
  offsetYEmu
28696
28696
  };
28697
28697
  }
28698
+ var EMU_PER_PT = 12700;
28699
+ var VML_UNIT_TO_EMU = {
28700
+ pt: EMU_PER_PT,
28701
+ in: EMU_PER_PT * 72,
28702
+ cm: EMU_PER_PT * 72 / 2.54,
28703
+ mm: EMU_PER_PT * 72 / 25.4,
28704
+ pc: EMU_PER_PT * 12,
28705
+ px: EMU_PER_PT * 72 / 96
28706
+ };
28707
+ function readVmlLength(style, property) {
28708
+ const match = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([0-9.]+)([a-z]*)`, "i").exec(style);
28709
+ if (!match) return null;
28710
+ const value = Number.parseFloat(match[1]);
28711
+ if (!Number.isFinite(value)) return null;
28712
+ const unit = match[2].toLowerCase();
28713
+ const perUnit = unit === "" ? EMU_PER_PT : VML_UNIT_TO_EMU[unit];
28714
+ if (perUnit === void 0) return null;
28715
+ return Math.round(value * perUnit);
28716
+ }
28717
+ function parsePict(pictXml) {
28718
+ if (getTagName(pictXml) !== "w:pict") return null;
28719
+ const imagedata = findFirstByTag(pictXml, "v:imagedata");
28720
+ if (!imagedata) return null;
28721
+ const relationshipId = getAttr(imagedata, "r:id") ?? getAttr(imagedata, "r:embed") ?? null;
28722
+ if (relationshipId === null) return null;
28723
+ const shape = findFirstByTag(pictXml, "v:shape") ?? findFirstByTag(pictXml, "v:rect");
28724
+ const style = shape ? getAttr(shape, "style") ?? "" : "";
28725
+ return {
28726
+ relationshipId,
28727
+ widthEmu: readVmlLength(style, "width"),
28728
+ heightEmu: readVmlLength(style, "height"),
28729
+ alt: (shape ? getAttr(shape, "alt") : void 0) ?? "",
28730
+ // VML positioning is a CSS-ish `style`, not the wp: anchor vocabulary the
28731
+ // anchor fields describe. Reporting it as inline keeps the renderer from
28732
+ // acting on offsets that were never read.
28733
+ inline: true,
28734
+ wrap: null,
28735
+ floatSide: null,
28736
+ behindDoc: false,
28737
+ offsetXEmu: null,
28738
+ offsetYEmu: null
28739
+ };
28740
+ }
28741
+ function parseAlternateContent(el) {
28742
+ if (getTagName(el) !== "mc:AlternateContent") return null;
28743
+ const branches = getChildren(el).filter((child) => {
28744
+ const tag = getTagName(child);
28745
+ return tag === "mc:Choice" || tag === "mc:Fallback";
28746
+ });
28747
+ const ordered = [
28748
+ ...branches.filter((b) => getTagName(b) === "mc:Choice"),
28749
+ ...branches.filter((b) => getTagName(b) === "mc:Fallback")
28750
+ ];
28751
+ for (const branch of ordered) {
28752
+ const drawing = findFirstByTag(branch, "w:drawing");
28753
+ if (drawing) {
28754
+ const info = parseDrawing(drawing);
28755
+ if (info && info.relationshipId !== null) return info;
28756
+ }
28757
+ const pict = findFirstByTag(branch, "w:pict");
28758
+ if (pict) {
28759
+ const info = parsePict(pict);
28760
+ if (info) return info;
28761
+ }
28762
+ }
28763
+ return null;
28764
+ }
28698
28765
  function readPosOffset(containerEl, positionTag) {
28699
28766
  const position = findFirstByTag(containerEl, positionTag);
28700
28767
  if (!position) return null;
@@ -42936,6 +43003,24 @@ ${e.toString()}`);
42936
43003
  }
42937
43004
  };
42938
43005
  }
43006
+ function imageNode(info, originalXml, marks) {
43007
+ return docxSchema.nodes.image_inline.create(
43008
+ {
43009
+ relationshipId: info.relationshipId,
43010
+ widthEmu: info.widthEmu,
43011
+ heightEmu: info.heightEmu,
43012
+ alt: info.alt,
43013
+ originalXml,
43014
+ wrap: info.wrap,
43015
+ floatSide: info.floatSide,
43016
+ behindDoc: info.behindDoc,
43017
+ offsetXEmu: info.offsetXEmu,
43018
+ offsetYEmu: info.offsetYEmu
43019
+ },
43020
+ void 0,
43021
+ marks
43022
+ );
43023
+ }
42939
43024
  function convertRunChild(child, marks, state) {
42940
43025
  if (child.kind === "text") {
42941
43026
  if (child.value.length === 0) return [];
@@ -42963,26 +43048,15 @@ ${e.toString()}`);
42963
43048
  }
42964
43049
  if (tag === "w:drawing") {
42965
43050
  const info = parseDrawing(child.xml);
42966
- if (info) {
42967
- return [
42968
- docxSchema.nodes.image_inline.create(
42969
- {
42970
- relationshipId: info.relationshipId,
42971
- widthEmu: info.widthEmu,
42972
- heightEmu: info.heightEmu,
42973
- alt: info.alt,
42974
- originalXml: child.xml,
42975
- wrap: info.wrap,
42976
- floatSide: info.floatSide,
42977
- behindDoc: info.behindDoc,
42978
- offsetXEmu: info.offsetXEmu,
42979
- offsetYEmu: info.offsetYEmu
42980
- },
42981
- void 0,
42982
- marks
42983
- )
42984
- ];
42985
- }
43051
+ if (info) return [imageNode(info, child.xml, marks)];
43052
+ }
43053
+ if (tag === "mc:AlternateContent") {
43054
+ const info = parseAlternateContent(child.xml);
43055
+ if (info) return [imageNode(info, child.xml, marks)];
43056
+ }
43057
+ if (tag === "w:pict") {
43058
+ const info = parsePict(child.xml);
43059
+ if (info) return [imageNode(info, child.xml, marks)];
42986
43060
  }
42987
43061
  return [
42988
43062
  docxSchema.nodes.opaque_inline.create({ xml: child.xml }, void 0, marks)
package/dist/src/cli.js CHANGED
@@ -44955,6 +44955,7 @@ var LoticsClient = class {
44955
44955
  formData.append("capabilities", JSON.stringify(args.capabilities));
44956
44956
  }
44957
44957
  formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
44958
+ formData.append("query_aliases", JSON.stringify(args.query_aliases ?? []));
44958
44959
  const url2 = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
44959
44960
  const response = await fetch(url2, {
44960
44961
  method: "POST",
@@ -64945,18 +64946,25 @@ var appWorkflowDeclarationSchema = zod_default.object({
64945
64946
  ).refine(
64946
64947
  (outputs) => outputs === void 0 || Object.values(outputs).every((o) => appWorkflowOutputDepth(o) <= MAX_APP_WORKFLOW_OUTPUT_DEPTH),
64947
64948
  { message: `output schema nesting exceeds the max depth of ${MAX_APP_WORKFLOW_OUTPUT_DEPTH}` }
64949
+ ),
64950
+ description: zod_default.string().optional().describe(
64951
+ "What this workflow does, in one line \u2014 read by an agent choosing between the app's aliases, the same job a query's `description` does. `lotics app workflow set` sends it, so it lives beside the body in version control; omitted, the workflow keeps the description already on it."
64948
64952
  )
64949
64953
  });
64950
64954
  var appWorkflowContractSchema = zod_default.object({
64951
64955
  inputs: zod_default.record(zod_default.string(), appWorkflowInputSchema).optional(),
64952
64956
  outputs: zod_default.record(zod_default.string(), appWorkflowOutputSchema).optional()
64953
64957
  });
64958
+ var MAX_APP_CAPABILITY_DESCRIPTION = 300;
64954
64959
  var appQueryDeclarationSchema = zod_default.object({
64955
64960
  ast: zod_default.unknown().describe(
64956
64961
  "Query AST template (a QueryNode). Validated server-side via parseQueryNode at deploy. May embed {{params.<name>}} tokens in filter value positions."
64957
64962
  ),
64958
64963
  params: zod_default.record(zod_default.string(), appWorkflowInputSchema).optional().describe(
64959
64964
  "Typed param schema. Keys are param names referenced as {{params.<name>}} in the ast; values declare type + constraints. The server validates the caller's params payload against this before interpolating. Omit for queries that take no params."
64965
+ ),
64966
+ description: zod_default.string().optional().describe(
64967
+ `What this query returns, in one line \u2014 read by an agent choosing between the app's aliases. An alias is a JS identifier, which names a query without saying what it covers. Capped at ${MAX_APP_CAPABILITY_DESCRIPTION} characters.`
64960
64968
  )
64961
64969
  });
64962
64970
  var appAgentDeclarationSchema = zod_default.object({
@@ -70243,7 +70251,10 @@ function writeAgentFiles(projectDir, agents) {
70243
70251
  }
70244
70252
  return written;
70245
70253
  }
70246
- async function writeWorkflowFiles(client, projectDir, app_id, workflows) {
70254
+ function generatedWorkflowDescription(appName, alias) {
70255
+ return `Workflow invoked by app "${appName}" via useWorkflow("${alias}").`;
70256
+ }
70257
+ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows) {
70247
70258
  const written = [];
70248
70259
  for (const [alias, declaration] of Object.entries(workflows)) {
70249
70260
  const res = await client.getAppWorkflow(app_id, alias);
@@ -70267,10 +70278,27 @@ async function writeWorkflowFiles(client, projectDir, app_id, workflows) {
70267
70278
  toWorkflowDtsDeclaration(declaration)
70268
70279
  );
70269
70280
  writeWorkflowFile(projectDir, alias, source, envelope);
70281
+ const rowDescription = res.result.description;
70282
+ if (typeof rowDescription === "string" && rowDescription !== "" && // Skip the server's GENERATED default. It is what an undescribed alias
70283
+ // carries, so writing it back would commit a line of noise per workflow
70284
+ // into every app that never authored one — and the manifest would then
70285
+ // push it back, entrenching it. Matched by reconstruction, not by
70286
+ // sniffing the wording, so it can only ever skip the exact default.
70287
+ rowDescription !== generatedWorkflowDescription(appName, alias)) {
70288
+ writeWorkflowDescription(projectDir, alias, rowDescription);
70289
+ }
70270
70290
  written.push(alias);
70271
70291
  }
70272
70292
  return written;
70273
70293
  }
70294
+ function writeWorkflowDescription(projectDir, alias, description) {
70295
+ const pkgPath2 = path5.join(projectDir, "package.json");
70296
+ const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
70297
+ const declaration = pkg2.lotics?.workflows?.[alias];
70298
+ if (!declaration) return;
70299
+ declaration.description = description;
70300
+ fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
70301
+ }
70274
70302
  async function fetchWorkflowGlobals(client, projectDir, app_id, alias, declaration) {
70275
70303
  try {
70276
70304
  const { dts, envelope_prefix, envelope_suffix } = await fetchWorkflowDts(
@@ -70712,7 +70740,7 @@ async function appPull(client, args) {
70712
70740
  });
70713
70741
  const workflows = app.workflows ?? {};
70714
70742
  if (Object.keys(workflows).length > 0) {
70715
- const written = await writeWorkflowFiles(client, targetPath, app.id, workflows);
70743
+ const written = await writeWorkflowFiles(client, targetPath, app.id, app.name, workflows);
70716
70744
  if (written.length > 0) {
70717
70745
  console.error(
70718
70746
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
@@ -70822,7 +70850,8 @@ async function appDeploy(client, args) {
70822
70850
  // typing artifact maintained by habit, so its keys answered this question
70823
70851
  // with whatever the author last remembered to write. Stop calling an
70824
70852
  // alias + redeploy to lift the guard before removing its binding.
70825
- workflow_aliases: called.workflows
70853
+ workflow_aliases: called.workflows,
70854
+ query_aliases: called.queries
70826
70855
  });
70827
70856
  writeAppMeta(projectDir, {
70828
70857
  ...meta3,
@@ -71189,7 +71218,15 @@ async function appWorkflowSet(client, args) {
71189
71218
  const res = await client.setAppWorkflow(meta3.app_id, args.alias, {
71190
71219
  source,
71191
71220
  inputs: declaration.inputs,
71192
- outputs: declaration.outputs
71221
+ outputs: declaration.outputs,
71222
+ // What an agent reads when choosing between this app's workflows. Carried
71223
+ // from the manifest, alongside `inputs`, so it lives in version control
71224
+ // beside the body and rides every push — the CLI sent no description at
71225
+ // all, which is why a CLI-authored app's aliases all rendered the generated
71226
+ // `Workflow invoked by app "X" via useWorkflow("y")`. Absent leaves
71227
+ // whatever is on the row, so this can never blank a description set
71228
+ // elsewhere.
71229
+ description: declaration.description
71193
71230
  });
71194
71231
  if (res.error) {
71195
71232
  console.error(`Failed to set workflow "${args.alias}": ${res.error}`);
@@ -71252,7 +71289,7 @@ async function appWorkflowPull(client) {
71252
71289
  console.error(`App ${meta3.app_id} has no bound workflows.`);
71253
71290
  return;
71254
71291
  }
71255
- const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, workflows);
71292
+ const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, app.name, workflows);
71256
71293
  console.error(
71257
71294
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
71258
71295
  );
@@ -71373,7 +71410,14 @@ function appUiLink(args) {
71373
71410
  console.error("No @lotics/ui dev-link alias present \u2014 nothing to remove.");
71374
71411
  return;
71375
71412
  }
71376
- const stripped = source.replace(new RegExp(`^\\s*\\{ find: ${escapeRegExp(UI_ALIAS_FIND_SOURCE)}.*$\\n?`, "m"), "");
71413
+ const stripped = source.replace(
71414
+ new RegExp(
71415
+ `\\n?[ \\t]*\\{ find: ${escapeRegExp(UI_ALIAS_FIND_SOURCE)}, ` + // The replacement is a JSON-encoded path, so it cannot contain an
71416
+ // unescaped quote — bounded, rather than greedy to end of line.
71417
+ String.raw`replacement: "(?:[^"\\]|\\.)*" \},`
71418
+ ),
71419
+ ""
71420
+ );
71377
71421
  fs4.writeFileSync(viteConfigPath, stripped);
71378
71422
  console.error(`Removed the @lotics/ui dev-link alias from ${viteConfigPath}.`);
71379
71423
  console.error("Restart `lotics app dev` and rm -rf node_modules/.vite to clear cached modules.");
@@ -89648,37 +89692,47 @@ function extractParagraphText(el) {
89648
89692
  }
89649
89693
  return parts.join("");
89650
89694
  }
89651
- function getRunsFromParagraph(el) {
89652
- const runs = [];
89695
+ function getParagraphTextSegments(el) {
89696
+ const segments = [];
89653
89697
  for (const child of getChildren(el)) {
89654
89698
  if (getTagName(child) !== "w:r") continue;
89655
- let text = "";
89656
- let hasText = false;
89699
+ let pending = [];
89700
+ const flush = () => {
89701
+ if (pending.length === 0) return;
89702
+ const text = pending.map(
89703
+ (node) => getChildren(node).reduce(
89704
+ (acc, tn) => typeof tn["#text"] === "string" ? acc + tn["#text"] : acc,
89705
+ ""
89706
+ )
89707
+ ).join("");
89708
+ segments.push({ element: child, textNodes: pending, text });
89709
+ pending = [];
89710
+ };
89657
89711
  for (const grandchild of getChildren(child)) {
89658
- if (getTagName(grandchild) !== "w:t") continue;
89659
- hasText = true;
89660
- for (const tn of getChildren(grandchild)) {
89661
- if (typeof tn["#text"] === "string") text += tn["#text"];
89712
+ if (getTagName(grandchild) === "w:t") {
89713
+ pending.push(grandchild);
89714
+ continue;
89662
89715
  }
89716
+ flush();
89663
89717
  }
89664
- if (hasText) runs.push({ element: child, text });
89718
+ flush();
89665
89719
  }
89666
- return runs;
89720
+ return segments;
89667
89721
  }
89668
- function writeRunText(runElement, newText) {
89669
- const children = getChildren(runElement);
89670
- const textNode = { "w:t": [{ "#text": newText }], ":@": { "@_xml:space": "preserve" } };
89671
- const indices = children.flatMap((c, i2) => getTagName(c) === "w:t" ? [i2] : []);
89672
- if (indices.length === 0) {
89673
- children.push(textNode);
89674
- return;
89722
+ function setSegmentText(segment, newText) {
89723
+ const children = getChildren(segment.element);
89724
+ const textNode = {
89725
+ "w:t": [{ "#text": newText }],
89726
+ ":@": { "@_xml:space": "preserve" }
89727
+ };
89728
+ const head = children.indexOf(segment.textNodes[0]);
89729
+ children[head] = textNode;
89730
+ for (let i2 = segment.textNodes.length - 1; i2 >= 1; i2--) {
89731
+ const at2 = children.indexOf(segment.textNodes[i2]);
89732
+ if (at2 >= 0) children.splice(at2, 1);
89675
89733
  }
89676
- children[indices[0]] = textNode;
89677
- for (let i2 = indices.length - 1; i2 >= 1; i2--) children.splice(indices[i2], 1);
89678
- }
89679
- function setRunText(run, newText) {
89680
- writeRunText(run.element, newText);
89681
- run.text = newText;
89734
+ segment.textNodes = [textNode];
89735
+ segment.text = newText;
89682
89736
  }
89683
89737
  function computeReplacementRanges(text, query, replacement, matchType, maxReplacements, startCount) {
89684
89738
  const ranges = [];
@@ -89713,39 +89767,39 @@ function computeReplacementRanges(text, query, replacement, matchType, maxReplac
89713
89767
  }
89714
89768
  return { ranges, count };
89715
89769
  }
89716
- function applyRangeReplacements(runs, ranges) {
89770
+ function applyRangeReplacements(segments, ranges) {
89717
89771
  if (ranges.length === 0) return;
89718
89772
  const bounds = [];
89719
89773
  let offset = 0;
89720
- for (const run of runs) {
89721
- bounds.push({ run, start: offset, end: offset + run.text.length });
89722
- offset += run.text.length;
89774
+ for (const segment of segments) {
89775
+ bounds.push({ segment, start: offset, end: offset + segment.text.length });
89776
+ offset += segment.text.length;
89723
89777
  }
89724
- const joined = runs.map((r) => r.text).join("");
89778
+ const joined = segments.map((s) => s.text).join("");
89725
89779
  const sorted = [...ranges].sort((a, b) => a.start - b.start);
89726
- for (const { run, start: runStart, end: runEnd } of bounds) {
89780
+ for (const { segment, start: segStart, end: segEnd } of bounds) {
89727
89781
  let next = "";
89728
- let cursor = runStart;
89782
+ let cursor = segStart;
89729
89783
  for (const range2 of sorted) {
89730
- if (range2.end <= runStart || range2.start >= runEnd) continue;
89731
- const keepUntil = Math.min(range2.start, runEnd);
89732
- const from = Math.max(cursor, runStart);
89784
+ if (range2.end <= segStart || range2.start >= segEnd) continue;
89785
+ const keepUntil = Math.min(range2.start, segEnd);
89786
+ const from = Math.max(cursor, segStart);
89733
89787
  if (keepUntil > from) next += joined.slice(from, keepUntil);
89734
- if (runStart <= range2.start && range2.start < runEnd) next += range2.replacement;
89788
+ if (segStart <= range2.start && range2.start < segEnd) next += range2.replacement;
89735
89789
  cursor = Math.max(cursor, range2.end);
89736
89790
  }
89737
- const tail = Math.max(cursor, runStart);
89738
- if (runEnd > tail) next += joined.slice(tail, runEnd);
89739
- if (next !== run.text) setRunText(run, next);
89791
+ const tail = Math.max(cursor, segStart);
89792
+ if (segEnd > tail) next += joined.slice(tail, segEnd);
89793
+ if (next !== segment.text) setSegmentText(segment, next);
89740
89794
  }
89741
89795
  }
89742
89796
  function replaceInParagraph(el, query, replacement, matchType, maxReplacements, replacementsMade) {
89743
89797
  if (!extractParagraphText(el)) return replacementsMade;
89744
- const runs = getRunsFromParagraph(el);
89745
- if (runs.length === 0) return replacementsMade;
89746
- const text = runs.length === 1 ? runs[0].text : runs.map((r) => r.text).join("");
89798
+ const segments = getParagraphTextSegments(el);
89799
+ if (segments.length === 0) return replacementsMade;
89800
+ const text = segments.length === 1 ? segments[0].text : segments.map((s) => s.text).join("");
89747
89801
  const { ranges, count } = computeReplacementRanges(text, query, replacement, matchType, maxReplacements, replacementsMade);
89748
- applyRangeReplacements(runs, ranges);
89802
+ applyRangeReplacements(segments, ranges);
89749
89803
  return count;
89750
89804
  }
89751
89805
  function replaceText(bodyElements, query, replacement, matchType = "contains", maxReplacements = Infinity) {
@@ -222,6 +222,7 @@ export declare class LoticsClient {
222
222
  queries?: Record<string, {
223
223
  ast: unknown;
224
224
  params?: Record<string, unknown>;
225
+ description?: string;
225
226
  }> | null;
226
227
  /**
227
228
  * Live alias → agent declaration map from `apps.agents`. Source of truth for
@@ -866,6 +867,7 @@ export declare class LoticsClient {
866
867
  setAppQuery(app_id: string, alias: string, declaration: {
867
868
  ast: unknown;
868
869
  params?: Record<string, unknown>;
870
+ description?: string;
869
871
  }): Promise<ToolExecuteResult>;
870
872
  /**
871
873
  * Bind (create or replace) an app agent by alias via the `set_app_agent` tool
@@ -1006,14 +1008,16 @@ export declare class LoticsClient {
1006
1008
  prev_version_id?: string | null;
1007
1009
  message?: string | null;
1008
1010
  /**
1009
- * Alias → query declaration map from `package.json#lotics.queries`. Each
1010
- * value is `{ ast, params? }` — a fixed query AST template and an optional
1011
- * typed param schema. Always sent (empty object when none declared) so the
1012
- * server overwrites apps.queries authoritatively.
1011
+ * Alias → query declaration map ECHOED BACK from the live row (see
1012
+ * `readLiveQueries`), not read from the manifest. Every key the row holds
1013
+ * must survive the round trip — `description` included — because a server
1014
+ * that treats a present map as authoritative writes exactly what it is
1015
+ * sent, so a lossy echo silently strips whatever it forgot.
1013
1016
  */
1014
1017
  queries?: Record<string, {
1015
1018
  ast: unknown;
1016
1019
  params?: Record<string, unknown>;
1020
+ description?: string;
1017
1021
  }>;
1018
1022
  /**
1019
1023
  * Opt-in app capabilities from `package.json#lotics.capabilities`. The CLI
@@ -1032,6 +1036,7 @@ export declare class LoticsClient {
1032
1036
  * (empty array when none declared).
1033
1037
  */
1034
1038
  workflow_aliases?: string[];
1039
+ query_aliases?: string[];
1035
1040
  }): Promise<{
1036
1041
  version_id: string;
1037
1042
  version_number: number;
@@ -723,6 +723,7 @@ export class LoticsClient {
723
723
  // server records what the served version calls — the remove_app_workflow
724
724
  // guard reads this back. These are the manifest KEYS only, never bindings.
725
725
  formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
726
+ formData.append("query_aliases", JSON.stringify(args.query_aliases ?? []));
726
727
  const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
727
728
  const response = await fetch(url, {
728
729
  method: "POST",
@@ -30,15 +30,15 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
30
30
  | `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |
31
31
  | `lotics app create <name> [path]` | Scaffold a Vite+React+TS custom-code app project; POST /v1/apps; npm install; vite build; upload as v1 |
32
32
  | `lotics app pull <app_id> [path]` | Download source archive from R2 (presigned), extract, npm install, stamp package.json's `lotics` field. With no `[path]`: refresh the cwd IN PLACE when it's already this app's own project (its manifest `app_id` matches — the documented `cd <app> && lotics app pull` flow), else clone into an `<name>/` subdir; this avoids the stray nested `./<name>/` subdir a pull-from-inside-the-app used to drop. — `workflows` and `agents` are sourced from the live App row (NOT the archived manifest), so `set_app_workflow` / `set_app_agent` authoring survives the pull. Regenerates `.lotics/app_{workflows,queries,agents}.d.ts` so `useWorkflow` / `useQuery` / `useAgentRun` stay typed. Also writes one `src/workflows/<alias>.ts` per bound workflow (faithful body from `get_app_workflow`) and one `src/agents/<alias>.md` per bound agent (its instructions, straight off the live row) — so the prose an author actually edits lives in a file, and pull always overwrites it from live, leaving no second copy to drift. A legacy workflow alias with no rendered source, or an agent with no instructions, warns and is skipped. The stamped `lotics.agents` map carries the TYPED half only (`inputs`/`outputs`/`tool_names`/`model_id`/…) — an agent's prose lives solely in its `.md`, so there is never a second local copy to desync; a stale `instructions` left by an older CLI is inert and disappears on the next pull |
33
- | `lotics app deploy -m <message>` | **`-m` is REQUIRED** (CLI errors without a non-empty message) — each deploy is a version row read back by `lotics app versions`, so a blank message loses the audit trail. npm run build; tar source + dist; POST /v1/apps/{id}/versions multipart. Carries code + capabilities only — **neither queries nor workflow/agent bindings are a deploy concern** (`set_app_workflow` / `remove_app_workflow` own `apps.workflows`; the manifest's `workflows` map is a pulled reflection used only for `useWorkflow` codegen). Deploy DOES send the manifest's `lotics.workflows` alias KEYS (not the bindings) as `workflow_aliases`, recorded on the version row so `remove_app_workflow` can refuse to unbind an alias the served version still declares. It also reports any `lotics.queries` alias whose declaration DIFFERS from the app's, naming both recoveries (`app query set --all` to push yours, `app pull` to adopt the app's) — a deploy no longer writes them, so the two are allowed to drift. After a successful deploy it **warns loudly about any alias the source CALLS that is NOT bound on the server** (a `getApp` diff via `warnIfUnboundAliases`) — since deploy never binds them, that would otherwise throw only at the app's first `useWorkflow` / `useAgentRun` call; the warning points to `lotics app workflow set` / `set_app_agent`. Advisory only (never fails the deploy). |
33
+ | `lotics app deploy -m <message>` | **`-m` is REQUIRED** (CLI errors without a non-empty message) — each deploy is a version row read back by `lotics app versions`, so a blank message loses the audit trail. npm run build; tar source + dist; POST /v1/apps/{id}/versions multipart. Carries code + capabilities only — **neither queries nor workflow/agent bindings are a deploy concern** (`set_app_workflow` / `remove_app_workflow` own `apps.workflows`; the manifest's `workflows` map is a pulled reflection, read by `useWorkflow` codegen and by `app workflow set`, never written by a deploy). Deploy DOES send the manifest's `lotics.workflows` alias KEYS (not the bindings) as `workflow_aliases`, recorded on the version row so `remove_app_workflow` can refuse to unbind an alias the served version still declares. It also reports any `lotics.queries` alias whose declaration DIFFERS from the app's, naming both recoveries (`app query set --all` to push yours, `app pull` to adopt the app's) — a deploy no longer writes them, so the two are allowed to drift. After a successful deploy it **warns loudly about any alias the source CALLS that is NOT bound on the server** (a `getApp` diff via `warnIfUnboundAliases`) — since deploy never binds them, that would otherwise throw only at the app's first `useWorkflow` / `useAgentRun` call; the warning points to `lotics app workflow set` / `set_app_agent`. Advisory only (never fails the deploy). |
34
34
  | `lotics app versions [app_id]` | `GET /v1/apps/{id}/versions` — print deploy history newest-first (version number, timestamp, deployer name, build status, the `-m` message; `*` marks the currently-served version). app_id from the local manifest, or pass one to inspect any app without pulling it. Admin-only server-side (mirrors deploy + source download). Answers "what shipped, when, by whom" — e.g. whether a fix was live at an incident's time. The deploy pipeline already persisted all of this in `app_versions`; this is the read surface. Title → stderr, table → stdout (pipeable). |
35
35
  | `lotics app codegen [path]` | Regenerate `.lotics/*` from the manifest + workspace schema **without a deploy**. The three `.d.ts` companions (`app_{workflows,queries,agents}.d.ts`) are always rewritten (synchronous, no network). When credentials resolve, also rewrites the **runtime** `.lotics/app_fields.ts` — **branched on whether the app is a package installation** (`getApp().package_id` set, from `generate_package_fields.ts`): a **linked/published** app emits the BINDING form (`F`/`OPT`/`ROLE` resolved from the installation's LIVE binding — via `appBinding` / the `binding` RPC — at module load through `getAppBinding()` + top-level await, so the source stays portable across every install); a **bespoke** app emits the BAKED form (`generate_app_fields.ts`) — a real `.ts` exporting `F` (table→field→`"fld_…"`) + `OPT` (table→select-field→option→`"opt_…"`) keyed by display-name aliases, for the tables the app's queries reference (+ optional `package.json#lotics.codegen.tables` allowlist). Both forms share the `F`/`OPT` shape (contract aliases derive from the same slugified display names), so a published origin's deployed source compiles unchanged. Writing the BINDING form also heals the project's vitest setup (`ensureAppVitestSetup`, folded into the same write boundary): the binding form awaits `getAppBinding()` (a network call) at module load, so without a stub `npm test` fails to collect any test that imports the app graph — the heal writes `vitest.setup.ts` (mocks only `getAppBinding`, returning an echo binding: any alias → a self-identifying `fld:test:…`/`opt:test:…`/`grp:test:…` id) if absent, and warns the one-liner to add to `vite.config.ts`'s `test.setupFiles` if the wiring is missing (TS source isn't safely munged, mirroring `ensureAppTsconfig`'s JSONC-tsconfig warn). New scaffolds ship both. Also refreshes each bound workflow's `.lotics/workflows/<alias>.globals.d.ts` + re-wraps its EXISTING `src/workflows/<alias>.ts` body in the current envelope (strips + re-wraps; never re-fetches the body, so local edits survive). A getApp / binding / schema / dts-fetch failure is non-fatal (warns, keeps the last-generated files). |
36
36
  | `lotics app workflow run <alias> '<json>'` | Execute a bound app workflow end-to-end via `appWorkflow`. `app_id` comes from the local manifest; the alias must be bound (`set_app_workflow`). Inputs ingest exactly like `lotics run` (inline JSON / `@file` / stdin — bulk inputs bypass `ARG_MAX`). Prints the full `{status,message,data,files,side_effects}` JSON to stdout + a one-line summary to stderr; exits non-zero on `status:"error"` (assertable). `--print-created` (alias `--report-effects`) renders the honest post-run harvest (GAP-58): created records grouped by table, a paste-ready `lotics run delete_records …` per table, then the **mandatory caveat** naming what cannot be auto-undone (external integrations + notifications) and that sub-workflows may have run. `--cleanup` (DEFAULT OFF, implies the report) additionally runs the deletes for harvested records ONLY — never files / external / notifications. Neither is a rollback — a rollback is structurally impossible here. |
37
- | `lotics app workflow set <alias>` | Push the edited `src/workflows/<alias>.ts` body through `set_app_workflow` (the single author of `apps.workflows`). Reads the body from disk (header + `/// <reference>` + `export {};` marker + the `__workflow` wrapper all stripped) + the typed `inputs`/`outputs` from `package.json#lotics.workflows.<alias>`; the **server** re-verifies the body and echoes the bound `outputs` (declared, else DERIVED from `return({ data })`). When the manifest declared NO `outputs`, the DERIVED echo is written back into `package.json#lotics.workflows.<alias>.outputs` (a SURGICAL write — preserves `knowledge`/`config` and every other manifest field) and that alias's types are refreshed in place, so `useWorkflow("<alias>")`'s `result.data` is typed immediately with no hand-copy and no second `lotics app codegen`; an explicitly-declared `outputs` is authoritative and never overwritten. Deploy still never authors workflows — this is a CLI convenience over the existing tool. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a verify failure. |
37
+ | `lotics app workflow set <alias>` | Push the edited `src/workflows/<alias>.ts` body through `set_app_workflow` (the single author of `apps.workflows`). Reads the body from disk (header + `/// <reference>` + `export {};` marker + the `__workflow` wrapper all stripped) + the typed `inputs`/`outputs` **and the `description`** from `package.json#lotics.workflows.<alias>`; the **server** re-verifies the body and echoes the bound `outputs` (declared, else DERIVED from `return({ data })`). The `description` is the one line an agent reads when choosing between the app's aliases (the workflow counterpart to a query's) — authored in the manifest so it lives beside the body in version control and rides every push; omit it and the workflow keeps whatever description it already has, so a push can never blank one set elsewhere. When the manifest declared NO `outputs`, the DERIVED echo is written back into `package.json#lotics.workflows.<alias>.outputs` (a SURGICAL write — preserves `knowledge`/`config` and every other manifest field) and that alias's types are refreshed in place, so `useWorkflow("<alias>")`'s `result.data` is typed immediately with no hand-copy and no second `lotics app codegen`; an explicitly-declared `outputs` is authoritative and never overwritten. Deploy still never authors workflows — this is a CLI convenience over the existing tool. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a verify failure. |
38
38
  | `lotics app agent set <alias>` | Push the edited `src/agents/<alias>.md` instructions back through `set_app_agent` — the agent mirror of `app workflow set`, and the deploy-free authoring path for `apps.agents`. Reads the prose from disk (the `<!-- lotics: … -->` header stripped) and the typed fields (`inputs`/`outputs`/`tool_names`/`model_id`/`effort_level`/`knowledge_doc_ids`/`query_aliases`/`workflow_aliases`) from `package.json#lotics.agents.<alias>`, then sends them as ONE declaration. That assembly is the point: **`set_app_agent` REPLACES the declaration rather than patching it**, so a hand-built payload that sets one field silently drops the instructions, the output schema and the model pin — a silent, unrecoverable edit against a live prompt. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a file that is empty once the header is stripped (refusing to push an empty prompt). `app pull` writes the file; edit, then `set`. |
39
39
  | `lotics app query set <alias>` \| `--all` | Push `package.json#lotics.queries` (`{ ast, params? }` per alias) to `apps.queries` through `set_app_query` — **the only author of a query binding**, the mirror of `app workflow set`. A deploy ships code and binds nothing. The **server** validates each one exactly as it always did (alias identifier, workspace-only tables, resolvable fields, declared params). `--all` pushes every declared alias, alias-sorted, stopping at the first failure and naming what already landed. Clear error + non-zero exit on an alias absent from the manifest or a validation failure. |
40
40
  | `lotics app agent run <app_id> <alias> ['<json>'\|@file\|stdin]` | Run a bound app agent end-to-end (GAP-87). A run needs no deployed UI bundle — just the app row + the bound agent declaration + member auth — so the **`app_id` is explicit** (not read from a local manifest). Inputs ingest exactly like `lotics run` (inline JSON / `@file` / stdin; empty = `{}`). Opens the run's SSE (`appAgentRunStream`), streams `text-delta` prose to **stderr** as live progress, then reports from the **settled run RECORD** (`listAgentRuns`, polled to a terminal status — the client stream can close a beat before the run settles, or drop while it runs on server-side): default prints the run's structured `output` (JSON) or final text to **stdout** + a status line to stderr; `--json` prints the full run summary to stdout. Selects THIS run by the `x-app-agent-run-id` header (ordering-independent). Exits 0 **only** when the settled status is `completed`; otherwise non-zero with the run's error surfaced. A settled run that never appears fails loudly (never a silent success). A fresh `session_id` is minted per run (self-contained); `--session <id>` continues an existing thread (prior runs become the agent's context). |
41
- | `lotics app workflow pull` | Rewrite every `src/workflows/<alias>.ts` from the server (faithful body per bound alias via `get_app_workflow`) **+ its `.lotics/workflows/<alias>.globals.d.ts`** (via `getAppWorkflowDts`, so the body is locally typecheckable via `lotics app workflow check`) without a full `app pull` (no source archive, no npm install). A legacy alias with no rendered source warns and is skipped; a dts-fetch failure is non-fatal (body still written with the fallback wrapper, typecheck degraded). Also idempotently patches the main `tsconfig.json` `exclude` to cover `src/workflows` + `.lotics/workflows` so a pre-existing app's `npm run typecheck` never loads the bodies or the colliding per-alias globals. |
41
+ | `lotics app workflow pull` | Rewrite every `src/workflows/<alias>.ts` from the server (faithful body per bound alias via `get_app_workflow`) **+ its `.lotics/workflows/<alias>.globals.d.ts`** (via `getAppWorkflowDts`, so the body is locally typecheckable via `lotics app workflow check`) without a full `app pull` (no source archive, no npm install). A legacy alias with no rendered source warns and is skipped; a dts-fetch failure is non-fatal (body still written with the fallback wrapper, typecheck degraded). Each alias's `description` is folded back into `package.json#lotics.workflows.<alias>` from the same read — the alias binding the manifest is otherwise stamped from carries `inputs`/`outputs` but not the description, which lives on the workflow ROW, so without this a pull would erase an authored one. The server's GENERATED default is skipped, so an app that never described its workflows gains no manifest noise. Also idempotently patches the main `tsconfig.json` `exclude` to cover `src/workflows` + `.lotics/workflows` so a pre-existing app's `npm run typecheck` never loads the bodies or the colliding per-alias globals. |
42
42
  | `lotics app workflow check [alias]` | Check the editable workflow bodies locally, no auth / no network, in the **server's own order** — parse, then type-check. **Parse** runs `parseWorkflowJs` from `@lotics/shared` (the SAME module `verifyWorkflow` calls, never a second implementation — that is what let the two diverge once) over the stripped body `set` would upload, with `toolNames: undefined` (the CLI ships no tool registry, so tool-name resolution stays a server check while every shape/scope rule runs here). A body the subset rejects reports **that error alone** and skips the compiler — it never reaches the server's compiler either, so tsc's opinion of it is noise. **Type-check** then builds an **isolated** `ts.Program` per alias from exactly that alias's `{body, globals}` pair — mirroring the server, which verifies one body at a time — so the per-alias ambient `trigger` never collides and `trigger.app_workflow.inputs` is checked against the right alias (GAP-59). All aliases run in ONE node process (N programs, not N `tsc` spawns), with the SAME compile options the server uses at set-time verify (lib `es2022` with no DOM, target ES2022, strict, NodeNext, `types:[]`, skipLibCheck) and the app's OWN `typescript` (resolved from its `node_modules`, never bundled into the CLI). What the compiler sees is the **checked source**, not the file: `rewriteAccumulatorAppends` from `@lotics/shared` — the SAME transform the server applies before its set-time compile — is applied in memory, so a pulled body's canonical `out = concat(out, [item])` accumulator checks green here exactly as it saves there (compiling the raw text went red on it), and the body on disk is never rewritten. Reports `<file>:<line>:<col> - <TS####\|subset>` at the **physical** line in `src/workflows/<alias>.ts`, so an editor jump lands on the offending code (these are deliberately NOT `set`'s body-relative numbers — `set` prints no file path, so there is no format to agree with); exits non-zero if any alias fails. Green is honest but not total: `set` additionally resolves names, lints and structurally validates against the live workspace — passes that need its tables and tool schemas, so they cannot run offline, and the success line says so. A bound alias with no body file yet warns + skips; a body with no globals errors (run a pull). |
43
43
  | `lotics app subdomain <new-subdomain>` | Rename the app's public `<slug>.lotics.app` address via `PUT /v1/apps/{id}/subdomain`. app_id comes from the local `package.json` manifest; the chosen slug must be a valid DNS label and free; the old address stops resolving. |
44
44
  | `lotics app rename "<new name>"` | Change the app's display name (launcher/title) via the `update_app` tool. app_id comes from the local `package.json` manifest; the public address (`subdomain`) and code (`deploy`) are unchanged. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.101.0",
3
+ "version": "0.102.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {