@lotics/cli 0.100.0 → 0.101.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
@@ -44769,8 +44769,8 @@ var LoticsClient = class {
44769
44769
  * no caller has to remember that a partial payload silently drops
44770
44770
  * `instructions`, `outputs` and the model pin.
44771
44771
  */
44772
- async setAppAgent(app_id, alias, declaration) {
44773
- return this.execute("set_app_agent", { app_id, alias, ...declaration });
44772
+ async setAppAgent(app_id, alias, patch) {
44773
+ return this.execute("set_app_agent", { app_id, alias, ...patch });
44774
44774
  }
44775
44775
  /**
44776
44776
  * Fetch one app workflow's faithful source + bound input/output schemas via
@@ -47256,12 +47256,58 @@ function undeclaredCapabilities(sourceText, declared) {
47256
47256
  }
47257
47257
  return used;
47258
47258
  }
47259
- function unboundAliases(declared, bound) {
47260
- const boundWorkflows = new Set(bound.workflows ?? []);
47261
- const boundAgents = new Set(bound.agents ?? []);
47259
+ function driftedQueryAliases(a, b) {
47260
+ const left = a ?? {};
47261
+ const right = b ?? {};
47262
+ return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].filter((alias) => canonicalJson(left[alias]) !== canonicalJson(right[alias])).sort();
47263
+ }
47264
+ function canonicalJson(value) {
47265
+ const normalize = (v) => {
47266
+ if (Array.isArray(v)) return v.map(normalize);
47267
+ if (v === null || typeof v !== "object") return v;
47268
+ const entries2 = Object.entries(v).sort(
47269
+ ([x2], [y]) => x2 < y ? -1 : x2 > y ? 1 : 0
47270
+ );
47271
+ return entries2.map(([key, val]) => [key, normalize(val)]);
47272
+ };
47273
+ return JSON.stringify(normalize(value));
47274
+ }
47275
+ var ALIAS_CALL_HOOKS = {
47276
+ queries: "useQuery",
47277
+ workflows: "useWorkflow",
47278
+ agents: "useAgentRun"
47279
+ };
47280
+ var LITERAL_ALIAS_ARG = `["'\`]([A-Za-z_$][A-Za-z0-9_$]*)["'\`]`;
47281
+ function calledAppAliases(sourceText) {
47282
+ const out = {
47283
+ queries: [],
47284
+ workflows: [],
47285
+ agents: [],
47286
+ dynamic: []
47287
+ };
47288
+ for (const [kind, hook] of Object.entries(ALIAS_CALL_HOOKS)) {
47289
+ const seen = /* @__PURE__ */ new Set();
47290
+ let isDynamic = false;
47291
+ for (const call of sourceText.matchAll(new RegExp(`\\b${hook}\\s*\\(`, "g"))) {
47292
+ const rest = sourceText.slice(call.index + call[0].length);
47293
+ const literal2 = new RegExp(`^\\s*${LITERAL_ALIAS_ARG}`).exec(rest);
47294
+ if (literal2) seen.add(literal2[1]);
47295
+ else isDynamic = true;
47296
+ }
47297
+ out[kind] = [...seen];
47298
+ if (isDynamic) out.dynamic.push(hook);
47299
+ }
47300
+ return out;
47301
+ }
47302
+ function unboundAliases(called, bound) {
47303
+ const missing = (from, against) => {
47304
+ const have = new Set(against ?? []);
47305
+ return [...from ?? []].filter((alias) => !have.has(alias));
47306
+ };
47262
47307
  return {
47263
- workflows: [...declared.workflows ?? []].filter((a) => !boundWorkflows.has(a)),
47264
- agents: [...declared.agents ?? []].filter((a) => !boundAgents.has(a))
47308
+ queries: missing(called.queries, bound.queries),
47309
+ workflows: missing(called.workflows, bound.workflows),
47310
+ agents: missing(called.agents, bound.agents)
47265
47311
  };
47266
47312
  }
47267
47313
 
@@ -69927,9 +69973,9 @@ function findPlaceableWindow(chars, from, width) {
69927
69973
  return null;
69928
69974
  }
69929
69975
 
69930
- // src/workflow_envelope.ts
69931
- var FALLBACK_ENVELOPE_PREFIX = "async function __workflow(): Promise<__WorkflowReturn | void> {\n";
69932
- var FALLBACK_ENVELOPE_SUFFIX = "\n}";
69976
+ // ../shared/src/workflow_envelope.ts
69977
+ var WORKFLOW_ENVELOPE_PREFIX = "async function __workflow(): Promise<__WorkflowReturn | void> {\n";
69978
+ var WORKFLOW_ENVELOPE_SUFFIX = "\n}";
69933
69979
  var WORKFLOW_WRAPPER_OPENER = /^\s*async\s+function\s+__workflow\s*\(/;
69934
69980
  function stripWorkflowHeader(content) {
69935
69981
  const lines = content.split("\n");
@@ -70074,6 +70120,25 @@ function checkWorkflowBodies(tsApi, aliases) {
70074
70120
  }));
70075
70121
  }
70076
70122
 
70123
+ // ../shared/src/agent_instructions.ts
70124
+ function agentFilePath(alias) {
70125
+ return `src/agents/${alias}.md`;
70126
+ }
70127
+ function agentFileHeader(alias, notes = []) {
70128
+ return `<!-- lotics: instructions for agent "${alias}".
70129
+ ` + notes.map((n) => ` ${n}
70130
+ `).join("") + ` This comment is stripped before the prose reaches the live prompt. -->`;
70131
+ }
70132
+ function wrapAgentFile(args) {
70133
+ return `${agentFileHeader(args.alias, args.notes)}
70134
+
70135
+ ${args.instructions.replace(/\s+$/, "")}
70136
+ `;
70137
+ }
70138
+ function stripAgentHeader(text) {
70139
+ return text.replace(/<!--\s*lotics:[\s\S]*?-->\n?/g, "").replace(/\n{3,}/g, "\n\n").trim();
70140
+ }
70141
+
70077
70142
  // src/app_commands.ts
70078
70143
  async function fetchLatestNpmVersion(packageName) {
70079
70144
  try {
@@ -70105,8 +70170,11 @@ var STALE_LOTICS_INCLUDES = /* @__PURE__ */ new Set([".lotics", "./.lotics", ".l
70105
70170
  function toWorkflowDtsDeclaration(d) {
70106
70171
  return { inputs: d.inputs, outputs: d.outputs };
70107
70172
  }
70173
+ function isMissingWorkflowBodyError(error51) {
70174
+ return /has no step source|cannot be rendered|unsupported step/i.test(error51);
70175
+ }
70108
70176
  function isUnknownWorkflowAliasError(err2) {
70109
- return err2 instanceof Error && /^400:/.test(err2.message) && /has no workflow alias/i.test(err2.message);
70177
+ return err2 instanceof Error && err2.message.startsWith("400:") && /has no workflow alias/i.test(err2.message);
70110
70178
  }
70111
70179
  function workflowFileHeader(alias) {
70112
70180
  const refPath = path5.join("..", "..", WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`).split(path5.sep).join("/");
@@ -70136,7 +70204,7 @@ function writeWorkflowGlobals(projectDir, alias, dts) {
70136
70204
  `);
70137
70205
  return file2;
70138
70206
  }
70139
- function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX }) {
70207
+ function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: WORKFLOW_ENVELOPE_PREFIX, suffix: WORKFLOW_ENVELOPE_SUFFIX }) {
70140
70208
  const dir = path5.join(projectDir, WORKFLOWS_DIR);
70141
70209
  fs4.mkdirSync(dir, { recursive: true });
70142
70210
  const file2 = workflowFilePath(projectDir, alias);
@@ -70146,24 +70214,20 @@ ${envelope.prefix}${body}${envelope.suffix}
70146
70214
  `);
70147
70215
  return file2;
70148
70216
  }
70149
- function agentFilePath(projectDir, alias) {
70150
- return path5.join(projectDir, AGENTS_DIR, `${alias}.md`);
70151
- }
70152
- function agentFileHeader(alias) {
70153
- return `<!-- lotics: instructions for agent "${alias}".
70154
- Pulled from the LIVE app row; edit here, then: lotics app agent set ${alias}
70155
- The typed fields (inputs/outputs/tool_names/model_id) live in package.json#lotics.agents.${alias} -->`;
70156
- }
70157
- function stripAgentHeader(text) {
70158
- return text.replace(/<!--\s*lotics:[\s\S]*?-->\n?/g, "").replace(/\n{3,}/g, "\n\n").trim();
70217
+ function agentFilePath2(projectDir, alias) {
70218
+ return path5.join(projectDir, ...agentFilePath(alias).split("/"));
70159
70219
  }
70220
+ var CLI_AGENT_NOTES = [
70221
+ "Pulled from the LIVE app row; edit here, then: lotics app agent set <alias>",
70222
+ "The typed fields (inputs/outputs/tool_names/model_id) live in package.json#lotics.agents"
70223
+ ];
70160
70224
  function writeAgentFile(projectDir, alias, instructions) {
70161
70225
  fs4.mkdirSync(path5.join(projectDir, AGENTS_DIR), { recursive: true });
70162
- const file2 = agentFilePath(projectDir, alias);
70163
- fs4.writeFileSync(file2, `${agentFileHeader(alias)}
70164
-
70165
- ${instructions.replace(/\s+$/, "")}
70166
- `);
70226
+ const file2 = agentFilePath2(projectDir, alias);
70227
+ fs4.writeFileSync(
70228
+ file2,
70229
+ wrapAgentFile({ alias, instructions, notes: CLI_AGENT_NOTES.map((n) => n.replace("<alias>", alias)) })
70230
+ );
70167
70231
  return file2;
70168
70232
  }
70169
70233
  function writeAgentFiles(projectDir, agents) {
@@ -70183,10 +70247,15 @@ async function writeWorkflowFiles(client, projectDir, app_id, workflows) {
70183
70247
  const written = [];
70184
70248
  for (const [alias, declaration] of Object.entries(workflows)) {
70185
70249
  const res = await client.getAppWorkflow(app_id, alias);
70186
- const source = res.error || res.result === null || typeof res.result !== "object" ? null : res.result.source;
70250
+ if (res.error && !isMissingWorkflowBodyError(res.error)) {
70251
+ throw new Error(
70252
+ `Could not read workflow "${alias}" of ${app_id}: ${res.error}. Reading app workflow source requires an admin member \u2014 check the role behind this API key.`
70253
+ );
70254
+ }
70255
+ const source = res.result === null || typeof res.result !== "object" ? null : res.result.source;
70187
70256
  if (typeof source !== "string" || source.trim() === "") {
70188
70257
  console.error(
70189
- `\u26A0 Skipped src/workflows/${alias}.ts \u2014 the server returned no readable source (${res.error ?? "legacy workflow with no rendered body"}).`
70258
+ `\u26A0 Skipped src/workflows/${alias}.ts \u2014 legacy workflow with no rendered body.`
70190
70259
  );
70191
70260
  continue;
70192
70261
  }
@@ -70216,7 +70285,7 @@ async function fetchWorkflowGlobals(client, projectDir, app_id, alias, declarati
70216
70285
  console.error(
70217
70286
  `\u26A0 Could not fetch workflow types for "${alias}" (${err2 instanceof Error ? err2.message : String(err2)}). Wrote the body with the fallback wrapper; its local typecheck may be degraded.`
70218
70287
  );
70219
- return { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX };
70288
+ return { prefix: WORKFLOW_ENVELOPE_PREFIX, suffix: WORKFLOW_ENVELOPE_SUFFIX };
70220
70289
  }
70221
70290
  }
70222
70291
  async function fetchWorkflowDts(client, app_id, alias, declaration) {
@@ -70684,7 +70753,16 @@ function readAppSourceText(projectDir) {
70684
70753
  async function appDeploy(client, args) {
70685
70754
  const projectDir = path5.resolve(args.projectDir ?? process.cwd());
70686
70755
  const meta3 = readAppMeta(projectDir);
70687
- const undeclared = undeclaredCapabilities(readAppSourceText(projectDir), meta3.capabilities);
70756
+ const sourceText = readAppSourceText(projectDir);
70757
+ const called = calledAppAliases(sourceText);
70758
+ if (called.dynamic.length > 0) {
70759
+ console.error(
70760
+ `
70761
+ \u26A0 This app calls ${called.dynamic.join(" / ")} with an alias it computes at runtime, which this deploy cannot read.
70762
+ The version records only the aliases named as literals, so removing a binding this app reaches dynamically will NOT be refused.`
70763
+ );
70764
+ }
70765
+ const undeclared = undeclaredCapabilities(sourceText, meta3.capabilities);
70688
70766
  if (undeclared.length > 0) {
70689
70767
  const block = JSON.stringify(Object.fromEntries(undeclared.map((c) => [c, true])));
70690
70768
  console.error(
@@ -70721,29 +70799,30 @@ async function appDeploy(client, args) {
70721
70799
  );
70722
70800
  console.error("Packaging dist...");
70723
70801
  await runTar(["-czf", tmpDist, "-C", distDir, "."], projectDir);
70802
+ const liveQueries = await readLiveQueries(client, meta3.app_id);
70724
70803
  console.error("Uploading...");
70725
70804
  try {
70726
70805
  const result = await client.deployAppVersion({
70806
+ queries: liveQueries,
70727
70807
  app_id: meta3.app_id,
70728
70808
  source_archive: fs4.readFileSync(tmpSource),
70729
70809
  dist_archive: fs4.readFileSync(tmpDist),
70730
70810
  prev_version_id: meta3.current_version_id,
70731
70811
  message: args.message,
70732
- // Sync apps.queries from the manifest. Server validates each query
70733
- // template (parseQueryNode, table access, param coverage).
70734
- queries: meta3.queries ?? {},
70735
- // Capabilities are manifest-authoritative (like queries): always send,
70812
+ // Capabilities ARE manifest-authoritative: always send,
70736
70813
  // defaulting to `{}` when the manifest declares none — so deleting the
70737
70814
  // `capabilities` block turns every capability OFF on the next deploy
70738
70815
  // (fail-safe; the declaration is the grant).
70739
70816
  capabilities: meta3.capabilities ?? {},
70740
70817
  // Workflow BINDINGS are NOT a deploy concern — set_app_workflow /
70741
- // remove_app_workflow own apps.workflows. But the alias KEYS of the
70742
- // manifest's `workflows` map ARE sent (never the bindings): they record
70743
- // which aliases this bundle declares, so remove_app_workflow can refuse
70744
- // to unbind an alias the served version still calls. Drop an alias from
70745
- // the manifest + redeploy to lift that guard before removing its binding.
70746
- workflow_aliases: Object.keys(meta3.workflows ?? {})
70818
+ // remove_app_workflow own apps.workflows. What IS sent is the set of
70819
+ // aliases this bundle CALLS (never the bindings), so remove_app_workflow
70820
+ // can refuse to unbind one the served version still uses. Read off the
70821
+ // source, the same rule the chat publish uses: the manifest's map is a
70822
+ // typing artifact maintained by habit, so its keys answered this question
70823
+ // with whatever the author last remembered to write. Stop calling an
70824
+ // alias + redeploy to lift the guard before removing its binding.
70825
+ workflow_aliases: called.workflows
70747
70826
  });
70748
70827
  writeAppMeta(projectDir, {
70749
70828
  ...meta3,
@@ -70755,7 +70834,8 @@ async function appDeploy(client, args) {
70755
70834
  try {
70756
70835
  const app = await client.getApp(meta3.app_id);
70757
70836
  warnIfUnbranded(app);
70758
- warnIfUnboundAliases(app, meta3.workflows ?? {}, meta3.agents ?? {});
70837
+ warnIfUnboundAliases(app, called);
70838
+ warnIfQueriesDiffer(app, meta3.queries ?? {});
70759
70839
  } catch (err2) {
70760
70840
  console.error(`(skipped post-deploy checks: ${err2.message})`);
70761
70841
  }
@@ -70786,17 +70866,47 @@ function warnIfUnbranded(app) {
70786
70866
  Find an icon: lotics run search_app_icons '{"query":"<word>"}'`
70787
70867
  );
70788
70868
  }
70789
- function warnIfUnboundAliases(app, declaredWorkflows, declaredAgents) {
70790
- const { workflows: unboundWorkflows, agents: unboundAgents } = unboundAliases(
70791
- { workflows: Object.keys(declaredWorkflows), agents: Object.keys(declaredAgents) },
70792
- { workflows: Object.keys(app.workflows ?? {}), agents: Object.keys(app.agents ?? {}) }
70869
+ async function readLiveQueries(client, appId) {
70870
+ try {
70871
+ const app = await client.getApp(appId);
70872
+ return app.queries ?? {};
70873
+ } catch (err2) {
70874
+ console.error(
70875
+ `Could not read ${appId}'s current queries, which this deploy has to send back unchanged: ${err2 instanceof Error ? err2.message : String(err2)}
70876
+ Nothing was deployed. Retry.`
70877
+ );
70878
+ process.exit(1);
70879
+ }
70880
+ }
70881
+ function warnIfQueriesDiffer(app, manifest) {
70882
+ const drifted = driftedQueryAliases(manifest, app.queries);
70883
+ if (drifted.length === 0) return;
70884
+ console.error(
70885
+ `
70886
+ \u26A0 ${drifted.length} quer${drifted.length === 1 ? "y" : "ies"} in package.json differ from the app: ${drifted.join(", ")}
70887
+ A deploy does not write them. Push yours with 'lotics app query set --all'
70888
+ (or one alias at a time), or adopt the app's with 'lotics app pull'.`
70793
70889
  );
70794
- if (unboundWorkflows.length === 0 && unboundAgents.length === 0) return;
70890
+ }
70891
+ function warnIfUnboundAliases(app, called) {
70892
+ const {
70893
+ queries: unboundQueries,
70894
+ workflows: unboundWorkflows,
70895
+ agents: unboundAgents
70896
+ } = unboundAliases(called, {
70897
+ queries: Object.keys(app.queries ?? {}),
70898
+ workflows: Object.keys(app.workflows ?? {}),
70899
+ agents: Object.keys(app.agents ?? {})
70900
+ });
70901
+ if (unboundQueries.length === 0 && unboundWorkflows.length === 0 && unboundAgents.length === 0) {
70902
+ return;
70903
+ }
70795
70904
  const lines = [
70796
- "\n\u26A0 The manifest declares aliases that are NOT bound on the server. Deploy ships code +",
70797
- ' queries only \u2014 it does NOT bind workflows/agents, so the app will throw "has no \u2026 alias"',
70798
- " the first time it calls them. Bind each one:"
70905
+ "\n\u26A0 This app CALLS aliases that are NOT bound on the server. A deploy ships code and",
70906
+ ' binds nothing, so the app will throw "has no \u2026 alias" the first time it calls them.',
70907
+ " Bind each one:"
70799
70908
  ];
70909
+ for (const alias of unboundQueries) lines.push(` \u2022 query "${alias}" \u2192 lotics app query set ${alias}`);
70800
70910
  for (const alias of unboundWorkflows) lines.push(` \u2022 workflow "${alias}" \u2192 lotics app workflow set ${alias}`);
70801
70911
  for (const alias of unboundAgents) lines.push(` \u2022 agent "${alias}" \u2192 bind with set_app_agent (lotics run set_app_agent \u2026)`);
70802
70912
  console.error(lines.join("\n"));
@@ -71031,7 +71141,7 @@ async function appAgentSet(client, args) {
71031
71141
  );
71032
71142
  process.exit(1);
71033
71143
  }
71034
- const file2 = agentFilePath(projectDir, args.alias);
71144
+ const file2 = agentFilePath2(projectDir, args.alias);
71035
71145
  if (!fs4.existsSync(file2)) {
71036
71146
  console.error(
71037
71147
  `No instructions at ${path5.relative(projectDir, file2)}. Run 'lotics app pull ${meta3.app_id}' to write ${AGENTS_DIR}/${args.alias}.md, then edit it.`
@@ -71045,13 +71155,13 @@ async function appAgentSet(client, args) {
71045
71155
  );
71046
71156
  process.exit(1);
71047
71157
  }
71048
- const res = await client.setAppAgent(meta3.app_id, args.alias, { ...live, instructions });
71158
+ const res = await client.setAppAgent(meta3.app_id, args.alias, { instructions });
71049
71159
  if (res.error) {
71050
71160
  console.error(`Failed to set agent "${args.alias}": ${res.error}`);
71051
71161
  process.exit(1);
71052
71162
  }
71053
71163
  console.error(
71054
- `Set agent "${args.alias}" (${instructions.length} chars of instructions` + (live.model_id ? `, ${live.model_id}` : "") + `). Typed fields taken from the live row.`
71164
+ `Set agent "${args.alias}" (${instructions.length} chars of instructions` + (live.model_id ? `, ${live.model_id}` : "") + `). Typed fields left as they are.`
71055
71165
  );
71056
71166
  }
71057
71167
  async function appWorkflowSet(client, args) {
@@ -71106,20 +71216,31 @@ async function appWorkflowSet(client, args) {
71106
71216
  async function appQuerySet(client, args) {
71107
71217
  const projectDir = process.cwd();
71108
71218
  const meta3 = readAppMeta(projectDir);
71109
- const declaration = meta3.queries?.[args.alias];
71110
- if (!declaration) {
71111
- console.error(
71112
- `No query "${args.alias}" in package.json#lotics.queries. Declare it there (alias \u2192 { ast, params? }) first.`
71113
- );
71114
- process.exit(1);
71219
+ const declared = meta3.queries ?? {};
71220
+ const aliases = "all" in args ? Object.keys(declared).sort() : [args.alias];
71221
+ if (aliases.length === 0) {
71222
+ console.error("No queries in package.json#lotics.queries \u2014 nothing to push.");
71223
+ return;
71115
71224
  }
71116
- const res = await client.setAppQuery(meta3.app_id, args.alias, declaration);
71117
- if (res.error) {
71118
- console.error(`Failed to set query "${args.alias}": ${res.error}`);
71119
- process.exit(1);
71225
+ const pushed = [];
71226
+ for (const alias of aliases) {
71227
+ const declaration = declared[alias];
71228
+ if (!declaration) {
71229
+ console.error(
71230
+ `No query "${alias}" in package.json#lotics.queries. Declare it there (alias \u2192 { ast, params? }) first.`
71231
+ );
71232
+ process.exit(1);
71233
+ }
71234
+ const res = await client.setAppQuery(meta3.app_id, alias, declaration);
71235
+ if (res.error) {
71236
+ if (pushed.length > 0) console.error(`Pushed before the failure: ${pushed.join(", ")}.`);
71237
+ console.error(`Failed to set query "${alias}": ${res.error}`);
71238
+ process.exit(1);
71239
+ }
71240
+ pushed.push(alias);
71120
71241
  }
71121
71242
  console.error(
71122
- `Set query "${args.alias}" on ${meta3.app_id}. (apps.queries is manifest-authoritative \u2014 the next 'lotics app deploy' re-syncs it.)`
71243
+ `Set ${pushed.length} quer${pushed.length === 1 ? "y" : "ies"} on ${meta3.app_id}: ${pushed.join(", ")}.`
71123
71244
  );
71124
71245
  }
71125
71246
  async function appWorkflowPull(client) {
@@ -92655,11 +92776,15 @@ Available workspaces:`);
92655
92776
  if (subcommand === "query") {
92656
92777
  const action = toolArgs;
92657
92778
  if (action === "set") {
92658
- const alias = restArgs[0];
92779
+ if (restArgs.includes("--all")) {
92780
+ await appQuerySet(client, { all: true });
92781
+ return;
92782
+ }
92783
+ const alias = restArgs.find((a) => !a.startsWith("--"));
92659
92784
  if (!alias) {
92660
- console.error("Usage: lotics app query set <alias>");
92785
+ console.error("Usage: lotics app query set <alias> | --all");
92661
92786
  console.error(
92662
- "Pushes package.json#lotics.queries.<alias> to apps.queries via set_app_query (no deploy)."
92787
+ "Pushes package.json#lotics.queries to apps.queries via set_app_query. A deploy does not."
92663
92788
  );
92664
92789
  process.exit(1);
92665
92790
  }
@@ -92668,7 +92793,10 @@ Available workspaces:`);
92668
92793
  }
92669
92794
  console.error("Usage:");
92670
92795
  console.error(
92671
- " lotics app query set <alias> Push package.json#lotics.queries.<alias> to apps.queries (no deploy)"
92796
+ " lotics app query set <alias> Push package.json#lotics.queries.<alias> to apps.queries"
92797
+ );
92798
+ console.error(
92799
+ " lotics app query set --all Push every declared query (a deploy does not write them)"
92672
92800
  );
92673
92801
  process.exit(1);
92674
92802
  }
@@ -879,12 +879,10 @@ export declare class LoticsClient {
879
879
  * `instructions`, `outputs` and the model pin.
880
880
  */
881
881
  setAppAgent(app_id: string, alias: string,
882
- /** The WHOLE declaration. Typed loosely on purpose: the caller builds it by
883
- * spreading the live one, so a field the server adds later is forwarded
884
- * without this signature (or the caller) having to learn about it. */
885
- declaration: Record<string, unknown> & {
886
- instructions: string;
887
- }): Promise<ToolExecuteResult>;
882
+ /** ONLY the fields being changed. The server merges against the stored
883
+ * declaration, so nothing a caller omits is lost and a field the server adds
884
+ * later needs no change here. Pass `null` to clear an optional field. */
885
+ patch: Record<string, unknown>): Promise<ToolExecuteResult>;
888
886
  /**
889
887
  * Fetch one app workflow's faithful source + bound input/output schemas via
890
888
  * `get_app_workflow`. `source` is the JS-subset body re-rendered from the
@@ -572,11 +572,11 @@ export class LoticsClient {
572
572
  * `instructions`, `outputs` and the model pin.
573
573
  */
574
574
  async setAppAgent(app_id, alias,
575
- /** The WHOLE declaration. Typed loosely on purpose: the caller builds it by
576
- * spreading the live one, so a field the server adds later is forwarded
577
- * without this signature (or the caller) having to learn about it. */
578
- declaration) {
579
- return this.execute("set_app_agent", { app_id, alias, ...declaration });
575
+ /** ONLY the fields being changed. The server merges against the stored
576
+ * declaration, so nothing a caller omits is lost and a field the server adds
577
+ * later needs no change here. Pass `null` to clear an optional field. */
578
+ patch) {
579
+ return this.execute("set_app_agent", { app_id, alias, ...patch });
580
580
  }
581
581
  /**
582
582
  * Fetch one app workflow's faithful source + bound input/output schemas via
@@ -30,13 +30,13 @@ 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 + queries + capabilities only — workflow bindings are NOT 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. After a successful deploy it also **warns loudly about any `lotics.workflows` / `lotics.agents` alias declared in the manifest but 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 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). |
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
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. |
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
- | `lotics app query set <alias>` | Push `package.json#lotics.queries.<alias>` (`{ ast, params? }`) to `apps.queries` through `set_app_query` — the deploy-free inner loop for named queries, the mirror of `app workflow set`. The **server** validates it exactly as a deploy does (alias identifier, workspace-only tables, resolvable fields, declared params). Note: `apps.queries` is manifest-authoritative, so the next `lotics app deploy` re-syncs the whole map from the manifest — keep the declaration in the manifest to survive. Clear error + non-zero exit on an alias absent from the manifest or a validation failure. |
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
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. |
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). |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.100.0",
3
+ "version": "0.101.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "scripts": {
19
19
  "build": "tsgo -p tsconfig.build.json && node scripts/build_cli.mjs",
20
20
  "typecheck": "tsgo --noEmit",
21
+ "lint": "oxlint",
21
22
  "test": "vitest run",
22
23
  "prepublishOnly": "npm run build"
23
24
  },