@lotics/cli 0.116.0 → 0.127.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -26822,12 +26822,12 @@ var require_tokens = __commonJS({
26822
26822
  });
26823
26823
  }
26824
26824
  exports2.assignCategoriesMapProp = assignCategoriesMapProp;
26825
- function singleAssignCategoriesToksMap(path8, nextNode) {
26826
- utils_1.forEach(path8, function(pathNode) {
26825
+ function singleAssignCategoriesToksMap(path10, nextNode) {
26826
+ utils_1.forEach(path10, function(pathNode) {
26827
26827
  nextNode.categoryMatchesMap[pathNode.tokenTypeIdx] = true;
26828
26828
  });
26829
26829
  utils_1.forEach(nextNode.CATEGORIES, function(nextCategory) {
26830
- var newPath = path8.concat(nextNode);
26830
+ var newPath = path10.concat(nextNode);
26831
26831
  if (!utils_1.contains(newPath, nextCategory)) {
26832
26832
  singleAssignCategoriesToksMap(newPath, nextCategory);
26833
26833
  }
@@ -28590,10 +28590,10 @@ var require_interpreter = __commonJS({
28590
28590
  /** @class */
28591
28591
  (function(_super) {
28592
28592
  __extends(AbstractNextPossibleTokensWalker2, _super);
28593
- function AbstractNextPossibleTokensWalker2(topProd, path8) {
28593
+ function AbstractNextPossibleTokensWalker2(topProd, path10) {
28594
28594
  var _this = _super.call(this) || this;
28595
28595
  _this.topProd = topProd;
28596
- _this.path = path8;
28596
+ _this.path = path10;
28597
28597
  _this.possibleTokTypes = [];
28598
28598
  _this.nextProductionName = "";
28599
28599
  _this.nextProductionOccurrence = 0;
@@ -28647,9 +28647,9 @@ var require_interpreter = __commonJS({
28647
28647
  /** @class */
28648
28648
  (function(_super) {
28649
28649
  __extends(NextAfterTokenWalker2, _super);
28650
- function NextAfterTokenWalker2(topProd, path8) {
28651
- var _this = _super.call(this, topProd, path8) || this;
28652
- _this.path = path8;
28650
+ function NextAfterTokenWalker2(topProd, path10) {
28651
+ var _this = _super.call(this, topProd, path10) || this;
28652
+ _this.path = path10;
28653
28653
  _this.nextTerminalName = "";
28654
28654
  _this.nextTerminalOccurrence = 0;
28655
28655
  _this.nextTerminalName = _this.path.lastTok.name;
@@ -29346,10 +29346,10 @@ var require_lookahead = __commonJS({
29346
29346
  }
29347
29347
  return result;
29348
29348
  }
29349
- function pathToHashKeys(path8) {
29349
+ function pathToHashKeys(path10) {
29350
29350
  var keys3 = [""];
29351
- for (var i2 = 0; i2 < path8.length; i2++) {
29352
- var tokType = path8[i2];
29351
+ for (var i2 = 0; i2 < path10.length; i2++) {
29352
+ var tokType = path10[i2];
29353
29353
  var longerKeys = [];
29354
29354
  for (var j = 0; j < keys3.length; j++) {
29355
29355
  var currShorterKey = keys3[j];
@@ -29661,9 +29661,9 @@ var require_checks = __commonJS({
29661
29661
  return errors2;
29662
29662
  }
29663
29663
  exports2.validateRuleIsOverridden = validateRuleIsOverridden;
29664
- function validateNoLeftRecursion(topRule, currRule, errMsgProvider, path8) {
29665
- if (path8 === void 0) {
29666
- path8 = [];
29664
+ function validateNoLeftRecursion(topRule, currRule, errMsgProvider, path10) {
29665
+ if (path10 === void 0) {
29666
+ path10 = [];
29667
29667
  }
29668
29668
  var errors2 = [];
29669
29669
  var nextNonTerminals = getFirstNoneTerminal(currRule.definition);
@@ -29676,15 +29676,15 @@ var require_checks = __commonJS({
29676
29676
  errors2.push({
29677
29677
  message: errMsgProvider.buildLeftRecursionError({
29678
29678
  topLevelRule: topRule,
29679
- leftRecursionPath: path8
29679
+ leftRecursionPath: path10
29680
29680
  }),
29681
29681
  type: parser_1.ParserDefinitionErrorType.LEFT_RECURSION,
29682
29682
  ruleName
29683
29683
  });
29684
29684
  }
29685
- var validNextSteps = utils2.difference(nextNonTerminals, path8.concat([topRule]));
29685
+ var validNextSteps = utils2.difference(nextNonTerminals, path10.concat([topRule]));
29686
29686
  var errorsFromNextSteps = utils2.map(validNextSteps, function(currRefRule) {
29687
- var newPath = utils2.cloneArr(path8);
29687
+ var newPath = utils2.cloneArr(path10);
29688
29688
  newPath.push(currRefRule);
29689
29689
  return validateNoLeftRecursion(topRule, currRefRule, errMsgProvider, newPath);
29690
29690
  });
@@ -37293,8 +37293,8 @@ var require_utils4 = __commonJS({
37293
37293
  var result = transform2[inputType][outputType](input);
37294
37294
  return result;
37295
37295
  };
37296
- exports2.resolve = function(path8) {
37297
- var parts = path8.split("/");
37296
+ exports2.resolve = function(path10) {
37297
+ var parts = path10.split("/");
37298
37298
  var result = [];
37299
37299
  for (var index = 0; index < parts.length; index++) {
37300
37300
  var part = parts[index];
@@ -43147,18 +43147,18 @@ var require_object = __commonJS({
43147
43147
  var object2 = new ZipObject(name2, zipObjectContent, o);
43148
43148
  this.files[name2] = object2;
43149
43149
  };
43150
- var parentFolder = function(path8) {
43151
- if (path8.slice(-1) === "/") {
43152
- path8 = path8.substring(0, path8.length - 1);
43150
+ var parentFolder = function(path10) {
43151
+ if (path10.slice(-1) === "/") {
43152
+ path10 = path10.substring(0, path10.length - 1);
43153
43153
  }
43154
- var lastSlash = path8.lastIndexOf("/");
43155
- return lastSlash > 0 ? path8.substring(0, lastSlash) : "";
43154
+ var lastSlash = path10.lastIndexOf("/");
43155
+ return lastSlash > 0 ? path10.substring(0, lastSlash) : "";
43156
43156
  };
43157
- var forceTrailingSlash = function(path8) {
43158
- if (path8.slice(-1) !== "/") {
43159
- path8 += "/";
43157
+ var forceTrailingSlash = function(path10) {
43158
+ if (path10.slice(-1) !== "/") {
43159
+ path10 += "/";
43160
43160
  }
43161
- return path8;
43161
+ return path10;
43162
43162
  };
43163
43163
  var folderAdd = function(name2, createFolders) {
43164
43164
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults2.createFolders;
@@ -44160,9 +44160,9 @@ var require_lib4 = __commonJS({
44160
44160
  // src/cli.ts
44161
44161
  import dns from "node:dns";
44162
44162
  import net2 from "node:net";
44163
- import fs9 from "node:fs";
44164
- import os2 from "node:os";
44165
- import path7 from "node:path";
44163
+ import fs11 from "node:fs";
44164
+ import os4 from "node:os";
44165
+ import path9 from "node:path";
44166
44166
  import readline from "node:readline";
44167
44167
 
44168
44168
  // ../shared/src/transport_error.ts
@@ -44181,16 +44181,81 @@ function transportErrorMessage(status, parsed) {
44181
44181
  }
44182
44182
 
44183
44183
  // src/client.ts
44184
+ import fs2 from "node:fs";
44185
+ import path2 from "node:path";
44186
+
44187
+ // src/invocation.ts
44184
44188
  import fs from "node:fs";
44189
+ import os from "node:os";
44185
44190
  import path from "node:path";
44191
+ function telemetryEnabled(env = process.env) {
44192
+ return env.LOTICS_TELEMETRY === "1";
44193
+ }
44194
+ function commandPath(argv) {
44195
+ const parts = [];
44196
+ for (const raw of argv) {
44197
+ if (parts.length >= 3) break;
44198
+ if (raw.startsWith("-") || raw.startsWith("@") || raw.startsWith("{")) break;
44199
+ if (raw.includes("/") || raw.includes(".")) break;
44200
+ if (!/^[a-z][a-z0-9_-]*$/i.test(raw)) break;
44201
+ if (isResourceId(raw)) break;
44202
+ parts.push(raw);
44203
+ }
44204
+ return parts.length > 0 ? parts.join(".") : "unknown";
44205
+ }
44206
+ function isResourceId(token) {
44207
+ const match2 = /^[a-z]{2,5}_([A-Za-z0-9]{8,})$/.exec(token);
44208
+ return match2 !== null && /[A-Z0-9]/.test(match2[1]);
44209
+ }
44210
+ function userAgent(version2) {
44211
+ return `lotics-cli/${version2} node/${process.version} ${os.platform()}`;
44212
+ }
44213
+ var invocation = null;
44214
+ function setInvocation(command, session, version2) {
44215
+ invocation = { command, session, userAgent: userAgent(version2) };
44216
+ }
44217
+ function getInvocation() {
44218
+ return invocation;
44219
+ }
44220
+ var IDLE_GAP_MS = 30 * 60 * 1e3;
44221
+ function sessionStorePath() {
44222
+ return path.join(os.homedir(), ".lotics", "session.json");
44223
+ }
44224
+ function readSession(file2) {
44225
+ try {
44226
+ const parsed = JSON.parse(fs.readFileSync(file2, "utf-8"));
44227
+ if (typeof parsed !== "object" || parsed === null) return null;
44228
+ const { id, last_seen } = parsed;
44229
+ if (typeof id !== "string" || typeof last_seen !== "number") return null;
44230
+ return { id, last_seen };
44231
+ } catch {
44232
+ return null;
44233
+ }
44234
+ }
44235
+ function sessionId(now = Date.now(), env = process.env) {
44236
+ if (!telemetryEnabled(env)) return null;
44237
+ const harnessSession = env.CLAUDE_CODE_SESSION_ID;
44238
+ if (harnessSession !== void 0 && harnessSession !== "") return harnessSession;
44239
+ const file2 = sessionStorePath();
44240
+ const stored = readSession(file2);
44241
+ const id = stored !== null && now - stored.last_seen < IDLE_GAP_MS ? stored.id : `cli_${now.toString(36)}${Math.random().toString(36).slice(2, 10)}`;
44242
+ try {
44243
+ fs.mkdirSync(path.dirname(file2), { recursive: true });
44244
+ fs.writeFileSync(file2, JSON.stringify({ id, last_seen: now }), { mode: 384 });
44245
+ } catch {
44246
+ }
44247
+ return id;
44248
+ }
44249
+
44250
+ // src/client.ts
44186
44251
  function findAvailableFilename(dir, filename, reserved) {
44187
44252
  const isTaken = (name2) => {
44188
- const full = path.join(dir, name2);
44253
+ const full = path2.join(dir, name2);
44189
44254
  if (reserved?.has(full)) return true;
44190
- return fs.existsSync(full);
44255
+ return fs2.existsSync(full);
44191
44256
  };
44192
44257
  const claim = (name2) => {
44193
- reserved?.add(path.join(dir, name2));
44258
+ reserved?.add(path2.join(dir, name2));
44194
44259
  return name2;
44195
44260
  };
44196
44261
  if (!isTaken(filename)) return claim(filename);
@@ -44219,7 +44284,7 @@ var MIME_MAP = {
44219
44284
  ".zip": "application/zip"
44220
44285
  };
44221
44286
  function getMimeType(filename) {
44222
- const ext = path.extname(filename).toLowerCase();
44287
+ const ext = path2.extname(filename).toLowerCase();
44223
44288
  return MIME_MAP[ext] ?? "application/octet-stream";
44224
44289
  }
44225
44290
  var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
@@ -44250,9 +44315,21 @@ var LoticsClient = class {
44250
44315
  }
44251
44316
  throw new Error(`${response.status}: ${message2}`);
44252
44317
  }
44318
+ /**
44319
+ * The backend's `log()` middleware registers `user-agent` and
44320
+ * `x-posthog-session-id` onto the per-request Logger, so they ride EVERY log
44321
+ * line that request emits — the validation 400, the tool error, the timing.
44322
+ * Sending them is therefore the whole of the correlation work: it turns an
44323
+ * anonymous API-key request into "`app workflow set`, from cli 0.117.0, the
44324
+ * fourth command of this session".
44325
+ */
44253
44326
  buildHeaders() {
44327
+ const invocation2 = getInvocation();
44254
44328
  const headers = {
44255
- "Authorization": `Bearer ${this.apiKey}`
44329
+ "Authorization": `Bearer ${this.apiKey}`,
44330
+ // Unversioned when the SDK is used as a library — there is no CLI process
44331
+ // whose version to name, and a wrong one is worse than none.
44332
+ "user-agent": invocation2?.userAgent ?? "lotics-cli"
44256
44333
  };
44257
44334
  if (this.workspaceId) {
44258
44335
  headers["x-workspace-id"] = this.workspaceId;
@@ -44260,10 +44337,16 @@ var LoticsClient = class {
44260
44337
  if (this.viewAsMemberId) {
44261
44338
  headers["x-view-as-member-id"] = this.viewAsMemberId;
44262
44339
  }
44340
+ if (invocation2) {
44341
+ headers["x-lotics-cli-command"] = invocation2.command;
44342
+ if (invocation2.session !== null) {
44343
+ headers["x-posthog-session-id"] = invocation2.session;
44344
+ }
44345
+ }
44263
44346
  return headers;
44264
44347
  }
44265
- async request(method, path8, body) {
44266
- const url2 = `${this.baseUrl}${path8}`;
44348
+ async request(method, path10, body) {
44349
+ const url2 = `${this.baseUrl}${path10}`;
44267
44350
  const headers = this.buildHeaders();
44268
44351
  const init = { method, headers };
44269
44352
  if (body !== void 0) {
@@ -44744,7 +44827,8 @@ var LoticsClient = class {
44744
44827
  ...body.inputs ? { inputs: body.inputs } : {},
44745
44828
  ...body.outputs ? { outputs: body.outputs } : {},
44746
44829
  ...body.name ? { name: body.name } : {},
44747
- ...body.description ? { description: body.description } : {}
44830
+ ...body.description ? { description: body.description } : {},
44831
+ ...body.expected_body_sha ? { expected_body_sha: body.expected_body_sha } : {}
44748
44832
  });
44749
44833
  }
44750
44834
  /**
@@ -44755,8 +44839,13 @@ var LoticsClient = class {
44755
44839
  * deploy validates the manifest. Note: `apps.queries` is manifest-authoritative,
44756
44840
  * so the next `lotics app deploy` overwrites this from the manifest.
44757
44841
  */
44758
- async setAppQuery(app_id, alias, declaration) {
44759
- return this.execute("set_app_query", { app_id, alias, declaration });
44842
+ async setAppQuery(app_id, alias, declaration, expected_sha) {
44843
+ return this.execute("set_app_query", {
44844
+ app_id,
44845
+ alias,
44846
+ declaration,
44847
+ ...expected_sha ? { expected_sha } : {}
44848
+ });
44760
44849
  }
44761
44850
  /**
44762
44851
  * Bind (create or replace) an app agent by alias via the `set_app_agent` tool
@@ -44955,6 +45044,7 @@ var LoticsClient = class {
44955
45044
  formData.append("capabilities", JSON.stringify(args.capabilities));
44956
45045
  }
44957
45046
  formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
45047
+ formData.append("agent_aliases", JSON.stringify(args.agent_aliases ?? []));
44958
45048
  formData.append("query_aliases", JSON.stringify(args.query_aliases ?? []));
44959
45049
  const url2 = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
44960
45050
  const response = await fetch(url2, {
@@ -44981,8 +45071,8 @@ var LoticsClient = class {
44981
45071
  throw new Error(`${response.status}: Failed to download file`);
44982
45072
  }
44983
45073
  const buffer = Buffer.from(await response.arrayBuffer());
44984
- const absolutePath = path.resolve(outputPath);
44985
- await fs.promises.writeFile(absolutePath, buffer);
45074
+ const absolutePath = path2.resolve(outputPath);
45075
+ await fs2.promises.writeFile(absolutePath, buffer);
44986
45076
  return absolutePath;
44987
45077
  }
44988
45078
  async downloadFileById(fileId, outputDir, options) {
@@ -44998,10 +45088,10 @@ var LoticsClient = class {
44998
45088
  const match2 = disposition.match(/filename="?([^";\n]+)"?/);
44999
45089
  const originalFilename = match2?.[1] ?? fileId;
45000
45090
  const buffer = Buffer.from(await response.arrayBuffer());
45001
- const dir = outputDir ? path.resolve(outputDir) : process.cwd();
45091
+ const dir = outputDir ? path2.resolve(outputDir) : process.cwd();
45002
45092
  const filename = findAvailableFilename(dir, originalFilename, options?.reserved);
45003
- const absolutePath = path.join(dir, filename);
45004
- await fs.promises.writeFile(absolutePath, buffer);
45093
+ const absolutePath = path2.join(dir, filename);
45094
+ await fs2.promises.writeFile(absolutePath, buffer);
45005
45095
  return { path: absolutePath, filename };
45006
45096
  }
45007
45097
  async downloadRecordFiles(recordId, fieldKey, outputDir) {
@@ -45029,9 +45119,9 @@ var LoticsClient = class {
45029
45119
  async uploadFiles(filePaths, options) {
45030
45120
  const formData = new FormData();
45031
45121
  for (let i2 = 0; i2 < filePaths.length; i2++) {
45032
- const absolutePath = path.resolve(filePaths[i2]);
45033
- const buffer = await fs.promises.readFile(absolutePath);
45034
- const filename = options?.filenames?.[i2] ?? path.basename(absolutePath);
45122
+ const absolutePath = path2.resolve(filePaths[i2]);
45123
+ const buffer = await fs2.promises.readFile(absolutePath);
45124
+ const filename = options?.filenames?.[i2] ?? path2.basename(absolutePath);
45035
45125
  const mimeType = getMimeType(filename);
45036
45126
  const blob = new Blob([buffer], { type: mimeType });
45037
45127
  formData.append("file", blob, filename);
@@ -45047,24 +45137,189 @@ var LoticsClient = class {
45047
45137
  }
45048
45138
  };
45049
45139
 
45140
+ // src/telemetry.ts
45141
+ import crypto from "node:crypto";
45142
+ import fs3 from "node:fs";
45143
+ import os2 from "node:os";
45144
+ import path3 from "node:path";
45145
+
45146
+ // ../shared/src/content_sha.ts
45147
+ import { createHash } from "node:crypto";
45148
+ function contentSha(text) {
45149
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
45150
+ }
45151
+ function declarationSha(value2) {
45152
+ return contentSha(JSON.stringify(canonicalize(value2)));
45153
+ }
45154
+ function canonicalize(value2) {
45155
+ if (Array.isArray(value2)) return value2.map(canonicalize);
45156
+ if (typeof value2 === "object" && value2 !== null) {
45157
+ const out = {};
45158
+ for (const key of Object.keys(value2).sort()) {
45159
+ out[key] = canonicalize(value2[key]);
45160
+ }
45161
+ return out;
45162
+ }
45163
+ return value2;
45164
+ }
45165
+
45166
+ // src/telemetry.ts
45167
+ var MAX_ERROR_CHARS = 2e3;
45168
+ var MAX_SPOOL_BYTES = 1024 * 1024;
45169
+ var MAX_FLUSH_EVENTS = 500;
45170
+ function spoolPath() {
45171
+ return path3.join(os2.homedir(), ".lotics", "telemetry.ndjson");
45172
+ }
45173
+ var stderrTail = "";
45174
+ function recordStderr(text) {
45175
+ stderrTail = (stderrTail + text).slice(-MAX_ERROR_CHARS);
45176
+ }
45177
+ function installStderrCapture() {
45178
+ const original = process.stderr.write.bind(process.stderr);
45179
+ process.stderr.write = ((chunk, encoding, cb) => {
45180
+ recordStderr(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
45181
+ return typeof encoding === "function" ? original(chunk, encoding) : original(chunk, encoding, cb);
45182
+ });
45183
+ }
45184
+ function stripNarration(tail) {
45185
+ return tail.split("\n").filter((line) => !line.startsWith("lotics \u2192 ")).join("\n").trim();
45186
+ }
45187
+ function argsShape(args, prefix = "", depth = 0) {
45188
+ if (depth > 6) return [];
45189
+ if (Array.isArray(args)) {
45190
+ const head = args.length > 0 ? argsShape(args[0], `${prefix}[]`, depth + 1) : [];
45191
+ return [`${prefix}:array`, ...head];
45192
+ }
45193
+ if (typeof args === "object" && args !== null) {
45194
+ const out = prefix === "" ? [] : [`${prefix}:object`];
45195
+ for (const [key, value2] of Object.entries(args)) {
45196
+ out.push(...argsShape(value2, prefix === "" ? key : `${prefix}.${key}`, depth + 1));
45197
+ }
45198
+ return out;
45199
+ }
45200
+ return prefix === "" ? [] : [`${prefix}:${args === null ? "null" : typeof args}`];
45201
+ }
45202
+ var pendingArgs = null;
45203
+ function noteToolArgs(args) {
45204
+ if (!telemetryEnabled()) return;
45205
+ pendingArgs = { args_sha: declarationSha(args), args_shape: argsShape(args) };
45206
+ }
45207
+ var pendingScope = {};
45208
+ function noteScope(scope) {
45209
+ if (!telemetryEnabled()) return;
45210
+ pendingScope = { ...pendingScope, ...scope };
45211
+ }
45212
+ function buildEvent(exitCode, durationMs, now) {
45213
+ const invocation2 = getInvocation();
45214
+ if (invocation2 === null || invocation2.session === null) return null;
45215
+ const message2 = exitCode === 0 ? "" : stripNarration(stderrTail);
45216
+ return {
45217
+ event_id: crypto.randomUUID(),
45218
+ ts: now.toISOString(),
45219
+ cli_session_id: invocation2.session,
45220
+ cli_command: invocation2.command,
45221
+ // `--flag=value` splits — the name is a fact about the command, the value is data.
45222
+ cli_flags: process.argv.slice(2).filter((a) => a.startsWith("-")).map((a) => a.split("=")[0]),
45223
+ exit_code: exitCode,
45224
+ duration_ms: durationMs,
45225
+ cli_version: invocation2.userAgent.split(" ")[0].replace("lotics-cli/", ""),
45226
+ ...message2 !== "" && { error_message: message2 },
45227
+ ...pendingArgs,
45228
+ ...pendingScope
45229
+ };
45230
+ }
45231
+ function appendEvent(event) {
45232
+ const file2 = spoolPath();
45233
+ try {
45234
+ fs3.mkdirSync(path3.dirname(file2), { recursive: true });
45235
+ if ((fs3.statSync(file2, { throwIfNoEntry: false })?.size ?? 0) > MAX_SPOOL_BYTES) {
45236
+ fs3.rmSync(file2, { force: true });
45237
+ }
45238
+ fs3.appendFileSync(file2, `${JSON.stringify(event)}
45239
+ `, { mode: 384 });
45240
+ } catch {
45241
+ }
45242
+ }
45243
+ function readSpool() {
45244
+ try {
45245
+ const events = [];
45246
+ const lines = [];
45247
+ for (const line of fs3.readFileSync(spoolPath(), "utf-8").split("\n")) {
45248
+ if (line.trim() === "") continue;
45249
+ try {
45250
+ events.push(JSON.parse(line));
45251
+ lines.push(line);
45252
+ } catch {
45253
+ }
45254
+ }
45255
+ return { events, lines };
45256
+ } catch {
45257
+ return { events: [], lines: [] };
45258
+ }
45259
+ }
45260
+ function clearSpool(sent) {
45261
+ try {
45262
+ const current = fs3.readFileSync(spoolPath(), "utf-8");
45263
+ fs3.writeFileSync(spoolPath(), current.startsWith(sent) ? current.slice(sent.length) : "", {
45264
+ mode: 384
45265
+ });
45266
+ } catch {
45267
+ }
45268
+ }
45269
+ function installTelemetry(startedAt2 = Date.now()) {
45270
+ if (!telemetryEnabled()) return;
45271
+ installStderrCapture();
45272
+ process.on("exit", (code) => {
45273
+ const event = buildEvent(code, Date.now() - startedAt2, /* @__PURE__ */ new Date());
45274
+ if (event !== null) appendEvent(event);
45275
+ });
45276
+ }
45277
+ function flushSpool(post, minEvents = 5) {
45278
+ if (!telemetryEnabled()) return;
45279
+ const { events, lines } = readSpool();
45280
+ if (events.length < minEvents) return;
45281
+ const batch = events.slice(0, MAX_FLUSH_EVENTS);
45282
+ const sent = `${lines.slice(0, batch.length).join("\n")}
45283
+ `;
45284
+ void post(batch).then(() => {
45285
+ clearSpool(sent);
45286
+ }).catch(() => {
45287
+ });
45288
+ }
45289
+ async function postEvents(baseUrl, apiKey, events, timeoutMs = 5e3) {
45290
+ const controller = new AbortController();
45291
+ const timer2 = setTimeout(() => controller.abort(), timeoutMs);
45292
+ try {
45293
+ const res = await fetch(`${baseUrl}/v1/cli/telemetry`, {
45294
+ method: "POST",
45295
+ headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
45296
+ body: JSON.stringify({ events }),
45297
+ signal: controller.signal
45298
+ });
45299
+ if (!res.ok) throw new Error(`telemetry flush: ${res.status}`);
45300
+ } finally {
45301
+ clearTimeout(timer2);
45302
+ }
45303
+ }
45304
+
45050
45305
  // src/config.ts
45051
- import fs2 from "node:fs";
45052
- import path2 from "node:path";
45053
- import os from "node:os";
45306
+ import fs4 from "node:fs";
45307
+ import path4 from "node:path";
45308
+ import os3 from "node:os";
45054
45309
  function globalConfigFile() {
45055
- return path2.join(os.homedir(), ".lotics", "config.json");
45310
+ return path4.join(os3.homedir(), ".lotics", "config.json");
45056
45311
  }
45057
45312
  function configFileForScope(scope) {
45058
45313
  if (scope === "local") {
45059
- return path2.join(process.cwd(), ".lotics", "config.json");
45314
+ return path4.join(process.cwd(), ".lotics", "config.json");
45060
45315
  }
45061
45316
  let dir = process.cwd();
45062
45317
  for (; ; ) {
45063
- const candidate = path2.join(dir, ".lotics", "config.json");
45064
- if (fs2.existsSync(candidate)) {
45318
+ const candidate = path4.join(dir, ".lotics", "config.json");
45319
+ if (fs4.existsSync(candidate)) {
45065
45320
  return candidate;
45066
45321
  }
45067
- const parent = path2.dirname(dir);
45322
+ const parent = path4.dirname(dir);
45068
45323
  if (parent === dir) {
45069
45324
  break;
45070
45325
  }
@@ -45074,25 +45329,25 @@ function configFileForScope(scope) {
45074
45329
  }
45075
45330
  function readConfigFile(file2) {
45076
45331
  try {
45077
- const raw = fs2.readFileSync(file2, "utf-8");
45332
+ const raw = fs4.readFileSync(file2, "utf-8");
45078
45333
  return JSON.parse(raw);
45079
45334
  } catch {
45080
45335
  return null;
45081
45336
  }
45082
45337
  }
45083
45338
  function writeConfigFile(file2, config2) {
45084
- fs2.mkdirSync(path2.dirname(file2), { recursive: true, mode: 448 });
45339
+ fs4.mkdirSync(path4.dirname(file2), { recursive: true, mode: 448 });
45085
45340
  const tmp = `${file2}.${process.pid}.tmp`;
45086
- fs2.writeFileSync(tmp, JSON.stringify(config2, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
45087
- fs2.chmodSync(tmp, 384);
45088
- fs2.renameSync(tmp, file2);
45341
+ fs4.writeFileSync(tmp, JSON.stringify(config2, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
45342
+ fs4.chmodSync(tmp, 384);
45343
+ fs4.renameSync(tmp, file2);
45089
45344
  }
45090
45345
  function saveConfig(config2, scope = "auto") {
45091
45346
  writeConfigFile(configFileForScope(scope), config2);
45092
45347
  }
45093
45348
  function deleteConfig(scope = "auto") {
45094
45349
  try {
45095
- fs2.unlinkSync(configFileForScope(scope));
45350
+ fs4.unlinkSync(configFileForScope(scope));
45096
45351
  } catch {
45097
45352
  }
45098
45353
  }
@@ -45319,20 +45574,20 @@ Update available: ${current} \u2192 ${latest}`);
45319
45574
  // src/version.ts
45320
45575
  import { readFileSync } from "node:fs";
45321
45576
  import { fileURLToPath } from "node:url";
45322
- import path3 from "node:path";
45323
- var pkgPath = path3.resolve(path3.dirname(fileURLToPath(import.meta.url)), "../../package.json");
45577
+ import path5 from "node:path";
45578
+ var pkgPath = path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "../../package.json");
45324
45579
  var pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
45325
45580
  var VERSION = pkg.version;
45326
45581
 
45327
45582
  // src/app_commands.ts
45328
- import fs4 from "node:fs";
45329
- import path5 from "node:path";
45583
+ import fs6 from "node:fs";
45584
+ import path7 from "node:path";
45330
45585
  import { spawn as spawn2 } from "node:child_process";
45331
45586
  import { randomUUID } from "node:crypto";
45332
45587
  import { tmpdir } from "node:os";
45333
45588
 
45334
45589
  // src/starter_template.ts
45335
- var STARTER_FALLBACK_UI_VERSION = "27.11.0";
45590
+ var STARTER_FALLBACK_UI_VERSION = "28.1.0";
45336
45591
  var STARTER_FALLBACK_SDK_VERSION = "0.52.0";
45337
45592
  var STARTER_REACT_NATIVE_VERSION = "0.85.3";
45338
45593
  var VITEST_SETUP_FILENAME = "vitest.setup.ts";
@@ -45538,7 +45793,6 @@ export default defineConfig({
45538
45793
  // const base = loticsResolve();
45539
45794
  // resolve: { ...base, alias: [...base.alias, { find: "x", replacement: "y" }] }
45540
45795
  resolve: loticsResolve(),
45541
- },
45542
45796
  optimizeDeps: {
45543
45797
  // The dev dep-optimizer must pre-bundle @lotics/ui's RN-ecosystem + markdown
45544
45798
  // CJS-interop deps or \`lotics app dev\` blanks the iframe ("does not provide an
@@ -46559,7 +46813,7 @@ function createUploadRelay(now = Date.now) {
46559
46813
  }
46560
46814
 
46561
46815
  // src/dev/file_relay.ts
46562
- import { createHash } from "node:crypto";
46816
+ import { createHash as createHash2 } from "node:crypto";
46563
46817
  var TTL_MS = 24 * 60 * 60 * 1e3;
46564
46818
  var MAX_ENTRIES = 5e3;
46565
46819
  var PRESIGNED_KEYS = /* @__PURE__ */ new Set(["url", "thumbnail_url", "preview_url"]);
@@ -46567,7 +46821,7 @@ var isFileObject = (o) => typeof o.filename === "string" && typeof o.mime_type =
46567
46821
  var isRemoteUrl = (v) => typeof v === "string" && /^https?:\/\//i.test(v);
46568
46822
  function createFileRelay(wrapperOrigin, now = Date.now) {
46569
46823
  const seen = /* @__PURE__ */ new Map();
46570
- const tokenFor = (url2) => createHash("sha256").update(url2).digest("hex").slice(0, 24);
46824
+ const tokenFor = (url2) => createHash2("sha256").update(url2).digest("hex").slice(0, 24);
46571
46825
  const remember = (url2) => {
46572
46826
  const token = tokenFor(url2);
46573
46827
  seen.set(token, { url: url2, at: now() });
@@ -46681,7 +46935,7 @@ async function startDevServer(args) {
46681
46935
  if (req.method === "POST" && url2 === "/_rpc") {
46682
46936
  try {
46683
46937
  const body = await readJson(req);
46684
- const startedAt = Date.now();
46938
+ const startedAt2 = Date.now();
46685
46939
  const result = await dispatchRpc(
46686
46940
  args.client,
46687
46941
  {
@@ -46691,7 +46945,7 @@ async function startDevServer(args) {
46691
46945
  },
46692
46946
  { commentsEnabled: args.commentsEnabled }
46693
46947
  );
46694
- const ms = Date.now() - startedAt;
46948
+ const ms = Date.now() - startedAt2;
46695
46949
  process.stderr.write(`[rpc] ${body.op} ${ms}ms
46696
46950
  `);
46697
46951
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -46844,13 +47098,13 @@ async function startDevServer(args) {
46844
47098
  const chunks = [];
46845
47099
  for await (const chunk of req) chunks.push(chunk);
46846
47100
  const bytes = Buffer.concat(chunks);
46847
- const startedAt = Date.now();
47101
+ const startedAt2 = Date.now();
46848
47102
  const upstream = await fetch(destination, {
46849
47103
  method: "PUT",
46850
47104
  body: bytes,
46851
47105
  headers: { "Content-Type": req.headers["content-type"] ?? "application/octet-stream" }
46852
47106
  });
46853
- const ms = Date.now() - startedAt;
47107
+ const ms = Date.now() - startedAt2;
46854
47108
  if (upstream.ok) {
46855
47109
  uploads.settle(fileId);
46856
47110
  process.stderr.write(`[upload] ${fileId} ${bytes.length}B ${ms}ms
@@ -47246,6 +47500,9 @@ function calledAppAliases(sourceText) {
47246
47500
  }
47247
47501
  return out;
47248
47502
  }
47503
+ function orphanedAliases(bound, called) {
47504
+ return unboundAliases(bound, called);
47505
+ }
47249
47506
  function unboundAliases(called, bound) {
47250
47507
  const missing = (from, against) => {
47251
47508
  const have = new Set(against ?? []);
@@ -47533,8 +47790,8 @@ export type AppRoles = typeof ROLE;
47533
47790
  }
47534
47791
 
47535
47792
  // src/app_workflow_check.ts
47536
- import fs3 from "node:fs";
47537
- import path4 from "node:path";
47793
+ import fs5 from "node:fs";
47794
+ import path6 from "node:path";
47538
47795
  import { createRequire } from "node:module";
47539
47796
 
47540
47797
  // ../shared/src/parse_workflow_js.ts
@@ -48306,10 +48563,10 @@ function mergeDefs(...defs) {
48306
48563
  function cloneDef(schema) {
48307
48564
  return mergeDefs(schema._zod.def);
48308
48565
  }
48309
- function getElementAtPath(obj, path8) {
48310
- if (!path8)
48566
+ function getElementAtPath(obj, path10) {
48567
+ if (!path10)
48311
48568
  return obj;
48312
- return path8.reduce((acc, key) => acc?.[key], obj);
48569
+ return path10.reduce((acc, key) => acc?.[key], obj);
48313
48570
  }
48314
48571
  function promiseAllObject(promisesObj) {
48315
48572
  const keys3 = Object.keys(promisesObj);
@@ -48718,11 +48975,11 @@ function explicitlyAborted(x2, startIndex = 0) {
48718
48975
  }
48719
48976
  return false;
48720
48977
  }
48721
- function prefixIssues(path8, issues) {
48978
+ function prefixIssues(path10, issues) {
48722
48979
  return issues.map((iss) => {
48723
48980
  var _a4;
48724
48981
  (_a4 = iss).path ?? (_a4.path = []);
48725
- iss.path.unshift(path8);
48982
+ iss.path.unshift(path10);
48726
48983
  return iss;
48727
48984
  });
48728
48985
  }
@@ -48869,16 +49126,16 @@ function flattenError(error52, mapper = (issue2) => issue2.message) {
48869
49126
  }
48870
49127
  function formatError(error52, mapper = (issue2) => issue2.message) {
48871
49128
  const fieldErrors = { _errors: [] };
48872
- const processError = (error53, path8 = []) => {
49129
+ const processError = (error53, path10 = []) => {
48873
49130
  for (const issue2 of error53.issues) {
48874
49131
  if (issue2.code === "invalid_union" && issue2.errors.length) {
48875
- issue2.errors.map((issues) => processError({ issues }, [...path8, ...issue2.path]));
49132
+ issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
48876
49133
  } else if (issue2.code === "invalid_key") {
48877
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
49134
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
48878
49135
  } else if (issue2.code === "invalid_element") {
48879
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
49136
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
48880
49137
  } else {
48881
- const fullpath = [...path8, ...issue2.path];
49138
+ const fullpath = [...path10, ...issue2.path];
48882
49139
  if (fullpath.length === 0) {
48883
49140
  fieldErrors._errors.push(mapper(issue2));
48884
49141
  } else {
@@ -48905,17 +49162,17 @@ function formatError(error52, mapper = (issue2) => issue2.message) {
48905
49162
  }
48906
49163
  function treeifyError(error52, mapper = (issue2) => issue2.message) {
48907
49164
  const result = { errors: [] };
48908
- const processError = (error53, path8 = []) => {
49165
+ const processError = (error53, path10 = []) => {
48909
49166
  var _a4, _b2;
48910
49167
  for (const issue2 of error53.issues) {
48911
49168
  if (issue2.code === "invalid_union" && issue2.errors.length) {
48912
- issue2.errors.map((issues) => processError({ issues }, [...path8, ...issue2.path]));
49169
+ issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
48913
49170
  } else if (issue2.code === "invalid_key") {
48914
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
49171
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
48915
49172
  } else if (issue2.code === "invalid_element") {
48916
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
49173
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
48917
49174
  } else {
48918
- const fullpath = [...path8, ...issue2.path];
49175
+ const fullpath = [...path10, ...issue2.path];
48919
49176
  if (fullpath.length === 0) {
48920
49177
  result.errors.push(mapper(issue2));
48921
49178
  continue;
@@ -48947,8 +49204,8 @@ function treeifyError(error52, mapper = (issue2) => issue2.message) {
48947
49204
  }
48948
49205
  function toDotPath(_path) {
48949
49206
  const segs = [];
48950
- const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
48951
- for (const seg of path8) {
49207
+ const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
49208
+ for (const seg of path10) {
48952
49209
  if (typeof seg === "number")
48953
49210
  segs.push(`[${seg}]`);
48954
49211
  else if (typeof seg === "symbol")
@@ -61640,13 +61897,13 @@ function resolveRef(ref2, ctx) {
61640
61897
  if (!ref2.startsWith("#")) {
61641
61898
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
61642
61899
  }
61643
- const path8 = ref2.slice(1).split("/").filter(Boolean);
61644
- if (path8.length === 0) {
61900
+ const path10 = ref2.slice(1).split("/").filter(Boolean);
61901
+ if (path10.length === 0) {
61645
61902
  return ctx.rootSchema;
61646
61903
  }
61647
61904
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
61648
- if (path8[0] === defsKey) {
61649
- const key = path8[1];
61905
+ if (path10[0] === defsKey) {
61906
+ const key = path10[1];
61650
61907
  if (!key || !ctx.defs[key]) {
61651
61908
  throw new Error(`Reference not found: ${ref2}`);
61652
61909
  }
@@ -64478,6 +64735,7 @@ var PICKER_MODEL_TIERS = MODEL_TIERS.filter(
64478
64735
  (tier) => tier !== UTILITY_MODEL_TIER
64479
64736
  );
64480
64737
  var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
64738
+ var PREFIX_CACHE_TTLS = ["5m", "1h"];
64481
64739
 
64482
64740
  // ../shared/src/schemas/apps.ts
64483
64741
  var queryColumnTypeSchema = zod_default.enum([
@@ -64903,6 +65161,9 @@ var appWorkflowDeclarationSchema = zod_default.object({
64903
65161
  ),
64904
65162
  description: zod_default.string().optional().describe(
64905
65163
  "What this workflow does, in one line \u2014 read by an agent choosing between the app's aliases, the same job a query's `description` does. `lotics app workflow set` sends it, so it lives beside the body in version control; omitted, the workflow keeps the description already on it."
65164
+ ),
65165
+ body_sha: zod_default.string().optional().describe(
65166
+ "Fingerprint of the body currently bound to this alias, written by the server on every accepted `set_app_workflow`. Never sent by a caller \u2014 it is the value a caller's `expected_body_sha` is checked against, so that a push built on a stale copy of the body is refused instead of silently overwriting whatever replaced it. Absent on a binding last written before the field existed, where no comparison is possible."
64906
65167
  )
64907
65168
  });
64908
65169
  var appWorkflowContractSchema = zod_default.object({
@@ -64941,6 +65202,9 @@ var appAgentDeclarationSchema = zod_default.object({
64941
65202
  effort_level: zod_default.enum(EFFORT_LEVELS).optional().describe(
64942
65203
  "Reasoning depth for adaptive-thinking tiers \u2014 one of the chosen tier's supported levels (validated against model_tier at declare time). Omit to use the model default; ignored on tiers without adaptive thinking."
64943
65204
  ),
65205
+ prefix_cache_ttl: zod_default.enum(PREFIX_CACHE_TTLS).optional().describe(
65206
+ `How long this agent's prompt-cache prefix (tools + system block) is kept warm. Omit \u2014 the default (5m, Anthropic's own) is right for essentially every agent. "1h" doubles the write price (2x the input rate against 1.25x) to buy only the 5m..1h band, and an entry nothing re-reads inside the hour is paid for twice over; declining it still leaves the prefix cached at the default. Set it only from measured cadence, never a guess.`
65207
+ ),
64944
65208
  inputs: zod_default.record(zod_default.string(), appWorkflowInputSchema).optional().describe(
64945
65209
  "Typed input schema for one run. Keys are input names; values declare type + constraints. The server validates the run payload against this before invoking; CLI codegen emits a typed `useAgentRun<alias>` signature. Omit for an untyped payload."
64946
65210
  ),
@@ -65782,10 +66046,10 @@ async function reduce2(arr, callback, initial2) {
65782
66046
  if (arguments.length < 3) {
65783
66047
  throw new Error("reduce: initial value is required \u2014 reduce(arr, (acc, x) => ..., initial)");
65784
66048
  }
65785
- const startedAt = Date.now();
66049
+ const startedAt2 = Date.now();
65786
66050
  let acc = initial2;
65787
66051
  for (let i2 = 0; i2 < arr.length; i2++) {
65788
- if (i2 % 1024 === 0 && Date.now() - startedAt > REDUCE_TIME_BUDGET_MS) {
66052
+ if (i2 % 1024 === 0 && Date.now() - startedAt2 > REDUCE_TIME_BUDGET_MS) {
65789
66053
  throw new Error(
65790
66054
  `reduce: exceeded time budget of ${REDUCE_TIME_BUDGET_MS}ms after ${i2} iterations`
65791
66055
  );
@@ -66313,9 +66577,9 @@ __export(expression_object_exports, {
66313
66577
  pick: () => pick3,
66314
66578
  values: () => values2
66315
66579
  });
66316
- function getNestedValue(obj, path8) {
66580
+ function getNestedValue(obj, path10) {
66317
66581
  if (obj == null) return void 0;
66318
- const parts = path8.split(".");
66582
+ const parts = path10.split(".");
66319
66583
  let value2 = obj;
66320
66584
  for (const part of parts) {
66321
66585
  if (value2 == null) return void 0;
@@ -66354,11 +66618,11 @@ function entries(obj) {
66354
66618
  }
66355
66619
  return Object.entries(obj);
66356
66620
  }
66357
- function get(obj, path8, defaultValue) {
66358
- if (typeof path8 !== "string") {
66621
+ function get(obj, path10, defaultValue) {
66622
+ if (typeof path10 !== "string") {
66359
66623
  throw new Error("get: path must be a string");
66360
66624
  }
66361
- const value2 = getNestedValue(obj, path8);
66625
+ const value2 = getNestedValue(obj, path10);
66362
66626
  return value2 !== void 0 ? value2 : defaultValue;
66363
66627
  }
66364
66628
  function pick3(obj, fields) {
@@ -68241,17 +68505,17 @@ function readFromChanges(chain2) {
68241
68505
  if (first3.kind === "bracket-number" || first3.kind === "bracket-expr") {
68242
68506
  fail(first3.node, "changes is keyed by field name, not numeric index.");
68243
68507
  }
68244
- const path8 = [{ at: "key", key: first3.key }];
68508
+ const path10 = [{ at: "key", key: first3.key }];
68245
68509
  for (const seg of segments.slice(1)) {
68246
68510
  if (seg.kind === "bracket-number") {
68247
- path8.push({ at: "index", index: seg.index });
68511
+ path10.push({ at: "index", index: seg.index });
68248
68512
  } else if (seg.kind === "bracket-expr") {
68249
- path8.push({ at: "index", index: seg.expr });
68513
+ path10.push({ at: "index", index: seg.expr });
68250
68514
  } else {
68251
- path8.push({ at: "key", key: seg.key });
68515
+ path10.push({ at: "key", key: seg.key });
68252
68516
  }
68253
68517
  }
68254
- return { kind: "read", source: { from: "trigger_changes" }, path: path8 };
68518
+ return { kind: "read", source: { from: "trigger_changes" }, path: path10 };
68255
68519
  });
68256
68520
  }
68257
68521
  var TRIGGER_RECORD_VIEWS = /* @__PURE__ */ new Map([
@@ -68269,17 +68533,17 @@ function readFromTrigger(chain2) {
68269
68533
  return view === "trigger_changes" ? readFromChanges(lowered) : readFromRecord(view, lowered);
68270
68534
  }
68271
68535
  return wrapOptionalGuards(chain2, (segments) => {
68272
- const path8 = [];
68536
+ const path10 = [];
68273
68537
  for (const seg of segments) {
68274
68538
  if (seg.kind === "bracket-number") {
68275
- path8.push({ at: "index", index: seg.index });
68539
+ path10.push({ at: "index", index: seg.index });
68276
68540
  } else if (seg.kind === "bracket-expr") {
68277
- path8.push({ at: "index", index: seg.expr });
68541
+ path10.push({ at: "index", index: seg.expr });
68278
68542
  } else {
68279
- path8.push({ at: "key", key: seg.key });
68543
+ path10.push({ at: "key", key: seg.key });
68280
68544
  }
68281
68545
  }
68282
- return { kind: "read", source: { from: "trigger" }, path: path8 };
68546
+ return { kind: "read", source: { from: "trigger" }, path: path10 };
68283
68547
  });
68284
68548
  }
68285
68549
  function readFromRuntime(chain2) {
@@ -68301,17 +68565,17 @@ function readFromRuntime(chain2) {
68301
68565
  const parked = PARKED_RUNTIME_KEYS.get(key);
68302
68566
  if (parked) fail(first3.node, parked, parkedRuntimeKeyConstruct(key));
68303
68567
  const source = { from: "runtime", key };
68304
- const path8 = [];
68568
+ const path10 = [];
68305
68569
  for (const seg of segments.slice(1)) {
68306
68570
  if (seg.kind === "bracket-number") {
68307
- path8.push({ at: "index", index: seg.index });
68571
+ path10.push({ at: "index", index: seg.index });
68308
68572
  } else if (seg.kind === "bracket-expr") {
68309
- path8.push({ at: "index", index: seg.expr });
68573
+ path10.push({ at: "index", index: seg.expr });
68310
68574
  } else {
68311
- path8.push({ at: "key", key: seg.key });
68575
+ path10.push({ at: "key", key: seg.key });
68312
68576
  }
68313
68577
  }
68314
- return { kind: "read", source, path: path8 };
68578
+ return { kind: "read", source, path: path10 };
68315
68579
  });
68316
68580
  }
68317
68581
  function readFromForeachItem(chain2) {
@@ -68347,17 +68611,17 @@ function readFromLexical(name2, chain2) {
68347
68611
  }
68348
68612
  function readFromStepOutput(stepId, chain2) {
68349
68613
  return wrapOptionalGuards(chain2, (segments) => {
68350
- const path8 = [];
68614
+ const path10 = [];
68351
68615
  for (const seg of segments) {
68352
68616
  if (seg.kind === "bracket-number") {
68353
- path8.push({ at: "index", index: seg.index });
68617
+ path10.push({ at: "index", index: seg.index });
68354
68618
  } else if (seg.kind === "bracket-expr") {
68355
- path8.push({ at: "index", index: seg.expr });
68619
+ path10.push({ at: "index", index: seg.expr });
68356
68620
  } else {
68357
- path8.push({ at: "key", key: seg.key });
68621
+ path10.push({ at: "key", key: seg.key });
68358
68622
  }
68359
68623
  }
68360
- return { kind: "read", source: { from: "step_output", step_id: stepId }, path: path8 };
68624
+ return { kind: "read", source: { from: "step_output", step_id: stepId }, path: path10 };
68361
68625
  });
68362
68626
  }
68363
68627
  function classifyRecordPath(segs) {
@@ -70011,7 +70275,7 @@ function subsetIssues(bodySource, offset) {
70011
70275
  ];
70012
70276
  }
70013
70277
  async function loadProjectTypescript(projectDir) {
70014
- const requireFromProject = createRequire(path4.join(projectDir, "package.json"));
70278
+ const requireFromProject = createRequire(path6.join(projectDir, "package.json"));
70015
70279
  let resolved;
70016
70280
  try {
70017
70281
  resolved = requireFromProject.resolve("typescript");
@@ -70024,12 +70288,12 @@ async function loadProjectTypescript(projectDir) {
70024
70288
  return mod2.default ?? mod2;
70025
70289
  }
70026
70290
  function pathToImportUrl(p) {
70027
- const resolved = path4.resolve(p);
70291
+ const resolved = path6.resolve(p);
70028
70292
  const prefixed = resolved.startsWith("/") ? resolved : `/${resolved}`;
70029
- return `file://${prefixed.split(path4.sep).join("/")}`;
70293
+ return `file://${prefixed.split(path6.sep).join("/")}`;
70030
70294
  }
70031
70295
  function checkOneWorkflowBody(tsApi, input) {
70032
- const wrapped = fs3.readFileSync(input.bodyPath, "utf-8");
70296
+ const wrapped = fs5.readFileSync(input.bodyPath, "utf-8");
70033
70297
  const offset = bodyLineOffset(wrapped);
70034
70298
  const subset = subsetIssues(stripWorkflowHeader(wrapped), offset);
70035
70299
  if (subset) return subset;
@@ -70076,7 +70340,7 @@ function checkOneWorkflowBody(tsApi, input) {
70076
70340
  }
70077
70341
  function createCheckedSourceHost(tsApi, options, bodyPath, bodyText) {
70078
70342
  const base = tsApi.createCompilerHost(options, false);
70079
- const key = (fileName) => base.getCanonicalFileName(path4.resolve(fileName).replace(/\\/g, "/"));
70343
+ const key = (fileName) => base.getCanonicalFileName(path6.resolve(fileName).replace(/\\/g, "/"));
70080
70344
  const bodyKey = key(bodyPath);
70081
70345
  return {
70082
70346
  ...base,
@@ -70145,6 +70409,14 @@ function orderedLike(value2, template) {
70145
70409
  [...first3, ...rest2].map((k) => [k, orderedLike(from[k], tmpl[k])])
70146
70410
  );
70147
70411
  }
70412
+ function toManifestWorkflows(live) {
70413
+ return Object.fromEntries(
70414
+ Object.entries(live).map(([alias, { body_sha: _serverState, ...declaration }]) => [
70415
+ alias,
70416
+ declaration
70417
+ ])
70418
+ );
70419
+ }
70148
70420
  function toManifestAgents(agents) {
70149
70421
  return Object.fromEntries(
70150
70422
  Object.entries(agents).map(([alias, { instructions: _prose, ...typed }]) => [alias, typed])
@@ -70163,27 +70435,58 @@ function sameSchema(a, b) {
70163
70435
  };
70164
70436
  return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
70165
70437
  }
70166
- function agentTypeDivergences(manifestAgents, liveAgents) {
70438
+ function workflowTypeDivergences(manifestWorkflows, liveWorkflows) {
70167
70439
  const out = [];
70168
- for (const [alias, declared] of Object.entries(manifestAgents ?? {})) {
70169
- const live = (liveAgents ?? {})[alias];
70440
+ for (const [alias, declared] of Object.entries(manifestWorkflows ?? {})) {
70441
+ const live = (liveWorkflows ?? {})[alias];
70170
70442
  if (!live) {
70171
- out.push(
70172
- `${alias} \u2014 typed locally but NOT bound on the app, so useAgentRun("${alias}") 400s at runtime`
70173
- );
70443
+ out.push({ alias });
70174
70444
  continue;
70175
70445
  }
70176
- for (const key of AGENT_TYPED_KEYS) {
70177
- if (!sameSchema(declared[key], live[key])) {
70178
- out.push(`${alias}.${key} \u2014 local types disagree with the live agent`);
70179
- }
70446
+ for (const key of ["inputs", "outputs"]) {
70447
+ if (!sameSchema(declared[key], live[key])) out.push({ alias, key });
70180
70448
  }
70181
70449
  }
70182
70450
  return out;
70183
70451
  }
70184
- var WORKFLOWS_DIR = path5.join("src", "workflows");
70185
- var WORKFLOW_GLOBALS_DIR = path5.join(".lotics", "workflows");
70186
- var AGENTS_DIR = path5.join("src", "agents");
70452
+ function syncedEntry(entry) {
70453
+ if (entry === void 0) return void 0;
70454
+ return typeof entry === "string" ? { content: entry } : entry;
70455
+ }
70456
+ function readSynced(projectDir) {
70457
+ try {
70458
+ const pkg2 = JSON.parse(
70459
+ fs6.readFileSync(path7.join(projectDir, "package.json"), "utf-8")
70460
+ );
70461
+ const raw = pkg2.lotics?.synced ?? {};
70462
+ return {
70463
+ workflows: normalizeSyncedMap(raw.workflows),
70464
+ agents: normalizeSyncedMap(raw.agents),
70465
+ queries: normalizeSyncedMap(raw.queries)
70466
+ };
70467
+ } catch {
70468
+ return { workflows: {}, agents: {}, queries: {} };
70469
+ }
70470
+ }
70471
+ function normalizeSyncedMap(map3) {
70472
+ return Object.fromEntries(
70473
+ Object.entries(map3 ?? {}).map(([alias, entry]) => [
70474
+ alias,
70475
+ syncedEntry(entry)
70476
+ ])
70477
+ );
70478
+ }
70479
+ function writeSynced(projectDir, kind, alias, entry) {
70480
+ const pkgPath2 = path7.join(projectDir, "package.json");
70481
+ const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
70482
+ if (!pkg2.lotics) return;
70483
+ pkg2.lotics.synced ??= {};
70484
+ pkg2.lotics.synced[kind] = { ...pkg2.lotics.synced[kind], [alias]: entry };
70485
+ fs6.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
70486
+ }
70487
+ var WORKFLOWS_DIR = path7.join("src", "workflows");
70488
+ var WORKFLOW_GLOBALS_DIR = path7.join(".lotics", "workflows");
70489
+ var AGENTS_DIR = path7.join("src", "agents");
70187
70490
  var WORKFLOW_TSCONFIG_EXCLUDES = ["src/workflows", ".lotics/workflows"];
70188
70491
  var LOTICS_INCLUDE_GLOB = ".lotics/**/*";
70189
70492
  var STALE_LOTICS_INCLUDES = /* @__PURE__ */ new Set([".lotics", "./.lotics", ".lotics/"]);
@@ -70193,11 +70496,8 @@ function toWorkflowDtsDeclaration(d) {
70193
70496
  function isMissingWorkflowBodyError(error52) {
70194
70497
  return /has no step source|cannot be rendered|unsupported step/i.test(error52);
70195
70498
  }
70196
- function isUnknownWorkflowAliasError(err2) {
70197
- return err2 instanceof Error && err2.message.startsWith("400:") && /has no workflow alias/i.test(err2.message);
70198
- }
70199
70499
  function workflowFileHeader(alias) {
70200
- const refPath = path5.join("..", "..", WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`).split(path5.sep).join("/");
70500
+ const refPath = path7.join("..", "..", WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`).split(path7.sep).join("/");
70201
70501
  return `/// <reference path="${refPath}" />
70202
70502
  // Auto-pulled workflow body for "${alias}". Edit the BODY between the wrapper
70203
70503
  // lines below, then check + push with:
@@ -70211,65 +70511,68 @@ export {};
70211
70511
  `;
70212
70512
  }
70213
70513
  function workflowFilePath(projectDir, alias) {
70214
- return path5.join(projectDir, WORKFLOWS_DIR, `${alias}.ts`);
70514
+ return path7.join(projectDir, WORKFLOWS_DIR, `${alias}.ts`);
70215
70515
  }
70216
70516
  var GLOBALS_SUFFIX = ".globals.d.ts";
70217
70517
  function workflowGlobalsPath(projectDir, alias) {
70218
- return path5.join(projectDir, WORKFLOW_GLOBALS_DIR, `${alias}${GLOBALS_SUFFIX}`);
70518
+ return path7.join(projectDir, WORKFLOW_GLOBALS_DIR, `${alias}${GLOBALS_SUFFIX}`);
70219
70519
  }
70220
70520
  function pruneOrphanWorkflowGlobals(projectDir, declared) {
70221
- const dir = path5.join(projectDir, WORKFLOW_GLOBALS_DIR);
70222
- if (!fs4.existsSync(dir)) return [];
70521
+ const dir = path7.join(projectDir, WORKFLOW_GLOBALS_DIR);
70522
+ if (!fs6.existsSync(dir)) return [];
70223
70523
  const removed = [];
70224
- for (const entry of fs4.readdirSync(dir)) {
70524
+ for (const entry of fs6.readdirSync(dir)) {
70225
70525
  if (!entry.endsWith(GLOBALS_SUFFIX)) continue;
70226
70526
  const alias = entry.slice(0, -GLOBALS_SUFFIX.length);
70227
70527
  if (declared.has(alias)) continue;
70228
- fs4.unlinkSync(path5.join(dir, entry));
70528
+ fs6.unlinkSync(path7.join(dir, entry));
70229
70529
  removed.push(alias);
70230
70530
  }
70231
70531
  return removed.sort();
70232
70532
  }
70233
70533
  function orphanWorkflowBodies(projectDir, declared) {
70234
- const dir = path5.join(projectDir, WORKFLOWS_DIR);
70235
- if (!fs4.existsSync(dir)) return [];
70236
- return fs4.readdirSync(dir).filter((e) => e.endsWith(".ts") && !e.endsWith(".d.ts") && !e.endsWith(".test.ts")).map((e) => e.slice(0, -".ts".length)).filter((alias) => !declared.has(alias)).sort();
70534
+ const dir = path7.join(projectDir, WORKFLOWS_DIR);
70535
+ if (!fs6.existsSync(dir)) return [];
70536
+ return fs6.readdirSync(dir).filter((e) => e.endsWith(".ts") && !e.endsWith(".d.ts") && !e.endsWith(".test.ts")).map((e) => e.slice(0, -".ts".length)).filter((alias) => !declared.has(alias)).sort();
70237
70537
  }
70238
70538
  function writeWorkflowGlobals(projectDir, alias, dts) {
70239
- const dir = path5.join(projectDir, WORKFLOW_GLOBALS_DIR);
70240
- fs4.mkdirSync(dir, { recursive: true });
70539
+ const dir = path7.join(projectDir, WORKFLOW_GLOBALS_DIR);
70540
+ fs6.mkdirSync(dir, { recursive: true });
70241
70541
  const file2 = workflowGlobalsPath(projectDir, alias);
70242
- fs4.writeFileSync(file2, `${dts.replace(/\s+$/, "")}
70542
+ fs6.writeFileSync(file2, `${dts.replace(/\s+$/, "")}
70243
70543
  `);
70244
70544
  return file2;
70245
70545
  }
70546
+ function normalizeWorkflowBody(source) {
70547
+ return source.replace(/\s+$/, "");
70548
+ }
70246
70549
  function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: WORKFLOW_ENVELOPE_PREFIX, suffix: WORKFLOW_ENVELOPE_SUFFIX }) {
70247
- const dir = path5.join(projectDir, WORKFLOWS_DIR);
70248
- fs4.mkdirSync(dir, { recursive: true });
70550
+ const dir = path7.join(projectDir, WORKFLOWS_DIR);
70551
+ fs6.mkdirSync(dir, { recursive: true });
70249
70552
  const file2 = workflowFilePath(projectDir, alias);
70250
- const body = source.replace(/\s+$/, "");
70251
- fs4.writeFileSync(file2, `${workflowFileHeader(alias)}
70553
+ const body = normalizeWorkflowBody(source);
70554
+ fs6.writeFileSync(file2, `${workflowFileHeader(alias)}
70252
70555
  ${envelope.prefix}${body}${envelope.suffix}
70253
70556
  `);
70254
70557
  return file2;
70255
70558
  }
70256
70559
  function agentFilePath2(projectDir, alias) {
70257
- return path5.join(projectDir, ...agentFilePath(alias).split("/"));
70560
+ return path7.join(projectDir, ...agentFilePath(alias).split("/"));
70258
70561
  }
70259
70562
  var CLI_AGENT_NOTES = [
70260
70563
  "Pulled from the LIVE app row; edit here, then: lotics app agent set <alias>",
70261
70564
  "The typed fields (inputs/outputs/tool_names/model_tier) live in package.json#lotics.agents"
70262
70565
  ];
70263
70566
  function writeAgentFile(projectDir, alias, instructions) {
70264
- fs4.mkdirSync(path5.join(projectDir, AGENTS_DIR), { recursive: true });
70567
+ fs6.mkdirSync(path7.join(projectDir, AGENTS_DIR), { recursive: true });
70265
70568
  const file2 = agentFilePath2(projectDir, alias);
70266
- fs4.writeFileSync(
70569
+ fs6.writeFileSync(
70267
70570
  file2,
70268
70571
  wrapAgentFile({ alias, instructions, notes: CLI_AGENT_NOTES.map((n) => n.replace("<alias>", alias)) })
70269
70572
  );
70270
70573
  return file2;
70271
70574
  }
70272
- function writeAgentFiles(projectDir, agents) {
70575
+ function writeAgentFiles(projectDir, agents, opts) {
70273
70576
  const written = [];
70274
70577
  for (const [alias, declaration] of Object.entries(agents)) {
70275
70578
  const instructions = declaration.instructions;
@@ -70277,15 +70580,33 @@ function writeAgentFiles(projectDir, agents) {
70277
70580
  console.error(`\u26A0 Skipped src/agents/${alias}.md \u2014 the live declaration carries no instructions.`);
70278
70581
  continue;
70279
70582
  }
70583
+ if (!opts.overwrite && localPromptUnpushed(projectDir, alias)) {
70584
+ opts.kept.push(`src/agents/${alias}.md`);
70585
+ continue;
70586
+ }
70280
70587
  writeAgentFile(projectDir, alias, instructions);
70281
70588
  written.push(alias);
70282
70589
  }
70283
70590
  return written;
70284
70591
  }
70592
+ function localPromptUnpushed(projectDir, alias) {
70593
+ const file2 = agentFilePath2(projectDir, alias);
70594
+ if (!fs6.existsSync(file2)) return false;
70595
+ const baseline = readSynced(projectDir).agents[alias]?.content;
70596
+ if (baseline === void 0) return true;
70597
+ return contentSha(stripAgentHeader(fs6.readFileSync(file2, "utf-8"))) !== baseline;
70598
+ }
70285
70599
  function generatedWorkflowDescription(appName, alias) {
70286
70600
  return `Workflow invoked by app "${appName}" via useWorkflow("${alias}").`;
70287
70601
  }
70288
- async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows) {
70602
+ function localBodyUnpushed(projectDir, alias) {
70603
+ const file2 = workflowFilePath(projectDir, alias);
70604
+ if (!fs6.existsSync(file2)) return false;
70605
+ const baseline = readSynced(projectDir).workflows[alias]?.content;
70606
+ if (baseline === void 0) return true;
70607
+ return contentSha(stripWorkflowHeader(fs6.readFileSync(file2, "utf-8"))) !== baseline;
70608
+ }
70609
+ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows, opts) {
70289
70610
  const written = [];
70290
70611
  for (const [alias, declaration] of Object.entries(workflows)) {
70291
70612
  const res = await client.getAppWorkflow(app_id, alias);
@@ -70294,7 +70615,9 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
70294
70615
  `Could not read workflow "${alias}" of ${app_id}: ${res.error}. Reading app workflow source requires an admin member \u2014 check the role behind this API key.`
70295
70616
  );
70296
70617
  }
70297
- const source = res.result === null || typeof res.result !== "object" ? null : res.result.source;
70618
+ const result = res.result === null || typeof res.result !== "object" ? null : res.result;
70619
+ const source = result?.source;
70620
+ const body_sha = typeof result?.body_sha === "string" ? result.body_sha : void 0;
70298
70621
  if (typeof source !== "string" || source.trim() === "") {
70299
70622
  console.error(
70300
70623
  `\u26A0 Skipped src/workflows/${alias}.ts \u2014 legacy workflow with no rendered body.`
@@ -70308,7 +70631,15 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
70308
70631
  alias,
70309
70632
  toWorkflowDtsDeclaration(declaration)
70310
70633
  );
70634
+ if (!opts.overwrite && localBodyUnpushed(projectDir, alias)) {
70635
+ opts.kept.push(`src/workflows/${alias}.ts`);
70636
+ continue;
70637
+ }
70311
70638
  writeWorkflowFile(projectDir, alias, source, envelope);
70639
+ writeSynced(projectDir, "workflows", alias, {
70640
+ content: contentSha(normalizeWorkflowBody(source)),
70641
+ ...body_sha ? { live: body_sha } : {}
70642
+ });
70312
70643
  const rowDescription = res.result.description;
70313
70644
  if (typeof rowDescription === "string" && rowDescription !== "" && // Skip the server's GENERATED default. It is what an undescribed alias
70314
70645
  // carries, so writing it back would commit a line of noise per workflow
@@ -70323,12 +70654,12 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
70323
70654
  return written;
70324
70655
  }
70325
70656
  function writeWorkflowDescription(projectDir, alias, description) {
70326
- const pkgPath2 = path5.join(projectDir, "package.json");
70327
- const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
70657
+ const pkgPath2 = path7.join(projectDir, "package.json");
70658
+ const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
70328
70659
  const declaration = pkg2.lotics?.workflows?.[alias];
70329
70660
  if (!declaration) return;
70330
70661
  declaration.description = description;
70331
- fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
70662
+ fs6.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
70332
70663
  }
70333
70664
  async function fetchWorkflowGlobals(client, projectDir, app_id, alias, declaration) {
70334
70665
  try {
@@ -70348,14 +70679,9 @@ async function fetchWorkflowGlobals(client, projectDir, app_id, alias, declarati
70348
70679
  }
70349
70680
  }
70350
70681
  async function fetchWorkflowDts(client, app_id, alias, declaration) {
70351
- try {
70352
- return await client.getAppWorkflowDts(app_id, alias);
70353
- } catch (err2) {
70354
- if (declaration && isUnknownWorkflowAliasError(err2)) {
70355
- return client.getAppWorkflowDts(app_id, alias, declaration);
70356
- }
70357
- throw err2;
70358
- }
70682
+ const declaresSchema = declaration !== void 0 && (declaration.inputs !== void 0 || declaration.outputs !== void 0);
70683
+ if (declaresSchema) return client.getAppWorkflowDts(app_id, alias, declaration);
70684
+ return client.getAppWorkflowDts(app_id, alias);
70359
70685
  }
70360
70686
  function runTar(args, cwd) {
70361
70687
  return new Promise((resolve2, reject2) => {
@@ -70383,25 +70709,26 @@ function runNpm(args, cwd) {
70383
70709
  }
70384
70710
  function readAppWorkspaceId(projectDir) {
70385
70711
  try {
70386
- const pkgPath2 = path5.join(projectDir, "package.json");
70387
- if (!fs4.existsSync(pkgPath2)) return null;
70388
- const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
70712
+ const pkgPath2 = path7.join(projectDir, "package.json");
70713
+ if (!fs6.existsSync(pkgPath2)) return null;
70714
+ const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
70389
70715
  return pkg2.lotics?.workspace_id ?? null;
70390
70716
  } catch {
70391
70717
  return null;
70392
70718
  }
70393
70719
  }
70394
70720
  function readAppMeta(projectDir) {
70395
- const pkgPath2 = path5.join(projectDir, "package.json");
70396
- if (!fs4.existsSync(pkgPath2)) {
70721
+ const pkgPath2 = path7.join(projectDir, "package.json");
70722
+ if (!fs6.existsSync(pkgPath2)) {
70397
70723
  throw new Error(`No package.json found at ${projectDir}. Run 'lotics app create' first.`);
70398
70724
  }
70399
- const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
70725
+ const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
70400
70726
  if (!pkg2.lotics?.app_id || !pkg2.lotics.workspace_id) {
70401
70727
  throw new Error(
70402
70728
  `package.json is missing the 'lotics' field with app_id and workspace_id. Was this folder created with 'lotics app create'?`
70403
70729
  );
70404
70730
  }
70731
+ noteScope({ app_id: pkg2.lotics.app_id });
70405
70732
  return {
70406
70733
  app_id: pkg2.lotics.app_id,
70407
70734
  workspace_id: pkg2.lotics.workspace_id,
@@ -70424,19 +70751,22 @@ var APP_META_KEYS = Object.keys({
70424
70751
  capabilities: true
70425
70752
  });
70426
70753
  function writeAppMeta(projectDir, meta3) {
70427
- const pkgPath2 = path5.join(projectDir, "package.json");
70428
- const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
70754
+ const pkgPath2 = path7.join(projectDir, "package.json");
70755
+ const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
70429
70756
  const existing = pkg2.lotics && typeof pkg2.lotics === "object" ? pkg2.lotics : {};
70757
+ const supplied = new Set(
70758
+ Object.entries(meta3).filter(([, value2]) => value2 !== void 0).map(([key]) => key)
70759
+ );
70430
70760
  const preserved = {};
70431
70761
  for (const [key, value2] of Object.entries(existing)) {
70432
- if (!APP_META_KEYS.includes(key)) preserved[key] = value2;
70762
+ if (!supplied.has(key)) preserved[key] = value2;
70433
70763
  }
70434
70764
  pkg2.lotics = { ...meta3, ...preserved };
70435
- fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
70765
+ fs6.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
70436
70766
  }
70437
70767
  function writeWorkflowOutputs(projectDir, alias, outputs) {
70438
- const pkgPath2 = path5.join(projectDir, "package.json");
70439
- const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
70768
+ const pkgPath2 = path7.join(projectDir, "package.json");
70769
+ const pkg2 = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
70440
70770
  const declaration = pkg2.lotics?.workflows?.[alias];
70441
70771
  if (!declaration) {
70442
70772
  throw new Error(
@@ -70444,11 +70774,24 @@ function writeWorkflowOutputs(projectDir, alias, outputs) {
70444
70774
  );
70445
70775
  }
70446
70776
  declaration.outputs = outputs;
70447
- fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
70777
+ fs6.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
70778
+ }
70779
+ function workflowBodyDrift(projectDir, workflows, synced = {}) {
70780
+ const drifted = [];
70781
+ for (const alias of Object.keys(workflows ?? {})) {
70782
+ const baseline = syncedEntry(synced[alias])?.content;
70783
+ if (baseline === void 0) continue;
70784
+ const file2 = workflowFilePath(projectDir, alias);
70785
+ if (!fs6.existsSync(file2)) continue;
70786
+ const source = stripWorkflowHeader(fs6.readFileSync(file2, "utf-8"));
70787
+ if (source.trim() === "" || contentSha(source) === baseline) continue;
70788
+ drifted.push(alias);
70789
+ }
70790
+ return drifted;
70448
70791
  }
70449
70792
  function ensureAppTsconfig(projectDir) {
70450
- const tsconfigPath = path5.join(projectDir, "tsconfig.json");
70451
- if (!fs4.existsSync(tsconfigPath)) {
70793
+ const tsconfigPath = path7.join(projectDir, "tsconfig.json");
70794
+ if (!fs6.existsSync(tsconfigPath)) {
70452
70795
  console.error(
70453
70796
  `\u26A0 No tsconfig.json at ${projectDir} \u2014 could not ensure "include" has "${LOTICS_INCLUDE_GLOB}" (a bare .lotics loads none of the generated types) or "exclude" has ${WORKFLOW_TSCONFIG_EXCLUDES.join(", ")}.`
70454
70797
  );
@@ -70456,7 +70799,7 @@ function ensureAppTsconfig(projectDir) {
70456
70799
  }
70457
70800
  let parsed;
70458
70801
  try {
70459
- parsed = JSON.parse(fs4.readFileSync(tsconfigPath, "utf-8"));
70802
+ parsed = JSON.parse(fs6.readFileSync(tsconfigPath, "utf-8"));
70460
70803
  } catch (err2) {
70461
70804
  console.error(
70462
70805
  `\u26A0 Could not parse tsconfig.json (${err2 instanceof Error ? err2.message : String(err2)}) \u2014 ensure "include" has "${LOTICS_INCLUDE_GLOB}" and "exclude" has ${WORKFLOW_TSCONFIG_EXCLUDES.join(", ")} manually.`
@@ -70489,7 +70832,7 @@ function ensureAppTsconfig(projectDir) {
70489
70832
  );
70490
70833
  }
70491
70834
  if (changes.length === 0) return;
70492
- fs4.writeFileSync(tsconfigPath, JSON.stringify(parsed, null, 2) + "\n");
70835
+ fs6.writeFileSync(tsconfigPath, JSON.stringify(parsed, null, 2) + "\n");
70493
70836
  console.error(`Patched tsconfig.json: ${changes.join("; ")}.`);
70494
70837
  }
70495
70838
  var LINK_TSCONFIG = ".lotics/tsconfig.link.json";
@@ -70499,15 +70842,15 @@ function writeDevLinkTsconfig(projectDir) {
70499
70842
  if (uiSrc) {
70500
70843
  paths["@lotics/ui/*"] = [`${uiSrc}/*`];
70501
70844
  for (const peer of readKitPeerNames(projectDir)) {
70502
- const typesDir = path5.join(projectDir, "node_modules", typesPackageFor(peer));
70503
- const runtimeDir = path5.join(projectDir, "node_modules", peer);
70504
- const target = fs4.existsSync(typesDir) ? `../node_modules/${typesPackageFor(peer)}` : fs4.existsSync(runtimeDir) ? `../node_modules/${peer}` : null;
70845
+ const typesDir = path7.join(projectDir, "node_modules", typesPackageFor(peer));
70846
+ const runtimeDir = path7.join(projectDir, "node_modules", peer);
70847
+ const target = fs6.existsSync(typesDir) ? `../node_modules/${typesPackageFor(peer)}` : fs6.existsSync(runtimeDir) ? `../node_modules/${peer}` : null;
70505
70848
  if (!target) continue;
70506
70849
  paths[peer] = [target];
70507
70850
  paths[`${peer}/*`] = [`${target}/*`];
70508
70851
  }
70509
70852
  }
70510
- const file2 = path5.join(projectDir, ".lotics", "tsconfig.link.json");
70853
+ const file2 = path7.join(projectDir, ".lotics", "tsconfig.link.json");
70511
70854
  const header = uiSrc ? `// GENERATED by \`lotics app codegen\` \u2014 do not edit.
70512
70855
  // LOTICS_UI_SRC is set, so @lotics/ui resolves to your working copy for tsc,
70513
70856
  // vitest, eslint and your editor \u2014 the same copy Vite is bundling. The peer
@@ -70518,52 +70861,52 @@ function writeDevLinkTsconfig(projectDir) {
70518
70861
  // LOTICS_UI_SRC is not set, so this is inert and @lotics/ui resolves from
70519
70862
  // node_modules as normal.
70520
70863
  `;
70521
- fs4.writeFileSync(file2, `${header}${JSON.stringify({ compilerOptions: { paths } }, null, 2)}
70864
+ fs6.writeFileSync(file2, `${header}${JSON.stringify({ compilerOptions: { paths } }, null, 2)}
70522
70865
  `);
70523
70866
  return file2;
70524
70867
  }
70525
70868
  function writeKitTypeAugmentation(projectDir) {
70526
70869
  const uiSrc = process.env.LOTICS_UI_SRC;
70527
- const source = uiSrc ? path5.join(uiSrc, "react_native.d.ts") : path5.join(projectDir, "node_modules", "@lotics", "ui", "src", "react_native.d.ts");
70528
- const target = path5.join(projectDir, ".lotics", "react_native.d.ts");
70529
- if (!fs4.existsSync(source)) {
70530
- if (fs4.existsSync(target)) fs4.rmSync(target);
70870
+ const source = uiSrc ? path7.join(uiSrc, "react_native.d.ts") : path7.join(projectDir, "node_modules", "@lotics", "ui", "src", "react_native.d.ts");
70871
+ const target = path7.join(projectDir, ".lotics", "react_native.d.ts");
70872
+ if (!fs6.existsSync(source)) {
70873
+ if (fs6.existsSync(target)) fs6.rmSync(target);
70531
70874
  return;
70532
70875
  }
70533
- fs4.writeFileSync(
70876
+ fs6.writeFileSync(
70534
70877
  target,
70535
70878
  `// GENERATED by \`lotics app codegen\` from @lotics/ui \u2014 do not edit.
70536
70879
  // The kit's web-only ViewStyle/TextStyle properties, which react-native does
70537
70880
  // not model. Delete any hand-copied src/react_native.d.ts; this replaces it.
70538
- ` + fs4.readFileSync(source, "utf-8")
70881
+ ` + fs6.readFileSync(source, "utf-8")
70539
70882
  );
70540
70883
  }
70541
70884
  function typesPackageFor(peer) {
70542
70885
  return peer.startsWith("@") ? `@types/${peer.slice(1).replace("/", "__")}` : `@types/${peer}`;
70543
70886
  }
70544
70887
  function readKitPeerNames(projectDir) {
70545
- const kitPkg = path5.join(projectDir, "node_modules", "@lotics", "ui", "package.json");
70546
- if (!fs4.existsSync(kitPkg)) return [];
70547
- const parsed = JSON.parse(fs4.readFileSync(kitPkg, "utf-8"));
70888
+ const kitPkg = path7.join(projectDir, "node_modules", "@lotics", "ui", "package.json");
70889
+ if (!fs6.existsSync(kitPkg)) return [];
70890
+ const parsed = JSON.parse(fs6.readFileSync(kitPkg, "utf-8"));
70548
70891
  const peers = parsed.peerDependencies;
70549
70892
  return peers && typeof peers === "object" ? Object.keys(peers) : [];
70550
70893
  }
70551
70894
  function writeAppDts(projectDir, manifest) {
70552
- const dotLotics = path5.join(projectDir, ".lotics");
70553
- fs4.mkdirSync(dotLotics, { recursive: true });
70895
+ const dotLotics = path7.join(projectDir, ".lotics");
70896
+ fs6.mkdirSync(dotLotics, { recursive: true });
70554
70897
  const written = [
70555
- [path5.join(dotLotics, "app_workflows.d.ts"), generateAppWorkflowsDts(manifest.workflows)],
70556
- [path5.join(dotLotics, "app_queries.d.ts"), generateAppQueriesDts(manifest.queries)],
70557
- [path5.join(dotLotics, "app_agents.d.ts"), generateAppAgentsDts(manifest.agents)]
70898
+ [path7.join(dotLotics, "app_workflows.d.ts"), generateAppWorkflowsDts(manifest.workflows)],
70899
+ [path7.join(dotLotics, "app_queries.d.ts"), generateAppQueriesDts(manifest.queries)],
70900
+ [path7.join(dotLotics, "app_agents.d.ts"), generateAppAgentsDts(manifest.agents)]
70558
70901
  ];
70559
- for (const [file2, content] of written) fs4.writeFileSync(file2, content);
70902
+ for (const [file2, content] of written) fs6.writeFileSync(file2, content);
70560
70903
  writeDevLinkTsconfig(projectDir);
70561
70904
  writeKitTypeAugmentation(projectDir);
70562
70905
  ensureAppTsconfig(projectDir);
70563
70906
  return written.map(([file2]) => file2);
70564
70907
  }
70565
70908
  function readCodegenTablesAllowlist(projectDir) {
70566
- const pkg2 = JSON.parse(fs4.readFileSync(path5.join(projectDir, "package.json"), "utf-8"));
70909
+ const pkg2 = JSON.parse(fs6.readFileSync(path7.join(projectDir, "package.json"), "utf-8"));
70567
70910
  const tables = pkg2.lotics?.codegen?.tables;
70568
70911
  return Array.isArray(tables) ? tables.filter((t) => typeof t === "string") : [];
70569
70912
  }
@@ -70576,17 +70919,17 @@ function resolveCodegenTableIds(projectDir, queries) {
70576
70919
  return [...ids];
70577
70920
  }
70578
70921
  function writeAppFields(projectDir, tables) {
70579
- const dotLotics = path5.join(projectDir, ".lotics");
70580
- fs4.mkdirSync(dotLotics, { recursive: true });
70581
- const file2 = path5.join(dotLotics, "app_fields.ts");
70582
- fs4.writeFileSync(file2, generateAppFields(tables));
70922
+ const dotLotics = path7.join(projectDir, ".lotics");
70923
+ fs6.mkdirSync(dotLotics, { recursive: true });
70924
+ const file2 = path7.join(dotLotics, "app_fields.ts");
70925
+ fs6.writeFileSync(file2, generateAppFields(tables));
70583
70926
  return file2;
70584
70927
  }
70585
70928
  function writeBindingAppFields(projectDir, binding) {
70586
- const dotLotics = path5.join(projectDir, ".lotics");
70587
- fs4.mkdirSync(dotLotics, { recursive: true });
70588
- const file2 = path5.join(dotLotics, "app_fields.ts");
70589
- fs4.writeFileSync(file2, generatePackageAppFields(binding));
70929
+ const dotLotics = path7.join(projectDir, ".lotics");
70930
+ fs6.mkdirSync(dotLotics, { recursive: true });
70931
+ const file2 = path7.join(dotLotics, "app_fields.ts");
70932
+ fs6.writeFileSync(file2, generatePackageAppFields(binding));
70590
70933
  ensureAppVitestSetup(projectDir);
70591
70934
  return file2;
70592
70935
  }
@@ -70608,6 +70951,15 @@ async function writeGeneratedAppFields(client, projectDir, app, queries) {
70608
70951
  } else {
70609
70952
  const tableIds = resolveCodegenTableIds(projectDir, queries);
70610
70953
  const tables = await client.getWorkspaceSchema(tableIds);
70954
+ const missing = tableIds.filter((id) => !tables.some((t) => t.id === id));
70955
+ if (missing.length > 0) {
70956
+ console.error(
70957
+ `\u26A0 Skipped .lotics/app_fields.ts \u2014 ${missing.length} table(s) the app queries could not be read: ${missing.join(", ")}.
70958
+ Writing anyway would bake a narrowed F/OPT map that compiles and then throws at runtime. Check this key's access to those tables.
70959
+ An existing app_fields.ts is kept and still works; a project that has none yet will fail to BUILD until this resolves \u2014 which is the loud failure, not a new problem.`
70960
+ );
70961
+ return;
70962
+ }
70611
70963
  const fieldsPath = writeAppFields(projectDir, tables);
70612
70964
  console.error(
70613
70965
  `Wrote ${fieldsPath} (baked form \u2014 bespoke; ${tables.length} table${tables.length === 1 ? "" : "s"})`
@@ -70619,7 +70971,7 @@ async function writeGeneratedAppFields(client, projectDir, app, queries) {
70619
70971
  }
70620
70972
  function warnAppFieldsUnwritten(projectDir, err2) {
70621
70973
  const reason = err2 instanceof Error ? err2.message : String(err2);
70622
- if (fs4.existsSync(path5.join(projectDir, ".lotics", "app_fields.ts"))) {
70974
+ if (fs6.existsSync(path7.join(projectDir, ".lotics", "app_fields.ts"))) {
70623
70975
  console.error(`\u26A0 Could not regenerate .lotics/app_fields.ts (${reason}). Kept the existing file.`);
70624
70976
  return;
70625
70977
  }
@@ -70630,8 +70982,8 @@ function warnAppFieldsUnwritten(projectDir, err2) {
70630
70982
  function warnAboutDevLink(projectDir, command) {
70631
70983
  const uiSrc = process.env.LOTICS_UI_SRC;
70632
70984
  if (!uiSrc) return;
70633
- const viteConfigPath = path5.join(projectDir, "vite.config.ts");
70634
- const config2 = fs4.existsSync(viteConfigPath) ? fs4.readFileSync(viteConfigPath, "utf-8") : "";
70985
+ const viteConfigPath = path7.join(projectDir, "vite.config.ts");
70986
+ const config2 = fs6.existsSync(viteConfigPath) ? fs6.readFileSync(viteConfigPath, "utf-8") : "";
70635
70987
  const linked = config2.includes("loticsResolve") || config2.includes("LOTICS_UI_SRC");
70636
70988
  if (!linked) {
70637
70989
  console.error(
@@ -70649,19 +71001,19 @@ function warnAboutDevLink(projectDir, command) {
70649
71001
  }
70650
71002
  }
70651
71003
  function ensureAppVitestSetup(projectDir) {
70652
- const setupPath = path5.join(projectDir, VITEST_SETUP_FILENAME);
70653
- if (!fs4.existsSync(setupPath)) {
70654
- fs4.writeFileSync(setupPath, VITEST_SETUP_CONTENT);
71004
+ const setupPath = path7.join(projectDir, VITEST_SETUP_FILENAME);
71005
+ if (!fs6.existsSync(setupPath)) {
71006
+ fs6.writeFileSync(setupPath, VITEST_SETUP_CONTENT);
70655
71007
  console.error(`Wrote ${VITEST_SETUP_FILENAME} (stubs getAppBinding so \`npm test\` collects).`);
70656
- } else if (!fs4.readFileSync(setupPath, "utf-8").includes("getAppBinding")) {
71008
+ } else if (!fs6.readFileSync(setupPath, "utf-8").includes("getAppBinding")) {
70657
71009
  console.error(
70658
71010
  `\u26A0 ${VITEST_SETUP_FILENAME} exists but does not stub getAppBinding \u2014 \`npm test\` will fail to collect any test that imports the app graph (the binding-form .lotics/app_fields.ts awaits a network call). Add the stub to it:
70659
71011
  ${VITEST_SETUP_CONTENT}`
70660
71012
  );
70661
71013
  }
70662
- const viteConfigPath = path5.join(projectDir, "vite.config.ts");
70663
- if (!fs4.existsSync(viteConfigPath)) return;
70664
- const viteConfig = fs4.readFileSync(viteConfigPath, "utf-8");
71014
+ const viteConfigPath = path7.join(projectDir, "vite.config.ts");
71015
+ if (!fs6.existsSync(viteConfigPath)) return;
71016
+ const viteConfig = fs6.readFileSync(viteConfigPath, "utf-8");
70665
71017
  if (/setupFiles\s*:/.test(viteConfig) && viteConfig.includes(VITEST_SETUP_FILENAME)) return;
70666
71018
  console.error(
70667
71019
  `\u26A0 vite.config.ts does not wire ${VITEST_SETUP_FILENAME} \u2014 \`npm test\` will fail to collect any test that imports the app graph (the binding-form .lotics/app_fields.ts awaits a network call). Add it to the \`test\` block:
@@ -70669,7 +71021,7 @@ ${VITEST_SETUP_CONTENT}`
70669
71021
  );
70670
71022
  }
70671
71023
  async function appCodegen(args) {
70672
- const projectDir = path5.resolve(args.projectDir ?? process.cwd());
71024
+ const projectDir = path7.resolve(args.projectDir ?? process.cwd());
70673
71025
  const meta3 = readAppMeta(projectDir);
70674
71026
  const dtsPaths = writeAppDts(projectDir, {
70675
71027
  workflows: meta3.workflows,
@@ -70683,7 +71035,7 @@ async function appCodegen(args) {
70683
71035
  }
70684
71036
  for (const alias of orphanWorkflowBodies(projectDir, declared)) {
70685
71037
  console.error(
70686
- `\u26A0 ${path5.join(WORKFLOWS_DIR, `${alias}.ts`)} is not declared in package.json#lotics.workflows \u2014 'workflow check' and 'workflow set' both skip it. Declare the alias or delete the file.`
71038
+ `\u26A0 ${path7.join(WORKFLOWS_DIR, `${alias}.ts`)} is not declared in package.json#lotics.workflows \u2014 'workflow check' and 'workflow set' both skip it. Declare the alias or delete the file.`
70687
71039
  );
70688
71040
  }
70689
71041
  if (!args.client) {
@@ -70734,8 +71086,8 @@ async function refreshWorkflowGlobals(client, projectDir, app_id, workflows) {
70734
71086
  }
70735
71087
  async function refreshWorkflowTypes(client, projectDir, app_id, alias, declaration) {
70736
71088
  const file2 = workflowFilePath(projectDir, alias);
70737
- if (!fs4.existsSync(file2)) return false;
70738
- const body = stripWorkflowHeader(fs4.readFileSync(file2, "utf-8"));
71089
+ if (!fs6.existsSync(file2)) return false;
71090
+ const body = stripWorkflowHeader(fs6.readFileSync(file2, "utf-8"));
70739
71091
  if (body.trim() === "") return false;
70740
71092
  const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias, declaration);
70741
71093
  writeWorkflowFile(projectDir, alias, body, envelope);
@@ -70747,14 +71099,14 @@ function stampPulledManifest(projectDir, args) {
70747
71099
  workspace_id: args.workspace_id,
70748
71100
  current_version_id: args.current_version_id,
70749
71101
  version_number: args.version_number,
70750
- workflows: args.workflows,
71102
+ workflows: toManifestWorkflows(args.workflows),
70751
71103
  queries: args.queries,
70752
71104
  // The prose is stripped here and only here: `args.agents` is the LIVE map
70753
71105
  // (its instructions feed `writeAgentFiles`), the manifest gets the typed half.
70754
71106
  agents: toManifestAgents(args.agents)
70755
71107
  });
70756
71108
  writeAppDts(projectDir, {
70757
- workflows: args.workflows,
71109
+ workflows: toManifestWorkflows(args.workflows),
70758
71110
  queries: args.queries,
70759
71111
  agents: toManifestAgents(args.agents)
70760
71112
  });
@@ -70765,21 +71117,21 @@ async function downloadToFile(url2, destPath) {
70765
71117
  throw new Error(`Failed to download ${url2}: ${response.status}`);
70766
71118
  }
70767
71119
  const buffer = Buffer.from(await response.arrayBuffer());
70768
- fs4.writeFileSync(destPath, buffer);
71120
+ fs6.writeFileSync(destPath, buffer);
70769
71121
  }
70770
71122
  function appDirName(name2) {
70771
71123
  const cleaned = name2.replace(/[/\\]+/g, "-").replace(/[<>:"|?*]/g, "").replace(/\s+/g, " ").trim().replace(/^[.\s-]+|[.\s-]+$/g, "");
70772
71124
  return cleaned || "app";
70773
71125
  }
70774
71126
  async function appCreate(client, args) {
70775
- const targetPath = path5.resolve(args.targetPath ?? appDirName(args.name));
70776
- if (fs4.existsSync(targetPath)) {
70777
- const entries2 = fs4.readdirSync(targetPath);
71127
+ const targetPath = path7.resolve(args.targetPath ?? appDirName(args.name));
71128
+ if (fs6.existsSync(targetPath)) {
71129
+ const entries2 = fs6.readdirSync(targetPath);
70778
71130
  if (entries2.length > 0) {
70779
71131
  throw new Error(`Target directory ${targetPath} is not empty.`);
70780
71132
  }
70781
71133
  }
70782
- fs4.mkdirSync(targetPath, { recursive: true });
71134
+ fs6.mkdirSync(targetPath, { recursive: true });
70783
71135
  const app = await client.createApp({ name: args.name });
70784
71136
  console.error(`Created app: ${app.name} (${app.id})`);
70785
71137
  const [uiLatest, sdkLatest] = await Promise.all([
@@ -70794,9 +71146,9 @@ async function appCreate(client, args) {
70794
71146
  sdk_version: sdkLatest ? `^${sdkLatest}` : void 0
70795
71147
  });
70796
71148
  for (const file2 of files) {
70797
- const fullPath = path5.join(targetPath, file2.path);
70798
- fs4.mkdirSync(path5.dirname(fullPath), { recursive: true });
70799
- fs4.writeFileSync(fullPath, file2.content);
71149
+ const fullPath = path7.join(targetPath, file2.path);
71150
+ fs6.mkdirSync(path7.dirname(fullPath), { recursive: true });
71151
+ fs6.writeFileSync(fullPath, file2.content);
70800
71152
  }
70801
71153
  console.error(`Scaffolded ${files.length} files into ${targetPath}`);
70802
71154
  writeAppDts(targetPath, {});
@@ -70809,7 +71161,7 @@ async function appCreate(client, args) {
70809
71161
  });
70810
71162
  console.error(`
70811
71163
  Ready. Next steps:`);
70812
- console.error(` cd ${path5.relative(process.cwd(), targetPath) || "."}`);
71164
+ console.error(` cd ${path7.relative(process.cwd(), targetPath) || "."}`);
70813
71165
  console.error(` # edit src/App.tsx, then:`);
70814
71166
  console.error(` lotics app deploy`);
70815
71167
  }
@@ -70866,11 +71218,11 @@ async function appVersions(client, args) {
70866
71218
  }
70867
71219
  }
70868
71220
  function defaultPullTarget(appId, appName) {
70869
- const cwdPkgPath = path5.join(process.cwd(), "package.json");
70870
- if (fs4.existsSync(cwdPkgPath)) {
71221
+ const cwdPkgPath = path7.join(process.cwd(), "package.json");
71222
+ if (fs6.existsSync(cwdPkgPath)) {
70871
71223
  let pkg2 = null;
70872
71224
  try {
70873
- pkg2 = JSON.parse(fs4.readFileSync(cwdPkgPath, "utf-8"));
71225
+ pkg2 = JSON.parse(fs6.readFileSync(cwdPkgPath, "utf-8"));
70874
71226
  } catch (err2) {
70875
71227
  console.error(
70876
71228
  `\u26A0 Could not parse ${cwdPkgPath} (${err2 instanceof Error ? err2.message : String(err2)}) \u2014 pulling into a fresh ${appDirName(appName)}/ subdir.`
@@ -70889,16 +71241,20 @@ async function appPull(client, args) {
70889
71241
  }
70890
71242
  const version2 = await client.getAppVersion(app.id, app.current_version_id);
70891
71243
  const sourceUrl = await client.getAppVersionSourceUrl(app.id, version2.id);
70892
- const targetPath = path5.resolve(args.targetPath ?? defaultPullTarget(app.id, app.name));
70893
- fs4.mkdirSync(targetPath, { recursive: true });
70894
- const tmpFile = path5.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
71244
+ const targetPath = path7.resolve(args.targetPath ?? defaultPullTarget(app.id, app.name));
71245
+ fs6.mkdirSync(targetPath, { recursive: true });
71246
+ const tmpFile = path7.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
70895
71247
  console.error(`Downloading source archive...`);
70896
71248
  await downloadToFile(sourceUrl, tmpFile);
71249
+ const keptLocal = [];
71250
+ const stagingDir = fs6.mkdtempSync(path7.join(tmpdir(), `lotics-pull-${app.id}-`));
70897
71251
  try {
71252
+ await runTar(["-xzf", tmpFile, "-C", stagingDir], stagingDir);
70898
71253
  console.error(`Extracting to ${targetPath}...`);
70899
- await runTar(["-xzf", tmpFile, "-C", targetPath], targetPath);
71254
+ keptLocal.push(...copyTree(stagingDir, targetPath, { overwrite: args.force === true }));
70900
71255
  } finally {
70901
- if (fs4.existsSync(tmpFile)) fs4.unlinkSync(tmpFile);
71256
+ if (fs6.existsSync(tmpFile)) fs6.unlinkSync(tmpFile);
71257
+ fs6.rmSync(stagingDir, { recursive: true, force: true });
70902
71258
  }
70903
71259
  stampPulledManifest(targetPath, {
70904
71260
  app_id: app.id,
@@ -70909,6 +71265,10 @@ async function appPull(client, args) {
70909
71265
  queries: app.queries ?? {},
70910
71266
  agents: app.agents ?? {}
70911
71267
  });
71268
+ for (const [alias, declaration] of Object.entries(app.queries ?? {})) {
71269
+ const sha = declarationSha(declaration);
71270
+ writeSynced(targetPath, "queries", alias, { content: sha, live: sha });
71271
+ }
70912
71272
  await writeGeneratedAppFields(
70913
71273
  client,
70914
71274
  targetPath,
@@ -70917,7 +71277,10 @@ async function appPull(client, args) {
70917
71277
  );
70918
71278
  const workflows = app.workflows ?? {};
70919
71279
  if (Object.keys(workflows).length > 0) {
70920
- const written = await writeWorkflowFiles(client, targetPath, app.id, app.name, workflows);
71280
+ const written = await writeWorkflowFiles(client, targetPath, app.id, app.name, workflows, {
71281
+ overwrite: args.force === true,
71282
+ kept: keptLocal
71283
+ });
70921
71284
  if (written.length > 0) {
70922
71285
  console.error(
70923
71286
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
@@ -70926,7 +71289,16 @@ async function appPull(client, args) {
70926
71289
  }
70927
71290
  const agents = app.agents ?? {};
70928
71291
  if (Object.keys(agents).length > 0) {
70929
- const written = writeAgentFiles(targetPath, agents);
71292
+ const written = writeAgentFiles(targetPath, agents, {
71293
+ overwrite: args.force === true,
71294
+ kept: keptLocal
71295
+ });
71296
+ for (const alias of written) {
71297
+ const instructions = agents[alias]?.instructions;
71298
+ if (instructions === void 0) continue;
71299
+ const sha = contentSha(instructions);
71300
+ writeSynced(targetPath, "agents", alias, { content: sha, live: sha });
71301
+ }
70930
71302
  if (written.length > 0) {
70931
71303
  console.error(
70932
71304
  `Wrote ${written.length} agent instruction ${written.length === 1 ? "file" : "files"} to ${AGENTS_DIR}/ (${written.join(", ")})`
@@ -70935,62 +71307,69 @@ async function appPull(client, args) {
70935
71307
  }
70936
71308
  console.error(`Installing npm dependencies...`);
70937
71309
  await runNpm(["install"], targetPath);
71310
+ reportKeptLocalFiles(keptLocal, app.id);
70938
71311
  console.error(`
70939
71312
  Ready. Next steps:`);
70940
- console.error(` cd ${path5.relative(process.cwd(), targetPath) || "."}`);
71313
+ console.error(` cd ${path7.relative(process.cwd(), targetPath) || "."}`);
70941
71314
  console.error(` # edit src/App.tsx`);
70942
71315
  console.error(` lotics app deploy`);
70943
71316
  }
70944
71317
  function readAppSourceText(projectDir) {
70945
- const srcDir = path5.join(projectDir, "src");
70946
- if (!fs4.existsSync(srcDir)) return "";
71318
+ const srcDir = path7.join(projectDir, "src");
71319
+ if (!fs6.existsSync(srcDir)) return "";
70947
71320
  const parts = [];
70948
71321
  const walk2 = (dir) => {
70949
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
70950
- const full = path5.join(dir, entry.name);
71322
+ for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
71323
+ const full = path7.join(dir, entry.name);
70951
71324
  if (entry.isDirectory()) walk2(full);
70952
- else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) parts.push(fs4.readFileSync(full, "utf8"));
71325
+ else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) parts.push(fs6.readFileSync(full, "utf8"));
70953
71326
  }
70954
71327
  };
70955
71328
  walk2(srcDir);
70956
71329
  return parts.join("\n");
70957
71330
  }
70958
71331
  async function appDeploy(client, args) {
70959
- const projectDir = path5.resolve(args.projectDir ?? process.cwd());
71332
+ const projectDir = path7.resolve(args.projectDir ?? process.cwd());
70960
71333
  const meta3 = readAppMeta(projectDir);
70961
71334
  warnAboutDevLink(projectDir, "deploy");
70962
71335
  const liveApp = await client.getApp(meta3.app_id);
70963
- const divergences = agentTypeDivergences(meta3.agents, liveApp.agents);
70964
- if (divergences.length > 0) {
70965
- console.error(
70966
- `
70967
- Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app:
70968
- ` + divergences.map((d) => ` \u2022 ${d}`).join("\n") + `
70969
-
70970
- lotics.agents is a REFLECTION of the app, not an authoring surface, but it is what
70971
- types useAgentRun \u2014 so this build was checked against an agent that does not exist.
70972
- To take the app's shape: lotics app codegen (refreshes the block + the types)
70973
- To CHANGE the agent instead: lotics run set_app_agent '{"app_id":"${meta3.app_id}","alias":"\u2026","outputs":{\u2026}}'
70974
- (send only what changes \u2014 the server merges), then codegen.`
70975
- );
70976
- process.exit(1);
71336
+ const pending = pendingBindings({
71337
+ projectDir,
71338
+ meta: meta3,
71339
+ synced: readSynced(projectDir).workflows,
71340
+ live: liveApp
71341
+ });
71342
+ if (!nothingPending(pending)) {
71343
+ await pushPendingBindings(client, projectDir, pending);
70977
71344
  }
70978
71345
  const sourceText = readAppSourceText(projectDir);
70979
71346
  const called = calledAppAliases(sourceText);
70980
- warnIfAgentProseDiffers(projectDir, liveApp.agents);
70981
71347
  warnIfDynamicAliases(called);
70982
71348
  warnIfUndeclaredCapabilities(sourceText, meta3.capabilities);
70983
71349
  writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
71350
+ try {
71351
+ await writeGeneratedAppFields(
71352
+ client,
71353
+ projectDir,
71354
+ { app_id: meta3.app_id, package_id: liveApp.package_id },
71355
+ meta3.queries ?? {}
71356
+ );
71357
+ } catch (err2) {
71358
+ console.error(
71359
+ `\u26A0 Could not regenerate .lotics/app_fields.ts (${err2.message}).
71360
+ The build will use whatever is on disk \u2014 run 'lotics app codegen' if this app is published.`
71361
+ );
71362
+ }
70984
71363
  console.error("Building...");
70985
71364
  await runNpm(["run", "build"], projectDir);
70986
- const distDir = path5.join(projectDir, "dist");
70987
- if (!fs4.existsSync(distDir)) {
71365
+ const distDir = path7.join(projectDir, "dist");
71366
+ if (!fs6.existsSync(distDir)) {
70988
71367
  throw new Error(
70989
71368
  `Build did not produce a dist/ directory in ${projectDir}. Check that 'npm run build' is configured correctly (Vite/etc. defaults to dist/).`
70990
71369
  );
70991
71370
  }
70992
- const tmpSource = path5.join(tmpdir(), `lotics-source-${Date.now()}.tar.gz`);
70993
- const tmpDist = path5.join(tmpdir(), `lotics-dist-${Date.now()}.tar.gz`);
71371
+ const tmpSource = path7.join(tmpdir(), `lotics-source-${Date.now()}.tar.gz`);
71372
+ const tmpDist = path7.join(tmpdir(), `lotics-dist-${Date.now()}.tar.gz`);
70994
71373
  try {
70995
71374
  console.error("Packaging source...");
70996
71375
  await runTar(
@@ -71014,10 +71393,10 @@ Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app
71014
71393
  const result = await client.deployAppVersion({
71015
71394
  queries: liveQueries,
71016
71395
  app_id: meta3.app_id,
71017
- source_archive: fs4.readFileSync(tmpSource),
71018
- dist_archive: fs4.readFileSync(tmpDist),
71396
+ source_archive: fs6.readFileSync(tmpSource),
71397
+ dist_archive: fs6.readFileSync(tmpDist),
71019
71398
  prev_version_id: meta3.current_version_id,
71020
- message: args.message,
71399
+ message: args.message ?? derivedDeployMessage(pending),
71021
71400
  // Capabilities ARE manifest-authoritative: always send,
71022
71401
  // defaulting to `{}` when the manifest declares none — so deleting the
71023
71402
  // `capabilities` block turns every capability OFF on the next deploy
@@ -71032,7 +71411,8 @@ Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app
71032
71411
  // with whatever the author last remembered to write. Stop calling an
71033
71412
  // alias + redeploy to lift the guard before removing its binding.
71034
71413
  workflow_aliases: called.workflows,
71035
- query_aliases: called.queries
71414
+ query_aliases: called.queries,
71415
+ agent_aliases: called.agents
71036
71416
  });
71037
71417
  writeAppMeta(projectDir, {
71038
71418
  ...meta3,
@@ -71041,14 +71421,24 @@ Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app
71041
71421
  });
71042
71422
  console.error(`Deployed v${result.version_number} (${result.version_id})`);
71043
71423
  console.error(`Bundle size: ${(result.bundle_size_bytes / 1024).toFixed(1)} KB`);
71424
+ let liveAfter;
71044
71425
  try {
71045
- const app = await client.getApp(meta3.app_id);
71046
- warnIfUnbranded(app);
71047
- warnIfUnboundAliases(app, called);
71048
- warnIfQueriesDiffer(app, meta3.queries ?? {});
71426
+ liveAfter = await client.getApp(meta3.app_id);
71049
71427
  } catch (err2) {
71050
71428
  console.error(`(skipped post-deploy checks: ${err2.message})`);
71051
71429
  }
71430
+ if (liveAfter) {
71431
+ warnIfUnbranded(liveAfter);
71432
+ warnIfUnboundAliases(liveAfter, called);
71433
+ }
71434
+ if (liveAfter) {
71435
+ await pruneOrphanedBindings(
71436
+ client,
71437
+ meta3.app_id,
71438
+ orphanedBindings(called, liveAfter),
71439
+ called.dynamic
71440
+ );
71441
+ }
71052
71442
  } catch (err2) {
71053
71443
  const e = err2;
71054
71444
  if (e.code === "VERSION_CONFLICT") {
@@ -71060,8 +71450,8 @@ Refusing to deploy \u2014 package.json#lotics.agents disagrees with the live app
71060
71450
  throw err2;
71061
71451
  }
71062
71452
  } finally {
71063
- if (fs4.existsSync(tmpSource)) fs4.unlinkSync(tmpSource);
71064
- if (fs4.existsSync(tmpDist)) fs4.unlinkSync(tmpDist);
71453
+ if (fs6.existsSync(tmpSource)) fs6.unlinkSync(tmpSource);
71454
+ if (fs6.existsSync(tmpDist)) fs6.unlinkSync(tmpDist);
71065
71455
  }
71066
71456
  }
71067
71457
  function warnIfUnbranded(app) {
@@ -71088,54 +71478,233 @@ async function readLiveQueries(client, appId) {
71088
71478
  process.exit(1);
71089
71479
  }
71090
71480
  }
71091
- function warnIfQueriesDiffer(app, manifest) {
71092
- const drifted = driftedQueryAliases(manifest, app.queries);
71093
- if (drifted.length === 0) return;
71094
- console.error(
71095
- `
71096
- \u26A0 ${drifted.length} quer${drifted.length === 1 ? "y" : "ies"} in package.json differ from the app: ${drifted.join(", ")}
71097
- A deploy does not write them. Push yours with 'lotics app query set --all'
71098
- (or one alias at a time), or adopt the app's with 'lotics app pull'.`
71099
- );
71100
- }
71101
71481
  async function appCheck(client, args = {}) {
71102
- const projectDir = path5.resolve(args.projectDir ?? process.cwd());
71482
+ const projectDir = path7.resolve(args.projectDir ?? process.cwd());
71103
71483
  const meta3 = readAppMeta(projectDir);
71104
71484
  const app = await client.getApp(meta3.app_id);
71105
71485
  const sourceText = readAppSourceText(projectDir);
71106
71486
  const called = calledAppAliases(sourceText);
71107
- const divergences = agentTypeDivergences(meta3.agents, app.agents);
71108
- for (const d of divergences) {
71109
- console.error(`\u2717 package.json#lotics.agents.${d}`);
71110
- }
71111
- if (divergences.length > 0) {
71112
- console.error(
71113
- ` A deploy REFUSES this: the bundle would be typed against an agent that does not exist.
71114
- Take the app's shape with 'lotics app pull ${meta3.app_id}', or change the agent with set_app_agent.`
71115
- );
71116
- }
71117
71487
  warnAboutDevLink(projectDir, "deploy");
71118
- warnIfAgentProseDiffers(projectDir, app.agents);
71488
+ warnAboutOrphanedBindings(called, app);
71489
+ const pending = pendingBindings({
71490
+ projectDir,
71491
+ meta: meta3,
71492
+ synced: readSynced(projectDir).workflows,
71493
+ live: app
71494
+ });
71495
+ reportPending(pending);
71119
71496
  warnIfDynamicAliases(called);
71120
71497
  warnIfUndeclaredCapabilities(sourceText, meta3.capabilities);
71121
71498
  warnIfUnboundAliases(app, called);
71122
- warnIfQueriesDiffer(app, meta3.queries ?? {});
71123
71499
  warnIfUnbranded(app);
71124
- if (divergences.length > 0) process.exit(1);
71500
+ if (!nothingPending(pending)) {
71501
+ process.exit(1);
71502
+ }
71125
71503
  console.error("Checked the app's bindings, capabilities and agent schemas \u2014 nothing blocking.");
71126
71504
  }
71127
- function warnIfAgentProseDiffers(projectDir, liveAgents) {
71505
+ async function pushPendingBindings(client, projectDir, pending) {
71506
+ const plan = [
71507
+ ...pending.queries.map((alias) => `query ${alias}`),
71508
+ ...pending.workflows.map((alias) => `workflow ${alias}`),
71509
+ ...pending.agents.map((alias) => `agent ${alias}`)
71510
+ ];
71511
+ console.error(
71512
+ `
71513
+ Pushing ${plan.length} binding${plan.length === 1 ? "" : "s"} ahead of the app: ${plan.join(", ")}`
71514
+ );
71515
+ for (const alias of pending.queries) {
71516
+ await appQuerySet(client, { alias, projectDir });
71517
+ }
71518
+ for (const alias of pending.workflows) {
71519
+ await appWorkflowSet(client, { alias, projectDir });
71520
+ }
71521
+ for (const alias of pending.agents) {
71522
+ await appAgentSet(client, { alias, projectDir });
71523
+ }
71524
+ }
71525
+ function pendingBindings(args) {
71526
+ const { projectDir, meta: meta3, synced, live } = args;
71527
+ const workflows = /* @__PURE__ */ new Set([
71528
+ ...workflowBodyDrift(projectDir, meta3.workflows, synced),
71529
+ ...divergedWorkflowAliases(meta3.workflows, live.workflows)
71530
+ ]);
71531
+ return {
71532
+ workflows: [...workflows].sort(),
71533
+ agents: [
71534
+ .../* @__PURE__ */ new Set([
71535
+ ...driftedAgentAliases(projectDir, live.agents),
71536
+ // A manifest declaration that disagrees with the live row is a push now,
71537
+ // the same as a workflow's. `agentTypeDivergences` renders one line per
71538
+ // (alias, key) for a human, and the alias is its first token — but an
71539
+ // alias that diverged on both `inputs` and `outputs` still needs exactly
71540
+ // one push.
71541
+ ...divergedAgentAliases(meta3.agents, live.agents)
71542
+ ])
71543
+ ].sort(),
71544
+ // Only aliases the manifest DECLARES. `driftedQueryAliases` is symmetric —
71545
+ // it unions both key sets — so an alias deleted locally but still bound live
71546
+ // reads as drift, and queueing it sends the author to a push that refuses
71547
+ // ("declare it first") for a declaration they deliberately removed. A
71548
+ // deletion is not a push, and this detector only decides what to push;
71549
+ // unbinding is `remove_app_query`, which no deploy runs for any artifact.
71550
+ queries: [...driftedQueryAliases(meta3.queries ?? {}, live.queries)].filter((alias) => (meta3.queries ?? {})[alias] !== void 0).sort()
71551
+ };
71552
+ }
71553
+ function reportPending(pending) {
71554
+ const lines = [
71555
+ ...pending.workflows.map((a) => ` \u2022 workflow "${a}" \u2014 lotics app workflow set ${a}`),
71556
+ ...pending.agents.map((a) => ` \u2022 agent "${a}" \u2014 lotics app agent set ${a}`),
71557
+ ...pending.queries.map((a) => ` \u2022 query "${a}" \u2014 lotics app query set ${a}`)
71558
+ ];
71559
+ if (lines.length === 0) return;
71560
+ console.error(
71561
+ `
71562
+ \u2717 ${lines.length} binding${lines.length === 1 ? " is" : "s are"} ahead of the app:
71563
+ ` + lines.join("\n") + `
71564
+
71565
+ A version shipped now would not reproduce what runs. 'lotics app deploy' pushes
71566
+ these for you; revert the file instead to drop the edit.`
71567
+ );
71568
+ }
71569
+ function nothingPending(pending) {
71570
+ return pending.workflows.length === 0 && pending.agents.length === 0 && pending.queries.length === 0;
71571
+ }
71572
+ function divergedAgentAliases(manifestAgents, liveAgents) {
71573
+ const out = [];
71574
+ for (const [alias, declared] of Object.entries(manifestAgents ?? {})) {
71575
+ const live = (liveAgents ?? {})[alias];
71576
+ if (!live) continue;
71577
+ if (AGENT_TYPED_KEYS.some((key) => !sameSchema(declared[key], live[key]))) out.push(alias);
71578
+ }
71579
+ return out;
71580
+ }
71581
+ function driftedAgentAliases(projectDir, liveAgents) {
71582
+ const drifted = [];
71128
71583
  for (const [alias, live] of Object.entries(liveAgents ?? {})) {
71129
71584
  const file2 = agentFilePath2(projectDir, alias);
71130
- if (!fs4.existsSync(file2)) continue;
71131
- const local = stripAgentHeader(fs4.readFileSync(file2, "utf-8"));
71585
+ if (!fs6.existsSync(file2)) continue;
71586
+ const local = stripAgentHeader(fs6.readFileSync(file2, "utf-8"));
71132
71587
  if (local === "" || local === (live.instructions ?? "")) continue;
71588
+ drifted.push(alias);
71589
+ }
71590
+ return drifted.sort();
71591
+ }
71592
+ function divergedWorkflowAliases(manifestWorkflows, liveWorkflows) {
71593
+ return [
71594
+ ...new Set(workflowTypeDivergences(manifestWorkflows, liveWorkflows).map((d) => d.alias))
71595
+ ].sort();
71596
+ }
71597
+ function orphanedBindings(called, live) {
71598
+ const agentDeclarations = Object.values(live.agents ?? {});
71599
+ const referenced = {
71600
+ queries: [...called.queries, ...agentDeclarations.flatMap((a) => a.query_aliases ?? [])],
71601
+ workflows: [...called.workflows, ...agentDeclarations.flatMap((a) => a.workflow_aliases ?? [])],
71602
+ // Nothing declares an agent but the client bundle — an agent cannot run another.
71603
+ agents: called.agents
71604
+ };
71605
+ return orphanedAliases(
71606
+ {
71607
+ queries: Object.keys(live.queries ?? {}),
71608
+ workflows: Object.keys(live.workflows ?? {}),
71609
+ agents: Object.keys(live.agents ?? {})
71610
+ },
71611
+ referenced
71612
+ );
71613
+ }
71614
+ function warnAboutOrphanedBindings(called, live) {
71615
+ const orphans = orphanedBindings(called, live);
71616
+ const lines = [
71617
+ ...orphans.queries.map((a) => ` \u2022 query ${a}`),
71618
+ ...orphans.workflows.map((a) => ` \u2022 workflow ${a}`),
71619
+ ...orphans.agents.map((a) => ` \u2022 agent ${a}`)
71620
+ ];
71621
+ if (lines.length === 0) return 0;
71622
+ console.error(
71623
+ `
71624
+ \u26A0 ${lines.length} binding(s) are live but named nowhere in this bundle:
71625
+ ` + lines.join("\n") + (called.dynamic.length > 0 ? `
71626
+
71627
+ This app computes an alias at runtime, so one of these may be called after
71628
+ all \u2014 a deploy therefore leaves them ALL in place. Make those call sites
71629
+ literal and a deploy will clean them up.` : `
71630
+
71631
+ A deploy unbinds these after it ships.`)
71632
+ );
71633
+ return lines.length;
71634
+ }
71635
+ async function pruneOrphanedBindings(client, appId, orphans, dynamic) {
71636
+ const targets = [
71637
+ ...orphans.queries.map((alias) => ({ tool: "remove_app_query", alias, label: `query ${alias}` })),
71638
+ ...orphans.workflows.map((alias) => ({
71639
+ tool: "remove_app_workflow",
71640
+ alias,
71641
+ label: `workflow ${alias}`
71642
+ })),
71643
+ ...orphans.agents.map((alias) => ({ tool: "remove_app_agent", alias, label: `agent ${alias}` }))
71644
+ ];
71645
+ if (targets.length === 0) return 0;
71646
+ if (dynamic.length > 0) {
71133
71647
  console.error(
71134
71648
  `
71135
- \u26A0 ${path5.relative(projectDir, file2)} differs from the live agent's instructions.
71136
- A deploy does not push prose. Send it with 'lotics app agent set ${alias}'.`
71649
+ \u26A0 Left ${targets.length} unused binding(s) in place: this app calls ${dynamic.join(" / ")} with an alias it computes at runtime, and a static scan cannot tell which binding that reaches \u2014 so any of these may be live:
71650
+ ` + targets.map((t) => ` \u2022 ${t.label}`).join("\n") + `
71651
+ Make those call sites literal and the next deploy will clean them up, or remove the ones you know are dead by hand.`
71137
71652
  );
71653
+ return targets.length;
71654
+ }
71655
+ let failed = 0;
71656
+ for (const target of targets) {
71657
+ const res = await client.execute(target.tool, { app_id: appId, alias: target.alias });
71658
+ if (res.error) {
71659
+ failed += 1;
71660
+ console.error(` \u2717 could not unbind ${target.label} \u2014 ${res.error}`);
71661
+ continue;
71662
+ }
71663
+ console.error(` \u2713 unbound ${target.label}`);
71138
71664
  }
71665
+ return failed;
71666
+ }
71667
+ function derivedDeployMessage(pending) {
71668
+ const parts = [
71669
+ ...pending.queries.map((a) => `query ${a}`),
71670
+ ...pending.workflows.map((a) => `workflow ${a}`),
71671
+ ...pending.agents.map((a) => `agent ${a}`)
71672
+ ];
71673
+ return parts.length === 0 ? "Deployed app code" : `Deployed app code; pushed ${parts.join(", ")}`;
71674
+ }
71675
+ function copyTree(from, to, opts, relative = "") {
71676
+ const kept = [];
71677
+ for (const entry of fs6.readdirSync(from, { withFileTypes: true })) {
71678
+ const src = path7.join(from, entry.name);
71679
+ const dest = path7.join(to, entry.name);
71680
+ const rel = path7.join(relative, entry.name);
71681
+ if (entry.isDirectory()) {
71682
+ fs6.mkdirSync(dest, { recursive: true });
71683
+ kept.push(...copyTree(src, dest, opts, rel));
71684
+ continue;
71685
+ }
71686
+ if (!opts.overwrite && fs6.existsSync(dest)) {
71687
+ if (fs6.readFileSync(src).equals(fs6.readFileSync(dest))) continue;
71688
+ kept.push(rel);
71689
+ continue;
71690
+ }
71691
+ fs6.copyFileSync(src, dest);
71692
+ }
71693
+ return kept;
71694
+ }
71695
+ function reportKeptLocalFiles(kept, appId) {
71696
+ if (kept.length === 0) return;
71697
+ console.error(
71698
+ `
71699
+ ${kept.length} local file(s) differ from the app and were KEPT, not overwritten:
71700
+ ` + kept.slice(0, 20).map((f) => ` \u2022 ${f}`).join("\n") + (kept.length > 20 ? `
71701
+ \u2026 and ${kept.length - 20} more` : ``) + `
71702
+
71703
+ Everything else was refreshed from the app. Your edits are intact.
71704
+ Ship them: lotics app deploy (pushes bodies, prompts and declarations)
71705
+ See the difference: lotics app check
71706
+ Take the app's copy: lotics app pull ${appId} --force (DISCARDS the files above)`
71707
+ );
71139
71708
  }
71140
71709
  function warnIfDynamicAliases(called) {
71141
71710
  if (called.dynamic.length === 0) return;
@@ -71179,7 +71748,7 @@ function warnIfUnboundAliases(app, called) {
71179
71748
  console.error(lines.join("\n"));
71180
71749
  }
71181
71750
  async function appDev(client, args) {
71182
- const projectDir = path5.resolve(args.projectDir ?? process.cwd());
71751
+ const projectDir = path7.resolve(args.projectDir ?? process.cwd());
71183
71752
  const meta3 = readAppMeta(projectDir);
71184
71753
  warnAboutDevLink(projectDir, "dev");
71185
71754
  writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
@@ -71350,11 +71919,11 @@ var AGENT_RUN_POLL = {
71350
71919
  existenceTimeoutMs: 5e3
71351
71920
  };
71352
71921
  var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
71353
- async function fetchSettledAgentRun(client, appId, sessionId, runId, timing) {
71922
+ async function fetchSettledAgentRun(client, appId, sessionId2, runId, timing) {
71354
71923
  const settleDeadline = Date.now() + timing.settleTimeoutMs;
71355
71924
  const existenceDeadline = Date.now() + timing.existenceTimeoutMs;
71356
71925
  for (; ; ) {
71357
- const { runs } = await client.listAgentRuns(appId, sessionId);
71926
+ const { runs } = await client.listAgentRuns(appId, sessionId2);
71358
71927
  const target = runId ? runs.find((r) => r.id === runId) : runs[runs.length - 1];
71359
71928
  if (target) {
71360
71929
  if (target.status !== "running" || Date.now() >= settleDeadline) return target;
@@ -71365,20 +71934,20 @@ async function fetchSettledAgentRun(client, appId, sessionId, runId, timing) {
71365
71934
  }
71366
71935
  }
71367
71936
  async function appAgentRun(client, args, timing = AGENT_RUN_POLL) {
71368
- const sessionId = args.sessionId ?? `cli-${randomUUID()}`;
71937
+ const sessionId2 = args.sessionId ?? `cli-${randomUUID()}`;
71369
71938
  const continuing = args.sessionId !== void 0;
71370
71939
  const res = await client.appAgentRunStream(args.app_id, args.alias, {
71371
- session_id: sessionId,
71940
+ session_id: sessionId2,
71372
71941
  input: args.input
71373
71942
  });
71374
71943
  const runId = res.headers.get("x-app-agent-run-id") ?? void 0;
71375
71944
  if (res.body) {
71376
71945
  await streamAgentTextDeltas(res.body, (piece) => process.stderr.write(piece));
71377
71946
  }
71378
- const run = await fetchSettledAgentRun(client, args.app_id, sessionId, runId, timing);
71947
+ const run = await fetchSettledAgentRun(client, args.app_id, sessionId2, runId, timing);
71379
71948
  if (!run) {
71380
71949
  console.error(`
71381
- Could not find the settled run for session ${sessionId} on ${args.app_id}.`);
71950
+ Could not find the settled run for session ${sessionId2} on ${args.app_id}.`);
71382
71951
  console.error("The run may still be in progress \u2014 re-check with `lotics run` against the app's agent-runs.");
71383
71952
  process.exit(1);
71384
71953
  }
@@ -71394,13 +71963,14 @@ Could not find the settled run for session ${sessionId} on ${args.app_id}.`);
71394
71963
  Agent "${args.alias}" run ${run.id} \u2192 ${run.status}${run.error_message ? `: ${run.error_message}` : ""}`
71395
71964
  );
71396
71965
  console.error(
71397
- continuing ? `Session: ${sessionId}` : `Session: ${sessionId} (fresh \u2014 pass --session ${sessionId} to continue this thread)`
71966
+ continuing ? `Session: ${sessionId2}` : `Session: ${sessionId2} (fresh \u2014 pass --session ${sessionId2} to continue this thread)`
71398
71967
  );
71399
71968
  if (run.status !== "completed") process.exit(1);
71400
71969
  }
71401
71970
  async function appAgentSet(client, args) {
71402
- const projectDir = process.cwd();
71971
+ const projectDir = args.projectDir ?? process.cwd();
71403
71972
  const meta3 = readAppMeta(projectDir);
71973
+ noteScope({ alias: args.alias });
71404
71974
  const app = await client.getApp(meta3.app_id);
71405
71975
  const live = app.agents?.[args.alias];
71406
71976
  if (!live) {
@@ -71409,31 +71979,51 @@ async function appAgentSet(client, args) {
71409
71979
  );
71410
71980
  process.exit(1);
71411
71981
  }
71982
+ const declared = meta3.agents?.[args.alias];
71983
+ const hasDeclaration = declared?.inputs !== void 0 || declared?.outputs !== void 0;
71412
71984
  const file2 = agentFilePath2(projectDir, args.alias);
71413
- if (!fs4.existsSync(file2)) {
71985
+ if (!fs6.existsSync(file2) && !hasDeclaration) {
71414
71986
  console.error(
71415
- `No instructions at ${path5.relative(projectDir, file2)}. Run 'lotics app pull ${meta3.app_id}' to write ${AGENTS_DIR}/${args.alias}.md, then edit it.`
71987
+ `No instructions at ${path7.relative(projectDir, file2)}, and nothing declared in package.json#lotics.agents.${args.alias} to push instead.
71988
+ Run 'lotics app pull ${meta3.app_id}' to write ${AGENTS_DIR}/${args.alias}.md, then edit it.`
71416
71989
  );
71417
71990
  process.exit(1);
71418
71991
  }
71419
- const instructions = stripAgentHeader(fs4.readFileSync(file2, "utf-8"));
71992
+ const instructions = fs6.existsSync(file2) ? stripAgentHeader(fs6.readFileSync(file2, "utf-8")) : void 0;
71420
71993
  if (instructions === "") {
71421
71994
  console.error(
71422
- `${path5.relative(projectDir, file2)} is empty after stripping the header \u2014 refusing to push an empty prompt.`
71995
+ `${path7.relative(projectDir, file2)} is empty after stripping the header \u2014 refusing to push an empty prompt.`
71423
71996
  );
71424
71997
  process.exit(1);
71425
71998
  }
71426
- const res = await client.setAppAgent(meta3.app_id, args.alias, { instructions });
71999
+ const proseBaseline = readSynced(projectDir).agents[args.alias]?.live;
72000
+ const res = await client.setAppAgent(meta3.app_id, args.alias, {
72001
+ // Prose and its precondition ride together. Gating a declaration-only push on
72002
+ // the prose baseline would refuse it because someone edited a PROMPT — a
72003
+ // precondition must guard what this call actually writes.
72004
+ ...instructions !== void 0 ? { instructions, ...proseBaseline ? { expected_instructions_sha: proseBaseline } : {} } : {},
72005
+ ...declared?.inputs !== void 0 ? { inputs: declared.inputs } : {},
72006
+ ...declared?.outputs !== void 0 ? { outputs: declared.outputs } : {}
72007
+ });
71427
72008
  if (res.error) {
71428
72009
  console.error(`Failed to set agent "${args.alias}": ${res.error}`);
71429
72010
  process.exit(1);
71430
72011
  }
72012
+ if (instructions !== void 0) {
72013
+ const proseSha = contentSha(instructions);
72014
+ writeSynced(projectDir, "agents", args.alias, { content: proseSha, live: proseSha });
72015
+ }
72016
+ const pushed = [
72017
+ ...instructions !== void 0 ? [`${instructions.length} chars of instructions`] : [],
72018
+ ...declared?.inputs !== void 0 ? ["inputs"] : [],
72019
+ ...declared?.outputs !== void 0 ? ["outputs"] : []
72020
+ ];
71431
72021
  console.error(
71432
- `Set agent "${args.alias}" (${instructions.length} chars of instructions` + (live.model_tier ? `, ${live.model_tier}` : "") + `). Typed fields left as they are.`
72022
+ `Set agent "${args.alias}" (${pushed.join(", ")}` + (live.model_tier ? `, ${live.model_tier}` : "") + `). Everything else left as it is.`
71433
72023
  );
71434
72024
  }
71435
72025
  async function appWorkflowSet(client, args) {
71436
- const projectDir = process.cwd();
72026
+ const projectDir = args.projectDir ?? process.cwd();
71437
72027
  const meta3 = readAppMeta(projectDir);
71438
72028
  const declaration = meta3.workflows?.[args.alias];
71439
72029
  if (!declaration) {
@@ -71443,15 +72033,18 @@ async function appWorkflowSet(client, args) {
71443
72033
  process.exit(1);
71444
72034
  }
71445
72035
  const file2 = workflowFilePath(projectDir, args.alias);
71446
- if (!fs4.existsSync(file2)) {
72036
+ if (!fs6.existsSync(file2)) {
71447
72037
  console.error(
71448
- `No workflow body at ${path5.relative(projectDir, file2)}. Run 'lotics app pull ${meta3.app_id}' to write src/workflows/${args.alias}.ts, then edit it.`
72038
+ `No workflow body at ${path7.relative(projectDir, file2)}.
72039
+ If "${args.alias}" is already bound: lotics app pull ${meta3.app_id} (writes the body)
72040
+ If it is a NEW alias you declared: create that file yourself, then re-run this.`
71449
72041
  );
71450
72042
  process.exit(1);
71451
72043
  }
71452
- const source = stripWorkflowHeader(fs4.readFileSync(file2, "utf-8"));
72044
+ noteScope({ alias: args.alias });
72045
+ const source = stripWorkflowHeader(fs6.readFileSync(file2, "utf-8"));
71453
72046
  if (source.trim() === "") {
71454
- console.error(`Workflow body ${path5.relative(projectDir, file2)} is empty after stripping the header.`);
72047
+ console.error(`Workflow body ${path7.relative(projectDir, file2)} is empty after stripping the header.`);
71455
72048
  process.exit(1);
71456
72049
  }
71457
72050
  const res = await client.setAppWorkflow(meta3.app_id, args.alias, {
@@ -71465,12 +72058,21 @@ async function appWorkflowSet(client, args) {
71465
72058
  // `Workflow invoked by app "X" via useWorkflow("y")`. Absent leaves
71466
72059
  // whatever is on the row, so this can never blank a description set
71467
72060
  // elsewhere.
71468
- description: declaration.description
72061
+ description: declaration.description,
72062
+ // The body this edit was built on. Turns the push into a conditional write:
72063
+ // if the live body moved since this project last saw it, the server refuses
72064
+ // rather than letting this copy overwrite a change nobody here has seen.
72065
+ expected_body_sha: readSynced(projectDir).workflows[args.alias]?.live
71469
72066
  });
71470
72067
  if (res.error) {
71471
72068
  console.error(`Failed to set workflow "${args.alias}": ${res.error}`);
71472
72069
  process.exit(1);
71473
72070
  }
72071
+ const pushedBodySha = res.result !== null && typeof res.result === "object" ? res.result.body_sha : void 0;
72072
+ writeSynced(projectDir, "workflows", args.alias, {
72073
+ content: contentSha(source),
72074
+ ...typeof pushedBodySha === "string" ? { live: pushedBodySha } : {}
72075
+ });
71474
72076
  const result = res.result ?? {};
71475
72077
  const workflowId = typeof result.workflow_id === "string" ? result.workflow_id : "(unknown)";
71476
72078
  console.error(`Set workflow "${args.alias}" \u2192 ${workflowId}`);
@@ -71490,10 +72092,11 @@ async function appWorkflowSet(client, args) {
71490
72092
  }
71491
72093
  }
71492
72094
  async function appQuerySet(client, args) {
71493
- const projectDir = process.cwd();
72095
+ const projectDir = args.projectDir ?? process.cwd();
71494
72096
  const meta3 = readAppMeta(projectDir);
71495
72097
  const declared = meta3.queries ?? {};
71496
72098
  const aliases = "all" in args ? Object.keys(declared).sort() : [args.alias];
72099
+ if (!("all" in args)) noteScope({ alias: args.alias });
71497
72100
  if (aliases.length === 0) {
71498
72101
  console.error("No queries in package.json#lotics.queries \u2014 nothing to push.");
71499
72102
  return;
@@ -71507,19 +72110,26 @@ async function appQuerySet(client, args) {
71507
72110
  );
71508
72111
  process.exit(1);
71509
72112
  }
71510
- const res = await client.setAppQuery(meta3.app_id, alias, declaration);
72113
+ const res = await client.setAppQuery(
72114
+ meta3.app_id,
72115
+ alias,
72116
+ declaration,
72117
+ readSynced(projectDir).queries[alias]?.live
72118
+ );
71511
72119
  if (res.error) {
71512
72120
  if (pushed.length > 0) console.error(`Pushed before the failure: ${pushed.join(", ")}.`);
71513
72121
  console.error(`Failed to set query "${alias}": ${res.error}`);
71514
72122
  process.exit(1);
71515
72123
  }
72124
+ const querySha = declarationSha(declaration);
72125
+ writeSynced(projectDir, "queries", alias, { content: querySha, live: querySha });
71516
72126
  pushed.push(alias);
71517
72127
  }
71518
72128
  console.error(
71519
72129
  `Set ${pushed.length} quer${pushed.length === 1 ? "y" : "ies"} on ${meta3.app_id}: ${pushed.join(", ")}.`
71520
72130
  );
71521
72131
  }
71522
- async function appWorkflowPull(client) {
72132
+ async function appWorkflowPull(client, args = {}) {
71523
72133
  const projectDir = process.cwd();
71524
72134
  const meta3 = readAppMeta(projectDir);
71525
72135
  const app = await client.getApp(meta3.app_id);
@@ -71528,10 +72138,15 @@ async function appWorkflowPull(client) {
71528
72138
  console.error(`App ${meta3.app_id} has no bound workflows.`);
71529
72139
  return;
71530
72140
  }
71531
- const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, app.name, workflows);
72141
+ const kept = [];
72142
+ const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, app.name, workflows, {
72143
+ overwrite: args.force === true,
72144
+ kept
72145
+ });
71532
72146
  console.error(
71533
72147
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
71534
72148
  );
72149
+ reportKeptLocalFiles(kept, meta3.app_id);
71535
72150
  ensureAppTsconfig(projectDir);
71536
72151
  }
71537
72152
  async function appWorkflowCheck(args) {
@@ -71558,15 +72173,15 @@ async function appWorkflowCheck(args) {
71558
72173
  for (const alias of aliases) {
71559
72174
  const bodyPath = workflowFilePath(projectDir, alias);
71560
72175
  const globalsPath = workflowGlobalsPath(projectDir, alias);
71561
- if (!fs4.existsSync(bodyPath)) {
72176
+ if (!fs6.existsSync(bodyPath)) {
71562
72177
  console.error(
71563
- `\u26A0 Skipped "${alias}" \u2014 no body at ${path5.relative(projectDir, bodyPath)}. Run 'lotics app workflow pull' to write it.`
72178
+ `\u26A0 Skipped "${alias}" \u2014 no body at ${path7.relative(projectDir, bodyPath)}. Run 'lotics app workflow pull' to write it.`
71564
72179
  );
71565
72180
  continue;
71566
72181
  }
71567
- if (!fs4.existsSync(globalsPath)) {
72182
+ if (!fs6.existsSync(globalsPath)) {
71568
72183
  console.error(
71569
- `Cannot check "${alias}" \u2014 missing types at ${path5.relative(projectDir, globalsPath)}. Run 'lotics app workflow pull' (or 'lotics app codegen') to fetch them.`
72184
+ `Cannot check "${alias}" \u2014 missing types at ${path7.relative(projectDir, globalsPath)}. Run 'lotics app workflow pull' (or 'lotics app codegen') to fetch them.`
71570
72185
  );
71571
72186
  process.exit(1);
71572
72187
  }
@@ -71585,7 +72200,7 @@ async function appWorkflowCheck(args) {
71585
72200
  function printWorkflowCheckResults(projectDir, results) {
71586
72201
  let totalErrors = 0;
71587
72202
  for (const r of results) {
71588
- const rel = path5.relative(projectDir, r.bodyPath);
72203
+ const rel = path7.relative(projectDir, r.bodyPath);
71589
72204
  if (r.issues.length === 0) {
71590
72205
  console.error(`\u2713 ${r.alias} (${rel}) \u2014 clean`);
71591
72206
  continue;
@@ -71614,6 +72229,7 @@ ${totalErrors} error${totalErrors === 1 ? "" : "s"} across ${results.length - pa
71614
72229
  function parseArgs(argv) {
71615
72230
  const flags = {
71616
72231
  json: false,
72232
+ force: false,
71617
72233
  timeout: void 0,
71618
72234
  output: void 0,
71619
72235
  as: void 0,
@@ -71646,6 +72262,9 @@ function parseArgs(argv) {
71646
72262
  case "--json":
71647
72263
  flags.json = true;
71648
72264
  break;
72265
+ case "--force":
72266
+ flags.force = true;
72267
+ break;
71649
72268
  case "--timeout":
71650
72269
  flags.timeout = parseInt(argv[++i2], 10);
71651
72270
  break;
@@ -71789,11 +72408,11 @@ async function validateOrgWorkspacePin(client, orgId, profile) {
71789
72408
  }
71790
72409
 
71791
72410
  // src/xlsx.ts
71792
- import fs6 from "node:fs";
72411
+ import fs8 from "node:fs";
71793
72412
 
71794
72413
  // src/file_command_io.ts
71795
- import fs5 from "node:fs";
71796
- import path6 from "node:path";
72414
+ import fs7 from "node:fs";
72415
+ import path8 from "node:path";
71797
72416
  var CliError = class extends Error {
71798
72417
  constructor(message2) {
71799
72418
  super(message2);
@@ -71814,14 +72433,14 @@ function rejectExtraArgs(rest2, arity, usage) {
71814
72433
  fail2(`${usage} takes ${arity} argument${arity === 1 ? "" : "s"} after <file>, got ${rest2.length}. Unused: ${rest2.slice(arity).join(" ")}`);
71815
72434
  }
71816
72435
  function writeFileAtomic(filePath, bytes) {
71817
- const dir = path6.dirname(path6.resolve(filePath));
71818
- const tmp = path6.join(dir, `.${path6.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
71819
- fs5.writeFileSync(tmp, bytes);
72436
+ const dir = path8.dirname(path8.resolve(filePath));
72437
+ const tmp = path8.join(dir, `.${path8.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
72438
+ fs7.writeFileSync(tmp, bytes);
71820
72439
  try {
71821
- fs5.renameSync(tmp, filePath);
72440
+ fs7.renameSync(tmp, filePath);
71822
72441
  } catch (error52) {
71823
72442
  try {
71824
- fs5.unlinkSync(tmp);
72443
+ fs7.unlinkSync(tmp);
71825
72444
  } catch {
71826
72445
  }
71827
72446
  throw error52;
@@ -74618,16 +75237,16 @@ var MatcherView = class {
74618
75237
  * @returns {string|undefined}
74619
75238
  */
74620
75239
  getCurrentTag() {
74621
- const path8 = this._matcher.path;
74622
- return path8.length > 0 ? path8[path8.length - 1].tag : void 0;
75240
+ const path10 = this._matcher.path;
75241
+ return path10.length > 0 ? path10[path10.length - 1].tag : void 0;
74623
75242
  }
74624
75243
  /**
74625
75244
  * Get current namespace.
74626
75245
  * @returns {string|undefined}
74627
75246
  */
74628
75247
  getCurrentNamespace() {
74629
- const path8 = this._matcher.path;
74630
- return path8.length > 0 ? path8[path8.length - 1].namespace : void 0;
75248
+ const path10 = this._matcher.path;
75249
+ return path10.length > 0 ? path10[path10.length - 1].namespace : void 0;
74631
75250
  }
74632
75251
  /**
74633
75252
  * Get current node's attribute value.
@@ -74635,9 +75254,9 @@ var MatcherView = class {
74635
75254
  * @returns {*}
74636
75255
  */
74637
75256
  getAttrValue(attrName) {
74638
- const path8 = this._matcher.path;
74639
- if (path8.length === 0) return void 0;
74640
- return path8[path8.length - 1].values?.[attrName];
75257
+ const path10 = this._matcher.path;
75258
+ if (path10.length === 0) return void 0;
75259
+ return path10[path10.length - 1].values?.[attrName];
74641
75260
  }
74642
75261
  /**
74643
75262
  * Check if current node has an attribute.
@@ -74645,9 +75264,9 @@ var MatcherView = class {
74645
75264
  * @returns {boolean}
74646
75265
  */
74647
75266
  hasAttr(attrName) {
74648
- const path8 = this._matcher.path;
74649
- if (path8.length === 0) return false;
74650
- const current = path8[path8.length - 1];
75267
+ const path10 = this._matcher.path;
75268
+ if (path10.length === 0) return false;
75269
+ const current = path10[path10.length - 1];
74651
75270
  return current.values !== void 0 && attrName in current.values;
74652
75271
  }
74653
75272
  /**
@@ -74673,18 +75292,18 @@ var MatcherView = class {
74673
75292
  * @returns {number}
74674
75293
  */
74675
75294
  getPosition() {
74676
- const path8 = this._matcher.path;
74677
- if (path8.length === 0) return -1;
74678
- return path8[path8.length - 1].position ?? 0;
75295
+ const path10 = this._matcher.path;
75296
+ if (path10.length === 0) return -1;
75297
+ return path10[path10.length - 1].position ?? 0;
74679
75298
  }
74680
75299
  /**
74681
75300
  * Get current node's repeat counter (occurrence count of this tag name).
74682
75301
  * @returns {number}
74683
75302
  */
74684
75303
  getCounter() {
74685
- const path8 = this._matcher.path;
74686
- if (path8.length === 0) return -1;
74687
- return path8[path8.length - 1].counter ?? 0;
75304
+ const path10 = this._matcher.path;
75305
+ if (path10.length === 0) return -1;
75306
+ return path10[path10.length - 1].counter ?? 0;
74688
75307
  }
74689
75308
  /**
74690
75309
  * Get current node's sibling index (alias for getPosition).
@@ -80342,10 +80961,10 @@ function resolveRelativePath(basePath, relativePath) {
80342
80961
  }
80343
80962
  return resolved.join("/");
80344
80963
  }
80345
- function findZipEntry(zipEntries, path8) {
80346
- if (zipEntries[path8]) return zipEntries[path8];
80347
- if (zipEntries[`xl/${path8}`]) return zipEntries[`xl/${path8}`];
80348
- const noSlash = path8.replace(/^\//, "");
80964
+ function findZipEntry(zipEntries, path10) {
80965
+ if (zipEntries[path10]) return zipEntries[path10];
80966
+ if (zipEntries[`xl/${path10}`]) return zipEntries[`xl/${path10}`];
80967
+ const noSlash = path10.replace(/^\//, "");
80349
80968
  if (zipEntries[noSlash]) return zipEntries[noSlash];
80350
80969
  return void 0;
80351
80970
  }
@@ -81974,9 +82593,9 @@ function parseExcelFromZip(zip, options) {
81974
82593
  }
81975
82594
  return result;
81976
82595
  }
81977
- function findEntry(zip, path8) {
81978
- if (zip[path8]) return zip[path8];
81979
- const lower2 = path8.toLowerCase();
82596
+ function findEntry(zip, path10) {
82597
+ if (zip[path10]) return zip[path10];
82598
+ const lower2 = path10.toLowerCase();
81980
82599
  for (const key of Object.keys(zip)) {
81981
82600
  if (key.toLowerCase() === lower2) return zip[key];
81982
82601
  }
@@ -82613,12 +83232,12 @@ function extractPivotRoundTripInfo(originalZip) {
82613
83232
  if (paths.length > 0) pivotTablesBySheetIndex.set(i2, paths);
82614
83233
  }
82615
83234
  const pivotXmlPaths = [];
82616
- for (const path8 of Object.keys(originalZip)) {
82617
- if (path8.startsWith("xl/pivotTables/") && path8.endsWith(".xml")) {
82618
- pivotXmlPaths.push(path8);
83235
+ for (const path10 of Object.keys(originalZip)) {
83236
+ if (path10.startsWith("xl/pivotTables/") && path10.endsWith(".xml")) {
83237
+ pivotXmlPaths.push(path10);
82619
83238
  }
82620
- if (path8.startsWith("xl/pivotCache/") && path8.endsWith(".xml")) {
82621
- pivotXmlPaths.push(path8);
83239
+ if (path10.startsWith("xl/pivotCache/") && path10.endsWith(".xml")) {
83240
+ pivotXmlPaths.push(path10);
82622
83241
  }
82623
83242
  }
82624
83243
  return { cachesByWorkbook, pivotTablesBySheetIndex, pivotXmlPaths };
@@ -82626,15 +83245,15 @@ function extractPivotRoundTripInfo(originalZip) {
82626
83245
  function decode3(bytes) {
82627
83246
  return new TextDecoder().decode(bytes);
82628
83247
  }
82629
- function pivotContentTypeFor(path8) {
82630
- if (path8.startsWith("xl/pivotTables/") && path8.endsWith(".xml")) {
82631
- return `<Override PartName="/${path8}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"/>`;
83248
+ function pivotContentTypeFor(path10) {
83249
+ if (path10.startsWith("xl/pivotTables/") && path10.endsWith(".xml")) {
83250
+ return `<Override PartName="/${path10}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"/>`;
82632
83251
  }
82633
- if (path8.includes("/pivotCacheDefinition") && path8.endsWith(".xml")) {
82634
- return `<Override PartName="/${path8}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"/>`;
83252
+ if (path10.includes("/pivotCacheDefinition") && path10.endsWith(".xml")) {
83253
+ return `<Override PartName="/${path10}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"/>`;
82635
83254
  }
82636
- if (path8.includes("/pivotCacheRecords") && path8.endsWith(".xml")) {
82637
- return `<Override PartName="/${path8}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"/>`;
83255
+ if (path10.includes("/pivotCacheRecords") && path10.endsWith(".xml")) {
83256
+ return `<Override PartName="/${path10}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"/>`;
82638
83257
  }
82639
83258
  return void 0;
82640
83259
  }
@@ -82707,16 +83326,16 @@ function exportWorkbook(workbook, originalZip) {
82707
83326
  regeneratedPaths.add(`xl/worksheets/sheet${i2 + 1}.xml`);
82708
83327
  regeneratedPaths.add(`xl/worksheets/_rels/sheet${i2 + 1}.xml.rels`);
82709
83328
  }
82710
- for (const path8 of Object.keys(originalZip)) {
82711
- if (path8.startsWith("xl/drawings/") || path8.startsWith("xl/charts/") || path8.startsWith("xl/tables/") || path8.startsWith("xl/media/")) {
82712
- regeneratedPaths.add(path8);
83329
+ for (const path10 of Object.keys(originalZip)) {
83330
+ if (path10.startsWith("xl/drawings/") || path10.startsWith("xl/charts/") || path10.startsWith("xl/tables/") || path10.startsWith("xl/media/")) {
83331
+ regeneratedPaths.add(path10);
82713
83332
  }
82714
83333
  }
82715
- for (const [path8, data2] of Object.entries(originalZip)) {
82716
- if (regeneratedPaths.has(path8)) continue;
82717
- const ext = path8.slice(path8.lastIndexOf(".") + 1).toLowerCase();
83334
+ for (const [path10, data2] of Object.entries(originalZip)) {
83335
+ if (regeneratedPaths.has(path10)) continue;
83336
+ const ext = path10.slice(path10.lastIndexOf(".") + 1).toLowerCase();
82718
83337
  if (!DECLARED_EXTENSIONS.includes(ext)) continue;
82719
- entries2[path8] = data2;
83338
+ entries2[path10] = data2;
82720
83339
  }
82721
83340
  }
82722
83341
  const sharedStrings = buildSharedStrings(workbook);
@@ -82732,8 +83351,8 @@ function exportWorkbook(workbook, originalZip) {
82732
83351
  buildWorkbookRels(workbook, pivotInfo)
82733
83352
  );
82734
83353
  const extraContentTypes = [];
82735
- for (const path8 of pivotInfo.pivotXmlPaths) {
82736
- const ct = pivotContentTypeFor(path8);
83354
+ for (const path10 of pivotInfo.pivotXmlPaths) {
83355
+ const ct = pivotContentTypeFor(path10);
82737
83356
  if (ct) extraContentTypes.push(ct);
82738
83357
  }
82739
83358
  let globalChartIndex = 1;
@@ -91756,14 +92375,14 @@ var DependencyGraph = class {
91756
92375
  stack.push({ cell: p, path: [cell, p] });
91757
92376
  }
91758
92377
  while (stack.length > 0) {
91759
- const { cell: current, path: path8 } = stack.pop();
91760
- if (current === cell) return path8;
92378
+ const { cell: current, path: path10 } = stack.pop();
92379
+ if (current === cell) return path10;
91761
92380
  if (visited.has(current)) continue;
91762
92381
  visited.add(current);
91763
92382
  const nextPrec = this.precedents.get(current);
91764
92383
  if (!nextPrec) continue;
91765
92384
  for (const p of nextPrec) {
91766
- stack.push({ cell: p, path: [...path8, p] });
92385
+ stack.push({ cell: p, path: [...path10, p] });
91767
92386
  }
91768
92387
  }
91769
92388
  return null;
@@ -92975,8 +93594,8 @@ var SIMPLE_SHEET_KEYS = /* @__PURE__ */ new Set(["name", "cells", "merges", "fre
92975
93594
  var MAX_COLUMN = 16384;
92976
93595
  var MAX_ROW = 1048576;
92977
93596
  function loadFile(filePath) {
92978
- if (!fs6.existsSync(filePath)) fail2(`File not found: ${filePath}`);
92979
- const buffer = fs6.readFileSync(filePath);
93597
+ if (!fs8.existsSync(filePath)) fail2(`File not found: ${filePath}`);
93598
+ const buffer = fs8.readFileSync(filePath);
92980
93599
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
92981
93600
  const originalZip = unzipXlsx(arrayBuffer);
92982
93601
  const parsed = parseExcelBuffer(arrayBuffer);
@@ -93026,7 +93645,7 @@ function lazyEngine(workbook) {
93026
93645
  };
93027
93646
  }
93028
93647
  function readToJson(filePath, filter3, opts) {
93029
- const buffer = fs6.readFileSync(filePath);
93648
+ const buffer = fs8.readFileSync(filePath);
93030
93649
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
93031
93650
  const parsed = parseExcelBuffer(arrayBuffer);
93032
93651
  let sheets = parsed.sheets;
@@ -93539,7 +94158,7 @@ async function runXlsxCommand(subcommand, toolArgs, restArgs) {
93539
94158
  }
93540
94159
 
93541
94160
  // src/docx.ts
93542
- import fs7 from "node:fs";
94161
+ import fs9 from "node:fs";
93543
94162
 
93544
94163
  // ../ooxml/src/doc/errors.ts
93545
94164
  var DocParseError = class extends Error {
@@ -98155,8 +98774,8 @@ async function docModelToDocx(model) {
98155
98774
  // ../ooxml/src/opc.ts
98156
98775
  var import_jszip = __toESM(require_lib4(), 1);
98157
98776
  function dropFolderEntries(zip) {
98158
- for (const [path8, entry] of Object.entries(zip.files)) {
98159
- if (entry.dir) delete zip.files[path8];
98777
+ for (const [path10, entry] of Object.entries(zip.files)) {
98778
+ if (entry.dir) delete zip.files[path10];
98160
98779
  }
98161
98780
  }
98162
98781
 
@@ -98388,11 +99007,11 @@ async function parseDocx(buffer) {
98388
99007
  const zip = await import_jszip3.default.loadAsync(buffer);
98389
99008
  const parts = /* @__PURE__ */ new Map();
98390
99009
  const filePromises = [];
98391
- zip.forEach((path8, file2) => {
99010
+ zip.forEach((path10, file2) => {
98392
99011
  if (file2.dir) return;
98393
99012
  filePromises.push(
98394
99013
  file2.async("uint8array").then((bytes) => {
98395
- parts.set(path8, bytes);
99014
+ parts.set(path10, bytes);
98396
99015
  })
98397
99016
  );
98398
99017
  });
@@ -98624,9 +99243,9 @@ var DEFAULT_PARTS = [
98624
99243
  ["word/theme/theme1.xml", () => encoder.encode(THEME_XML)]
98625
99244
  ];
98626
99245
  function fillDefaultParts(parts) {
98627
- for (const [path8, makeBytes] of DEFAULT_PARTS) {
98628
- if (!parts.has(path8)) {
98629
- parts.set(path8, makeBytes());
99246
+ for (const [path10, makeBytes] of DEFAULT_PARTS) {
99247
+ if (!parts.has(path10)) {
99248
+ parts.set(path10, makeBytes());
98630
99249
  }
98631
99250
  }
98632
99251
  return parts;
@@ -98636,9 +99255,9 @@ function fillDefaultParts(parts) {
98636
99255
  async function serializeDocx(doc) {
98637
99256
  const zip = new import_jszip4.default();
98638
99257
  const parts = fillDefaultParts(new Map(doc.parts));
98639
- for (const [path8, bytes] of parts) {
98640
- if (path8 === "word/document.xml") continue;
98641
- zip.file(path8, bytes);
99258
+ for (const [path10, bytes] of parts) {
99259
+ if (path10 === "word/document.xml") continue;
99260
+ zip.file(path10, bytes);
98642
99261
  }
98643
99262
  const documentXml = serializeDocumentXml(doc);
98644
99263
  zip.file("word/document.xml", documentXml);
@@ -98725,8 +99344,8 @@ var DEFAULT_DOC_ATTRS = {
98725
99344
  "@_xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
98726
99345
  };
98727
99346
  async function loadFile2(filePath) {
98728
- if (!fs7.existsSync(filePath)) fail2(`File not found: ${filePath}`);
98729
- const bytes = fs7.readFileSync(filePath);
99347
+ if (!fs9.existsSync(filePath)) fail2(`File not found: ${filePath}`);
99348
+ const bytes = fs9.readFileSync(filePath);
98730
99349
  if (isLegacyDoc(bytes)) {
98731
99350
  const upgraded = await saveDocxToBuffer(await loadDocxFromBuffer(bytes));
98732
99351
  return parseDocx(new Uint8Array(upgraded));
@@ -99211,10 +99830,10 @@ async function runDocxCommand(subcommand, toolArgs, restArgs) {
99211
99830
  }
99212
99831
 
99213
99832
  // src/knowledge.ts
99214
- import fs8 from "node:fs";
99833
+ import fs10 from "node:fs";
99215
99834
  function readBodyFile(filePath) {
99216
- if (!fs8.existsSync(filePath)) fail2(`File not found: ${filePath}`);
99217
- return fs8.readFileSync(filePath, "utf-8");
99835
+ if (!fs10.existsSync(filePath)) fail2(`File not found: ${filePath}`);
99836
+ return fs10.readFileSync(filePath, "utf-8");
99218
99837
  }
99219
99838
  function resolveBody(flags, required2) {
99220
99839
  if (flags.from !== void 0 && flags.content !== void 0) {
@@ -99551,6 +100170,7 @@ async function runPreviewCommand(filePath, flags) {
99551
100170
  }
99552
100171
 
99553
100172
  // src/cli.ts
100173
+ var startedAt = Date.now();
99554
100174
  dns.setDefaultResultOrder("ipv4first");
99555
100175
  net2.setDefaultAutoSelectFamilyAttemptTimeout(2e3);
99556
100176
  function printHelp() {
@@ -99614,7 +100234,8 @@ COMMANDS
99614
100234
  lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
99615
100235
  from the manifest + workspace schema \u2014 no deploy
99616
100236
  lotics app check Run every deploy pre-flight WITHOUT building or
99617
- shipping: agent schemas vs the live app, aliases
100237
+ shipping: agent schemas vs the live app, workflow
100238
+ declarations and BODIES vs what is live, aliases
99618
100239
  the code calls but nothing bound, undeclared
99619
100240
  capabilities, query drift. Exits 1 on what a
99620
100241
  deploy would refuse, so CI can gate on it
@@ -99763,8 +100384,8 @@ function prompt(question) {
99763
100384
  });
99764
100385
  });
99765
100386
  }
99766
- async function publicPost(path8, body) {
99767
- const response = await fetch(`${API_BASE_URL}${path8}`, {
100387
+ async function publicPost(path10, body) {
100388
+ const response = await fetch(`${API_BASE_URL}${path10}`, {
99768
100389
  method: "POST",
99769
100390
  headers: { "Content-Type": "application/json" },
99770
100391
  body: JSON.stringify(body)
@@ -99861,6 +100482,8 @@ function requireClient(flags, appWorkspaceId) {
99861
100482
  process.exit(1);
99862
100483
  }
99863
100484
  const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
100485
+ noteScope({ org_id: ctx.orgId ?? void 0, workspace_id: ctx.workspaceId });
100486
+ flushSpool((events) => postEvents(API_BASE_URL, ctx.apiKey, events));
99864
100487
  return {
99865
100488
  client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId }),
99866
100489
  ctx
@@ -99875,6 +100498,7 @@ var SOURCE_LABELS = {
99875
100498
  global_profile: "global active profile"
99876
100499
  };
99877
100500
  function announceTarget(ctx, workspaceId) {
100501
+ noteScope({ org_id: ctx.orgId ?? void 0, workspace_id: workspaceId });
99878
100502
  const org = ctx.orgName ?? ctx.orgId ?? "(org from key)";
99879
100503
  if (ctx.source === "global_profile") {
99880
100504
  console.error(
@@ -99929,13 +100553,13 @@ async function resolveWorkspace(client, ctx) {
99929
100553
  function resolveUploadPaths(rawPaths) {
99930
100554
  const result = [];
99931
100555
  for (const p of rawPaths) {
99932
- const resolved = path7.resolve(p);
99933
- const stat = fs9.statSync(resolved);
100556
+ const resolved = path9.resolve(p);
100557
+ const stat = fs11.statSync(resolved);
99934
100558
  if (stat.isDirectory()) {
99935
- const entries2 = fs9.readdirSync(resolved, { withFileTypes: true });
100559
+ const entries2 = fs11.readdirSync(resolved, { withFileTypes: true });
99936
100560
  for (const entry of entries2) {
99937
100561
  if (entry.isFile()) {
99938
- result.push(path7.join(resolved, entry.name));
100562
+ result.push(path9.join(resolved, entry.name));
99939
100563
  }
99940
100564
  }
99941
100565
  } else {
@@ -99945,6 +100569,8 @@ function resolveUploadPaths(rawPaths) {
99945
100569
  return result;
99946
100570
  }
99947
100571
  async function main() {
100572
+ setInvocation(commandPath(process.argv.slice(2)), sessionId(), VERSION);
100573
+ installTelemetry(startedAt);
99948
100574
  const parsed = parseArgs(process.argv.slice(2));
99949
100575
  const { restArgs, flags } = parsed;
99950
100576
  let { command, subcommand, toolArgs } = parsed;
@@ -100058,13 +100684,13 @@ async function main() {
100058
100684
  if (command === "preview") {
100059
100685
  if (subcommand && isStoredFileId(subcommand)) {
100060
100686
  const { client: client2 } = requireClient(flags);
100061
- const tmpDir = fs9.mkdtempSync(path7.join(os2.tmpdir(), "lotics-preview-"));
100687
+ const tmpDir = fs11.mkdtempSync(path9.join(os4.tmpdir(), "lotics-preview-"));
100062
100688
  try {
100063
100689
  const { path: localPath, filename } = await client2.downloadFileById(subcommand, tmpDir);
100064
100690
  const output = flags.output ?? defaultPreviewOutputPath(filename, process.cwd());
100065
100691
  await runPreviewCommand(localPath, { output });
100066
100692
  } finally {
100067
- fs9.rmSync(tmpDir, { recursive: true, force: true });
100693
+ fs11.rmSync(tmpDir, { recursive: true, force: true });
100068
100694
  }
100069
100695
  return;
100070
100696
  }
@@ -100337,17 +100963,11 @@ Available workspaces:`);
100337
100963
  process.exit(1);
100338
100964
  }
100339
100965
  const targetPath = restArgs[0];
100340
- await appPull(client, { app_id: appId, targetPath });
100966
+ await appPull(client, { app_id: appId, targetPath, force: flags.force === true });
100341
100967
  return;
100342
100968
  }
100343
100969
  if (subcommand === "deploy") {
100344
- const message2 = (flags.message ?? toolArgs)?.trim();
100345
- if (!message2) {
100346
- console.error('Usage: lotics app deploy -m "<what changed + why>"');
100347
- console.error("A deploy message is required \u2014 it's the version's audit trail (see `lotics app versions`).");
100348
- process.exit(1);
100349
- }
100350
- await appDeploy(client, { message: message2 });
100970
+ await appDeploy(client, { message: (flags.message ?? toolArgs)?.trim() || void 0 });
100351
100971
  return;
100352
100972
  }
100353
100973
  if (subcommand === "subdomain") {
@@ -100402,7 +101022,7 @@ Available workspaces:`);
100402
101022
  const ingested = await ingestJsonArgs({
100403
101023
  rawArg: restArgs[1],
100404
101024
  stdinIsTTY: process.stdin.isTTY ?? false,
100405
- readFile: (p) => fs9.readFileSync(p, "utf-8"),
101025
+ readFile: (p) => fs11.readFileSync(p, "utf-8"),
100406
101026
  readStdin
100407
101027
  });
100408
101028
  if (ingested.kind === "error") {
@@ -100428,7 +101048,7 @@ Available workspaces:`);
100428
101048
  return;
100429
101049
  }
100430
101050
  if (action === "pull") {
100431
- await appWorkflowPull(client);
101051
+ await appWorkflowPull(client, { force: flags.force === true });
100432
101052
  return;
100433
101053
  }
100434
101054
  workflowUsage();
@@ -100453,7 +101073,7 @@ Available workspaces:`);
100453
101073
  const ingested = await ingestJsonArgs({
100454
101074
  rawArg: restArgs[2],
100455
101075
  stdinIsTTY: process.stdin.isTTY ?? false,
100456
- readFile: (p) => fs9.readFileSync(p, "utf-8"),
101076
+ readFile: (p) => fs11.readFileSync(p, "utf-8"),
100457
101077
  readStdin
100458
101078
  });
100459
101079
  if (ingested.kind === "error") {
@@ -100609,7 +101229,7 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
100609
101229
  const ingested = await ingestJsonArgs({
100610
101230
  rawArg: toolArgs,
100611
101231
  stdinIsTTY: process.stdin.isTTY ?? false,
100612
- readFile: (p) => fs9.readFileSync(p, "utf-8"),
101232
+ readFile: (p) => fs11.readFileSync(p, "utf-8"),
100613
101233
  readStdin
100614
101234
  });
100615
101235
  if (ingested.kind === "error") {
@@ -100617,6 +101237,7 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
100617
101237
  process.exit(1);
100618
101238
  }
100619
101239
  const args = ingested.args;
101240
+ noteToolArgs(args);
100620
101241
  const timeoutMs = flags.timeout ?? 6e4;
100621
101242
  const result = await client.execute(toolName, args, { format: "text", timeoutMs });
100622
101243
  if (result.error) {