@lotics/cli 0.100.0 → 0.100.2

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,6 +47256,24 @@ function undeclaredCapabilities(sourceText, declared) {
47256
47256
  }
47257
47257
  return used;
47258
47258
  }
47259
+ var ALIAS_CALL_HOOKS = { workflows: "useWorkflow", agents: "useAgentRun" };
47260
+ var LITERAL_ALIAS_ARG = `["'\`]([A-Za-z_$][A-Za-z0-9_$]*)["'\`]`;
47261
+ function calledAppAliases(sourceText) {
47262
+ const out = { workflows: [], agents: [], dynamic: [] };
47263
+ for (const [kind, hook] of Object.entries(ALIAS_CALL_HOOKS)) {
47264
+ const seen = /* @__PURE__ */ new Set();
47265
+ let isDynamic = false;
47266
+ for (const call of sourceText.matchAll(new RegExp(`\\b${hook}\\s*\\(`, "g"))) {
47267
+ const rest = sourceText.slice(call.index + call[0].length);
47268
+ const literal2 = new RegExp(`^\\s*${LITERAL_ALIAS_ARG}`).exec(rest);
47269
+ if (literal2) seen.add(literal2[1]);
47270
+ else isDynamic = true;
47271
+ }
47272
+ out[kind] = [...seen];
47273
+ if (isDynamic) out.dynamic.push(hook);
47274
+ }
47275
+ return out;
47276
+ }
47259
47277
  function unboundAliases(declared, bound) {
47260
47278
  const boundWorkflows = new Set(bound.workflows ?? []);
47261
47279
  const boundAgents = new Set(bound.agents ?? []);
@@ -69927,9 +69945,9 @@ function findPlaceableWindow(chars, from, width) {
69927
69945
  return null;
69928
69946
  }
69929
69947
 
69930
- // src/workflow_envelope.ts
69931
- var FALLBACK_ENVELOPE_PREFIX = "async function __workflow(): Promise<__WorkflowReturn | void> {\n";
69932
- var FALLBACK_ENVELOPE_SUFFIX = "\n}";
69948
+ // ../shared/src/workflow_envelope.ts
69949
+ var WORKFLOW_ENVELOPE_PREFIX = "async function __workflow(): Promise<__WorkflowReturn | void> {\n";
69950
+ var WORKFLOW_ENVELOPE_SUFFIX = "\n}";
69933
69951
  var WORKFLOW_WRAPPER_OPENER = /^\s*async\s+function\s+__workflow\s*\(/;
69934
69952
  function stripWorkflowHeader(content) {
69935
69953
  const lines = content.split("\n");
@@ -70074,6 +70092,25 @@ function checkWorkflowBodies(tsApi, aliases) {
70074
70092
  }));
70075
70093
  }
70076
70094
 
70095
+ // ../shared/src/agent_instructions.ts
70096
+ function agentFilePath(alias) {
70097
+ return `src/agents/${alias}.md`;
70098
+ }
70099
+ function agentFileHeader(alias, notes = []) {
70100
+ return `<!-- lotics: instructions for agent "${alias}".
70101
+ ` + notes.map((n) => ` ${n}
70102
+ `).join("") + ` This comment is stripped before the prose reaches the live prompt. -->`;
70103
+ }
70104
+ function wrapAgentFile(args) {
70105
+ return `${agentFileHeader(args.alias, args.notes)}
70106
+
70107
+ ${args.instructions.replace(/\s+$/, "")}
70108
+ `;
70109
+ }
70110
+ function stripAgentHeader(text) {
70111
+ return text.replace(/<!--\s*lotics:[\s\S]*?-->\n?/g, "").replace(/\n{3,}/g, "\n\n").trim();
70112
+ }
70113
+
70077
70114
  // src/app_commands.ts
70078
70115
  async function fetchLatestNpmVersion(packageName) {
70079
70116
  try {
@@ -70105,8 +70142,11 @@ var STALE_LOTICS_INCLUDES = /* @__PURE__ */ new Set([".lotics", "./.lotics", ".l
70105
70142
  function toWorkflowDtsDeclaration(d) {
70106
70143
  return { inputs: d.inputs, outputs: d.outputs };
70107
70144
  }
70145
+ function isMissingWorkflowBodyError(error51) {
70146
+ return /has no step source|cannot be rendered|unsupported step/i.test(error51);
70147
+ }
70108
70148
  function isUnknownWorkflowAliasError(err2) {
70109
- return err2 instanceof Error && /^400:/.test(err2.message) && /has no workflow alias/i.test(err2.message);
70149
+ return err2 instanceof Error && err2.message.startsWith("400:") && /has no workflow alias/i.test(err2.message);
70110
70150
  }
70111
70151
  function workflowFileHeader(alias) {
70112
70152
  const refPath = path5.join("..", "..", WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`).split(path5.sep).join("/");
@@ -70136,7 +70176,7 @@ function writeWorkflowGlobals(projectDir, alias, dts) {
70136
70176
  `);
70137
70177
  return file2;
70138
70178
  }
70139
- function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX }) {
70179
+ function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: WORKFLOW_ENVELOPE_PREFIX, suffix: WORKFLOW_ENVELOPE_SUFFIX }) {
70140
70180
  const dir = path5.join(projectDir, WORKFLOWS_DIR);
70141
70181
  fs4.mkdirSync(dir, { recursive: true });
70142
70182
  const file2 = workflowFilePath(projectDir, alias);
@@ -70146,24 +70186,20 @@ ${envelope.prefix}${body}${envelope.suffix}
70146
70186
  `);
70147
70187
  return file2;
70148
70188
  }
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();
70189
+ function agentFilePath2(projectDir, alias) {
70190
+ return path5.join(projectDir, ...agentFilePath(alias).split("/"));
70159
70191
  }
70192
+ var CLI_AGENT_NOTES = [
70193
+ "Pulled from the LIVE app row; edit here, then: lotics app agent set <alias>",
70194
+ "The typed fields (inputs/outputs/tool_names/model_id) live in package.json#lotics.agents"
70195
+ ];
70160
70196
  function writeAgentFile(projectDir, alias, instructions) {
70161
70197
  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
- `);
70198
+ const file2 = agentFilePath2(projectDir, alias);
70199
+ fs4.writeFileSync(
70200
+ file2,
70201
+ wrapAgentFile({ alias, instructions, notes: CLI_AGENT_NOTES.map((n) => n.replace("<alias>", alias)) })
70202
+ );
70167
70203
  return file2;
70168
70204
  }
70169
70205
  function writeAgentFiles(projectDir, agents) {
@@ -70183,10 +70219,15 @@ async function writeWorkflowFiles(client, projectDir, app_id, workflows) {
70183
70219
  const written = [];
70184
70220
  for (const [alias, declaration] of Object.entries(workflows)) {
70185
70221
  const res = await client.getAppWorkflow(app_id, alias);
70186
- const source = res.error || res.result === null || typeof res.result !== "object" ? null : res.result.source;
70222
+ if (res.error && !isMissingWorkflowBodyError(res.error)) {
70223
+ throw new Error(
70224
+ `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.`
70225
+ );
70226
+ }
70227
+ const source = res.result === null || typeof res.result !== "object" ? null : res.result.source;
70187
70228
  if (typeof source !== "string" || source.trim() === "") {
70188
70229
  console.error(
70189
- `\u26A0 Skipped src/workflows/${alias}.ts \u2014 the server returned no readable source (${res.error ?? "legacy workflow with no rendered body"}).`
70230
+ `\u26A0 Skipped src/workflows/${alias}.ts \u2014 legacy workflow with no rendered body.`
70190
70231
  );
70191
70232
  continue;
70192
70233
  }
@@ -70216,7 +70257,7 @@ async function fetchWorkflowGlobals(client, projectDir, app_id, alias, declarati
70216
70257
  console.error(
70217
70258
  `\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
70259
  );
70219
- return { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX };
70260
+ return { prefix: WORKFLOW_ENVELOPE_PREFIX, suffix: WORKFLOW_ENVELOPE_SUFFIX };
70220
70261
  }
70221
70262
  }
70222
70263
  async function fetchWorkflowDts(client, app_id, alias, declaration) {
@@ -70684,7 +70725,16 @@ function readAppSourceText(projectDir) {
70684
70725
  async function appDeploy(client, args) {
70685
70726
  const projectDir = path5.resolve(args.projectDir ?? process.cwd());
70686
70727
  const meta3 = readAppMeta(projectDir);
70687
- const undeclared = undeclaredCapabilities(readAppSourceText(projectDir), meta3.capabilities);
70728
+ const sourceText = readAppSourceText(projectDir);
70729
+ const called = calledAppAliases(sourceText);
70730
+ if (called.dynamic.length > 0) {
70731
+ console.error(
70732
+ `
70733
+ \u26A0 This app calls ${called.dynamic.join(" / ")} with an alias it computes at runtime, which this deploy cannot read.
70734
+ The version records only the aliases named as literals, so removing a binding this app reaches dynamically will NOT be refused.`
70735
+ );
70736
+ }
70737
+ const undeclared = undeclaredCapabilities(sourceText, meta3.capabilities);
70688
70738
  if (undeclared.length > 0) {
70689
70739
  const block = JSON.stringify(Object.fromEntries(undeclared.map((c) => [c, true])));
70690
70740
  console.error(
@@ -70738,12 +70788,14 @@ async function appDeploy(client, args) {
70738
70788
  // (fail-safe; the declaration is the grant).
70739
70789
  capabilities: meta3.capabilities ?? {},
70740
70790
  // 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 ?? {})
70791
+ // remove_app_workflow own apps.workflows. What IS sent is the set of
70792
+ // aliases this bundle CALLS (never the bindings), so remove_app_workflow
70793
+ // can refuse to unbind one the served version still uses. Read off the
70794
+ // source, the same rule the chat publish uses: the manifest's map is a
70795
+ // typing artifact maintained by habit, so its keys answered this question
70796
+ // with whatever the author last remembered to write. Stop calling an
70797
+ // alias + redeploy to lift the guard before removing its binding.
70798
+ workflow_aliases: called.workflows
70747
70799
  });
70748
70800
  writeAppMeta(projectDir, {
70749
70801
  ...meta3,
@@ -70755,7 +70807,7 @@ async function appDeploy(client, args) {
70755
70807
  try {
70756
70808
  const app = await client.getApp(meta3.app_id);
70757
70809
  warnIfUnbranded(app);
70758
- warnIfUnboundAliases(app, meta3.workflows ?? {}, meta3.agents ?? {});
70810
+ warnIfUnboundAliases(app, called);
70759
70811
  } catch (err2) {
70760
70812
  console.error(`(skipped post-deploy checks: ${err2.message})`);
70761
70813
  }
@@ -70786,14 +70838,14 @@ function warnIfUnbranded(app) {
70786
70838
  Find an icon: lotics run search_app_icons '{"query":"<word>"}'`
70787
70839
  );
70788
70840
  }
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 ?? {}) }
70793
- );
70841
+ function warnIfUnboundAliases(app, called) {
70842
+ const { workflows: unboundWorkflows, agents: unboundAgents } = unboundAliases(called, {
70843
+ workflows: Object.keys(app.workflows ?? {}),
70844
+ agents: Object.keys(app.agents ?? {})
70845
+ });
70794
70846
  if (unboundWorkflows.length === 0 && unboundAgents.length === 0) return;
70795
70847
  const lines = [
70796
- "\n\u26A0 The manifest declares aliases that are NOT bound on the server. Deploy ships code +",
70848
+ "\n\u26A0 This app CALLS aliases that are NOT bound on the server. Deploy ships code +",
70797
70849
  ' queries only \u2014 it does NOT bind workflows/agents, so the app will throw "has no \u2026 alias"',
70798
70850
  " the first time it calls them. Bind each one:"
70799
70851
  ];
@@ -71031,7 +71083,7 @@ async function appAgentSet(client, args) {
71031
71083
  );
71032
71084
  process.exit(1);
71033
71085
  }
71034
- const file2 = agentFilePath(projectDir, args.alias);
71086
+ const file2 = agentFilePath2(projectDir, args.alias);
71035
71087
  if (!fs4.existsSync(file2)) {
71036
71088
  console.error(
71037
71089
  `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 +71097,13 @@ async function appAgentSet(client, args) {
71045
71097
  );
71046
71098
  process.exit(1);
71047
71099
  }
71048
- const res = await client.setAppAgent(meta3.app_id, args.alias, { ...live, instructions });
71100
+ const res = await client.setAppAgent(meta3.app_id, args.alias, { instructions });
71049
71101
  if (res.error) {
71050
71102
  console.error(`Failed to set agent "${args.alias}": ${res.error}`);
71051
71103
  process.exit(1);
71052
71104
  }
71053
71105
  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.`
71106
+ `Set agent "${args.alias}" (${instructions.length} chars of instructions` + (live.model_id ? `, ${live.model_id}` : "") + `). Typed fields left as they are.`
71055
71107
  );
71056
71108
  }
71057
71109
  async function appWorkflowSet(client, args) {
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.100.0",
3
+ "version": "0.100.2",
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
  },