@lotics/cli 0.92.0 → 0.94.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/README.md CHANGED
@@ -217,6 +217,14 @@ lotics app workflow set issueInvoice # push the edited src/workflows/issu
217
217
  # authoritative, so the next `app deploy` re-syncs it — keep the manifest current.
218
218
  lotics app query set openInvoices # push package.json#lotics.queries.openInvoices
219
219
 
220
+ # Run a bound app agent end-to-end (no deployed UI needed — app row + declaration
221
+ # + member auth). Streams progress to stderr; reports the SETTLED run (structured
222
+ # output / final text) to stdout; exits 0 only when the run completed.
223
+ lotics app agent run app_abc recognize '{"image_file_id":"fil_..."}'
224
+ cat input.json | lotics app agent run app_abc recognize # inputs via stdin/@file
225
+ lotics app agent run app_abc recognize --json # full run summary to stdout
226
+ lotics app agent run app_abc recognize --session cli-123 '{}' # continue an existing thread
227
+
220
228
  # Dev-link @lotics/ui to packages/ui/src for live HMR (Vite alias; deploy bundles it)
221
229
  lotics ui link card # monorepo: packages/ui/src found automatically
222
230
  lotics ui link card --ui-src /abs/monorepo/packages/ui/src # external app (e.g. ~/lotics_apps)
package/dist/src/cli.js CHANGED
@@ -29586,6 +29586,7 @@ var require_lib3 = __commonJS({
29586
29586
  import dns from "node:dns";
29587
29587
  import net2 from "node:net";
29588
29588
  import fs9 from "node:fs";
29589
+ import os2 from "node:os";
29589
29590
  import path7 from "node:path";
29590
29591
  import readline from "node:readline";
29591
29592
 
@@ -30198,11 +30199,18 @@ var LoticsClient = class {
30198
30199
  * `async function __workflow(): …` wrapper the server compiles inside, so the
30199
30200
  * local typecheck mirrors the set-time verdict. Mirrors
30200
30201
  * POST /v1/apps/{app_id}/workflows/{alias}/dts.
30202
+ *
30203
+ * `declaration` (the manifest's `{ inputs?, outputs? }`) is posted as the body
30204
+ * `{ declaration }` ONLY when the alias isn't `set` on the server yet — the
30205
+ * server then synthesizes the dts from the declared schemas instead of 400ing
30206
+ * "no workflow alias". A registered alias needs no declaration (the server's
30207
+ * own bound contract wins), so the field is omitted in that case.
30201
30208
  */
30202
- async getAppWorkflowDts(app_id, alias) {
30209
+ async getAppWorkflowDts(app_id, alias, declaration) {
30203
30210
  return this.request(
30204
30211
  "POST",
30205
- `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/dts`
30212
+ `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/dts`,
30213
+ declaration ? { declaration } : void 0
30206
30214
  );
30207
30215
  }
30208
30216
  /**
@@ -30224,6 +30232,37 @@ var LoticsClient = class {
30224
30232
  if (!res.ok) await this.throwResponseError(res);
30225
30233
  return res;
30226
30234
  }
30235
+ /**
30236
+ * Continue a PARKED (`awaiting_input`) agent run with the user's answer to its
30237
+ * pending `ask_user_choice` — returns the RAW streamed continuation `Response`,
30238
+ * exactly like `appAgentRunStream`. Mirrors
30239
+ * POST /v1/apps/{app_id}/agent-runs/{run_id}/continue.
30240
+ */
30241
+ async appAgentRunContinueStream(app_id, run_id, body, signal) {
30242
+ const res = await fetch(
30243
+ `${this.baseUrl}/v1/apps/${encodeURIComponent(app_id)}/agent-runs/${encodeURIComponent(run_id)}/continue`,
30244
+ {
30245
+ method: "POST",
30246
+ headers: { ...this.buildHeaders(), "Content-Type": "application/json" },
30247
+ body: JSON.stringify(body),
30248
+ signal
30249
+ }
30250
+ );
30251
+ if (!res.ok) await this.throwResponseError(res);
30252
+ return res;
30253
+ }
30254
+ /**
30255
+ * A session's app-agent run history, oldest-first (the run just started is the
30256
+ * last, and its exact id is on the stream response's `x-app-agent-run-id`
30257
+ * header). Transcript excluded; structured `output`/`input` included. Mirrors
30258
+ * GET /v1/apps/{app_id}/agent-runs.
30259
+ */
30260
+ async listAgentRuns(app_id, session_id) {
30261
+ return this.request(
30262
+ "GET",
30263
+ `/v1/apps/${encodeURIComponent(app_id)}/agent-runs?session_id=${encodeURIComponent(session_id)}`
30264
+ );
30265
+ }
30227
30266
  /**
30228
30267
  * Mint a presigned URL for uploading a file into an app. Mirrors
30229
30268
  * POST /v1/apps/{app_id}/files/upload-url.
@@ -30555,6 +30594,16 @@ function upsertProfile(orgId, fields) {
30555
30594
  latest_version: config2.latest_version
30556
30595
  });
30557
30596
  }
30597
+ function clearProfileWorkspace(orgId) {
30598
+ const config2 = loadGlobalConfig();
30599
+ const existing = config2?.profiles?.[orgId];
30600
+ if (!existing) return;
30601
+ const { workspace_id: _dropped, ...rest } = existing;
30602
+ saveGlobalConfig({
30603
+ ...config2,
30604
+ profiles: { ...config2.profiles, [orgId]: { ...rest } }
30605
+ });
30606
+ }
30558
30607
  function removeProfile(orgId) {
30559
30608
  const config2 = loadGlobalConfig();
30560
30609
  if (!config2?.profiles?.[orgId]) return;
@@ -30653,6 +30702,7 @@ var VERSION = pkg.version;
30653
30702
  import fs4 from "node:fs";
30654
30703
  import path5 from "node:path";
30655
30704
  import { spawn as spawn2 } from "node:child_process";
30705
+ import { randomUUID } from "node:crypto";
30656
30706
  import { tmpdir } from "node:os";
30657
30707
 
30658
30708
  // src/starter_template.ts
@@ -31778,14 +31828,14 @@ function buildWrapperPage(args) {
31778
31828
  // protocol expects (stream-chunk* -> stream-end). Mirrors the production
31779
31829
  // host in frontend/features/app_ui/app_iframe_host.tsx.
31780
31830
  const streamingRuns = {};
31781
- async function handleAgentRun(id, payload) {
31831
+ async function handleAgentRun(id, op, payload) {
31782
31832
  const controller = new AbortController();
31783
31833
  streamingRuns[id] = controller;
31784
31834
  try {
31785
31835
  const res = await fetch("/_agent_run", {
31786
31836
  method: "POST",
31787
31837
  headers: { "content-type": "application/json" },
31788
- body: JSON.stringify({ app_id: APP_ID, op: "agentRun", payload: payload }),
31838
+ body: JSON.stringify({ app_id: APP_ID, op: op, payload: payload }),
31789
31839
  signal: controller.signal,
31790
31840
  });
31791
31841
  if (!res.ok || !res.body) {
@@ -31793,6 +31843,9 @@ function buildWrapperPage(args) {
31793
31843
  try { detail = JSON.parse(detail).message || detail; } catch (_) {}
31794
31844
  throw new Error(detail || ("HTTP " + res.status));
31795
31845
  }
31846
+ // Forward the run id (poll-recovery / cancel / continue key on it).
31847
+ const runId = res.headers.get("x-app-agent-run-id");
31848
+ if (runId) iframe.contentWindow.postMessage({ id: id, type: "run-id", runId: runId }, VITE_ORIGIN);
31796
31849
  const reader = res.body.getReader();
31797
31850
  const decoder = new TextDecoder();
31798
31851
  for (;;) {
@@ -31826,7 +31879,7 @@ function buildWrapperPage(args) {
31826
31879
  }
31827
31880
  if (typeof msg.op !== "string") return;
31828
31881
  // Streaming agent run \u2014 many messages back, not a single result.
31829
- if (msg.op === "agentRun") { handleAgentRun(msg.id, msg.payload); return; }
31882
+ if (msg.op === "agentRun" || msg.op === "agentRunContinue") { handleAgentRun(msg.id, msg.op, msg.payload); return; }
31830
31883
  const startedAt = performance.now();
31831
31884
  try {
31832
31885
  const data = msg.op === "upload"
@@ -32065,18 +32118,37 @@ async function startDevServer(args) {
32065
32118
  });
32066
32119
  try {
32067
32120
  const body = await readJson(req);
32068
- const p = body.payload ?? {};
32069
- if (typeof p.alias !== "string" || typeof p.session_id !== "string") {
32070
- throw new Error("agentRun payload must include `alias` and `session_id`");
32071
- }
32072
- const upstream = await args.client.appAgentRunStream(
32073
- body.app_id,
32074
- p.alias,
32075
- { session_id: p.session_id, input: p.input ?? {} },
32076
- ac.signal
32077
- );
32121
+ let upstream;
32122
+ if (body.op === "agentRunContinue") {
32123
+ const p = body.payload ?? {};
32124
+ if (typeof p.run_id !== "string" || typeof p.tool_call_id !== "string" || typeof p.output !== "object" || p.output === null) {
32125
+ throw new Error("agentRunContinue payload must include `run_id`, `tool_call_id`, and `output`");
32126
+ }
32127
+ upstream = await args.client.appAgentRunContinueStream(
32128
+ body.app_id,
32129
+ p.run_id,
32130
+ { tool_call_id: p.tool_call_id, output: p.output },
32131
+ ac.signal
32132
+ );
32133
+ } else {
32134
+ const p = body.payload ?? {};
32135
+ if (typeof p.alias !== "string" || typeof p.session_id !== "string") {
32136
+ throw new Error("agentRun payload must include `alias` and `session_id`");
32137
+ }
32138
+ upstream = await args.client.appAgentRunStream(
32139
+ body.app_id,
32140
+ p.alias,
32141
+ { session_id: p.session_id, input: p.input ?? {} },
32142
+ ac.signal
32143
+ );
32144
+ }
32078
32145
  if (!upstream.body) throw new Error("agent run returned no stream body");
32079
- res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform" });
32146
+ const runId = upstream.headers.get("x-app-agent-run-id");
32147
+ res.writeHead(200, {
32148
+ "Content-Type": "text/event-stream",
32149
+ "Cache-Control": "no-cache, no-transform",
32150
+ ...runId ? { "x-app-agent-run-id": runId } : {}
32151
+ });
32080
32152
  const reader = upstream.body.getReader();
32081
32153
  for (; ; ) {
32082
32154
  const { value, done } = await reader.read();
@@ -32932,6 +33004,12 @@ var LOTICS_INCLUDE_GLOB = ".lotics/**/*";
32932
33004
  var STALE_LOTICS_INCLUDES = /* @__PURE__ */ new Set([".lotics", "./.lotics", ".lotics/"]);
32933
33005
  var FALLBACK_ENVELOPE_PREFIX = "async function __workflow(): Promise<__WorkflowReturn | void> {\n";
32934
33006
  var FALLBACK_ENVELOPE_SUFFIX = "\n}";
33007
+ function toWorkflowDtsDeclaration(d) {
33008
+ return { inputs: d.inputs, outputs: d.outputs };
33009
+ }
33010
+ function isUnknownWorkflowAliasError(err2) {
33011
+ return err2 instanceof Error && /^400:/.test(err2.message) && /has no workflow alias/i.test(err2.message);
33012
+ }
32935
33013
  function workflowFileHeader(alias) {
32936
33014
  const refPath = path5.join("..", "..", WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`).split(path5.sep).join("/");
32937
33015
  return `/// <reference path="${refPath}" />
@@ -32992,9 +33070,9 @@ function stripWorkflowHeader(content) {
32992
33070
  }
32993
33071
  return lines.slice(start + 1).join("\n").replace(/\s+$/, "");
32994
33072
  }
32995
- async function writeWorkflowFiles(client, projectDir, app_id, aliases) {
33073
+ async function writeWorkflowFiles(client, projectDir, app_id, workflows) {
32996
33074
  const written = [];
32997
- for (const alias of aliases) {
33075
+ for (const [alias, declaration] of Object.entries(workflows)) {
32998
33076
  const res = await client.getAppWorkflow(app_id, alias);
32999
33077
  const source = res.error || res.result === null || typeof res.result !== "object" ? null : res.result.source;
33000
33078
  if (typeof source !== "string" || source.trim() === "") {
@@ -33003,15 +33081,26 @@ async function writeWorkflowFiles(client, projectDir, app_id, aliases) {
33003
33081
  );
33004
33082
  continue;
33005
33083
  }
33006
- const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias);
33084
+ const envelope = await fetchWorkflowGlobals(
33085
+ client,
33086
+ projectDir,
33087
+ app_id,
33088
+ alias,
33089
+ toWorkflowDtsDeclaration(declaration)
33090
+ );
33007
33091
  writeWorkflowFile(projectDir, alias, source, envelope);
33008
33092
  written.push(alias);
33009
33093
  }
33010
33094
  return written;
33011
33095
  }
33012
- async function fetchWorkflowGlobals(client, projectDir, app_id, alias) {
33096
+ async function fetchWorkflowGlobals(client, projectDir, app_id, alias, declaration) {
33013
33097
  try {
33014
- const { dts, envelope_prefix, envelope_suffix } = await client.getAppWorkflowDts(app_id, alias);
33098
+ const { dts, envelope_prefix, envelope_suffix } = await fetchWorkflowDts(
33099
+ client,
33100
+ app_id,
33101
+ alias,
33102
+ declaration
33103
+ );
33015
33104
  writeWorkflowGlobals(projectDir, alias, dts);
33016
33105
  return { prefix: envelope_prefix, suffix: envelope_suffix };
33017
33106
  } catch (err2) {
@@ -33021,6 +33110,16 @@ async function fetchWorkflowGlobals(client, projectDir, app_id, alias) {
33021
33110
  return { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX };
33022
33111
  }
33023
33112
  }
33113
+ async function fetchWorkflowDts(client, app_id, alias, declaration) {
33114
+ try {
33115
+ return await client.getAppWorkflowDts(app_id, alias);
33116
+ } catch (err2) {
33117
+ if (declaration && isUnknownWorkflowAliasError(err2)) {
33118
+ return client.getAppWorkflowDts(app_id, alias, declaration);
33119
+ }
33120
+ throw err2;
33121
+ }
33122
+ }
33024
33123
  function runTar(args, cwd) {
33025
33124
  return new Promise((resolve2, reject2) => {
33026
33125
  const proc = spawn2("tar", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
@@ -33067,10 +33166,37 @@ function readAppMeta(projectDir) {
33067
33166
  capabilities: pkg2.lotics.capabilities
33068
33167
  };
33069
33168
  }
33169
+ var APP_META_KEYS = Object.keys({
33170
+ app_id: true,
33171
+ workspace_id: true,
33172
+ current_version_id: true,
33173
+ version_number: true,
33174
+ workflows: true,
33175
+ queries: true,
33176
+ agents: true,
33177
+ capabilities: true
33178
+ });
33070
33179
  function writeAppMeta(projectDir, meta3) {
33071
33180
  const pkgPath2 = path5.join(projectDir, "package.json");
33072
33181
  const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
33073
- pkg2.lotics = meta3;
33182
+ const existing = pkg2.lotics && typeof pkg2.lotics === "object" ? pkg2.lotics : {};
33183
+ const preserved = {};
33184
+ for (const [key, value] of Object.entries(existing)) {
33185
+ if (!APP_META_KEYS.includes(key)) preserved[key] = value;
33186
+ }
33187
+ pkg2.lotics = { ...meta3, ...preserved };
33188
+ fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
33189
+ }
33190
+ function writeWorkflowOutputs(projectDir, alias, outputs) {
33191
+ const pkgPath2 = path5.join(projectDir, "package.json");
33192
+ const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
33193
+ const declaration = pkg2.lotics?.workflows?.[alias];
33194
+ if (!declaration) {
33195
+ throw new Error(
33196
+ `package.json#lotics.workflows.${alias} disappeared before the derived-outputs write-back.`
33197
+ );
33198
+ }
33199
+ declaration.outputs = outputs;
33074
33200
  fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
33075
33201
  }
33076
33202
  function ensureAppTsconfig(projectDir) {
@@ -33205,19 +33331,29 @@ async function appCodegen(args) {
33205
33331
  `\u26A0 Could not regenerate .lotics/app_fields.ts (${err2 instanceof Error ? err2.message : String(err2)}). Kept the existing file.`
33206
33332
  );
33207
33333
  }
33208
- await refreshWorkflowGlobals(args.client, projectDir, meta3.app_id, Object.keys(meta3.workflows ?? {}));
33334
+ await refreshWorkflowGlobals(args.client, projectDir, meta3.app_id, meta3.workflows ?? {});
33209
33335
  }
33210
- async function refreshWorkflowGlobals(client, projectDir, app_id, aliases) {
33211
- for (const alias of aliases) {
33212
- const file2 = workflowFilePath(projectDir, alias);
33213
- if (!fs4.existsSync(file2)) continue;
33214
- const body = stripWorkflowHeader(fs4.readFileSync(file2, "utf-8"));
33215
- if (body.trim() === "") continue;
33216
- const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias);
33217
- writeWorkflowFile(projectDir, alias, body, envelope);
33218
- console.error(`Refreshed ${path5.relative(projectDir, file2)} + its workflow types`);
33336
+ async function refreshWorkflowGlobals(client, projectDir, app_id, workflows) {
33337
+ for (const [alias, declaration] of Object.entries(workflows)) {
33338
+ const refreshed = await refreshWorkflowTypes(
33339
+ client,
33340
+ projectDir,
33341
+ app_id,
33342
+ alias,
33343
+ toWorkflowDtsDeclaration(declaration)
33344
+ );
33345
+ if (refreshed) console.error(`Refreshed workflow types for ${alias}`);
33219
33346
  }
33220
33347
  }
33348
+ async function refreshWorkflowTypes(client, projectDir, app_id, alias, declaration) {
33349
+ const file2 = workflowFilePath(projectDir, alias);
33350
+ if (!fs4.existsSync(file2)) return false;
33351
+ const body = stripWorkflowHeader(fs4.readFileSync(file2, "utf-8"));
33352
+ if (body.trim() === "") return false;
33353
+ const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias, declaration);
33354
+ writeWorkflowFile(projectDir, alias, body, envelope);
33355
+ return true;
33356
+ }
33221
33357
  function stampPulledManifest(projectDir, args) {
33222
33358
  writeAppMeta(projectDir, {
33223
33359
  app_id: args.app_id,
@@ -33380,9 +33516,9 @@ async function appPull(client, args) {
33380
33516
  queries: app.queries ?? {},
33381
33517
  agents: app.agents ?? {}
33382
33518
  });
33383
- const aliases = Object.keys(app.workflows ?? {});
33384
- if (aliases.length > 0) {
33385
- const written = await writeWorkflowFiles(client, targetPath, app.id, aliases);
33519
+ const workflows = app.workflows ?? {};
33520
+ if (Object.keys(workflows).length > 0) {
33521
+ const written = await writeWorkflowFiles(client, targetPath, app.id, workflows);
33386
33522
  if (written.length > 0) {
33387
33523
  console.error(
33388
33524
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
@@ -33660,6 +33796,96 @@ async function appExecuteWorkflow(client, args) {
33660
33796
  }
33661
33797
  if (status === "error" || cleanupFailed) process.exit(1);
33662
33798
  }
33799
+ async function streamAgentTextDeltas(body, onText) {
33800
+ const reader = body.getReader();
33801
+ const decoder = new TextDecoder();
33802
+ let buffer = "";
33803
+ let accumulated = "";
33804
+ try {
33805
+ for (; ; ) {
33806
+ const { value, done } = await reader.read();
33807
+ if (done) break;
33808
+ buffer += decoder.decode(value, { stream: true });
33809
+ const frames = buffer.split("\n\n");
33810
+ buffer = frames.pop() ?? "";
33811
+ for (const frame of frames) {
33812
+ for (const line of frame.split("\n")) {
33813
+ if (!line.startsWith("data:")) continue;
33814
+ const payload = line.slice(5).trim();
33815
+ if (!payload || payload === "[DONE]") continue;
33816
+ let chunk;
33817
+ try {
33818
+ chunk = JSON.parse(payload);
33819
+ } catch {
33820
+ continue;
33821
+ }
33822
+ if (chunk.type === "text-delta" && chunk.delta) {
33823
+ accumulated += chunk.delta;
33824
+ onText(chunk.delta);
33825
+ }
33826
+ }
33827
+ }
33828
+ }
33829
+ } catch {
33830
+ } finally {
33831
+ reader.releaseLock();
33832
+ }
33833
+ return accumulated;
33834
+ }
33835
+ var AGENT_RUN_POLL = {
33836
+ intervalMs: 1e3,
33837
+ settleTimeoutMs: 21 * 60 * 1e3,
33838
+ existenceTimeoutMs: 5e3
33839
+ };
33840
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
33841
+ async function fetchSettledAgentRun(client, appId, sessionId, runId, timing) {
33842
+ const settleDeadline = Date.now() + timing.settleTimeoutMs;
33843
+ const existenceDeadline = Date.now() + timing.existenceTimeoutMs;
33844
+ for (; ; ) {
33845
+ const { runs } = await client.listAgentRuns(appId, sessionId);
33846
+ const target = runId ? runs.find((r) => r.id === runId) : runs[runs.length - 1];
33847
+ if (target) {
33848
+ if (target.status !== "running" || Date.now() >= settleDeadline) return target;
33849
+ } else if (Date.now() >= existenceDeadline) {
33850
+ return void 0;
33851
+ }
33852
+ await sleep(timing.intervalMs);
33853
+ }
33854
+ }
33855
+ async function appAgentRun(client, args, timing = AGENT_RUN_POLL) {
33856
+ const sessionId = args.sessionId ?? `cli-${randomUUID()}`;
33857
+ const continuing = args.sessionId !== void 0;
33858
+ const res = await client.appAgentRunStream(args.app_id, args.alias, {
33859
+ session_id: sessionId,
33860
+ input: args.input
33861
+ });
33862
+ const runId = res.headers.get("x-app-agent-run-id") ?? void 0;
33863
+ if (res.body) {
33864
+ await streamAgentTextDeltas(res.body, (piece) => process.stderr.write(piece));
33865
+ }
33866
+ const run = await fetchSettledAgentRun(client, args.app_id, sessionId, runId, timing);
33867
+ if (!run) {
33868
+ console.error(`
33869
+ Could not find the settled run for session ${sessionId} on ${args.app_id}.`);
33870
+ console.error("The run may still be in progress \u2014 re-check with `lotics run` against the app's agent-runs.");
33871
+ process.exit(1);
33872
+ }
33873
+ if (args.json) {
33874
+ console.log(JSON.stringify(run, null, 2));
33875
+ } else if (run.output !== null && typeof run.output === "object") {
33876
+ console.log(JSON.stringify(run.output, null, 2));
33877
+ } else if (typeof run.output === "string") {
33878
+ console.log(run.output);
33879
+ }
33880
+ console.error(
33881
+ `
33882
+ Agent "${args.alias}" run ${run.id} \u2192 ${run.status}${run.error_message ? `: ${run.error_message}` : ""}`
33883
+ );
33884
+ console.error(
33885
+ continuing ? `Session: ${sessionId}` : `Session: ${sessionId} (fresh \u2014 pass --session ${sessionId} to continue this thread)`
33886
+ );
33887
+ if (run.status !== "completed") process.exit(1);
33888
+ }
33663
33889
  async function appWorkflowSet(client, args) {
33664
33890
  const projectDir = process.cwd();
33665
33891
  const meta3 = readAppMeta(projectDir);
@@ -33694,8 +33920,19 @@ async function appWorkflowSet(client, args) {
33694
33920
  const result = res.result ?? {};
33695
33921
  const workflowId = typeof result.workflow_id === "string" ? result.workflow_id : "(unknown)";
33696
33922
  console.error(`Set workflow "${args.alias}" \u2192 ${workflowId}`);
33697
- if (result.outputs && typeof result.outputs === "object") {
33698
- console.error(` result.data schema: ${JSON.stringify(result.outputs)}`);
33923
+ const derivedOutputs = result.outputs !== null && typeof result.outputs === "object" && !Array.isArray(result.outputs) ? result.outputs : null;
33924
+ if (!declaration.outputs && derivedOutputs && Object.keys(derivedOutputs).length > 0) {
33925
+ writeWorkflowOutputs(projectDir, args.alias, derivedOutputs);
33926
+ console.error(
33927
+ ` Wrote the derived result.data schema into package.json#lotics.workflows.${args.alias}.outputs.`
33928
+ );
33929
+ const refreshed = await refreshWorkflowTypes(client, projectDir, meta3.app_id, args.alias, {
33930
+ inputs: declaration.inputs,
33931
+ outputs: derivedOutputs
33932
+ });
33933
+ if (refreshed) console.error(` Refreshed workflow types for ${args.alias}.`);
33934
+ } else if (derivedOutputs) {
33935
+ console.error(` result.data schema: ${JSON.stringify(derivedOutputs)}`);
33699
33936
  }
33700
33937
  }
33701
33938
  async function appQuerySet(client, args) {
@@ -33721,12 +33958,12 @@ async function appWorkflowPull(client) {
33721
33958
  const projectDir = process.cwd();
33722
33959
  const meta3 = readAppMeta(projectDir);
33723
33960
  const app = await client.getApp(meta3.app_id);
33724
- const aliases = Object.keys(app.workflows ?? {});
33725
- if (aliases.length === 0) {
33961
+ const workflows = app.workflows ?? {};
33962
+ if (Object.keys(workflows).length === 0) {
33726
33963
  console.error(`App ${meta3.app_id} has no bound workflows.`);
33727
33964
  return;
33728
33965
  }
33729
- const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, aliases);
33966
+ const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, workflows);
33730
33967
  console.error(
33731
33968
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
33732
33969
  );
@@ -33891,6 +34128,7 @@ function parseArgs(argv) {
33891
34128
  content: void 0,
33892
34129
  timezone: void 0,
33893
34130
  message: void 0,
34131
+ session: void 0,
33894
34132
  local: false,
33895
34133
  all: false,
33896
34134
  yes: false,
@@ -33952,6 +34190,9 @@ function parseArgs(argv) {
33952
34190
  case "--message":
33953
34191
  flags.message = argv[++i2];
33954
34192
  break;
34193
+ case "--session":
34194
+ flags.session = argv[++i2];
34195
+ break;
33955
34196
  case "--local":
33956
34197
  flags.local = true;
33957
34198
  break;
@@ -34022,6 +34263,36 @@ async function ingestJsonArgs(opts) {
34022
34263
  }
34023
34264
  }
34024
34265
 
34266
+ // src/org_commands.ts
34267
+ function printWorkspaceList(workspaces, currentId) {
34268
+ for (const ws of workspaces) {
34269
+ const marker = ws.id === currentId ? " (current)" : "";
34270
+ console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
34271
+ }
34272
+ }
34273
+ async function validateOrgWorkspacePin(client, orgId, profile) {
34274
+ const pinned = profile.workspace_id;
34275
+ if (!pinned) return;
34276
+ let workspaces;
34277
+ try {
34278
+ workspaces = await client.listWorkspaces();
34279
+ } catch (error51) {
34280
+ const message = error51 instanceof Error ? error51.message : String(error51);
34281
+ console.error(`Could not validate the pinned workspace (${message}) \u2014 keeping ${pinned}.`);
34282
+ return;
34283
+ }
34284
+ if (workspaces.some((w) => w.id === pinned)) return;
34285
+ clearProfileWorkspace(orgId);
34286
+ console.error(`Pinned workspace ${pinned} is no longer in ${profile.org_name} \u2014 cleared the stale pin.`);
34287
+ if (workspaces.length === 0) {
34288
+ console.error("This organization has no workspaces yet.");
34289
+ return;
34290
+ }
34291
+ console.error("Select one with:\n");
34292
+ console.error(" lotics workspace select <id>\n");
34293
+ printWorkspaceList(workspaces);
34294
+ }
34295
+
34025
34296
  // src/xlsx.ts
34026
34297
  import fs6 from "node:fs";
34027
34298
 
@@ -47171,13 +47442,50 @@ function unmergeCellsCommand(workbook, sheetIndex, mergeRef) {
47171
47442
  }
47172
47443
  };
47173
47444
  }
47445
+ function isInertCell(cell) {
47446
+ return (cell.value === null || cell.value === "") && cell.formula === void 0 && cell.styleIndex === 0 && (cell.originalXfIndex === void 0 || cell.originalXfIndex === 0) && (cell.numFmtCode === "General" || cell.numFmtCode === "") && cell.richText === void 0 && cell.error === void 0 && cell.content.type === "plain" && cell.content.text === "";
47447
+ }
47448
+ function clampInertCells(sheet) {
47449
+ let minRow = Infinity;
47450
+ let maxRow = 0;
47451
+ let minCol = Infinity;
47452
+ let maxCol = 0;
47453
+ let hasMeaningful = false;
47454
+ for (const [ref, cell] of sheet.cells) {
47455
+ if (isInertCell(cell)) continue;
47456
+ const rc = refToRowColSafe(ref);
47457
+ if (!rc) continue;
47458
+ hasMeaningful = true;
47459
+ if (rc.row < minRow) minRow = rc.row;
47460
+ if (rc.row > maxRow) maxRow = rc.row;
47461
+ if (rc.col < minCol) minCol = rc.col;
47462
+ if (rc.col > maxCol) maxCol = rc.col;
47463
+ }
47464
+ const pruned = /* @__PURE__ */ new Map();
47465
+ for (const [ref, cell] of sheet.cells) {
47466
+ if (!isInertCell(cell)) continue;
47467
+ if (!hasMeaningful) {
47468
+ pruned.set(ref, cell);
47469
+ continue;
47470
+ }
47471
+ const rc = refToRowColSafe(ref);
47472
+ if (!rc) continue;
47473
+ if (rc.row < minRow || rc.row > maxRow || rc.col < minCol || rc.col > maxCol) {
47474
+ pruned.set(ref, cell);
47475
+ }
47476
+ }
47477
+ for (const ref of pruned.keys()) sheet.cells.delete(ref);
47478
+ return pruned;
47479
+ }
47174
47480
  function insertRowsCommand(workbook, sheetIndex, at, count) {
47175
47481
  const sheet = workbook.sheets[sheetIndex];
47176
47482
  let mergeSnapshot = [];
47483
+ let inertSnapshot = /* @__PURE__ */ new Map();
47177
47484
  return {
47178
47485
  description: `Insert ${count} row${count > 1 ? "s" : ""} at ${at}`,
47179
47486
  execute() {
47180
47487
  mergeSnapshot = [...sheet.mergedCells];
47488
+ inertSnapshot = clampInertCells(sheet);
47181
47489
  const toMove = [];
47182
47490
  for (const [ref] of sheet.cells) {
47183
47491
  const rc = refToRowColSafe(ref);
@@ -47221,6 +47529,7 @@ function insertRowsCommand(workbook, sheetIndex, at, count) {
47221
47529
  sheet.rowHeights.set(r - count, h);
47222
47530
  }
47223
47531
  for (let r = at; r < at + count; r++) sheet.rowHeights.delete(r);
47532
+ for (const [ref, cell] of inertSnapshot) sheet.cells.set(ref, cell);
47224
47533
  workbook.emit({ type: "rows_deleted", sheet: sheetIndex, at, count });
47225
47534
  }
47226
47535
  };
@@ -47235,10 +47544,12 @@ function deleteRowsCommand(workbook, sheetIndex, at, count) {
47235
47544
  }
47236
47545
  }
47237
47546
  let mergeSnapshot = [];
47547
+ let inertSnapshot = /* @__PURE__ */ new Map();
47238
47548
  return {
47239
47549
  description: `Delete ${count} row${count > 1 ? "s" : ""} at ${at}`,
47240
47550
  execute() {
47241
47551
  mergeSnapshot = [...sheet.mergedCells];
47552
+ inertSnapshot = clampInertCells(sheet);
47242
47553
  for (const ref of snapshot.keys()) sheet.cells.delete(ref);
47243
47554
  const toMove = [];
47244
47555
  for (const [ref] of sheet.cells) {
@@ -47271,6 +47582,7 @@ function deleteRowsCommand(workbook, sheetIndex, at, count) {
47271
47582
  for (const [ref, snap] of snapshot) {
47272
47583
  sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
47273
47584
  }
47585
+ for (const [ref, cell] of inertSnapshot) sheet.cells.set(ref, cell);
47274
47586
  workbook.emit({ type: "rows_inserted", sheet: sheetIndex, at, count });
47275
47587
  }
47276
47588
  };
@@ -47278,10 +47590,12 @@ function deleteRowsCommand(workbook, sheetIndex, at, count) {
47278
47590
  function insertColsCommand(workbook, sheetIndex, at, count) {
47279
47591
  const sheet = workbook.sheets[sheetIndex];
47280
47592
  let mergeSnapshot = [];
47593
+ let inertSnapshot = /* @__PURE__ */ new Map();
47281
47594
  return {
47282
47595
  description: `Insert ${count} column${count > 1 ? "s" : ""} at ${at}`,
47283
47596
  execute() {
47284
47597
  mergeSnapshot = [...sheet.mergedCells];
47598
+ inertSnapshot = clampInertCells(sheet);
47285
47599
  const toMove = [];
47286
47600
  for (const [ref] of sheet.cells) {
47287
47601
  const rc = refToRowColSafe(ref);
@@ -47325,6 +47639,7 @@ function insertColsCommand(workbook, sheetIndex, at, count) {
47325
47639
  sheet.colWidths.set(c - count, w);
47326
47640
  }
47327
47641
  for (let c = at; c < at + count; c++) sheet.colWidths.delete(c);
47642
+ for (const [ref, cell] of inertSnapshot) sheet.cells.set(ref, cell);
47328
47643
  workbook.emit({ type: "cols_deleted", sheet: sheetIndex, at, count });
47329
47644
  }
47330
47645
  };
@@ -47339,10 +47654,12 @@ function deleteColsCommand(workbook, sheetIndex, at, count) {
47339
47654
  }
47340
47655
  }
47341
47656
  let mergeSnapshot = [];
47657
+ let inertSnapshot = /* @__PURE__ */ new Map();
47342
47658
  return {
47343
47659
  description: `Delete ${count} column${count > 1 ? "s" : ""} at ${at}`,
47344
47660
  execute() {
47345
47661
  mergeSnapshot = [...sheet.mergedCells];
47662
+ inertSnapshot = clampInertCells(sheet);
47346
47663
  for (const ref of snapshot.keys()) sheet.cells.delete(ref);
47347
47664
  const toMove = [];
47348
47665
  for (const [ref] of sheet.cells) {
@@ -47375,6 +47692,7 @@ function deleteColsCommand(workbook, sheetIndex, at, count) {
47375
47692
  for (const [ref, snap] of snapshot) {
47376
47693
  sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
47377
47694
  }
47695
+ for (const [ref, cell] of inertSnapshot) sheet.cells.set(ref, cell);
47378
47696
  workbook.emit({ type: "cols_inserted", sheet: sheetIndex, at, count });
47379
47697
  }
47380
47698
  };
@@ -47597,26 +47915,37 @@ function lazyEngine(workbook) {
47597
47915
  }
47598
47916
  };
47599
47917
  }
47600
- function readToJson(filePath) {
47918
+ function readToJson(filePath, filter2) {
47601
47919
  const buffer = fs6.readFileSync(filePath);
47602
47920
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
47603
47921
  const parsed = parseExcelBuffer(arrayBuffer);
47922
+ let sheets = parsed.sheets;
47923
+ if (filter2?.sheet !== void 0) {
47924
+ const match = sheets.filter((s) => s.name === filter2.sheet);
47925
+ if (match.length === 0) {
47926
+ const names = parsed.sheets.map((s) => `"${s.name}"`).join(", ");
47927
+ fail(`Sheet not found: "${filter2.sheet}". Available: ${names}`);
47928
+ }
47929
+ sheets = match;
47930
+ }
47604
47931
  return {
47605
47932
  activeSheetIndex: parsed.activeSheetIndex,
47606
- sheets: parsed.sheets.map((s) => ({
47933
+ sheets: sheets.map((s) => ({
47607
47934
  name: s.name,
47608
47935
  totalRowCount: s.totalRowCount,
47609
47936
  truncated: s.truncated,
47610
- cells: cellsFromParsedRows(s.rows),
47937
+ cells: cellsFromParsedRows(s.rows, filter2?.range),
47611
47938
  merges: s.mergedCells.map((m) => `${rowColToRef(m.startRow, m.startCol)}:${rowColToRef(m.endRow, m.endCol)}`),
47612
47939
  freeze: s.freezePane ? { row: s.freezePane.frozenRows, col: s.freezePane.frozenCols } : void 0
47613
47940
  }))
47614
47941
  };
47615
47942
  }
47616
- function cellsFromParsedRows(rows) {
47943
+ function cellsFromParsedRows(rows, range) {
47617
47944
  const out = {};
47618
47945
  for (const row of rows) {
47946
+ if (range && (row.index < range.startRow || row.index > range.endRow)) continue;
47619
47947
  for (const cell of row.cells) {
47948
+ if (range && (cell.column < range.startCol || cell.column > range.endCol)) continue;
47620
47949
  const ref = rowColToRef(row.index, cell.column);
47621
47950
  const value = cell.typedValue ?? cell.value;
47622
47951
  const entry = { value };
@@ -47688,9 +48017,34 @@ function valueToInput(value) {
47688
48017
  if (value === null || value === void 0) return "";
47689
48018
  return String(value);
47690
48019
  }
47691
- function xlsxRead(filePath) {
47692
- if (!filePath) fail("Usage: lotics xlsx read <file>");
47693
- const data = readToJson(filePath);
48020
+ function readOptionFlag(rest, name) {
48021
+ const i2 = rest.indexOf(name);
48022
+ if (i2 < 0) return void 0;
48023
+ const val = rest[i2 + 1];
48024
+ if (val === void 0 || val.startsWith("--")) fail(`Missing value for ${name}`);
48025
+ return val;
48026
+ }
48027
+ function splitOptionalSheetRef(spec) {
48028
+ const idx = spec.indexOf("!");
48029
+ if (idx < 0) return { sheet: void 0, ref: spec };
48030
+ return { sheet: spec.slice(0, idx), ref: spec.slice(idx + 1) };
48031
+ }
48032
+ function xlsxRead(filePath, rest) {
48033
+ if (!filePath) fail("Usage: lotics xlsx read <file> [--sheet <name>] [--range <sheet>!<A1:G60>]");
48034
+ const rangeArg = readOptionFlag(rest, "--range");
48035
+ const sheetArg = readOptionFlag(rest, "--sheet");
48036
+ let filter2;
48037
+ if (rangeArg !== void 0) {
48038
+ const { sheet, ref } = splitOptionalSheetRef(rangeArg);
48039
+ const targetSheet = sheet ?? sheetArg;
48040
+ if (targetSheet === void 0) {
48041
+ fail("--range without a <sheet>! prefix requires --sheet <name>");
48042
+ }
48043
+ filter2 = { sheet: targetSheet, range: parseRange2(ref) };
48044
+ } else if (sheetArg !== void 0) {
48045
+ filter2 = { sheet: sheetArg };
48046
+ }
48047
+ const data = readToJson(filePath, filter2);
47694
48048
  console.log(JSON.stringify(data, null, 2));
47695
48049
  }
47696
48050
  function xlsxWrite(filePath, json2) {
@@ -47927,7 +48281,10 @@ function printXlsxHelp() {
47927
48281
  console.error(`Lotics xlsx commands \u2014 manipulate .xlsx files in place.
47928
48282
  Uses Lotics' own xlsx engine; round-trips faithfully with the Lotics editor and templates.
47929
48283
 
47930
- lotics xlsx read <file> Dump file as JSON (sheets, cells, merges, freeze)
48284
+ lotics xlsx read <file> [--sheet <name>] [--range <sheet>!<A1:G60>]
48285
+ Dump file as JSON (sheets, cells, merges, freeze).
48286
+ --sheet limits to one sheet; --range to a cell window
48287
+ (its <sheet>! prefix is optional when --sheet is given)
47931
48288
  lotics xlsx write <file> '<json>' Create .xlsx from {"sheets":[{"name","cells":{"A1":...}}]}
47932
48289
  lotics xlsx set-cell <file> <sheet>!<ref> '<v>' Set cell value (prefix '=' for formula)
47933
48290
  lotics xlsx clear-range <file> <sheet>!<range> Clear all cells in a range
@@ -47949,7 +48306,7 @@ Edit ops mutate the file atomically (temp file + rename).`);
47949
48306
  async function runXlsxCommand(subcommand, toolArgs, restArgs) {
47950
48307
  switch (subcommand) {
47951
48308
  case "read":
47952
- return xlsxRead(toolArgs);
48309
+ return xlsxRead(toolArgs, restArgs);
47953
48310
  case "write":
47954
48311
  return xlsxWrite(toolArgs, restArgs[0]);
47955
48312
  case "set-cell":
@@ -67999,7 +68356,7 @@ import { readFileSync as readFileSync2, writeFileSync, existsSync, mkdtempSync,
67999
68356
  import { tmpdir as tmpdir2 } from "node:os";
68000
68357
  import { join, dirname, resolve, extname, basename } from "node:path";
68001
68358
  import { fileURLToPath as fileURLToPath2 } from "node:url";
68002
- import { setTimeout as sleep } from "node:timers/promises";
68359
+ import { setTimeout as sleep2 } from "node:timers/promises";
68003
68360
  var HERE = dirname(fileURLToPath2(import.meta.url));
68004
68361
  function fail2(msg) {
68005
68362
  console.error(msg);
@@ -68057,8 +68414,14 @@ async function cdpConnect(wsUrl) {
68057
68414
  });
68058
68415
  return { send, close: () => ws.close() };
68059
68416
  }
68417
+ function isStoredFileId(target) {
68418
+ return /^fil_[A-Za-z0-9]+$/.test(target);
68419
+ }
68420
+ function defaultPreviewOutputPath(filename, cwd) {
68421
+ return join(cwd, basename(filename, extname(filename)) + ".png");
68422
+ }
68060
68423
  async function runPreviewCommand(filePath, flags) {
68061
- if (!filePath) fail2("Usage: lotics preview <file.docx|.xlsx> [--out <file.png>]");
68424
+ if (!filePath) fail2("Usage: lotics preview <file.docx|.xlsx | fil_id> [-o <file.png>]");
68062
68425
  const abs = resolve(filePath);
68063
68426
  if (!existsSync(abs)) fail2(`File not found: ${abs}`);
68064
68427
  const ext = extname(abs).toLowerCase();
@@ -68125,7 +68488,7 @@ async function runPreviewCommand(filePath, flags) {
68125
68488
  const p = parseInt(readFileSync2(portFile, "utf8").split("\n")[0], 10);
68126
68489
  if (p) cdpPort = p;
68127
68490
  }
68128
- if (!cdpPort) await sleep(100);
68491
+ if (!cdpPort) await sleep2(100);
68129
68492
  }
68130
68493
  if (!cdpPort) throw new Error("Chrome did not expose a debugging port (launch failed?).");
68131
68494
  let target;
@@ -68135,7 +68498,7 @@ async function runPreviewCommand(filePath, flags) {
68135
68498
  target = list.find((t) => t.type === "page");
68136
68499
  } catch {
68137
68500
  }
68138
- if (!target?.webSocketDebuggerUrl) await sleep(100);
68501
+ if (!target?.webSocketDebuggerUrl) await sleep2(100);
68139
68502
  }
68140
68503
  if (!target?.webSocketDebuggerUrl) throw new Error("No Chrome page target available.");
68141
68504
  const cdp = await cdpConnect(target.webSocketDebuggerUrl);
@@ -68157,7 +68520,7 @@ async function runPreviewCommand(filePath, flags) {
68157
68520
  if (v.warnings?.length) warnings.push(...v.warnings);
68158
68521
  break;
68159
68522
  }
68160
- await sleep(75);
68523
+ await sleep2(75);
68161
68524
  }
68162
68525
  if (!done) throw new Error("Render timed out (page never signaled completion).");
68163
68526
  if (err2) throw new Error(`Render engine error: ${err2}`);
@@ -68260,6 +68623,10 @@ COMMANDS
68260
68623
  lotics app query set <alias> Push package.json#lotics.queries.<alias> to
68261
68624
  apps.queries via set_app_query (no deploy;
68262
68625
  re-synced by the next deploy from the manifest)
68626
+ lotics app agent run <app_id> <alias> '<json>' Run a bound app agent end-to-end
68627
+ (inputs: inline JSON, @file, or stdin; streams
68628
+ progress to stderr, reports the settled run;
68629
+ --session <id> continues a thread; --json)
68263
68630
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
68264
68631
  lotics app rename "<new name>" Rename the app's display name (launcher title)
68265
68632
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
@@ -68287,8 +68654,10 @@ COMMANDS
68287
68654
  lotics file download <file_id> Download a file by ID (alias: lotics download)
68288
68655
  lotics file download record <record_id> <field_key>
68289
68656
  Download all files on a record file field
68290
- lotics file preview <file> [-o png] Render a .docx/.xlsx to a PNG (frontend engines;
68291
- needs a Chrome/Chromium on the machine)
68657
+ lotics file preview <file|fil_id> [-o png]
68658
+ Render a .docx/.xlsx to a PNG \u2014 a local path OR a
68659
+ stored file id (downloaded first). Frontend engines;
68660
+ needs a Chrome/Chromium on the machine
68292
68661
 
68293
68662
  FLAGS
68294
68663
  --json Full JSON output (default is human-readable text)
@@ -68495,12 +68864,6 @@ var SOURCE_LABELS = {
68495
68864
  local_pointer: "local .lotics/config.json (pin)",
68496
68865
  global_profile: "global active profile"
68497
68866
  };
68498
- function printWorkspaceList(workspaces, currentId) {
68499
- for (const ws of workspaces) {
68500
- const marker = ws.id === currentId ? " (current)" : "";
68501
- console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
68502
- }
68503
- }
68504
68867
  async function resolveWorkspace(client, ctx) {
68505
68868
  if (ctx.workspaceId) {
68506
68869
  client.setWorkspaceId(ctx.workspaceId);
@@ -68654,6 +69017,18 @@ async function main() {
68654
69017
  return;
68655
69018
  }
68656
69019
  if (command === "preview") {
69020
+ if (subcommand && isStoredFileId(subcommand)) {
69021
+ const { client: client2 } = requireClient(flags);
69022
+ const tmpDir = fs9.mkdtempSync(path7.join(os2.tmpdir(), "lotics-preview-"));
69023
+ try {
69024
+ const { path: localPath, filename } = await client2.downloadFileById(subcommand, tmpDir);
69025
+ const output = flags.output ?? defaultPreviewOutputPath(filename, process.cwd());
69026
+ await runPreviewCommand(localPath, { output });
69027
+ } finally {
69028
+ fs9.rmSync(tmpDir, { recursive: true, force: true });
69029
+ }
69030
+ return;
69031
+ }
68657
69032
  await runPreviewCommand(subcommand, flags);
68658
69033
  return;
68659
69034
  }
@@ -68715,6 +69090,7 @@ async function main() {
68715
69090
  console.error("Note: a local pin (.lotics/config.json) overrides the global default in this directory. Use --local to change the pin here.");
68716
69091
  }
68717
69092
  }
69093
+ await validateOrgWorkspacePin(new LoticsClient({ apiKey: profile.api_key }), orgId, profile);
68718
69094
  return;
68719
69095
  }
68720
69096
  if (subcommand && subcommand !== "list") {
@@ -68764,6 +69140,7 @@ async function main() {
68764
69140
  console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
68765
69141
  console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
68766
69142
  console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
69143
+ console.error(" lotics app agent run <app_id> <alias> '<json>' Run a bound app agent (streams progress, reports the settled run)");
68767
69144
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
68768
69145
  console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
68769
69146
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
@@ -69022,6 +69399,42 @@ Available workspaces:`);
69022
69399
  }
69023
69400
  workflowUsage();
69024
69401
  }
69402
+ if (subcommand === "agent") {
69403
+ const action = toolArgs;
69404
+ const agentUsage = () => {
69405
+ console.error("Usage: lotics app agent run <app_id> <alias> ['<json>'|@inputs.json|stdin] [--session <id>] [--json]");
69406
+ console.error(" cat inputs.json | lotics app agent run <app_id> <alias> (read inputs from stdin)");
69407
+ console.error("Streams the run's progress to stderr; reports the settled run (structured output / text) to stdout.");
69408
+ console.error("--session <id> continues an existing thread; omitted mints a fresh session per run.");
69409
+ process.exit(1);
69410
+ };
69411
+ if (action === "run") {
69412
+ const appId = restArgs[0];
69413
+ const alias = restArgs[1];
69414
+ if (!appId || !alias) {
69415
+ agentUsage();
69416
+ }
69417
+ const ingested = await ingestJsonArgs({
69418
+ rawArg: restArgs[2],
69419
+ stdinIsTTY: process.stdin.isTTY ?? false,
69420
+ readFile: (p) => fs9.readFileSync(p, "utf-8"),
69421
+ readStdin
69422
+ });
69423
+ if (ingested.kind === "error") {
69424
+ console.error(ingested.message);
69425
+ process.exit(1);
69426
+ }
69427
+ await appAgentRun(client, {
69428
+ app_id: appId,
69429
+ alias,
69430
+ input: ingested.args,
69431
+ sessionId: flags.session,
69432
+ json: flags.json
69433
+ });
69434
+ return;
69435
+ }
69436
+ agentUsage();
69437
+ }
69025
69438
  if (subcommand === "query") {
69026
69439
  const action = toolArgs;
69027
69440
  if (action === "set") {
@@ -50,6 +50,31 @@ export interface ToolExecuteResult {
50
50
  model_output?: string;
51
51
  error?: string;
52
52
  }
53
+ /**
54
+ * A settled (or in-flight) app-agent run, the transcript-excluded projection
55
+ * `GET /v1/apps/{app_id}/agent-runs` returns. `output` is the STRUCTURED result
56
+ * for a typed agent (an object) or the final text for a free-text agent (a
57
+ * string); `status` is `running` until the run settles to `completed` / `error`
58
+ * / `aborted`. The authoritative record `lotics app agent run` reports from
59
+ * (never the stream).
60
+ */
61
+ export interface AppAgentRunSummary {
62
+ id: string;
63
+ app_id: string;
64
+ agent_alias: string;
65
+ session_id: string;
66
+ status: string;
67
+ input: Record<string, unknown> | null;
68
+ output: string | Record<string, unknown> | null;
69
+ usage: {
70
+ input_tokens: number;
71
+ output_tokens: number;
72
+ } | null;
73
+ error_message: string | null;
74
+ triggered_by_member_id: string | null;
75
+ started_at: string;
76
+ completed_at: string | null;
77
+ }
53
78
  export interface ToolInfo {
54
79
  name: string;
55
80
  description: string;
@@ -858,8 +883,17 @@ export declare class LoticsClient {
858
883
  * `async function __workflow(): …` wrapper the server compiles inside, so the
859
884
  * local typecheck mirrors the set-time verdict. Mirrors
860
885
  * POST /v1/apps/{app_id}/workflows/{alias}/dts.
886
+ *
887
+ * `declaration` (the manifest's `{ inputs?, outputs? }`) is posted as the body
888
+ * `{ declaration }` ONLY when the alias isn't `set` on the server yet — the
889
+ * server then synthesizes the dts from the declared schemas instead of 400ing
890
+ * "no workflow alias". A registered alias needs no declaration (the server's
891
+ * own bound contract wins), so the field is omitted in that case.
861
892
  */
862
- getAppWorkflowDts(app_id: string, alias: string): Promise<{
893
+ getAppWorkflowDts(app_id: string, alias: string, declaration?: {
894
+ inputs?: Record<string, unknown>;
895
+ outputs?: Record<string, unknown>;
896
+ }): Promise<{
863
897
  dts: string;
864
898
  envelope_prefix: string;
865
899
  envelope_suffix: string;
@@ -874,6 +908,25 @@ export declare class LoticsClient {
874
908
  session_id: string;
875
909
  input: Record<string, unknown>;
876
910
  }, signal?: AbortSignal): Promise<Response>;
911
+ /**
912
+ * Continue a PARKED (`awaiting_input`) agent run with the user's answer to its
913
+ * pending `ask_user_choice` — returns the RAW streamed continuation `Response`,
914
+ * exactly like `appAgentRunStream`. Mirrors
915
+ * POST /v1/apps/{app_id}/agent-runs/{run_id}/continue.
916
+ */
917
+ appAgentRunContinueStream(app_id: string, run_id: string, body: {
918
+ tool_call_id: string;
919
+ output: Record<string, unknown>;
920
+ }, signal?: AbortSignal): Promise<Response>;
921
+ /**
922
+ * A session's app-agent run history, oldest-first (the run just started is the
923
+ * last, and its exact id is on the stream response's `x-app-agent-run-id`
924
+ * header). Transcript excluded; structured `output`/`input` included. Mirrors
925
+ * GET /v1/apps/{app_id}/agent-runs.
926
+ */
927
+ listAgentRuns(app_id: string, session_id: string): Promise<{
928
+ runs: AppAgentRunSummary[];
929
+ }>;
877
930
  /**
878
931
  * Mint a presigned URL for uploading a file into an app. Mirrors
879
932
  * POST /v1/apps/{app_id}/files/upload-url.
@@ -608,9 +608,15 @@ export class LoticsClient {
608
608
  * `async function __workflow(): …` wrapper the server compiles inside, so the
609
609
  * local typecheck mirrors the set-time verdict. Mirrors
610
610
  * POST /v1/apps/{app_id}/workflows/{alias}/dts.
611
+ *
612
+ * `declaration` (the manifest's `{ inputs?, outputs? }`) is posted as the body
613
+ * `{ declaration }` ONLY when the alias isn't `set` on the server yet — the
614
+ * server then synthesizes the dts from the declared schemas instead of 400ing
615
+ * "no workflow alias". A registered alias needs no declaration (the server's
616
+ * own bound contract wins), so the field is omitted in that case.
611
617
  */
612
- async getAppWorkflowDts(app_id, alias) {
613
- return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/dts`);
618
+ async getAppWorkflowDts(app_id, alias, declaration) {
619
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/dts`, declaration ? { declaration } : undefined);
614
620
  }
615
621
  /**
616
622
  * Open a streaming agent run and return the RAW streamed `Response` (the
@@ -629,6 +635,32 @@ export class LoticsClient {
629
635
  await this.throwResponseError(res);
630
636
  return res;
631
637
  }
638
+ /**
639
+ * Continue a PARKED (`awaiting_input`) agent run with the user's answer to its
640
+ * pending `ask_user_choice` — returns the RAW streamed continuation `Response`,
641
+ * exactly like `appAgentRunStream`. Mirrors
642
+ * POST /v1/apps/{app_id}/agent-runs/{run_id}/continue.
643
+ */
644
+ async appAgentRunContinueStream(app_id, run_id, body, signal) {
645
+ const res = await fetch(`${this.baseUrl}/v1/apps/${encodeURIComponent(app_id)}/agent-runs/${encodeURIComponent(run_id)}/continue`, {
646
+ method: "POST",
647
+ headers: { ...this.buildHeaders(), "Content-Type": "application/json" },
648
+ body: JSON.stringify(body),
649
+ signal,
650
+ });
651
+ if (!res.ok)
652
+ await this.throwResponseError(res);
653
+ return res;
654
+ }
655
+ /**
656
+ * A session's app-agent run history, oldest-first (the run just started is the
657
+ * last, and its exact id is on the stream response's `x-app-agent-run-id`
658
+ * header). Transcript excluded; structured `output`/`input` included. Mirrors
659
+ * GET /v1/apps/{app_id}/agent-runs.
660
+ */
661
+ async listAgentRuns(app_id, session_id) {
662
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/agent-runs?session_id=${encodeURIComponent(session_id)}`);
663
+ }
632
664
  /**
633
665
  * Mint a presigned URL for uploading a file into an app. Mirrors
634
666
  * POST /v1/apps/{app_id}/files/upload-url.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.92.0",
3
+ "version": "0.94.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {