@lotics/cli 0.93.0 → 0.94.1

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
@@ -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,25 @@ 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
+ }
30227
30254
  /**
30228
30255
  * A session's app-agent run history, oldest-first (the run just started is the
30229
30256
  * last, and its exact id is on the stream response's `x-app-agent-run-id`
@@ -30236,6 +30263,27 @@ var LoticsClient = class {
30236
30263
  `/v1/apps/${encodeURIComponent(app_id)}/agent-runs?session_id=${encodeURIComponent(session_id)}`
30237
30264
  );
30238
30265
  }
30266
+ /**
30267
+ * A single run by id — the poll read a client follows after its stream drops
30268
+ * (a parked `awaiting_input` row carries `pending_interactive` so the question
30269
+ * survives reconnection). Mirrors GET /v1/apps/{app_id}/agent-runs/{run_id}.
30270
+ */
30271
+ async getAgentRun(app_id, run_id) {
30272
+ return this.request(
30273
+ "GET",
30274
+ `/v1/apps/${encodeURIComponent(app_id)}/agent-runs/${encodeURIComponent(run_id)}`
30275
+ );
30276
+ }
30277
+ /**
30278
+ * Request cancellation of an in-flight (or parked) run. Mirrors
30279
+ * POST /v1/apps/{app_id}/agent-runs/{run_id}/cancel.
30280
+ */
30281
+ async cancelAgentRun(app_id, run_id) {
30282
+ return this.request(
30283
+ "POST",
30284
+ `/v1/apps/${encodeURIComponent(app_id)}/agent-runs/${encodeURIComponent(run_id)}/cancel`
30285
+ );
30286
+ }
30239
30287
  /**
30240
30288
  * Mint a presigned URL for uploading a file into an app. Mirrors
30241
30289
  * POST /v1/apps/{app_id}/files/upload-url.
@@ -30921,7 +30969,11 @@ export default defineConfig({
30921
30969
  },
30922
30970
  build: {
30923
30971
  outDir: "dist",
30924
- sourcemap: true,
30972
+ // No source maps in the production build: they ship the original source
30973
+ // (comments, logic) as a static asset, and a password-gated app is served
30974
+ // openly at the asset layer \u2014 the map would expose the very source the gate
30975
+ // exists to protect. Source maps aren't needed to run the app.
30976
+ sourcemap: false,
30925
30977
  // Top-level await (package projects' generated .lotics/app_fields.ts
30926
30978
  // resolves the installation binding at module load) needs es2022 \u2014 Vite's
30927
30979
  // default 'modules' baseline is es2020 and esbuild hard-fails TLA there.
@@ -31459,6 +31511,9 @@ var SUPPORTED_OPS = /* @__PURE__ */ new Set([
31459
31511
  "binding",
31460
31512
  "upload_url",
31461
31513
  "upload_complete",
31514
+ "agentRuns",
31515
+ "agentRun.get",
31516
+ "agentRun.cancel",
31462
31517
  "comments.list",
31463
31518
  "comments.create",
31464
31519
  "comments.update",
@@ -31543,6 +31598,27 @@ async function dispatchRpc(client, body, opts) {
31543
31598
  // Comment ops mirror the production iframe-host's `handleCommentRpc` return
31544
31599
  // shapes so the SDK hooks behave identically in dev. App authority + the
31545
31600
  // `comments` capability + tenant floor are enforced server-side.
31601
+ case "agentRuns": {
31602
+ const p = body.payload;
31603
+ if (!p || typeof p.session_id !== "string") {
31604
+ throw new Error("agentRuns payload must include session_id");
31605
+ }
31606
+ return client.listAgentRuns(body.app_id, p.session_id);
31607
+ }
31608
+ case "agentRun.get": {
31609
+ const p = body.payload;
31610
+ if (!p || typeof p.run_id !== "string") {
31611
+ throw new Error("agentRun.get payload must include run_id");
31612
+ }
31613
+ return client.getAgentRun(body.app_id, p.run_id);
31614
+ }
31615
+ case "agentRun.cancel": {
31616
+ const p = body.payload;
31617
+ if (!p || typeof p.run_id !== "string") {
31618
+ throw new Error("agentRun.cancel payload must include run_id");
31619
+ }
31620
+ return client.cancelAgentRun(body.app_id, p.run_id);
31621
+ }
31546
31622
  case "comments.list": {
31547
31623
  const p = body.payload;
31548
31624
  if (!p || typeof p.record_id !== "string") {
@@ -31801,14 +31877,14 @@ function buildWrapperPage(args) {
31801
31877
  // protocol expects (stream-chunk* -> stream-end). Mirrors the production
31802
31878
  // host in frontend/features/app_ui/app_iframe_host.tsx.
31803
31879
  const streamingRuns = {};
31804
- async function handleAgentRun(id, payload) {
31880
+ async function handleAgentRun(id, op, payload) {
31805
31881
  const controller = new AbortController();
31806
31882
  streamingRuns[id] = controller;
31807
31883
  try {
31808
31884
  const res = await fetch("/_agent_run", {
31809
31885
  method: "POST",
31810
31886
  headers: { "content-type": "application/json" },
31811
- body: JSON.stringify({ app_id: APP_ID, op: "agentRun", payload: payload }),
31887
+ body: JSON.stringify({ app_id: APP_ID, op: op, payload: payload }),
31812
31888
  signal: controller.signal,
31813
31889
  });
31814
31890
  if (!res.ok || !res.body) {
@@ -31816,6 +31892,9 @@ function buildWrapperPage(args) {
31816
31892
  try { detail = JSON.parse(detail).message || detail; } catch (_) {}
31817
31893
  throw new Error(detail || ("HTTP " + res.status));
31818
31894
  }
31895
+ // Forward the run id (poll-recovery / cancel / continue key on it).
31896
+ const runId = res.headers.get("x-app-agent-run-id");
31897
+ if (runId) iframe.contentWindow.postMessage({ id: id, type: "run-id", runId: runId }, VITE_ORIGIN);
31819
31898
  const reader = res.body.getReader();
31820
31899
  const decoder = new TextDecoder();
31821
31900
  for (;;) {
@@ -31849,7 +31928,7 @@ function buildWrapperPage(args) {
31849
31928
  }
31850
31929
  if (typeof msg.op !== "string") return;
31851
31930
  // Streaming agent run \u2014 many messages back, not a single result.
31852
- if (msg.op === "agentRun") { handleAgentRun(msg.id, msg.payload); return; }
31931
+ if (msg.op === "agentRun" || msg.op === "agentRunContinue") { handleAgentRun(msg.id, msg.op, msg.payload); return; }
31853
31932
  const startedAt = performance.now();
31854
31933
  try {
31855
31934
  const data = msg.op === "upload"
@@ -32088,18 +32167,37 @@ async function startDevServer(args) {
32088
32167
  });
32089
32168
  try {
32090
32169
  const body = await readJson(req);
32091
- const p = body.payload ?? {};
32092
- if (typeof p.alias !== "string" || typeof p.session_id !== "string") {
32093
- throw new Error("agentRun payload must include `alias` and `session_id`");
32094
- }
32095
- const upstream = await args.client.appAgentRunStream(
32096
- body.app_id,
32097
- p.alias,
32098
- { session_id: p.session_id, input: p.input ?? {} },
32099
- ac.signal
32100
- );
32170
+ let upstream;
32171
+ if (body.op === "agentRunContinue") {
32172
+ const p = body.payload ?? {};
32173
+ if (typeof p.run_id !== "string" || typeof p.tool_call_id !== "string" || typeof p.output !== "object" || p.output === null) {
32174
+ throw new Error("agentRunContinue payload must include `run_id`, `tool_call_id`, and `output`");
32175
+ }
32176
+ upstream = await args.client.appAgentRunContinueStream(
32177
+ body.app_id,
32178
+ p.run_id,
32179
+ { tool_call_id: p.tool_call_id, output: p.output },
32180
+ ac.signal
32181
+ );
32182
+ } else {
32183
+ const p = body.payload ?? {};
32184
+ if (typeof p.alias !== "string" || typeof p.session_id !== "string") {
32185
+ throw new Error("agentRun payload must include `alias` and `session_id`");
32186
+ }
32187
+ upstream = await args.client.appAgentRunStream(
32188
+ body.app_id,
32189
+ p.alias,
32190
+ { session_id: p.session_id, input: p.input ?? {} },
32191
+ ac.signal
32192
+ );
32193
+ }
32101
32194
  if (!upstream.body) throw new Error("agent run returned no stream body");
32102
- res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform" });
32195
+ const runId = upstream.headers.get("x-app-agent-run-id");
32196
+ res.writeHead(200, {
32197
+ "Content-Type": "text/event-stream",
32198
+ "Cache-Control": "no-cache, no-transform",
32199
+ ...runId ? { "x-app-agent-run-id": runId } : {}
32200
+ });
32103
32201
  const reader = upstream.body.getReader();
32104
32202
  for (; ; ) {
32105
32203
  const { value, done } = await reader.read();
@@ -32955,6 +33053,12 @@ var LOTICS_INCLUDE_GLOB = ".lotics/**/*";
32955
33053
  var STALE_LOTICS_INCLUDES = /* @__PURE__ */ new Set([".lotics", "./.lotics", ".lotics/"]);
32956
33054
  var FALLBACK_ENVELOPE_PREFIX = "async function __workflow(): Promise<__WorkflowReturn | void> {\n";
32957
33055
  var FALLBACK_ENVELOPE_SUFFIX = "\n}";
33056
+ function toWorkflowDtsDeclaration(d) {
33057
+ return { inputs: d.inputs, outputs: d.outputs };
33058
+ }
33059
+ function isUnknownWorkflowAliasError(err2) {
33060
+ return err2 instanceof Error && /^400:/.test(err2.message) && /has no workflow alias/i.test(err2.message);
33061
+ }
32958
33062
  function workflowFileHeader(alias) {
32959
33063
  const refPath = path5.join("..", "..", WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`).split(path5.sep).join("/");
32960
33064
  return `/// <reference path="${refPath}" />
@@ -33015,9 +33119,9 @@ function stripWorkflowHeader(content) {
33015
33119
  }
33016
33120
  return lines.slice(start + 1).join("\n").replace(/\s+$/, "");
33017
33121
  }
33018
- async function writeWorkflowFiles(client, projectDir, app_id, aliases) {
33122
+ async function writeWorkflowFiles(client, projectDir, app_id, workflows) {
33019
33123
  const written = [];
33020
- for (const alias of aliases) {
33124
+ for (const [alias, declaration] of Object.entries(workflows)) {
33021
33125
  const res = await client.getAppWorkflow(app_id, alias);
33022
33126
  const source = res.error || res.result === null || typeof res.result !== "object" ? null : res.result.source;
33023
33127
  if (typeof source !== "string" || source.trim() === "") {
@@ -33026,15 +33130,26 @@ async function writeWorkflowFiles(client, projectDir, app_id, aliases) {
33026
33130
  );
33027
33131
  continue;
33028
33132
  }
33029
- const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias);
33133
+ const envelope = await fetchWorkflowGlobals(
33134
+ client,
33135
+ projectDir,
33136
+ app_id,
33137
+ alias,
33138
+ toWorkflowDtsDeclaration(declaration)
33139
+ );
33030
33140
  writeWorkflowFile(projectDir, alias, source, envelope);
33031
33141
  written.push(alias);
33032
33142
  }
33033
33143
  return written;
33034
33144
  }
33035
- async function fetchWorkflowGlobals(client, projectDir, app_id, alias) {
33145
+ async function fetchWorkflowGlobals(client, projectDir, app_id, alias, declaration) {
33036
33146
  try {
33037
- const { dts, envelope_prefix, envelope_suffix } = await client.getAppWorkflowDts(app_id, alias);
33147
+ const { dts, envelope_prefix, envelope_suffix } = await fetchWorkflowDts(
33148
+ client,
33149
+ app_id,
33150
+ alias,
33151
+ declaration
33152
+ );
33038
33153
  writeWorkflowGlobals(projectDir, alias, dts);
33039
33154
  return { prefix: envelope_prefix, suffix: envelope_suffix };
33040
33155
  } catch (err2) {
@@ -33044,6 +33159,16 @@ async function fetchWorkflowGlobals(client, projectDir, app_id, alias) {
33044
33159
  return { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX };
33045
33160
  }
33046
33161
  }
33162
+ async function fetchWorkflowDts(client, app_id, alias, declaration) {
33163
+ try {
33164
+ return await client.getAppWorkflowDts(app_id, alias);
33165
+ } catch (err2) {
33166
+ if (declaration && isUnknownWorkflowAliasError(err2)) {
33167
+ return client.getAppWorkflowDts(app_id, alias, declaration);
33168
+ }
33169
+ throw err2;
33170
+ }
33171
+ }
33047
33172
  function runTar(args, cwd) {
33048
33173
  return new Promise((resolve2, reject2) => {
33049
33174
  const proc = spawn2("tar", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
@@ -33090,10 +33215,37 @@ function readAppMeta(projectDir) {
33090
33215
  capabilities: pkg2.lotics.capabilities
33091
33216
  };
33092
33217
  }
33218
+ var APP_META_KEYS = Object.keys({
33219
+ app_id: true,
33220
+ workspace_id: true,
33221
+ current_version_id: true,
33222
+ version_number: true,
33223
+ workflows: true,
33224
+ queries: true,
33225
+ agents: true,
33226
+ capabilities: true
33227
+ });
33093
33228
  function writeAppMeta(projectDir, meta3) {
33094
33229
  const pkgPath2 = path5.join(projectDir, "package.json");
33095
33230
  const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
33096
- pkg2.lotics = meta3;
33231
+ const existing = pkg2.lotics && typeof pkg2.lotics === "object" ? pkg2.lotics : {};
33232
+ const preserved = {};
33233
+ for (const [key, value] of Object.entries(existing)) {
33234
+ if (!APP_META_KEYS.includes(key)) preserved[key] = value;
33235
+ }
33236
+ pkg2.lotics = { ...meta3, ...preserved };
33237
+ fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
33238
+ }
33239
+ function writeWorkflowOutputs(projectDir, alias, outputs) {
33240
+ const pkgPath2 = path5.join(projectDir, "package.json");
33241
+ const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
33242
+ const declaration = pkg2.lotics?.workflows?.[alias];
33243
+ if (!declaration) {
33244
+ throw new Error(
33245
+ `package.json#lotics.workflows.${alias} disappeared before the derived-outputs write-back.`
33246
+ );
33247
+ }
33248
+ declaration.outputs = outputs;
33097
33249
  fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
33098
33250
  }
33099
33251
  function ensureAppTsconfig(projectDir) {
@@ -33228,19 +33380,29 @@ async function appCodegen(args) {
33228
33380
  `\u26A0 Could not regenerate .lotics/app_fields.ts (${err2 instanceof Error ? err2.message : String(err2)}). Kept the existing file.`
33229
33381
  );
33230
33382
  }
33231
- await refreshWorkflowGlobals(args.client, projectDir, meta3.app_id, Object.keys(meta3.workflows ?? {}));
33383
+ await refreshWorkflowGlobals(args.client, projectDir, meta3.app_id, meta3.workflows ?? {});
33232
33384
  }
33233
- async function refreshWorkflowGlobals(client, projectDir, app_id, aliases) {
33234
- for (const alias of aliases) {
33235
- const file2 = workflowFilePath(projectDir, alias);
33236
- if (!fs4.existsSync(file2)) continue;
33237
- const body = stripWorkflowHeader(fs4.readFileSync(file2, "utf-8"));
33238
- if (body.trim() === "") continue;
33239
- const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias);
33240
- writeWorkflowFile(projectDir, alias, body, envelope);
33241
- console.error(`Refreshed ${path5.relative(projectDir, file2)} + its workflow types`);
33385
+ async function refreshWorkflowGlobals(client, projectDir, app_id, workflows) {
33386
+ for (const [alias, declaration] of Object.entries(workflows)) {
33387
+ const refreshed = await refreshWorkflowTypes(
33388
+ client,
33389
+ projectDir,
33390
+ app_id,
33391
+ alias,
33392
+ toWorkflowDtsDeclaration(declaration)
33393
+ );
33394
+ if (refreshed) console.error(`Refreshed workflow types for ${alias}`);
33242
33395
  }
33243
33396
  }
33397
+ async function refreshWorkflowTypes(client, projectDir, app_id, alias, declaration) {
33398
+ const file2 = workflowFilePath(projectDir, alias);
33399
+ if (!fs4.existsSync(file2)) return false;
33400
+ const body = stripWorkflowHeader(fs4.readFileSync(file2, "utf-8"));
33401
+ if (body.trim() === "") return false;
33402
+ const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias, declaration);
33403
+ writeWorkflowFile(projectDir, alias, body, envelope);
33404
+ return true;
33405
+ }
33244
33406
  function stampPulledManifest(projectDir, args) {
33245
33407
  writeAppMeta(projectDir, {
33246
33408
  app_id: args.app_id,
@@ -33403,9 +33565,9 @@ async function appPull(client, args) {
33403
33565
  queries: app.queries ?? {},
33404
33566
  agents: app.agents ?? {}
33405
33567
  });
33406
- const aliases = Object.keys(app.workflows ?? {});
33407
- if (aliases.length > 0) {
33408
- const written = await writeWorkflowFiles(client, targetPath, app.id, aliases);
33568
+ const workflows = app.workflows ?? {};
33569
+ if (Object.keys(workflows).length > 0) {
33570
+ const written = await writeWorkflowFiles(client, targetPath, app.id, workflows);
33409
33571
  if (written.length > 0) {
33410
33572
  console.error(
33411
33573
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
@@ -33807,8 +33969,19 @@ async function appWorkflowSet(client, args) {
33807
33969
  const result = res.result ?? {};
33808
33970
  const workflowId = typeof result.workflow_id === "string" ? result.workflow_id : "(unknown)";
33809
33971
  console.error(`Set workflow "${args.alias}" \u2192 ${workflowId}`);
33810
- if (result.outputs && typeof result.outputs === "object") {
33811
- console.error(` result.data schema: ${JSON.stringify(result.outputs)}`);
33972
+ const derivedOutputs = result.outputs !== null && typeof result.outputs === "object" && !Array.isArray(result.outputs) ? result.outputs : null;
33973
+ if (!declaration.outputs && derivedOutputs && Object.keys(derivedOutputs).length > 0) {
33974
+ writeWorkflowOutputs(projectDir, args.alias, derivedOutputs);
33975
+ console.error(
33976
+ ` Wrote the derived result.data schema into package.json#lotics.workflows.${args.alias}.outputs.`
33977
+ );
33978
+ const refreshed = await refreshWorkflowTypes(client, projectDir, meta3.app_id, args.alias, {
33979
+ inputs: declaration.inputs,
33980
+ outputs: derivedOutputs
33981
+ });
33982
+ if (refreshed) console.error(` Refreshed workflow types for ${args.alias}.`);
33983
+ } else if (derivedOutputs) {
33984
+ console.error(` result.data schema: ${JSON.stringify(derivedOutputs)}`);
33812
33985
  }
33813
33986
  }
33814
33987
  async function appQuerySet(client, args) {
@@ -33834,12 +34007,12 @@ async function appWorkflowPull(client) {
33834
34007
  const projectDir = process.cwd();
33835
34008
  const meta3 = readAppMeta(projectDir);
33836
34009
  const app = await client.getApp(meta3.app_id);
33837
- const aliases = Object.keys(app.workflows ?? {});
33838
- if (aliases.length === 0) {
34010
+ const workflows = app.workflows ?? {};
34011
+ if (Object.keys(workflows).length === 0) {
33839
34012
  console.error(`App ${meta3.app_id} has no bound workflows.`);
33840
34013
  return;
33841
34014
  }
33842
- const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, aliases);
34015
+ const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, workflows);
33843
34016
  console.error(
33844
34017
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
33845
34018
  );
@@ -47318,13 +47491,50 @@ function unmergeCellsCommand(workbook, sheetIndex, mergeRef) {
47318
47491
  }
47319
47492
  };
47320
47493
  }
47494
+ function isInertCell(cell) {
47495
+ 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 === "";
47496
+ }
47497
+ function clampInertCells(sheet) {
47498
+ let minRow = Infinity;
47499
+ let maxRow = 0;
47500
+ let minCol = Infinity;
47501
+ let maxCol = 0;
47502
+ let hasMeaningful = false;
47503
+ for (const [ref, cell] of sheet.cells) {
47504
+ if (isInertCell(cell)) continue;
47505
+ const rc = refToRowColSafe(ref);
47506
+ if (!rc) continue;
47507
+ hasMeaningful = true;
47508
+ if (rc.row < minRow) minRow = rc.row;
47509
+ if (rc.row > maxRow) maxRow = rc.row;
47510
+ if (rc.col < minCol) minCol = rc.col;
47511
+ if (rc.col > maxCol) maxCol = rc.col;
47512
+ }
47513
+ const pruned = /* @__PURE__ */ new Map();
47514
+ for (const [ref, cell] of sheet.cells) {
47515
+ if (!isInertCell(cell)) continue;
47516
+ if (!hasMeaningful) {
47517
+ pruned.set(ref, cell);
47518
+ continue;
47519
+ }
47520
+ const rc = refToRowColSafe(ref);
47521
+ if (!rc) continue;
47522
+ if (rc.row < minRow || rc.row > maxRow || rc.col < minCol || rc.col > maxCol) {
47523
+ pruned.set(ref, cell);
47524
+ }
47525
+ }
47526
+ for (const ref of pruned.keys()) sheet.cells.delete(ref);
47527
+ return pruned;
47528
+ }
47321
47529
  function insertRowsCommand(workbook, sheetIndex, at, count) {
47322
47530
  const sheet = workbook.sheets[sheetIndex];
47323
47531
  let mergeSnapshot = [];
47532
+ let inertSnapshot = /* @__PURE__ */ new Map();
47324
47533
  return {
47325
47534
  description: `Insert ${count} row${count > 1 ? "s" : ""} at ${at}`,
47326
47535
  execute() {
47327
47536
  mergeSnapshot = [...sheet.mergedCells];
47537
+ inertSnapshot = clampInertCells(sheet);
47328
47538
  const toMove = [];
47329
47539
  for (const [ref] of sheet.cells) {
47330
47540
  const rc = refToRowColSafe(ref);
@@ -47368,6 +47578,7 @@ function insertRowsCommand(workbook, sheetIndex, at, count) {
47368
47578
  sheet.rowHeights.set(r - count, h);
47369
47579
  }
47370
47580
  for (let r = at; r < at + count; r++) sheet.rowHeights.delete(r);
47581
+ for (const [ref, cell] of inertSnapshot) sheet.cells.set(ref, cell);
47371
47582
  workbook.emit({ type: "rows_deleted", sheet: sheetIndex, at, count });
47372
47583
  }
47373
47584
  };
@@ -47382,10 +47593,12 @@ function deleteRowsCommand(workbook, sheetIndex, at, count) {
47382
47593
  }
47383
47594
  }
47384
47595
  let mergeSnapshot = [];
47596
+ let inertSnapshot = /* @__PURE__ */ new Map();
47385
47597
  return {
47386
47598
  description: `Delete ${count} row${count > 1 ? "s" : ""} at ${at}`,
47387
47599
  execute() {
47388
47600
  mergeSnapshot = [...sheet.mergedCells];
47601
+ inertSnapshot = clampInertCells(sheet);
47389
47602
  for (const ref of snapshot.keys()) sheet.cells.delete(ref);
47390
47603
  const toMove = [];
47391
47604
  for (const [ref] of sheet.cells) {
@@ -47418,6 +47631,7 @@ function deleteRowsCommand(workbook, sheetIndex, at, count) {
47418
47631
  for (const [ref, snap] of snapshot) {
47419
47632
  sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
47420
47633
  }
47634
+ for (const [ref, cell] of inertSnapshot) sheet.cells.set(ref, cell);
47421
47635
  workbook.emit({ type: "rows_inserted", sheet: sheetIndex, at, count });
47422
47636
  }
47423
47637
  };
@@ -47425,10 +47639,12 @@ function deleteRowsCommand(workbook, sheetIndex, at, count) {
47425
47639
  function insertColsCommand(workbook, sheetIndex, at, count) {
47426
47640
  const sheet = workbook.sheets[sheetIndex];
47427
47641
  let mergeSnapshot = [];
47642
+ let inertSnapshot = /* @__PURE__ */ new Map();
47428
47643
  return {
47429
47644
  description: `Insert ${count} column${count > 1 ? "s" : ""} at ${at}`,
47430
47645
  execute() {
47431
47646
  mergeSnapshot = [...sheet.mergedCells];
47647
+ inertSnapshot = clampInertCells(sheet);
47432
47648
  const toMove = [];
47433
47649
  for (const [ref] of sheet.cells) {
47434
47650
  const rc = refToRowColSafe(ref);
@@ -47472,6 +47688,7 @@ function insertColsCommand(workbook, sheetIndex, at, count) {
47472
47688
  sheet.colWidths.set(c - count, w);
47473
47689
  }
47474
47690
  for (let c = at; c < at + count; c++) sheet.colWidths.delete(c);
47691
+ for (const [ref, cell] of inertSnapshot) sheet.cells.set(ref, cell);
47475
47692
  workbook.emit({ type: "cols_deleted", sheet: sheetIndex, at, count });
47476
47693
  }
47477
47694
  };
@@ -47486,10 +47703,12 @@ function deleteColsCommand(workbook, sheetIndex, at, count) {
47486
47703
  }
47487
47704
  }
47488
47705
  let mergeSnapshot = [];
47706
+ let inertSnapshot = /* @__PURE__ */ new Map();
47489
47707
  return {
47490
47708
  description: `Delete ${count} column${count > 1 ? "s" : ""} at ${at}`,
47491
47709
  execute() {
47492
47710
  mergeSnapshot = [...sheet.mergedCells];
47711
+ inertSnapshot = clampInertCells(sheet);
47493
47712
  for (const ref of snapshot.keys()) sheet.cells.delete(ref);
47494
47713
  const toMove = [];
47495
47714
  for (const [ref] of sheet.cells) {
@@ -47522,6 +47741,7 @@ function deleteColsCommand(workbook, sheetIndex, at, count) {
47522
47741
  for (const [ref, snap] of snapshot) {
47523
47742
  sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
47524
47743
  }
47744
+ for (const [ref, cell] of inertSnapshot) sheet.cells.set(ref, cell);
47525
47745
  workbook.emit({ type: "cols_inserted", sheet: sheetIndex, at, count });
47526
47746
  }
47527
47747
  };
@@ -47744,26 +47964,37 @@ function lazyEngine(workbook) {
47744
47964
  }
47745
47965
  };
47746
47966
  }
47747
- function readToJson(filePath) {
47967
+ function readToJson(filePath, filter2) {
47748
47968
  const buffer = fs6.readFileSync(filePath);
47749
47969
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
47750
47970
  const parsed = parseExcelBuffer(arrayBuffer);
47971
+ let sheets = parsed.sheets;
47972
+ if (filter2?.sheet !== void 0) {
47973
+ const match = sheets.filter((s) => s.name === filter2.sheet);
47974
+ if (match.length === 0) {
47975
+ const names = parsed.sheets.map((s) => `"${s.name}"`).join(", ");
47976
+ fail(`Sheet not found: "${filter2.sheet}". Available: ${names}`);
47977
+ }
47978
+ sheets = match;
47979
+ }
47751
47980
  return {
47752
47981
  activeSheetIndex: parsed.activeSheetIndex,
47753
- sheets: parsed.sheets.map((s) => ({
47982
+ sheets: sheets.map((s) => ({
47754
47983
  name: s.name,
47755
47984
  totalRowCount: s.totalRowCount,
47756
47985
  truncated: s.truncated,
47757
- cells: cellsFromParsedRows(s.rows),
47986
+ cells: cellsFromParsedRows(s.rows, filter2?.range),
47758
47987
  merges: s.mergedCells.map((m) => `${rowColToRef(m.startRow, m.startCol)}:${rowColToRef(m.endRow, m.endCol)}`),
47759
47988
  freeze: s.freezePane ? { row: s.freezePane.frozenRows, col: s.freezePane.frozenCols } : void 0
47760
47989
  }))
47761
47990
  };
47762
47991
  }
47763
- function cellsFromParsedRows(rows) {
47992
+ function cellsFromParsedRows(rows, range) {
47764
47993
  const out = {};
47765
47994
  for (const row of rows) {
47995
+ if (range && (row.index < range.startRow || row.index > range.endRow)) continue;
47766
47996
  for (const cell of row.cells) {
47997
+ if (range && (cell.column < range.startCol || cell.column > range.endCol)) continue;
47767
47998
  const ref = rowColToRef(row.index, cell.column);
47768
47999
  const value = cell.typedValue ?? cell.value;
47769
48000
  const entry = { value };
@@ -47835,9 +48066,34 @@ function valueToInput(value) {
47835
48066
  if (value === null || value === void 0) return "";
47836
48067
  return String(value);
47837
48068
  }
47838
- function xlsxRead(filePath) {
47839
- if (!filePath) fail("Usage: lotics xlsx read <file>");
47840
- const data = readToJson(filePath);
48069
+ function readOptionFlag(rest, name) {
48070
+ const i2 = rest.indexOf(name);
48071
+ if (i2 < 0) return void 0;
48072
+ const val = rest[i2 + 1];
48073
+ if (val === void 0 || val.startsWith("--")) fail(`Missing value for ${name}`);
48074
+ return val;
48075
+ }
48076
+ function splitOptionalSheetRef(spec) {
48077
+ const idx = spec.indexOf("!");
48078
+ if (idx < 0) return { sheet: void 0, ref: spec };
48079
+ return { sheet: spec.slice(0, idx), ref: spec.slice(idx + 1) };
48080
+ }
48081
+ function xlsxRead(filePath, rest) {
48082
+ if (!filePath) fail("Usage: lotics xlsx read <file> [--sheet <name>] [--range <sheet>!<A1:G60>]");
48083
+ const rangeArg = readOptionFlag(rest, "--range");
48084
+ const sheetArg = readOptionFlag(rest, "--sheet");
48085
+ let filter2;
48086
+ if (rangeArg !== void 0) {
48087
+ const { sheet, ref } = splitOptionalSheetRef(rangeArg);
48088
+ const targetSheet = sheet ?? sheetArg;
48089
+ if (targetSheet === void 0) {
48090
+ fail("--range without a <sheet>! prefix requires --sheet <name>");
48091
+ }
48092
+ filter2 = { sheet: targetSheet, range: parseRange2(ref) };
48093
+ } else if (sheetArg !== void 0) {
48094
+ filter2 = { sheet: sheetArg };
48095
+ }
48096
+ const data = readToJson(filePath, filter2);
47841
48097
  console.log(JSON.stringify(data, null, 2));
47842
48098
  }
47843
48099
  function xlsxWrite(filePath, json2) {
@@ -48074,7 +48330,10 @@ function printXlsxHelp() {
48074
48330
  console.error(`Lotics xlsx commands \u2014 manipulate .xlsx files in place.
48075
48331
  Uses Lotics' own xlsx engine; round-trips faithfully with the Lotics editor and templates.
48076
48332
 
48077
- lotics xlsx read <file> Dump file as JSON (sheets, cells, merges, freeze)
48333
+ lotics xlsx read <file> [--sheet <name>] [--range <sheet>!<A1:G60>]
48334
+ Dump file as JSON (sheets, cells, merges, freeze).
48335
+ --sheet limits to one sheet; --range to a cell window
48336
+ (its <sheet>! prefix is optional when --sheet is given)
48078
48337
  lotics xlsx write <file> '<json>' Create .xlsx from {"sheets":[{"name","cells":{"A1":...}}]}
48079
48338
  lotics xlsx set-cell <file> <sheet>!<ref> '<v>' Set cell value (prefix '=' for formula)
48080
48339
  lotics xlsx clear-range <file> <sheet>!<range> Clear all cells in a range
@@ -48096,7 +48355,7 @@ Edit ops mutate the file atomically (temp file + rename).`);
48096
48355
  async function runXlsxCommand(subcommand, toolArgs, restArgs) {
48097
48356
  switch (subcommand) {
48098
48357
  case "read":
48099
- return xlsxRead(toolArgs);
48358
+ return xlsxRead(toolArgs, restArgs);
48100
48359
  case "write":
48101
48360
  return xlsxWrite(toolArgs, restArgs[0]);
48102
48361
  case "set-cell":
@@ -68204,8 +68463,14 @@ async function cdpConnect(wsUrl) {
68204
68463
  });
68205
68464
  return { send, close: () => ws.close() };
68206
68465
  }
68466
+ function isStoredFileId(target) {
68467
+ return /^fil_[A-Za-z0-9]+$/.test(target);
68468
+ }
68469
+ function defaultPreviewOutputPath(filename, cwd) {
68470
+ return join(cwd, basename(filename, extname(filename)) + ".png");
68471
+ }
68207
68472
  async function runPreviewCommand(filePath, flags) {
68208
- if (!filePath) fail2("Usage: lotics preview <file.docx|.xlsx> [--out <file.png>]");
68473
+ if (!filePath) fail2("Usage: lotics preview <file.docx|.xlsx | fil_id> [-o <file.png>]");
68209
68474
  const abs = resolve(filePath);
68210
68475
  if (!existsSync(abs)) fail2(`File not found: ${abs}`);
68211
68476
  const ext = extname(abs).toLowerCase();
@@ -68438,8 +68703,10 @@ COMMANDS
68438
68703
  lotics file download <file_id> Download a file by ID (alias: lotics download)
68439
68704
  lotics file download record <record_id> <field_key>
68440
68705
  Download all files on a record file field
68441
- lotics file preview <file> [-o png] Render a .docx/.xlsx to a PNG (frontend engines;
68442
- needs a Chrome/Chromium on the machine)
68706
+ lotics file preview <file|fil_id> [-o png]
68707
+ Render a .docx/.xlsx to a PNG \u2014 a local path OR a
68708
+ stored file id (downloaded first). Frontend engines;
68709
+ needs a Chrome/Chromium on the machine
68443
68710
 
68444
68711
  FLAGS
68445
68712
  --json Full JSON output (default is human-readable text)
@@ -68799,6 +69066,18 @@ async function main() {
68799
69066
  return;
68800
69067
  }
68801
69068
  if (command === "preview") {
69069
+ if (subcommand && isStoredFileId(subcommand)) {
69070
+ const { client: client2 } = requireClient(flags);
69071
+ const tmpDir = fs9.mkdtempSync(path7.join(os2.tmpdir(), "lotics-preview-"));
69072
+ try {
69073
+ const { path: localPath, filename } = await client2.downloadFileById(subcommand, tmpDir);
69074
+ const output = flags.output ?? defaultPreviewOutputPath(filename, process.cwd());
69075
+ await runPreviewCommand(localPath, { output });
69076
+ } finally {
69077
+ fs9.rmSync(tmpDir, { recursive: true, force: true });
69078
+ }
69079
+ return;
69080
+ }
68802
69081
  await runPreviewCommand(subcommand, flags);
68803
69082
  return;
68804
69083
  }
@@ -74,6 +74,13 @@ export interface AppAgentRunSummary {
74
74
  triggered_by_member_id: string | null;
75
75
  started_at: string;
76
76
  completed_at: string | null;
77
+ /** Single-run GET only, while `status` is "awaiting_input": the pending ask
78
+ * derived from the transcript — what /continue answers. */
79
+ pending_interactive?: {
80
+ tool_call_id: string;
81
+ tool_name: string;
82
+ input: Record<string, unknown>;
83
+ };
77
84
  }
78
85
  export interface ToolInfo {
79
86
  name: string;
@@ -883,8 +890,17 @@ export declare class LoticsClient {
883
890
  * `async function __workflow(): …` wrapper the server compiles inside, so the
884
891
  * local typecheck mirrors the set-time verdict. Mirrors
885
892
  * POST /v1/apps/{app_id}/workflows/{alias}/dts.
893
+ *
894
+ * `declaration` (the manifest's `{ inputs?, outputs? }`) is posted as the body
895
+ * `{ declaration }` ONLY when the alias isn't `set` on the server yet — the
896
+ * server then synthesizes the dts from the declared schemas instead of 400ing
897
+ * "no workflow alias". A registered alias needs no declaration (the server's
898
+ * own bound contract wins), so the field is omitted in that case.
886
899
  */
887
- getAppWorkflowDts(app_id: string, alias: string): Promise<{
900
+ getAppWorkflowDts(app_id: string, alias: string, declaration?: {
901
+ inputs?: Record<string, unknown>;
902
+ outputs?: Record<string, unknown>;
903
+ }): Promise<{
888
904
  dts: string;
889
905
  envelope_prefix: string;
890
906
  envelope_suffix: string;
@@ -899,6 +915,16 @@ export declare class LoticsClient {
899
915
  session_id: string;
900
916
  input: Record<string, unknown>;
901
917
  }, signal?: AbortSignal): Promise<Response>;
918
+ /**
919
+ * Continue a PARKED (`awaiting_input`) agent run with the user's answer to its
920
+ * pending `ask_user_choice` — returns the RAW streamed continuation `Response`,
921
+ * exactly like `appAgentRunStream`. Mirrors
922
+ * POST /v1/apps/{app_id}/agent-runs/{run_id}/continue.
923
+ */
924
+ appAgentRunContinueStream(app_id: string, run_id: string, body: {
925
+ tool_call_id: string;
926
+ output: Record<string, unknown>;
927
+ }, signal?: AbortSignal): Promise<Response>;
902
928
  /**
903
929
  * A session's app-agent run history, oldest-first (the run just started is the
904
930
  * last, and its exact id is on the stream response's `x-app-agent-run-id`
@@ -908,6 +934,21 @@ export declare class LoticsClient {
908
934
  listAgentRuns(app_id: string, session_id: string): Promise<{
909
935
  runs: AppAgentRunSummary[];
910
936
  }>;
937
+ /**
938
+ * A single run by id — the poll read a client follows after its stream drops
939
+ * (a parked `awaiting_input` row carries `pending_interactive` so the question
940
+ * survives reconnection). Mirrors GET /v1/apps/{app_id}/agent-runs/{run_id}.
941
+ */
942
+ getAgentRun(app_id: string, run_id: string): Promise<{
943
+ run: AppAgentRunSummary;
944
+ }>;
945
+ /**
946
+ * Request cancellation of an in-flight (or parked) run. Mirrors
947
+ * POST /v1/apps/{app_id}/agent-runs/{run_id}/cancel.
948
+ */
949
+ cancelAgentRun(app_id: string, run_id: string): Promise<{
950
+ ok: true;
951
+ }>;
911
952
  /**
912
953
  * Mint a presigned URL for uploading a file into an app. Mirrors
913
954
  * 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,23 @@ 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
+ }
632
655
  /**
633
656
  * A session's app-agent run history, oldest-first (the run just started is the
634
657
  * last, and its exact id is on the stream response's `x-app-agent-run-id`
@@ -638,6 +661,21 @@ export class LoticsClient {
638
661
  async listAgentRuns(app_id, session_id) {
639
662
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/agent-runs?session_id=${encodeURIComponent(session_id)}`);
640
663
  }
664
+ /**
665
+ * A single run by id — the poll read a client follows after its stream drops
666
+ * (a parked `awaiting_input` row carries `pending_interactive` so the question
667
+ * survives reconnection). Mirrors GET /v1/apps/{app_id}/agent-runs/{run_id}.
668
+ */
669
+ async getAgentRun(app_id, run_id) {
670
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/agent-runs/${encodeURIComponent(run_id)}`);
671
+ }
672
+ /**
673
+ * Request cancellation of an in-flight (or parked) run. Mirrors
674
+ * POST /v1/apps/{app_id}/agent-runs/{run_id}/cancel.
675
+ */
676
+ async cancelAgentRun(app_id, run_id) {
677
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/agent-runs/${encodeURIComponent(run_id)}/cancel`);
678
+ }
641
679
  /**
642
680
  * Mint a presigned URL for uploading a file into an app. Mirrors
643
681
  * 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.93.0",
3
+ "version": "0.94.1",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {