@lotics/cli 0.92.0 → 0.93.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
@@ -30224,6 +30224,18 @@ var LoticsClient = class {
30224
30224
  if (!res.ok) await this.throwResponseError(res);
30225
30225
  return res;
30226
30226
  }
30227
+ /**
30228
+ * A session's app-agent run history, oldest-first (the run just started is the
30229
+ * last, and its exact id is on the stream response's `x-app-agent-run-id`
30230
+ * header). Transcript excluded; structured `output`/`input` included. Mirrors
30231
+ * GET /v1/apps/{app_id}/agent-runs.
30232
+ */
30233
+ async listAgentRuns(app_id, session_id) {
30234
+ return this.request(
30235
+ "GET",
30236
+ `/v1/apps/${encodeURIComponent(app_id)}/agent-runs?session_id=${encodeURIComponent(session_id)}`
30237
+ );
30238
+ }
30227
30239
  /**
30228
30240
  * Mint a presigned URL for uploading a file into an app. Mirrors
30229
30241
  * POST /v1/apps/{app_id}/files/upload-url.
@@ -30555,6 +30567,16 @@ function upsertProfile(orgId, fields) {
30555
30567
  latest_version: config2.latest_version
30556
30568
  });
30557
30569
  }
30570
+ function clearProfileWorkspace(orgId) {
30571
+ const config2 = loadGlobalConfig();
30572
+ const existing = config2?.profiles?.[orgId];
30573
+ if (!existing) return;
30574
+ const { workspace_id: _dropped, ...rest } = existing;
30575
+ saveGlobalConfig({
30576
+ ...config2,
30577
+ profiles: { ...config2.profiles, [orgId]: { ...rest } }
30578
+ });
30579
+ }
30558
30580
  function removeProfile(orgId) {
30559
30581
  const config2 = loadGlobalConfig();
30560
30582
  if (!config2?.profiles?.[orgId]) return;
@@ -30653,6 +30675,7 @@ var VERSION = pkg.version;
30653
30675
  import fs4 from "node:fs";
30654
30676
  import path5 from "node:path";
30655
30677
  import { spawn as spawn2 } from "node:child_process";
30678
+ import { randomUUID } from "node:crypto";
30656
30679
  import { tmpdir } from "node:os";
30657
30680
 
30658
30681
  // src/starter_template.ts
@@ -33660,6 +33683,96 @@ async function appExecuteWorkflow(client, args) {
33660
33683
  }
33661
33684
  if (status === "error" || cleanupFailed) process.exit(1);
33662
33685
  }
33686
+ async function streamAgentTextDeltas(body, onText) {
33687
+ const reader = body.getReader();
33688
+ const decoder = new TextDecoder();
33689
+ let buffer = "";
33690
+ let accumulated = "";
33691
+ try {
33692
+ for (; ; ) {
33693
+ const { value, done } = await reader.read();
33694
+ if (done) break;
33695
+ buffer += decoder.decode(value, { stream: true });
33696
+ const frames = buffer.split("\n\n");
33697
+ buffer = frames.pop() ?? "";
33698
+ for (const frame of frames) {
33699
+ for (const line of frame.split("\n")) {
33700
+ if (!line.startsWith("data:")) continue;
33701
+ const payload = line.slice(5).trim();
33702
+ if (!payload || payload === "[DONE]") continue;
33703
+ let chunk;
33704
+ try {
33705
+ chunk = JSON.parse(payload);
33706
+ } catch {
33707
+ continue;
33708
+ }
33709
+ if (chunk.type === "text-delta" && chunk.delta) {
33710
+ accumulated += chunk.delta;
33711
+ onText(chunk.delta);
33712
+ }
33713
+ }
33714
+ }
33715
+ }
33716
+ } catch {
33717
+ } finally {
33718
+ reader.releaseLock();
33719
+ }
33720
+ return accumulated;
33721
+ }
33722
+ var AGENT_RUN_POLL = {
33723
+ intervalMs: 1e3,
33724
+ settleTimeoutMs: 21 * 60 * 1e3,
33725
+ existenceTimeoutMs: 5e3
33726
+ };
33727
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
33728
+ async function fetchSettledAgentRun(client, appId, sessionId, runId, timing) {
33729
+ const settleDeadline = Date.now() + timing.settleTimeoutMs;
33730
+ const existenceDeadline = Date.now() + timing.existenceTimeoutMs;
33731
+ for (; ; ) {
33732
+ const { runs } = await client.listAgentRuns(appId, sessionId);
33733
+ const target = runId ? runs.find((r) => r.id === runId) : runs[runs.length - 1];
33734
+ if (target) {
33735
+ if (target.status !== "running" || Date.now() >= settleDeadline) return target;
33736
+ } else if (Date.now() >= existenceDeadline) {
33737
+ return void 0;
33738
+ }
33739
+ await sleep(timing.intervalMs);
33740
+ }
33741
+ }
33742
+ async function appAgentRun(client, args, timing = AGENT_RUN_POLL) {
33743
+ const sessionId = args.sessionId ?? `cli-${randomUUID()}`;
33744
+ const continuing = args.sessionId !== void 0;
33745
+ const res = await client.appAgentRunStream(args.app_id, args.alias, {
33746
+ session_id: sessionId,
33747
+ input: args.input
33748
+ });
33749
+ const runId = res.headers.get("x-app-agent-run-id") ?? void 0;
33750
+ if (res.body) {
33751
+ await streamAgentTextDeltas(res.body, (piece) => process.stderr.write(piece));
33752
+ }
33753
+ const run = await fetchSettledAgentRun(client, args.app_id, sessionId, runId, timing);
33754
+ if (!run) {
33755
+ console.error(`
33756
+ Could not find the settled run for session ${sessionId} on ${args.app_id}.`);
33757
+ console.error("The run may still be in progress \u2014 re-check with `lotics run` against the app's agent-runs.");
33758
+ process.exit(1);
33759
+ }
33760
+ if (args.json) {
33761
+ console.log(JSON.stringify(run, null, 2));
33762
+ } else if (run.output !== null && typeof run.output === "object") {
33763
+ console.log(JSON.stringify(run.output, null, 2));
33764
+ } else if (typeof run.output === "string") {
33765
+ console.log(run.output);
33766
+ }
33767
+ console.error(
33768
+ `
33769
+ Agent "${args.alias}" run ${run.id} \u2192 ${run.status}${run.error_message ? `: ${run.error_message}` : ""}`
33770
+ );
33771
+ console.error(
33772
+ continuing ? `Session: ${sessionId}` : `Session: ${sessionId} (fresh \u2014 pass --session ${sessionId} to continue this thread)`
33773
+ );
33774
+ if (run.status !== "completed") process.exit(1);
33775
+ }
33663
33776
  async function appWorkflowSet(client, args) {
33664
33777
  const projectDir = process.cwd();
33665
33778
  const meta3 = readAppMeta(projectDir);
@@ -33891,6 +34004,7 @@ function parseArgs(argv) {
33891
34004
  content: void 0,
33892
34005
  timezone: void 0,
33893
34006
  message: void 0,
34007
+ session: void 0,
33894
34008
  local: false,
33895
34009
  all: false,
33896
34010
  yes: false,
@@ -33952,6 +34066,9 @@ function parseArgs(argv) {
33952
34066
  case "--message":
33953
34067
  flags.message = argv[++i2];
33954
34068
  break;
34069
+ case "--session":
34070
+ flags.session = argv[++i2];
34071
+ break;
33955
34072
  case "--local":
33956
34073
  flags.local = true;
33957
34074
  break;
@@ -34022,6 +34139,36 @@ async function ingestJsonArgs(opts) {
34022
34139
  }
34023
34140
  }
34024
34141
 
34142
+ // src/org_commands.ts
34143
+ function printWorkspaceList(workspaces, currentId) {
34144
+ for (const ws of workspaces) {
34145
+ const marker = ws.id === currentId ? " (current)" : "";
34146
+ console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
34147
+ }
34148
+ }
34149
+ async function validateOrgWorkspacePin(client, orgId, profile) {
34150
+ const pinned = profile.workspace_id;
34151
+ if (!pinned) return;
34152
+ let workspaces;
34153
+ try {
34154
+ workspaces = await client.listWorkspaces();
34155
+ } catch (error51) {
34156
+ const message = error51 instanceof Error ? error51.message : String(error51);
34157
+ console.error(`Could not validate the pinned workspace (${message}) \u2014 keeping ${pinned}.`);
34158
+ return;
34159
+ }
34160
+ if (workspaces.some((w) => w.id === pinned)) return;
34161
+ clearProfileWorkspace(orgId);
34162
+ console.error(`Pinned workspace ${pinned} is no longer in ${profile.org_name} \u2014 cleared the stale pin.`);
34163
+ if (workspaces.length === 0) {
34164
+ console.error("This organization has no workspaces yet.");
34165
+ return;
34166
+ }
34167
+ console.error("Select one with:\n");
34168
+ console.error(" lotics workspace select <id>\n");
34169
+ printWorkspaceList(workspaces);
34170
+ }
34171
+
34025
34172
  // src/xlsx.ts
34026
34173
  import fs6 from "node:fs";
34027
34174
 
@@ -67999,7 +68146,7 @@ import { readFileSync as readFileSync2, writeFileSync, existsSync, mkdtempSync,
67999
68146
  import { tmpdir as tmpdir2 } from "node:os";
68000
68147
  import { join, dirname, resolve, extname, basename } from "node:path";
68001
68148
  import { fileURLToPath as fileURLToPath2 } from "node:url";
68002
- import { setTimeout as sleep } from "node:timers/promises";
68149
+ import { setTimeout as sleep2 } from "node:timers/promises";
68003
68150
  var HERE = dirname(fileURLToPath2(import.meta.url));
68004
68151
  function fail2(msg) {
68005
68152
  console.error(msg);
@@ -68125,7 +68272,7 @@ async function runPreviewCommand(filePath, flags) {
68125
68272
  const p = parseInt(readFileSync2(portFile, "utf8").split("\n")[0], 10);
68126
68273
  if (p) cdpPort = p;
68127
68274
  }
68128
- if (!cdpPort) await sleep(100);
68275
+ if (!cdpPort) await sleep2(100);
68129
68276
  }
68130
68277
  if (!cdpPort) throw new Error("Chrome did not expose a debugging port (launch failed?).");
68131
68278
  let target;
@@ -68135,7 +68282,7 @@ async function runPreviewCommand(filePath, flags) {
68135
68282
  target = list.find((t) => t.type === "page");
68136
68283
  } catch {
68137
68284
  }
68138
- if (!target?.webSocketDebuggerUrl) await sleep(100);
68285
+ if (!target?.webSocketDebuggerUrl) await sleep2(100);
68139
68286
  }
68140
68287
  if (!target?.webSocketDebuggerUrl) throw new Error("No Chrome page target available.");
68141
68288
  const cdp = await cdpConnect(target.webSocketDebuggerUrl);
@@ -68157,7 +68304,7 @@ async function runPreviewCommand(filePath, flags) {
68157
68304
  if (v.warnings?.length) warnings.push(...v.warnings);
68158
68305
  break;
68159
68306
  }
68160
- await sleep(75);
68307
+ await sleep2(75);
68161
68308
  }
68162
68309
  if (!done) throw new Error("Render timed out (page never signaled completion).");
68163
68310
  if (err2) throw new Error(`Render engine error: ${err2}`);
@@ -68260,6 +68407,10 @@ COMMANDS
68260
68407
  lotics app query set <alias> Push package.json#lotics.queries.<alias> to
68261
68408
  apps.queries via set_app_query (no deploy;
68262
68409
  re-synced by the next deploy from the manifest)
68410
+ lotics app agent run <app_id> <alias> '<json>' Run a bound app agent end-to-end
68411
+ (inputs: inline JSON, @file, or stdin; streams
68412
+ progress to stderr, reports the settled run;
68413
+ --session <id> continues a thread; --json)
68263
68414
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
68264
68415
  lotics app rename "<new name>" Rename the app's display name (launcher title)
68265
68416
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
@@ -68495,12 +68646,6 @@ var SOURCE_LABELS = {
68495
68646
  local_pointer: "local .lotics/config.json (pin)",
68496
68647
  global_profile: "global active profile"
68497
68648
  };
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
68649
  async function resolveWorkspace(client, ctx) {
68505
68650
  if (ctx.workspaceId) {
68506
68651
  client.setWorkspaceId(ctx.workspaceId);
@@ -68715,6 +68860,7 @@ async function main() {
68715
68860
  console.error("Note: a local pin (.lotics/config.json) overrides the global default in this directory. Use --local to change the pin here.");
68716
68861
  }
68717
68862
  }
68863
+ await validateOrgWorkspacePin(new LoticsClient({ apiKey: profile.api_key }), orgId, profile);
68718
68864
  return;
68719
68865
  }
68720
68866
  if (subcommand && subcommand !== "list") {
@@ -68764,6 +68910,7 @@ async function main() {
68764
68910
  console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
68765
68911
  console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
68766
68912
  console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
68913
+ console.error(" lotics app agent run <app_id> <alias> '<json>' Run a bound app agent (streams progress, reports the settled run)");
68767
68914
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
68768
68915
  console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
68769
68916
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
@@ -69022,6 +69169,42 @@ Available workspaces:`);
69022
69169
  }
69023
69170
  workflowUsage();
69024
69171
  }
69172
+ if (subcommand === "agent") {
69173
+ const action = toolArgs;
69174
+ const agentUsage = () => {
69175
+ console.error("Usage: lotics app agent run <app_id> <alias> ['<json>'|@inputs.json|stdin] [--session <id>] [--json]");
69176
+ console.error(" cat inputs.json | lotics app agent run <app_id> <alias> (read inputs from stdin)");
69177
+ console.error("Streams the run's progress to stderr; reports the settled run (structured output / text) to stdout.");
69178
+ console.error("--session <id> continues an existing thread; omitted mints a fresh session per run.");
69179
+ process.exit(1);
69180
+ };
69181
+ if (action === "run") {
69182
+ const appId = restArgs[0];
69183
+ const alias = restArgs[1];
69184
+ if (!appId || !alias) {
69185
+ agentUsage();
69186
+ }
69187
+ const ingested = await ingestJsonArgs({
69188
+ rawArg: restArgs[2],
69189
+ stdinIsTTY: process.stdin.isTTY ?? false,
69190
+ readFile: (p) => fs9.readFileSync(p, "utf-8"),
69191
+ readStdin
69192
+ });
69193
+ if (ingested.kind === "error") {
69194
+ console.error(ingested.message);
69195
+ process.exit(1);
69196
+ }
69197
+ await appAgentRun(client, {
69198
+ app_id: appId,
69199
+ alias,
69200
+ input: ingested.args,
69201
+ sessionId: flags.session,
69202
+ json: flags.json
69203
+ });
69204
+ return;
69205
+ }
69206
+ agentUsage();
69207
+ }
69025
69208
  if (subcommand === "query") {
69026
69209
  const action = toolArgs;
69027
69210
  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;
@@ -874,6 +899,15 @@ export declare class LoticsClient {
874
899
  session_id: string;
875
900
  input: Record<string, unknown>;
876
901
  }, signal?: AbortSignal): Promise<Response>;
902
+ /**
903
+ * A session's app-agent run history, oldest-first (the run just started is the
904
+ * last, and its exact id is on the stream response's `x-app-agent-run-id`
905
+ * header). Transcript excluded; structured `output`/`input` included. Mirrors
906
+ * GET /v1/apps/{app_id}/agent-runs.
907
+ */
908
+ listAgentRuns(app_id: string, session_id: string): Promise<{
909
+ runs: AppAgentRunSummary[];
910
+ }>;
877
911
  /**
878
912
  * Mint a presigned URL for uploading a file into an app. Mirrors
879
913
  * POST /v1/apps/{app_id}/files/upload-url.
@@ -629,6 +629,15 @@ export class LoticsClient {
629
629
  await this.throwResponseError(res);
630
630
  return res;
631
631
  }
632
+ /**
633
+ * A session's app-agent run history, oldest-first (the run just started is the
634
+ * last, and its exact id is on the stream response's `x-app-agent-run-id`
635
+ * header). Transcript excluded; structured `output`/`input` included. Mirrors
636
+ * GET /v1/apps/{app_id}/agent-runs.
637
+ */
638
+ async listAgentRuns(app_id, session_id) {
639
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/agent-runs?session_id=${encodeURIComponent(session_id)}`);
640
+ }
632
641
  /**
633
642
  * Mint a presigned URL for uploading a file into an app. Mirrors
634
643
  * 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.93.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {