@floless/app 0.52.0 → 0.54.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.
@@ -5489,7 +5489,7 @@ var require_thread_stream = __commonJS({
5489
5489
  var { version } = require_package();
5490
5490
  var { EventEmitter: EventEmitter2 } = require("events");
5491
5491
  var { Worker: Worker2 } = require("worker_threads");
5492
- var { join: join28 } = require("path");
5492
+ var { join: join29 } = require("path");
5493
5493
  var { pathToFileURL } = require("url");
5494
5494
  var { wait } = require_wait();
5495
5495
  var {
@@ -5540,7 +5540,7 @@ var require_thread_stream = __commonJS({
5540
5540
  function createWorker(stream, opts) {
5541
5541
  const { filename, workerData } = opts;
5542
5542
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
5543
- const toExecute = bundlerOverrides["thread-stream-worker"] || join28(__dirname, "lib", "worker.js");
5543
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join29(__dirname, "lib", "worker.js");
5544
5544
  const worker = new Worker2(toExecute, {
5545
5545
  ...opts.workerOpts,
5546
5546
  name: opts.workerOpts?.name || "thread-stream",
@@ -6008,7 +6008,7 @@ var require_transport = __commonJS({
6008
6008
  var { createRequire: createRequire5 } = require("module");
6009
6009
  var { existsSync: existsSync24 } = require("node:fs");
6010
6010
  var getCallers = require_caller();
6011
- var { join: join28, isAbsolute: isAbsolute2, sep: sep4 } = require("node:path");
6011
+ var { join: join29, isAbsolute: isAbsolute2, sep: sep4 } = require("node:path");
6012
6012
  var { fileURLToPath: fileURLToPath6 } = require("node:url");
6013
6013
  var sleep = require_atomic_sleep();
6014
6014
  var onExit = require_on_exit_leak_free();
@@ -6161,7 +6161,7 @@ var require_transport = __commonJS({
6161
6161
  throw new Error("only one of target or targets can be specified");
6162
6162
  }
6163
6163
  if (targets) {
6164
- target = bundlerOverrides["pino-worker"] || join28(__dirname, "worker.js");
6164
+ target = bundlerOverrides["pino-worker"] || join29(__dirname, "worker.js");
6165
6165
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
6166
6166
  return {
6167
6167
  ...dest,
@@ -6179,7 +6179,7 @@ var require_transport = __commonJS({
6179
6179
  });
6180
6180
  });
6181
6181
  } else if (pipeline2) {
6182
- target = bundlerOverrides["pino-worker"] || join28(__dirname, "worker.js");
6182
+ target = bundlerOverrides["pino-worker"] || join29(__dirname, "worker.js");
6183
6183
  options.pipelines = [pipeline2.map((dest) => {
6184
6184
  return {
6185
6185
  ...dest,
@@ -6202,7 +6202,7 @@ var require_transport = __commonJS({
6202
6202
  return origin;
6203
6203
  }
6204
6204
  if (origin === "pino/file") {
6205
- return join28(__dirname, "..", "file.js");
6205
+ return join29(__dirname, "..", "file.js");
6206
6206
  }
6207
6207
  let fixTarget2;
6208
6208
  for (const filePath of callers) {
@@ -7182,7 +7182,7 @@ var require_safe_stable_stringify = __commonJS({
7182
7182
  return circularValue;
7183
7183
  }
7184
7184
  let res = "";
7185
- let join28 = ",";
7185
+ let join29 = ",";
7186
7186
  const originalIndentation = indentation;
7187
7187
  if (Array.isArray(value)) {
7188
7188
  if (value.length === 0) {
@@ -7196,7 +7196,7 @@ var require_safe_stable_stringify = __commonJS({
7196
7196
  indentation += spacer;
7197
7197
  res += `
7198
7198
  ${indentation}`;
7199
- join28 = `,
7199
+ join29 = `,
7200
7200
  ${indentation}`;
7201
7201
  }
7202
7202
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -7204,13 +7204,13 @@ ${indentation}`;
7204
7204
  for (; i < maximumValuesToStringify - 1; i++) {
7205
7205
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
7206
7206
  res += tmp2 !== void 0 ? tmp2 : "null";
7207
- res += join28;
7207
+ res += join29;
7208
7208
  }
7209
7209
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
7210
7210
  res += tmp !== void 0 ? tmp : "null";
7211
7211
  if (value.length - 1 > maximumBreadth) {
7212
7212
  const removedKeys = value.length - maximumBreadth - 1;
7213
- res += `${join28}"... ${getItemCount(removedKeys)} not stringified"`;
7213
+ res += `${join29}"... ${getItemCount(removedKeys)} not stringified"`;
7214
7214
  }
7215
7215
  if (spacer !== "") {
7216
7216
  res += `
@@ -7231,7 +7231,7 @@ ${originalIndentation}`;
7231
7231
  let separator = "";
7232
7232
  if (spacer !== "") {
7233
7233
  indentation += spacer;
7234
- join28 = `,
7234
+ join29 = `,
7235
7235
  ${indentation}`;
7236
7236
  whitespace = " ";
7237
7237
  }
@@ -7245,13 +7245,13 @@ ${indentation}`;
7245
7245
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
7246
7246
  if (tmp !== void 0) {
7247
7247
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
7248
- separator = join28;
7248
+ separator = join29;
7249
7249
  }
7250
7250
  }
7251
7251
  if (keyLength > maximumBreadth) {
7252
7252
  const removedKeys = keyLength - maximumBreadth;
7253
7253
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
7254
- separator = join28;
7254
+ separator = join29;
7255
7255
  }
7256
7256
  if (spacer !== "" && separator.length > 1) {
7257
7257
  res = `
@@ -7292,7 +7292,7 @@ ${originalIndentation}`;
7292
7292
  }
7293
7293
  const originalIndentation = indentation;
7294
7294
  let res = "";
7295
- let join28 = ",";
7295
+ let join29 = ",";
7296
7296
  if (Array.isArray(value)) {
7297
7297
  if (value.length === 0) {
7298
7298
  return "[]";
@@ -7305,7 +7305,7 @@ ${originalIndentation}`;
7305
7305
  indentation += spacer;
7306
7306
  res += `
7307
7307
  ${indentation}`;
7308
- join28 = `,
7308
+ join29 = `,
7309
7309
  ${indentation}`;
7310
7310
  }
7311
7311
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -7313,13 +7313,13 @@ ${indentation}`;
7313
7313
  for (; i < maximumValuesToStringify - 1; i++) {
7314
7314
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
7315
7315
  res += tmp2 !== void 0 ? tmp2 : "null";
7316
- res += join28;
7316
+ res += join29;
7317
7317
  }
7318
7318
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
7319
7319
  res += tmp !== void 0 ? tmp : "null";
7320
7320
  if (value.length - 1 > maximumBreadth) {
7321
7321
  const removedKeys = value.length - maximumBreadth - 1;
7322
- res += `${join28}"... ${getItemCount(removedKeys)} not stringified"`;
7322
+ res += `${join29}"... ${getItemCount(removedKeys)} not stringified"`;
7323
7323
  }
7324
7324
  if (spacer !== "") {
7325
7325
  res += `
@@ -7332,7 +7332,7 @@ ${originalIndentation}`;
7332
7332
  let whitespace = "";
7333
7333
  if (spacer !== "") {
7334
7334
  indentation += spacer;
7335
- join28 = `,
7335
+ join29 = `,
7336
7336
  ${indentation}`;
7337
7337
  whitespace = " ";
7338
7338
  }
@@ -7341,7 +7341,7 @@ ${indentation}`;
7341
7341
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
7342
7342
  if (tmp !== void 0) {
7343
7343
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
7344
- separator = join28;
7344
+ separator = join29;
7345
7345
  }
7346
7346
  }
7347
7347
  if (spacer !== "" && separator.length > 1) {
@@ -7399,20 +7399,20 @@ ${originalIndentation}`;
7399
7399
  indentation += spacer;
7400
7400
  let res2 = `
7401
7401
  ${indentation}`;
7402
- const join29 = `,
7402
+ const join30 = `,
7403
7403
  ${indentation}`;
7404
7404
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
7405
7405
  let i = 0;
7406
7406
  for (; i < maximumValuesToStringify - 1; i++) {
7407
7407
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
7408
7408
  res2 += tmp2 !== void 0 ? tmp2 : "null";
7409
- res2 += join29;
7409
+ res2 += join30;
7410
7410
  }
7411
7411
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
7412
7412
  res2 += tmp !== void 0 ? tmp : "null";
7413
7413
  if (value.length - 1 > maximumBreadth) {
7414
7414
  const removedKeys = value.length - maximumBreadth - 1;
7415
- res2 += `${join29}"... ${getItemCount(removedKeys)} not stringified"`;
7415
+ res2 += `${join30}"... ${getItemCount(removedKeys)} not stringified"`;
7416
7416
  }
7417
7417
  res2 += `
7418
7418
  ${originalIndentation}`;
@@ -7428,16 +7428,16 @@ ${originalIndentation}`;
7428
7428
  return '"[Object]"';
7429
7429
  }
7430
7430
  indentation += spacer;
7431
- const join28 = `,
7431
+ const join29 = `,
7432
7432
  ${indentation}`;
7433
7433
  let res = "";
7434
7434
  let separator = "";
7435
7435
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
7436
7436
  if (isTypedArrayWithEntries(value)) {
7437
- res += stringifyTypedArray(value, join28, maximumBreadth);
7437
+ res += stringifyTypedArray(value, join29, maximumBreadth);
7438
7438
  keys = keys.slice(value.length);
7439
7439
  maximumPropertiesToStringify -= value.length;
7440
- separator = join28;
7440
+ separator = join29;
7441
7441
  }
7442
7442
  if (deterministic) {
7443
7443
  keys = sort(keys, comparator);
@@ -7448,13 +7448,13 @@ ${indentation}`;
7448
7448
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
7449
7449
  if (tmp !== void 0) {
7450
7450
  res += `${separator}${strEscape(key2)}: ${tmp}`;
7451
- separator = join28;
7451
+ separator = join29;
7452
7452
  }
7453
7453
  }
7454
7454
  if (keyLength > maximumBreadth) {
7455
7455
  const removedKeys = keyLength - maximumBreadth;
7456
7456
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
7457
- separator = join28;
7457
+ separator = join29;
7458
7458
  }
7459
7459
  if (separator !== "") {
7460
7460
  res = `
@@ -41991,7 +41991,7 @@ var require_send = __commonJS({
41991
41991
  var { parseTokenList } = require_parseTokenList();
41992
41992
  var { createHttpError } = require_createHttpError();
41993
41993
  var extname2 = path.extname;
41994
- var join28 = path.join;
41994
+ var join29 = path.join;
41995
41995
  var normalize2 = path.normalize;
41996
41996
  var resolve6 = path.resolve;
41997
41997
  var sep4 = path.sep;
@@ -42078,7 +42078,7 @@ var require_send = __commonJS({
42078
42078
  return { statusCode: 403 };
42079
42079
  }
42080
42080
  parts = path2.split(sep4);
42081
- path2 = normalize2(join28(root, path2));
42081
+ path2 = normalize2(join29(root, path2));
42082
42082
  } else {
42083
42083
  if (UP_PATH_REGEXP.test(path2)) {
42084
42084
  debug('malicious path "%s"', path2);
@@ -42361,7 +42361,7 @@ var require_send = __commonJS({
42361
42361
  let err2;
42362
42362
  for (let i = 0; i < options.index.length; i++) {
42363
42363
  const index = options.index[i];
42364
- const p = join28(path2, index);
42364
+ const p = join29(path2, index);
42365
42365
  const { error, stat: stat4 } = await tryStat(p);
42366
42366
  if (error) {
42367
42367
  err2 = error;
@@ -50827,8 +50827,8 @@ var import_node_readline2 = require("node:readline");
50827
50827
 
50828
50828
  // index.ts
50829
50829
  var import_node_url5 = require("node:url");
50830
- var import_node_path26 = require("node:path");
50831
- var import_node_os18 = require("node:os");
50830
+ var import_node_path27 = require("node:path");
50831
+ var import_node_os19 = require("node:os");
50832
50832
  var import_node_fs29 = require("node:fs");
50833
50833
  var import_node_child_process7 = require("node:child_process");
50834
50834
 
@@ -52856,7 +52856,7 @@ function appVersion() {
52856
52856
  return resolveVersion({
52857
52857
  isSea: isSea2(),
52858
52858
  sqVersionXml: readSqVersionXml(),
52859
- define: true ? "0.52.0" : void 0,
52859
+ define: true ? "0.54.0" : void 0,
52860
52860
  pkgVersion: readPkgVersion()
52861
52861
  });
52862
52862
  }
@@ -52866,7 +52866,7 @@ function resolveChannel(s) {
52866
52866
  return "dev";
52867
52867
  }
52868
52868
  function appChannel() {
52869
- return resolveChannel({ isSea: isSea2(), define: true ? "0.52.0" : void 0 });
52869
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.54.0" : void 0 });
52870
52870
  }
52871
52871
 
52872
52872
  // workflow-update.ts
@@ -53893,7 +53893,7 @@ function contractToScene(contractInput) {
53893
53893
  color: pickColor(profile, i, contract)
53894
53894
  }));
53895
53895
  return {
53896
- scene: { meta: { name: contract.title || "Steel takeoff", units: "mm", up: "z" }, groups, elements },
53896
+ scene: { meta: { name: contract.title || "Steel Model", units: "mm", up: "z" }, groups, elements },
53897
53897
  skipped
53898
53898
  };
53899
53899
  }
@@ -53958,6 +53958,10 @@ function contractToBom(contractInput) {
53958
53958
  };
53959
53959
  }
53960
53960
 
53961
+ // bom-export.ts
53962
+ var import_node_os12 = require("node:os");
53963
+ var import_node_path15 = require("node:path");
53964
+
53961
53965
  // node_modules/write-excel-file/modules/export/writeXlsxFileNode.js
53962
53966
  var import_node_fs18 = __toESM(require("node:fs"), 1);
53963
53967
 
@@ -58155,6 +58159,10 @@ function pipe(readableStream, writableStream) {
58155
58159
  }
58156
58160
 
58157
58161
  // bom-export.ts
58162
+ function bomExportPath(appId, format) {
58163
+ const root = process.env.FLOLESS_HOME ?? (0, import_node_path15.join)((0, import_node_os12.homedir)(), ".floless");
58164
+ return (0, import_node_path15.join)(root, "exports", appId, `${appId}-bom.${format}`);
58165
+ }
58158
58166
  function csvField(v) {
58159
58167
  let s = String(v);
58160
58168
  if (typeof v === "string" && /^[=+\-@\t\r]/.test(s)) s = `'${s}`;
@@ -58182,7 +58190,7 @@ async function bomToXlsx(bom) {
58182
58190
 
58183
58191
  // scene-bake.ts
58184
58192
  var import_node_fs19 = require("node:fs");
58185
- var import_node_path15 = require("node:path");
58193
+ var import_node_path16 = require("node:path");
58186
58194
  var import_yaml6 = __toESM(require_dist6(), 1);
58187
58195
  function bakeSceneIntoApp(sourcePath, scene, agent = "viewer-3d") {
58188
58196
  const doc = (0, import_yaml6.parseDocument)((0, import_node_fs19.readFileSync)(sourcePath, "utf8"));
@@ -58210,7 +58218,7 @@ function bakeNodeConfigById(sourcePath, nodeId, patch) {
58210
58218
  }
58211
58219
  function writeViewer3dApp(dir, appId, scene) {
58212
58220
  (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
58213
- const flo = (0, import_node_path15.join)(dir, `${appId}.flo`);
58221
+ const flo = (0, import_node_path16.join)(dir, `${appId}.flo`);
58214
58222
  (0, import_node_fs19.writeFileSync)(flo, [
58215
58223
  `app: ${appId}`,
58216
58224
  "version: 0.1.0",
@@ -58233,7 +58241,7 @@ function writeViewer3dApp(dir, appId, scene) {
58233
58241
  }
58234
58242
  function writeIfcApp(dir, appId, scene, outputPath) {
58235
58243
  (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
58236
- const flo = (0, import_node_path15.join)(dir, `${appId}.flo`);
58244
+ const flo = (0, import_node_path16.join)(dir, `${appId}.flo`);
58237
58245
  (0, import_node_fs19.writeFileSync)(flo, [
58238
58246
  `app: ${appId}`,
58239
58247
  "version: 0.1.0",
@@ -58258,7 +58266,7 @@ function writeIfcApp(dir, appId, scene, outputPath) {
58258
58266
  }
58259
58267
  function writeTeklaApp(dir, appId, scene, teklaVersion = "2026.0") {
58260
58268
  (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
58261
- const flo = (0, import_node_path15.join)(dir, `${appId}.flo`);
58269
+ const flo = (0, import_node_path16.join)(dir, `${appId}.flo`);
58262
58270
  (0, import_node_fs19.writeFileSync)(flo, [
58263
58271
  `app: ${appId}`,
58264
58272
  "version: 0.1.0",
@@ -58285,6 +58293,41 @@ function writeTeklaApp(dir, appId, scene, teklaVersion = "2026.0") {
58285
58293
  return flo;
58286
58294
  }
58287
58295
 
58296
+ // bom-export-bake.ts
58297
+ var EXPORT_NODES = [
58298
+ { id: "csv-export", format: "csv" },
58299
+ { id: "xlsx-export", format: "xlsx" }
58300
+ ];
58301
+ async function bakeBomExportNodes(sourcePath, appId, contract) {
58302
+ const baked = [];
58303
+ const bom = contract != null ? contractToBom(contract) : null;
58304
+ const bytesFor = {
58305
+ csv: bom ? bomToCsv(bom) : null,
58306
+ xlsx: bom ? (await bomToXlsx(bom)).toString("base64") : null
58307
+ };
58308
+ for (const { id, format } of EXPORT_NODES) {
58309
+ const patch = { path: bomExportPath(appId, format) };
58310
+ const bytes = bytesFor[format];
58311
+ if (bytes != null) patch.bytes = bytes;
58312
+ try {
58313
+ bakeNodeConfigById(sourcePath, id, patch);
58314
+ baked.push(id);
58315
+ } catch (e) {
58316
+ if (!(e instanceof Error && /no node with id/.test(e.message))) throw e;
58317
+ }
58318
+ }
58319
+ return baked;
58320
+ }
58321
+ function exportPathsNeedingBake(nodes, appId) {
58322
+ const out = [];
58323
+ for (const { id, format } of EXPORT_NODES) {
58324
+ const node = nodes.find((n) => n.id === id);
58325
+ if (!node || node.agent !== "file" || node.frozen) continue;
58326
+ if (node.config?.path !== bomExportPath(appId, format)) out.push(id);
58327
+ }
58328
+ return out;
58329
+ }
58330
+
58288
58331
  // tekla-version.ts
58289
58332
  var import_node_child_process3 = require("node:child_process");
58290
58333
  var import_node_util = require("node:util");
@@ -58322,7 +58365,7 @@ async function teklaVersionToBake() {
58322
58365
  }
58323
58366
 
58324
58367
  // downstream-bake.ts
58325
- function bakeDownstream(sourcePath, contract) {
58368
+ async function bakeDownstream(sourcePath, appId, contract) {
58326
58369
  const { scene } = contractToScene(contract);
58327
58370
  let sceneElements = 0;
58328
58371
  try {
@@ -58339,7 +58382,8 @@ function bakeDownstream(sourcePath, contract) {
58339
58382
  } catch (e) {
58340
58383
  if (!(e instanceof Error && /no node with id "bom"/.test(e.message))) throw e;
58341
58384
  }
58342
- return { sceneElements, bomRows };
58385
+ const exportNodes = (await bakeBomExportNodes(sourcePath, appId, contract)).length;
58386
+ return { sceneElements, bomRows, exportNodes };
58343
58387
  }
58344
58388
 
58345
58389
  // steel-confidence.ts
@@ -58623,12 +58667,12 @@ function scoreContract2(contractInput) {
58623
58667
 
58624
58668
  // app-lifecycle.ts
58625
58669
  var import_node_fs22 = require("node:fs");
58626
- var import_node_os13 = require("node:os");
58627
- var import_node_path18 = require("node:path");
58670
+ var import_node_os14 = require("node:os");
58671
+ var import_node_path19 = require("node:path");
58628
58672
 
58629
58673
  // routines.ts
58630
58674
  var import_node_fs21 = require("node:fs");
58631
- var import_node_path17 = require("node:path");
58675
+ var import_node_path18 = require("node:path");
58632
58676
 
58633
58677
  // sse.ts
58634
58678
  var clients = /* @__PURE__ */ new Set();
@@ -58666,10 +58710,10 @@ function clientCount() {
58666
58710
 
58667
58711
  // trigger-sessions.ts
58668
58712
  var import_node_fs20 = require("node:fs");
58669
- var import_node_os12 = require("node:os");
58670
- var import_node_path16 = require("node:path");
58713
+ var import_node_os13 = require("node:os");
58714
+ var import_node_path17 = require("node:path");
58671
58715
  var import_yaml7 = __toESM(require_dist6(), 1);
58672
- var AGENTS_DIR2 = process.env.AWARE_HOME ? (0, import_node_path16.join)(process.env.AWARE_HOME, "agents") : (0, import_node_path16.join)((0, import_node_os12.homedir)(), ".aware", "agents");
58716
+ var AGENTS_DIR2 = process.env.AWARE_HOME ? (0, import_node_path17.join)(process.env.AWARE_HOME, "agents") : (0, import_node_path17.join)((0, import_node_os13.homedir)(), ".aware", "agents");
58673
58717
  function summarizeFire(data) {
58674
58718
  if (!data) return "event";
58675
58719
  const type = typeof data.type === "string" ? data.type : "";
@@ -58686,7 +58730,7 @@ function mapTriggerState(phase) {
58686
58730
  function isHostBacked(agent, agentsDir = AGENTS_DIR2) {
58687
58731
  const safe = (n) => !n.includes("/") && !n.includes("\\") && !n.includes("..");
58688
58732
  if (!safe(agent)) return false;
58689
- const manifestPath = (0, import_node_path16.join)(agentsDir, agent, "manifest.yaml");
58733
+ const manifestPath = (0, import_node_path17.join)(agentsDir, agent, "manifest.yaml");
58690
58734
  if (!(0, import_node_fs20.existsSync)(manifestPath)) return false;
58691
58735
  try {
58692
58736
  const doc = (0, import_yaml7.parse)((0, import_node_fs20.readFileSync)(manifestPath, "utf8"));
@@ -58974,7 +59018,7 @@ var RoutineError = class extends Error {
58974
59018
  }
58975
59019
  status;
58976
59020
  };
58977
- var ROUTINES_FILE = (0, import_node_path17.join)(flolessRoot, "routines.json");
59021
+ var ROUTINES_FILE = (0, import_node_path18.join)(flolessRoot, "routines.json");
58978
59022
  var routines = [];
58979
59023
  var loaded = false;
58980
59024
  function ensureLoaded() {
@@ -59440,10 +59484,10 @@ var AppLifecycleError = class extends Error {
59440
59484
  }
59441
59485
  };
59442
59486
  function appsDir() {
59443
- return process.env.AWARE_HOME ? (0, import_node_path18.join)(process.env.AWARE_HOME, "apps") : (0, import_node_path18.join)((0, import_node_os13.homedir)(), ".aware", "apps");
59487
+ return process.env.AWARE_HOME ? (0, import_node_path19.join)(process.env.AWARE_HOME, "apps") : (0, import_node_path19.join)((0, import_node_os14.homedir)(), ".aware", "apps");
59444
59488
  }
59445
59489
  function appDirPath(id) {
59446
- return (0, import_node_path18.join)(appsDir(), id);
59490
+ return (0, import_node_path19.join)(appsDir(), id);
59447
59491
  }
59448
59492
  function appInstalled(id) {
59449
59493
  return APP_ID3.test(id) && (0, import_node_fs22.existsSync)(appDirPath(id));
@@ -59548,8 +59592,8 @@ function isGatedAwareRoute(url, method) {
59548
59592
  // autostart.mjs
59549
59593
  var import_node_child_process4 = require("node:child_process");
59550
59594
  var import_node_fs23 = require("node:fs");
59551
- var import_node_os14 = require("node:os");
59552
- var import_node_path19 = require("node:path");
59595
+ var import_node_os15 = require("node:os");
59596
+ var import_node_path20 = require("node:path");
59553
59597
 
59554
59598
  // teardown.mjs
59555
59599
  var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
@@ -59670,7 +59714,7 @@ function logLine(msg) {
59670
59714
  function registerAutostart(exePath) {
59671
59715
  if (!isWin) return;
59672
59716
  const xml = buildAutostartTaskXml(exePath, currentUserId());
59673
- const tmp = (0, import_node_path19.join)((0, import_node_os14.tmpdir)(), `floless-autostart-${process.pid}-${Date.now()}.xml`);
59717
+ const tmp = (0, import_node_path20.join)((0, import_node_os15.tmpdir)(), `floless-autostart-${process.pid}-${Date.now()}.xml`);
59674
59718
  (0, import_node_fs23.writeFileSync)(tmp, "\uFEFF" + xml, { encoding: "utf16le" });
59675
59719
  try {
59676
59720
  (0, import_node_child_process4.execFileSync)("schtasks", ["/Create", "/TN", TASK_NAME, "/XML", tmp, "/F"], {
@@ -59720,23 +59764,23 @@ var import_node_crypto6 = require("node:crypto");
59720
59764
  var import_node_fs25 = require("node:fs");
59721
59765
  var import_node_stream3 = require("node:stream");
59722
59766
  var import_promises = require("node:stream/promises");
59723
- var import_node_path21 = require("node:path");
59767
+ var import_node_path22 = require("node:path");
59724
59768
 
59725
59769
  // post-update-marker.mjs
59726
59770
  var import_node_fs24 = require("node:fs");
59727
- var import_node_os15 = require("node:os");
59728
- var import_node_path20 = require("node:path");
59771
+ var import_node_os16 = require("node:os");
59772
+ var import_node_path21 = require("node:path");
59729
59773
  var FRESH_MS = 12e4;
59730
59774
  function markerPath() {
59731
59775
  const override = (process.env.FLOLESS_POST_UPDATE_MARKER ?? "").trim();
59732
59776
  if (override) return override;
59733
- const root = process.env.FLOLESS_HOME ?? (0, import_node_path20.join)((0, import_node_os15.homedir)(), ".floless");
59734
- return (0, import_node_path20.join)(root, ".post-update");
59777
+ const root = process.env.FLOLESS_HOME ?? (0, import_node_path21.join)((0, import_node_os16.homedir)(), ".floless");
59778
+ return (0, import_node_path21.join)(root, ".post-update");
59735
59779
  }
59736
59780
  function legacyMarkerPath() {
59737
59781
  if ((process.env.FLOLESS_POST_UPDATE_MARKER ?? "").trim()) return null;
59738
59782
  try {
59739
- return (0, import_node_path20.join)((0, import_node_path20.dirname)((0, import_node_path20.dirname)(process.execPath)), ".floless-post-update");
59783
+ return (0, import_node_path21.join)((0, import_node_path21.dirname)((0, import_node_path21.dirname)(process.execPath)), ".floless-post-update");
59740
59784
  } catch {
59741
59785
  return null;
59742
59786
  }
@@ -59777,13 +59821,13 @@ function currentVersion() {
59777
59821
  return appVersion();
59778
59822
  }
59779
59823
  function installRoot() {
59780
- return (0, import_node_path21.dirname)((0, import_node_path21.dirname)(process.execPath));
59824
+ return (0, import_node_path22.dirname)((0, import_node_path22.dirname)(process.execPath));
59781
59825
  }
59782
59826
  function updateExePath() {
59783
- return (0, import_node_path21.join)(installRoot(), "Update.exe");
59827
+ return (0, import_node_path22.join)(installRoot(), "Update.exe");
59784
59828
  }
59785
59829
  function packagesDir() {
59786
- return (0, import_node_path21.join)(installRoot(), "packages");
59830
+ return (0, import_node_path22.join)(installRoot(), "packages");
59787
59831
  }
59788
59832
  function feedUrl() {
59789
59833
  const env2 = (process.env.FLOLESS_UPDATE_URL ?? "").trim().replace(/\/+$/, "");
@@ -59884,7 +59928,7 @@ async function downloadPackage(asset) {
59884
59928
  const want = asset.SHA1.toUpperCase();
59885
59929
  const dir = packagesDir();
59886
59930
  (0, import_node_fs25.mkdirSync)(dir, { recursive: true });
59887
- const dest = (0, import_node_path21.join)(dir, asset.FileName);
59931
+ const dest = (0, import_node_path22.join)(dir, asset.FileName);
59888
59932
  if ((0, import_node_fs25.existsSync)(dest) && await sha1OfFile(dest) === want) return dest;
59889
59933
  const res = await authedFetch(`${feedUrl()}/${encodeURIComponent(asset.FileName)}`, {
59890
59934
  redirect: "follow",
@@ -60140,12 +60184,12 @@ function isTraceCorrupt(events) {
60140
60184
 
60141
60185
  // launch.mjs
60142
60186
  var import_node_child_process6 = require("node:child_process");
60143
- var import_node_path22 = require("node:path");
60187
+ var import_node_path23 = require("node:path");
60144
60188
  var import_node_url3 = require("node:url");
60145
60189
  var import_node_fs26 = require("node:fs");
60146
60190
  var import_node_http = __toESM(require("node:http"), 1);
60147
60191
  var import_node_readline = require("node:readline");
60148
- var __dirname2 = (0, import_node_path22.dirname)((0, import_node_url3.fileURLToPath)(__import_meta_url));
60192
+ var __dirname2 = (0, import_node_path23.dirname)((0, import_node_url3.fileURLToPath)(__import_meta_url));
60149
60193
  var PORT = Number(process.env.PORT ?? 4317);
60150
60194
  var HEALTH_URL = `http://127.0.0.1:${PORT}/api/health`;
60151
60195
  var BROWSER_URL = `http://floless.localhost:${PORT}`;
@@ -60217,7 +60261,7 @@ async function waitHealthy(timeoutMs = 3e4) {
60217
60261
  function resolveServerStart() {
60218
60262
  const packaged = /flolessapp\.exe$/i.test(process.execPath);
60219
60263
  if (packaged) return { cmd: process.execPath, args: ["--serve"], shell: false };
60220
- const bundle = (0, import_node_path22.join)(__dirname2, "dist", "floless-server.cjs");
60264
+ const bundle = (0, import_node_path23.join)(__dirname2, "dist", "floless-server.cjs");
60221
60265
  if ((0, import_node_fs26.existsSync)(bundle)) return { cmd: process.execPath, args: [bundle, "--serve"], shell: false };
60222
60266
  return { cmd: "npm", args: ["run", "start"], shell: isWin2 };
60223
60267
  }
@@ -60371,8 +60415,8 @@ function taskkillArgs(pid, { tree = true } = {}) {
60371
60415
  }
60372
60416
  function killSupervisor({ tree = true } = {}) {
60373
60417
  if (!isWin2) return;
60374
- const isNpmChannel = /^node(\.exe)?$/i.test((0, import_node_path22.basename)(process.execPath));
60375
- const scriptMatch = isNpmChannel ? (0, import_node_path22.basename)((0, import_node_url3.fileURLToPath)(__import_meta_url)) : void 0;
60418
+ const isNpmChannel = /^node(\.exe)?$/i.test((0, import_node_path23.basename)(process.execPath));
60419
+ const scriptMatch = isNpmChannel ? (0, import_node_path23.basename)((0, import_node_url3.fileURLToPath)(__import_meta_url)) : void 0;
60376
60420
  const realExe = resolveRealInstallExe(process.execPath);
60377
60421
  const exeMatch = realExe === process.execPath ? process.execPath : [process.execPath, realExe];
60378
60422
  const pids = supervisorPidsToKill(enumerateProcesses(), process.pid, exeMatch, scriptMatch);
@@ -60537,7 +60581,7 @@ async function runAction(arg, flagArgv = [], selfVersion = null) {
60537
60581
  }
60538
60582
  await action(parseTeardownFlags(flagArgv));
60539
60583
  }
60540
- var entry = (0, import_node_path22.basename)(process.argv[1] ?? "").toLowerCase();
60584
+ var entry = (0, import_node_path23.basename)(process.argv[1] ?? "").toLowerCase();
60541
60585
  if (entry === "launch.mjs") {
60542
60586
  runAction(process.argv[2], process.argv.slice(3)).catch((e) => {
60543
60587
  log(`error: ${e?.message ?? e}`);
@@ -60612,8 +60656,8 @@ function awareUpgradeBlockReason(s) {
60612
60656
 
60613
60657
  // skill-sync.ts
60614
60658
  var import_node_fs27 = require("node:fs");
60615
- var import_node_os16 = require("node:os");
60616
- var import_node_path23 = require("node:path");
60659
+ var import_node_os17 = require("node:os");
60660
+ var import_node_path24 = require("node:path");
60617
60661
  var import_node_url4 = require("node:url");
60618
60662
  var import_yaml8 = __toESM(require_dist6(), 1);
60619
60663
 
@@ -60636,7 +60680,7 @@ var PRODUCT_SKILLS = [
60636
60680
  "floless-app-steel-takeoff",
60637
60681
  // read a steel drawing into an editable steel.takeoff/v1 contract (the Steel Takeoff reader)
60638
60682
  "floless-app-tweak-contract",
60639
- // handle a tweak-contract request from the steel-takeoff contract editor (Slice 2 AI round-trip)
60683
+ // handle a tweak-contract request from the Steel Model contract editor (Slice 2 AI round-trip)
60640
60684
  "floless-app-ui",
60641
60685
  // compose Custom Panels (~/.floless/ui/extensions.json) for the Dashboard
60642
60686
  "floless-app-workflows",
@@ -60650,12 +60694,12 @@ function selectShippedSkillNames(names) {
60650
60694
  }
60651
60695
 
60652
60696
  // skill-sync.ts
60653
- var __dirname3 = (0, import_node_path23.dirname)((0, import_node_url4.fileURLToPath)(__import_meta_url));
60697
+ var __dirname3 = (0, import_node_path24.dirname)((0, import_node_url4.fileURLToPath)(__import_meta_url));
60654
60698
  function bundledSkillsRoot() {
60655
60699
  const candidates = [
60656
- (0, import_node_path23.join)(__dirname3, "skills"),
60657
- (0, import_node_path23.join)((0, import_node_path23.dirname)(process.execPath), "skills"),
60658
- (0, import_node_path23.join)(__dirname3, "..", ".claude", "skills")
60700
+ (0, import_node_path24.join)(__dirname3, "skills"),
60701
+ (0, import_node_path24.join)((0, import_node_path24.dirname)(process.execPath), "skills"),
60702
+ (0, import_node_path24.join)(__dirname3, "..", ".claude", "skills")
60659
60703
  ];
60660
60704
  return candidates.find((p) => (0, import_node_fs27.existsSync)(p)) ?? null;
60661
60705
  }
@@ -60664,11 +60708,11 @@ function targetConfigDirs() {
60664
60708
  if (override) {
60665
60709
  return override.split(";").map((d) => d.trim()).filter(Boolean).map((dir) => ({ runtime: "custom", dir }));
60666
60710
  }
60667
- const home = (0, import_node_os16.homedir)();
60711
+ const home = (0, import_node_os17.homedir)();
60668
60712
  return [
60669
- { runtime: "claude", dir: (0, import_node_path23.join)(home, ".claude") },
60670
- { runtime: "codex", dir: (0, import_node_path23.join)(home, ".codex") },
60671
- { runtime: "opencode", dir: (0, import_node_path23.join)(home, ".opencode") }
60713
+ { runtime: "claude", dir: (0, import_node_path24.join)(home, ".claude") },
60714
+ { runtime: "codex", dir: (0, import_node_path24.join)(home, ".codex") },
60715
+ { runtime: "opencode", dir: (0, import_node_path24.join)(home, ".opencode") }
60672
60716
  ];
60673
60717
  }
60674
60718
  function skillVersion(skillMdPath) {
@@ -60719,7 +60763,7 @@ function bundledSkills(root) {
60719
60763
  }
60720
60764
  const out = [];
60721
60765
  for (const name of entries) {
60722
- const dir = (0, import_node_path23.join)(root, name);
60766
+ const dir = (0, import_node_path24.join)(root, name);
60723
60767
  let isDir = false;
60724
60768
  try {
60725
60769
  isDir = (0, import_node_fs27.statSync)(dir).isDirectory();
@@ -60727,7 +60771,7 @@ function bundledSkills(root) {
60727
60771
  isDir = false;
60728
60772
  }
60729
60773
  if (!isDir) continue;
60730
- const v = skillVersion((0, import_node_path23.join)(dir, "SKILL.md"));
60774
+ const v = skillVersion((0, import_node_path24.join)(dir, "SKILL.md"));
60731
60775
  if (!v) continue;
60732
60776
  out.push({ name, dir, version: v });
60733
60777
  }
@@ -60741,10 +60785,10 @@ function syncSkills() {
60741
60785
  if (!skills.length) return results;
60742
60786
  for (const { runtime, dir: cfg } of targetConfigDirs()) {
60743
60787
  if (!(0, import_node_fs27.existsSync)(cfg)) continue;
60744
- const skillsDir = (0, import_node_path23.join)(cfg, "skills");
60788
+ const skillsDir = (0, import_node_path24.join)(cfg, "skills");
60745
60789
  for (const s of skills) {
60746
- const dest = (0, import_node_path23.join)(skillsDir, s.name);
60747
- const installedMd = (0, import_node_path23.join)(dest, "SKILL.md");
60790
+ const dest = (0, import_node_path24.join)(skillsDir, s.name);
60791
+ const installedMd = (0, import_node_path24.join)(dest, "SKILL.md");
60748
60792
  const installed = (0, import_node_fs27.existsSync)(installedMd) ? skillVersion(installedMd) : null;
60749
60793
  const action = decideAction(installed, s.version);
60750
60794
  if (action === "installed" || action === "updated") {
@@ -60763,8 +60807,8 @@ function syncSkills() {
60763
60807
  }
60764
60808
 
60765
60809
  // watch.ts
60766
- var import_node_os17 = require("node:os");
60767
- var import_node_path25 = require("node:path");
60810
+ var import_node_os18 = require("node:os");
60811
+ var import_node_path26 = require("node:path");
60768
60812
  var import_node_fs28 = require("node:fs");
60769
60813
 
60770
60814
  // node_modules/chokidar/esm/index.js
@@ -60776,7 +60820,7 @@ var sysPath2 = __toESM(require("path"), 1);
60776
60820
  // node_modules/readdirp/esm/index.js
60777
60821
  var import_promises2 = require("node:fs/promises");
60778
60822
  var import_node_stream4 = require("node:stream");
60779
- var import_node_path24 = require("node:path");
60823
+ var import_node_path25 = require("node:path");
60780
60824
  var EntryTypes = {
60781
60825
  FILE_TYPE: "files",
60782
60826
  DIR_TYPE: "directories",
@@ -60851,7 +60895,7 @@ var ReaddirpStream = class extends import_node_stream4.Readable {
60851
60895
  this._wantsDir = type ? DIR_TYPES.has(type) : false;
60852
60896
  this._wantsFile = type ? FILE_TYPES.has(type) : false;
60853
60897
  this._wantsEverything = type === EntryTypes.EVERYTHING_TYPE;
60854
- this._root = (0, import_node_path24.resolve)(root);
60898
+ this._root = (0, import_node_path25.resolve)(root);
60855
60899
  this._isDirent = !opts.alwaysStat;
60856
60900
  this._statsProp = this._isDirent ? "dirent" : "stats";
60857
60901
  this._rdOptions = { encoding: "utf8", withFileTypes: this._isDirent };
@@ -60922,8 +60966,8 @@ var ReaddirpStream = class extends import_node_stream4.Readable {
60922
60966
  let entry2;
60923
60967
  const basename5 = this._isDirent ? dirent.name : dirent;
60924
60968
  try {
60925
- const fullPath = (0, import_node_path24.resolve)((0, import_node_path24.join)(path, basename5));
60926
- entry2 = { path: (0, import_node_path24.relative)(this._root, fullPath), fullPath, basename: basename5 };
60969
+ const fullPath = (0, import_node_path25.resolve)((0, import_node_path25.join)(path, basename5));
60970
+ entry2 = { path: (0, import_node_path25.relative)(this._root, fullPath), fullPath, basename: basename5 };
60927
60971
  entry2[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
60928
60972
  } catch (err2) {
60929
60973
  this._onError(err2);
@@ -60957,7 +61001,7 @@ var ReaddirpStream = class extends import_node_stream4.Readable {
60957
61001
  }
60958
61002
  if (entryRealPathStats.isDirectory()) {
60959
61003
  const len2 = entryRealPath.length;
60960
- if (full.startsWith(entryRealPath) && full.substr(len2, 1) === import_node_path24.sep) {
61004
+ if (full.startsWith(entryRealPath) && full.substr(len2, 1) === import_node_path25.sep) {
60961
61005
  const recursiveError = new Error(`Circular symlink detected: "${full}" points to "${entryRealPath}"`);
60962
61006
  recursiveError.code = RECURSIVE_ERROR_CODE;
60963
61007
  return this._onError(recursiveError);
@@ -62465,19 +62509,19 @@ function appIdFromLogPath(path) {
62465
62509
  return i >= 0 && parts[i + 1] ? parts[i + 1] : null;
62466
62510
  }
62467
62511
  function samePath(a, b) {
62468
- const ra = (0, import_node_path25.resolve)(a);
62469
- const rb = (0, import_node_path25.resolve)(b);
62512
+ const ra = (0, import_node_path26.resolve)(a);
62513
+ const rb = (0, import_node_path26.resolve)(b);
62470
62514
  return process.platform === "win32" ? ra.toLowerCase() === rb.toLowerCase() : ra === rb;
62471
62515
  }
62472
62516
  function underDir(path, dir) {
62473
- const rp = (0, import_node_path25.resolve)(path);
62474
- const rd = (0, import_node_path25.resolve)(dir);
62517
+ const rp = (0, import_node_path26.resolve)(path);
62518
+ const rd = (0, import_node_path26.resolve)(dir);
62475
62519
  const [p, d] = process.platform === "win32" ? [rp.toLowerCase(), rd.toLowerCase()] : [rp, rd];
62476
- return p === d || p.startsWith(d + import_node_path25.sep);
62520
+ return p === d || p.startsWith(d + import_node_path26.sep);
62477
62521
  }
62478
62522
  function startWatcher() {
62479
- const awareDir = process.env.AWARE_HOME ?? (0, import_node_path25.join)((0, import_node_os17.homedir)(), ".aware");
62480
- const credentialsDir = (0, import_node_path25.join)(awareDir, "credentials");
62523
+ const awareDir = process.env.AWARE_HOME ?? (0, import_node_path26.join)((0, import_node_os18.homedir)(), ".aware");
62524
+ const credentialsDir = (0, import_node_path26.join)(awareDir, "credentials");
62481
62525
  if (!(0, import_node_fs28.existsSync)(credentialsDir)) {
62482
62526
  try {
62483
62527
  (0, import_node_fs28.mkdirSync)(credentialsDir, { recursive: true });
@@ -62490,7 +62534,7 @@ function startWatcher() {
62490
62534
  } catch {
62491
62535
  }
62492
62536
  }
62493
- const targets = ["apps", "logs", "credentials"].map((d) => (0, import_node_path25.join)(awareDir, d)).filter((p) => (0, import_node_fs28.existsSync)(p));
62537
+ const targets = ["apps", "logs", "credentials"].map((d) => (0, import_node_path26.join)(awareDir, d)).filter((p) => (0, import_node_fs28.existsSync)(p));
62494
62538
  if ((0, import_node_fs28.existsSync)(uiDir)) targets.push(uiDir);
62495
62539
  if (targets.length === 0) {
62496
62540
  return null;
@@ -62524,7 +62568,7 @@ function startWatcher() {
62524
62568
  const id = appIdFromLogPath(path);
62525
62569
  if (!id) return;
62526
62570
  try {
62527
- broadcast({ type: "trace-file", id, runId: path.split(import_node_path25.sep).pop()?.replace(/\.jsonl$/, "") ?? null, events: parseTrace((0, import_node_fs28.readFileSync)(path, "utf8")) });
62571
+ broadcast({ type: "trace-file", id, runId: path.split(import_node_path26.sep).pop()?.replace(/\.jsonl$/, "") ?? null, events: parseTrace((0, import_node_fs28.readFileSync)(path, "utf8")) });
62528
62572
  } catch {
62529
62573
  }
62530
62574
  }
@@ -62533,10 +62577,10 @@ function startWatcher() {
62533
62577
  }
62534
62578
 
62535
62579
  // index.ts
62536
- var __dirname4 = (0, import_node_path26.dirname)((0, import_node_url5.fileURLToPath)(__import_meta_url));
62537
- var WEB_ROOT = [(0, import_node_path26.join)(__dirname4, "web"), (0, import_node_path26.join)((0, import_node_path26.dirname)(process.execPath), "web"), (0, import_node_path26.join)(__dirname4, "..", "web")].find(
62580
+ var __dirname4 = (0, import_node_path27.dirname)((0, import_node_url5.fileURLToPath)(__import_meta_url));
62581
+ var WEB_ROOT = [(0, import_node_path27.join)(__dirname4, "web"), (0, import_node_path27.join)((0, import_node_path27.dirname)(process.execPath), "web"), (0, import_node_path27.join)(__dirname4, "..", "web")].find(
62538
62582
  (p) => (0, import_node_fs29.existsSync)(p)
62539
- ) ?? (0, import_node_path26.join)(__dirname4, "..", "web");
62583
+ ) ?? (0, import_node_path27.join)(__dirname4, "..", "web");
62540
62584
  var PORT2 = Number(process.env.PORT ?? 4317);
62541
62585
  var HOST = "127.0.0.1";
62542
62586
  var crashHandlersInstalled = false;
@@ -62860,12 +62904,12 @@ async function startServer() {
62860
62904
  _wfUpdating.add(id);
62861
62905
  const bdir = backupDir(id, fromVersion, Date.now());
62862
62906
  try {
62863
- (0, import_node_fs29.mkdirSync)((0, import_node_path26.dirname)(bdir), { recursive: true });
62907
+ (0, import_node_fs29.mkdirSync)((0, import_node_path27.dirname)(bdir), { recursive: true });
62864
62908
  (0, import_node_fs29.cpSync)(appDirPath2, bdir, { recursive: true });
62865
62909
  (0, import_node_fs29.cpSync)(t.path, srcPath);
62866
62910
  if (contract != null) {
62867
62911
  bakeContractIntoApp(srcPath, contract);
62868
- bakeDownstream(srcPath, contract);
62912
+ await bakeDownstream(srcPath, id, contract);
62869
62913
  }
62870
62914
  await aware.compile(srcPath);
62871
62915
  broadcast({ type: "apps-changed", id });
@@ -63027,7 +63071,7 @@ async function startServer() {
63027
63071
  const sourcePath = readApp(req.params.appId).source.path;
63028
63072
  try {
63029
63073
  bakeContractIntoApp(sourcePath, doc);
63030
- bakeDownstream(sourcePath, doc);
63074
+ await bakeDownstream(sourcePath, req.params.appId, doc);
63031
63075
  } catch (e) {
63032
63076
  return reply.status(422).send({ ok: false, error: e instanceof Error ? e.message : "bake failed" });
63033
63077
  }
@@ -63146,7 +63190,7 @@ async function startServer() {
63146
63190
  }
63147
63191
  const companionId = `${req.params.appId}-ifc`;
63148
63192
  const filename = `${req.params.appId}.ifc`;
63149
- const outPath = (0, import_node_path26.join)(appPath(companionId), filename);
63193
+ const outPath = (0, import_node_path27.join)(appPath(companionId), filename);
63150
63194
  let flo;
63151
63195
  try {
63152
63196
  flo = writeIfcApp(appPath(companionId), companionId, scene, outPath);
@@ -63248,10 +63292,9 @@ async function startServer() {
63248
63292
  if (bom.rows.filter((r) => r.Profile !== "TOTAL").length === 0) {
63249
63293
  return reply.status(422).send({ ok: false, error: "no priced members to export \u2014 every member is an RFI (no AISC size / weight yet)" });
63250
63294
  }
63251
- const root = process.env.FLOLESS_HOME ?? (0, import_node_path26.join)((0, import_node_os18.homedir)(), ".floless");
63252
- const dir = (0, import_node_path26.join)(root, "exports", appId);
63295
+ const outPath = bomExportPath(appId, format);
63296
+ const dir = (0, import_node_path27.dirname)(outPath);
63253
63297
  const filename = `${appId}-bom.${format}`;
63254
- const outPath = (0, import_node_path26.join)(dir, filename);
63255
63298
  try {
63256
63299
  (0, import_node_fs29.mkdirSync)(dir, { recursive: true });
63257
63300
  if (format === "csv") {
@@ -63354,11 +63397,11 @@ async function startServer() {
63354
63397
  if (appExists(id)) {
63355
63398
  return reply.status(409).send({ ok: false, error: `a workflow named "${id}" is already installed \u2014 remove it first, or rename the file's app: id`, code: "exists", id });
63356
63399
  }
63357
- const stageRoot = (0, import_node_fs29.mkdtempSync)((0, import_node_path26.join)((0, import_node_os18.tmpdir)(), "floless-import-"));
63400
+ const stageRoot = (0, import_node_fs29.mkdtempSync)((0, import_node_path27.join)((0, import_node_os19.tmpdir)(), "floless-import-"));
63358
63401
  try {
63359
- const stageDir = (0, import_node_path26.join)(stageRoot, id);
63402
+ const stageDir = (0, import_node_path27.join)(stageRoot, id);
63360
63403
  (0, import_node_fs29.mkdirSync)(stageDir);
63361
- (0, import_node_fs29.writeFileSync)((0, import_node_path26.join)(stageDir, `${id}.flo`), content);
63404
+ (0, import_node_fs29.writeFileSync)((0, import_node_path27.join)(stageDir, `${id}.flo`), content);
63362
63405
  await aware.install(stageDir);
63363
63406
  } catch (err2) {
63364
63407
  try {
@@ -63382,13 +63425,13 @@ async function startServer() {
63382
63425
  }
63383
63426
  const inputs = appData.inputs.map((i) => ({ name: i.name, type: i.type }));
63384
63427
  const baked = bakeFloSource(appData.source.text, inputs);
63385
- const tmpRoot = (0, import_node_fs29.mkdtempSync)((0, import_node_path26.join)((0, import_node_os18.tmpdir)(), "floless-bake-"));
63386
- const backupDir2 = (0, import_node_path26.join)(tmpRoot, `${id}-backup`);
63387
- const bakeDir = (0, import_node_path26.join)(tmpRoot, id);
63428
+ const tmpRoot = (0, import_node_fs29.mkdtempSync)((0, import_node_path27.join)((0, import_node_os19.tmpdir)(), "floless-bake-"));
63429
+ const backupDir2 = (0, import_node_path27.join)(tmpRoot, `${id}-backup`);
63430
+ const bakeDir = (0, import_node_path27.join)(tmpRoot, id);
63388
63431
  (0, import_node_fs29.cpSync)(appDir(id), backupDir2, { recursive: true });
63389
63432
  (0, import_node_fs29.cpSync)(appDir(id), bakeDir, { recursive: true });
63390
63433
  const floName = appData.source.path.split(/[\\/]/).pop();
63391
- (0, import_node_fs29.writeFileSync)((0, import_node_path26.join)(bakeDir, floName), baked);
63434
+ (0, import_node_fs29.writeFileSync)((0, import_node_path27.join)(bakeDir, floName), baked);
63392
63435
  let appInstalled2 = true;
63393
63436
  try {
63394
63437
  await aware.uninstall(id);
@@ -63410,7 +63453,7 @@ async function startServer() {
63410
63453
  throw installErr;
63411
63454
  }
63412
63455
  try {
63413
- await aware.compile((0, import_node_path26.join)(appDir(id), floName));
63456
+ await aware.compile((0, import_node_path27.join)(appDir(id), floName));
63414
63457
  } catch (compileErr) {
63415
63458
  app.log.warn({ id, compileErr: String(compileErr) }, "bake: post-install recompile failed (app baked but may need a manual Compile)");
63416
63459
  }
@@ -63420,7 +63463,7 @@ async function startServer() {
63420
63463
  if (appInstalled2) (0, import_node_fs29.rmSync)(tmpRoot, { recursive: true, force: true });
63421
63464
  }
63422
63465
  });
63423
- const graftAgentsDir = () => (0, import_node_path26.join)((0, import_node_os18.homedir)(), ".aware", "agents");
63466
+ const graftAgentsDir = () => (0, import_node_path27.join)((0, import_node_os19.homedir)(), ".aware", "agents");
63424
63467
  app.post("/api/graft/match", async (req, reply) => {
63425
63468
  const { glob } = req.body ?? {};
63426
63469
  if (!glob) return reply.status(400).send({ ok: false, error: "glob required" });
@@ -63437,7 +63480,7 @@ async function startServer() {
63437
63480
  if (!sourceKind || !sourceRef) {
63438
63481
  return reply.status(400).send({ ok: false, error: "sourceKind and sourceRef required" });
63439
63482
  }
63440
- const tempHome = (0, import_node_fs29.mkdtempSync)((0, import_node_path26.join)((0, import_node_os18.tmpdir)(), "floless-graft-"));
63483
+ const tempHome = (0, import_node_fs29.mkdtempSync)((0, import_node_path27.join)((0, import_node_os19.tmpdir)(), "floless-graft-"));
63441
63484
  let result;
63442
63485
  try {
63443
63486
  result = await aware.build({
@@ -63462,7 +63505,7 @@ async function startServer() {
63462
63505
  const token = (0, import_node_crypto7.randomUUID)();
63463
63506
  registerStage(token, tempHome, result.agentId);
63464
63507
  const preview = buildPreview(manifest, sourceKind, sourceRef, token);
63465
- if ((0, import_node_fs29.existsSync)((0, import_node_path26.join)(graftAgentsDir(), result.agentId))) {
63508
+ if ((0, import_node_fs29.existsSync)((0, import_node_path27.join)(graftAgentsDir(), result.agentId))) {
63466
63509
  preview.warnings.unshift(`An agent named "${result.agentId}" is already installed \u2014 creating it will overwrite it.`);
63467
63510
  }
63468
63511
  return { ok: true, preview };
@@ -63512,6 +63555,33 @@ async function startServer() {
63512
63555
  }
63513
63556
  return false;
63514
63557
  }
63558
+ let _fileAgentReady = false;
63559
+ let _fileEnsureInflight = null;
63560
+ async function ensureSteelExportReady(id) {
63561
+ let appData;
63562
+ try {
63563
+ appData = readApp(id);
63564
+ } catch {
63565
+ return;
63566
+ }
63567
+ if (!appData.requires.some((r) => r.split("@")[0] === "file")) return;
63568
+ if (!_fileAgentReady) {
63569
+ _fileEnsureInflight ??= aware.ensureAgentInstalled("file").then(() => {
63570
+ _fileAgentReady = true;
63571
+ }).catch((e) => app.log.warn({ id, err: String(e) }, "ensure file agent before run failed (run will surface any real error)")).finally(() => {
63572
+ _fileEnsureInflight = null;
63573
+ });
63574
+ await _fileEnsureInflight;
63575
+ }
63576
+ if (appData.runnable && exportPathsNeedingBake(appData.nodes, id).length > 0) {
63577
+ try {
63578
+ await bakeBomExportNodes(appData.source.path, id);
63579
+ await aware.compile(appData.source.path);
63580
+ } catch (e) {
63581
+ app.log.warn({ id, err: String(e) }, "bake export path before run failed (run will surface any real error)");
63582
+ }
63583
+ }
63584
+ }
63515
63585
  app.post(
63516
63586
  "/api/run",
63517
63587
  async (req, reply) => {
@@ -63519,6 +63589,7 @@ async function startServer() {
63519
63589
  if (!id) return reply.status(400).send({ ok: false, error: "id required" });
63520
63590
  const blocked = hostRunBlocked(appProvider(id));
63521
63591
  if (blocked) return reply.send({ ok: false, error: blocked, blocked: true, simulate: !!simulate });
63592
+ if (!simulate) await ensureSteelExportReady(id);
63522
63593
  let result;
63523
63594
  try {
63524
63595
  result = await aware.run(id, { dryRun, simulate, inputs });
@@ -40,7 +40,7 @@ documented in `floless-app-bridge`; the five types map to:
40
40
  |---|---|---|
41
41
  | `tweak` | "Tweak node **`<nodeId>`** in **`<appId>`** — *<instruction>*" | `floless-app-workflows` |
42
42
  | `use-template` | "Add template **<template.name>** to **`<appId>`**" | `floless-app-workflows` |
43
- | `tweak-contract` | "Edit the steel-takeoff contract for **`<appId>`** — *<instruction>* (+N screenshot(s))" | `floless-app-tweak-contract` |
43
+ | `tweak-contract` | "Edit the Steel Model contract for **`<appId>`** — *<instruction>* (+N screenshot(s))" | `floless-app-tweak-contract` |
44
44
  | `ui-customize` | "Customize the Dashboard (optionally `panel <panelId>`) — *<instruction>*" | `floless-app-ui` |
45
45
  | `rebake` | "Re-read the **<inputName>** drawing (<sourceName>) in **`<appId>`** — *<instruction>*" | `floless-app-rebake` |
46
46
 
@@ -2,7 +2,7 @@
2
2
  name: floless-app-steel-model
3
3
  description: The front-door guide for turning a structural steel drawing SET into an editable model in floless.app — the "Steel Model" flow. This skill orchestrates the whole pipeline — set up a project folder, learn the set into plain-English knowledge files, provision the right AWARE agents (region-specific steel detailer plus any bake targets), confirm which sheets are plans / elevations / details, then hand off to the Steel Takeoff reader to read each plan into a steel.takeoff/v1 contract — with explicit confirm gates between stages. Use it when a user wants to model steel from a drawing SET or asks things like "model this steel job from the drawings", "take off this whole set", "start a steel takeoff project", "turn these structural drawings into a model", "I have a PDF set of framing plans", or "/floless-app-steel-model". It is re-runnable — resume at any stage. For a single already-isolated framing plan the Steel Takeoff reader (floless-app-steel-takeoff) can be used directly; this guide is for driving a real job end to end.
4
4
  metadata:
5
- version: 0.1.0
5
+ version: 0.2.0
6
6
  ---
7
7
 
8
8
  # Steel Model — guide a steel drawing set into an editable model
@@ -127,18 +127,22 @@ the user at the editor:
127
127
  - **Read each framing plan** via **`floless-app-steel-takeoff`** — it runs the filter pre-stage
128
128
  (isolate the steel linework by layer/thickness/colour), the vision read, the AISC/EN/BS lookup
129
129
  via the detailer, honors `target_confidence`, writes the `steel.takeoff/v1` contract, and opens
130
- the overlay editor. One contract per `steel-takeoff` app; multi-sheet sets ride the filter's
130
+ the overlay editor. One contract per `steel-model` app; multi-sheet sets ride the filter's
131
131
  `sheets[]` + the editor's plan switcher.
132
132
  - **Pre-fill the connection library** from `company-rules.md` (stage 2): emit the firm's rows into
133
133
  the contract's `connections[]` with `source:"company-rules"`; the user reviews/assigns them per
134
134
  member end in the editor's **Connections** panel (the per-end picker + confidence card).
135
135
  - **Point at the rest of the canvas:** the **filter** view (refine the kept linework), **Approve**
136
- (bake the lock — the determinism gate), **3D** model, **IFC** / **Tekla** export, the **BOM**, the
137
- **confidence report**, and the **callout bubbles** (click a section/elevation/detail mark on a
138
- plan to jump to the referenced sheet).
136
+ (bake the lock — the determinism gate), **3D** model, **IFC** / **Tekla** export, the **BOM**
137
+ (exportable to **CSV / Excel**), the **confidence report**, and the **callout bubbles** (click a
138
+ section/elevation/detail mark on a plan to jump to the referenced sheet).
139
139
 
140
140
  **Done** when each framing plan has an approved, editor-reviewed contract and the user knows which
141
- on-canvas tool does what. The exports (3D/IFC/Tekla/BOM) are on-demand from the editor.
141
+ on-canvas tool does what. The 3D/IFC/Tekla exports are on-demand from the editor; the **BOM is
142
+ exported two ways** — on-demand via the editor's *Export BOM (CSV)/(Excel)* buttons, **and written
143
+ to `~/.floless/exports/steel-model/steel-model-bom.{csv,xlsx}` automatically every time the workflow
144
+ is Run** (two `file/write` export steps at the end of the chain). Tell the user they can **freeze**
145
+ those export steps (right-click → Freeze) once the BOM is right, so a re-Run won't overwrite the files.
142
146
 
143
147
  ---
144
148
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: floless-app-steel-takeoff
3
- description: Read a structural steel drawing into an editable steel.takeoff/v1 contract in floless.app — the "Steel Takeoff" reader. Triggers on a pending floless `rebake` request for the `steel-takeoff` app (queued from its "Re-read & re-bake ▸" button), or asks like "read this framing plan into a takeoff", "turn this structural drawing into a takeoff", "bake my drawing into steel-takeoff", "extract members from this PDF". Teaches the host AI to READ a steel drawing (PDF/image) with its own vision at COMPOSE time, build a steel.takeoff/v1 contract (members + profiles + elevations + weights from AISC lookup), write it to the contract store, and hand control to the editor — no model in the run path, no API key.
3
+ description: Read a structural steel drawing into an editable steel.takeoff/v1 contract in floless.app — the "Steel Takeoff" reader. Triggers on a pending floless `rebake` request for the `steel-model` app (queued from its "Re-read & re-bake ▸" button), or asks like "read this framing plan into a takeoff", "turn this structural drawing into a takeoff", "bake my drawing into steel-model", "extract members from this PDF". Teaches the host AI to READ a steel drawing (PDF/image) with its own vision at COMPOSE time, build a steel.takeoff/v1 contract (members + profiles + elevations + weights from AISC lookup), write it to the contract store, and hand control to the editor — no model in the run path, no API key.
4
4
  metadata:
5
5
  version: 0.1.0
6
6
  ---
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## What this is
11
11
 
12
- `steel-takeoff` is a FloLess app that turns a structural steel drawing into an editable
12
+ `steel-model` is a FloLess app that turns a structural steel drawing into an editable
13
13
  **takeoff contract** — members, profiles, AISC weights, T.O. elevations, connection detail refs
14
14
  — rendered in the overlay editor, then baked at Approve into the app's `read` node.
15
15
 
@@ -34,7 +34,7 @@ but do not attempt them here.
34
34
  ### 1. Find the drawing
35
35
 
36
36
  From a `rebake` request: `GET http://localhost:<port>/api/requests` → the entry with
37
- `type:"rebake"` and `appId:"steel-takeoff"` carries `snapshots: ["<abs path>"]` (the drawing
37
+ `type:"rebake"` and `appId:"steel-model"` carries `snapshots: ["<abs path>"]` (the drawing
38
38
  file) and optionally `sourceName` (the original filename — use it for the `source.name`
39
39
  provenance field). If the user just attaches or points at a drawing in prose, skip the lookup
40
40
  and proceed from that path.
@@ -72,7 +72,7 @@ native/display coords):
72
72
  `*BEAM*STEL*`, `*COL*`, `*BRACE*`, `*JOIST*`) → default **on**; others off. Mark
73
73
  `drawable: false` on pure text/dimension/title-block layers.
74
74
  5. Set `filter.mode: "layer"`, `filter.active: 0`. **PUT** the contract
75
- (`PUT /api/contract/steel-takeoff`). On a re-read, preserve any existing per-facet `on` flags.
75
+ (`PUT /api/contract/steel-model`). On a re-read, preserve any existing per-facet `on` flags.
76
76
 
77
77
  Then tell the user to open the **Filter** node, flip through the sheets, narrow to the steel, and
78
78
  **Save**. The saved `on` flags + `mode` are what step 2 reads — **per sheet** (each sheet's elements
@@ -180,7 +180,7 @@ excluded from the average and counted separately as the headline drag).
180
180
  **The score endpoint is your gauge — call it, don't guess:**
181
181
 
182
182
  ```
183
- POST http://localhost:<port>/api/contract/steel-takeoff/score
183
+ POST http://localhost:<port>/api/contract/steel-model/score
184
184
  Content-Type: application/json
185
185
  { "contract": <your draft contract>, "target": <target_confidence, e.g. 70> }
186
186
  ```
@@ -309,7 +309,7 @@ Key field rules:
309
309
  ### 5. Write to the contract store
310
310
 
311
311
  ```
312
- PUT http://localhost:<port>/api/contract/steel-takeoff
312
+ PUT http://localhost:<port>/api/contract/steel-model
313
313
  Content-Type: application/json
314
314
 
315
315
  <contract JSON body>
@@ -317,7 +317,7 @@ Content-Type: application/json
317
317
 
318
318
  The server schema-validates the body. If it responds with `400`, read the error, fix the
319
319
  offending fields, and re-PUT. You can read back the current contract with
320
- `GET /api/contract/steel-takeoff`.
320
+ `GET /api/contract/steel-model`.
321
321
 
322
322
  ### 6. Hand off to the user
323
323
 
@@ -347,7 +347,7 @@ DELETE http://localhost:<port>/api/requests/<id>
347
347
 
348
348
  Never write client, firm, or project identifiers into files, commits, or responses. The
349
349
  contract carries the source path in `source.path` and an embedded raster in `raster_b64` — both
350
- are machine-local (stored under `~/.floless/contracts/steel-takeoff.json` by the server and
350
+ are machine-local (stored under `~/.floless/contracts/steel-model.json` by the server and
351
351
  never committed). Clip title blocks from any embedded preview.
352
352
 
353
353
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: floless-app-tweak-contract
3
- description: This skill should be used when there is a pending floless `tweak-contract` request — the user typed an instruction in the steel-takeoff contract editor and clicked Send (Ask AI). Also triggers on asks like "edit the steel takeoff contract via AI", "adjust the steel takeoff in the contract editor", "the user asked me to change the steel contract", "pick up my tweak-contract request", "update the member profile in the contract". It teaches the host AI to read the request, fetch the current steel.takeoff/v1 contract, apply the instruction (and any pasted screenshots), PUT the updated contract back, and resolve the request.
3
+ description: This skill should be used when there is a pending floless `tweak-contract` request — the user typed an instruction in the Steel Model contract editor and clicked Send (Ask AI). Also triggers on asks like "edit the steel takeoff contract via AI", "adjust the steel takeoff in the contract editor", "the user asked me to change the steel contract", "pick up my tweak-contract request", "update the member profile in the contract". It teaches the host AI to read the request, fetch the current steel.takeoff/v1 contract, apply the instruction (and any pasted screenshots), PUT the updated contract back, and resolve the request.
4
4
  metadata:
5
5
  version: 0.1.0
6
6
  ---
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## What this is
11
11
 
12
- The floless.app steel-takeoff contract editor surfaces an **Ask AI** panel: the user types
12
+ The floless.app Steel Model contract editor surfaces an **Ask AI** panel: the user types
13
13
  an instruction ("change all W12X26 to W14X30", "add a new beam on sheet S-202", "mark this
14
14
  member as moment-frame") and optionally pastes screenshots, then clicks **Send**. That records
15
15
  a `tweak-contract` request in the reverse channel. You pick it up here, edit the contract
@@ -2,7 +2,7 @@
2
2
  name: floless-app-workflows
3
3
  description: This skill should be used when building, modifying, or reasoning about FloLess .flo workflows for floless.app — especially reusable apps that take inputs (e.g. a phase number) and run across different Tekla models, exec nodes that run Roslyn C# against the live model, the in-app HTML Viewer, the Code tab, or "Debug in VS". Covers the exec contract, the install→validate→compile→run loop, app inputs templating, and the server seams (aware-adapter, app-reader, index).
4
4
  metadata:
5
- version: 0.1.0
5
+ version: 0.2.0
6
6
  ---
7
7
 
8
8
  # Building reusable .flo workflows in floless.app
@@ -270,6 +270,29 @@ floless.app play (designed, now unblocked): a "Bake into agent" affordance that
270
270
  `.flo` to add `exposes-as-agent` + `exposed-commands` mirroring the app's `inputs:`. Thin-UI: a
271
271
  manifest edit + recompile, no engine. See the design doc for the Stage-1 plan.
272
272
 
273
+ ## Write a file on Run (the `file` agent) — VERIFIED, AWARE 0.81.0+
274
+
275
+ A node can **land an artifact on disk as part of the run** with the `_core/file` agent (steel-model's
276
+ `csv-export` / `xlsx-export` nodes do exactly this). Key facts (all verified):
277
+
278
+ - **`file` is a builtin** (`transport: builtin {}`) — handled in-process, **no host binary**, like
279
+ `html-report` / `viewer-3d`. But its **manifest must be installed** (`aware agent install file`):
280
+ absent, the run fails with an opaque IO error; present, `aware app run` dispatches it. floless
281
+ ensures it before Run for any app that `requires: file@1.0.x` (see `ensureSteelExportReady`, index.ts).
282
+ - **Verbs:** `write` lands `bytes` at `path` (`encoding: text | base64`); `write-csv` takes
283
+ `{ path, columns, rows }` → RFC-4180. For a **binary** artifact (`.xlsx`, image, zip) **floless
284
+ generates the bytes in TS and the node base64-lands them** — AWARE has no xlsx serializer, so the
285
+ binary stays host-side (we reuse `bomToXlsx` and write `encoding: base64`).
286
+ - **`file/write*` are `mode: write`** (a generic arbitrary-path writer mutates external state), so an
287
+ **un-frozen** export node MUST carry a `safety:` block — `safety: { snapshot: false }` is enough. A
288
+ **frozen** node skips the safety preflight AND skips dispatch (it replays its pinned output, no
289
+ write) — that is the "freeze the export so Run won't regenerate it" affordance.
290
+ - **Path:** the producer owns `path`. A **bare relative path resolves to the run process's cwd**
291
+ (it'll litter the server dir), so floless bakes an **absolute** `~/.floless/exports/<appId>/…` path
292
+ into the node — on Approve via `bakeDownstream` → `bakeBomExportNodes` (path + real bytes), and as a
293
+ safety net before Run via `ensureSteelExportReady` (path only; idempotent, recompiles once). The
294
+ committed template carries a bare filename + a small example payload so the step is valid out of the box.
295
+
273
296
  ## How the UI surfaces map to AWARE state
274
297
 
275
298
  - **Code tab** shows the node's real source: `node.config.code` (the exec C#), not lockfile
@@ -1,6 +1,6 @@
1
- app: steel-takeoff
2
- version: 0.2.0
3
- display-name: Steel Takeoff
1
+ app: steel-model
2
+ version: 0.3.0
3
+ display-name: Steel Model
4
4
  publisher: floless
5
5
  module: steel-detailer
6
6
  description: |
@@ -14,6 +14,10 @@ exposes-as-agent: false
14
14
  # Workflow changelog (FloLess-published). Newest first; the app shows entries newer than the
15
15
  # installed version when offering an update. Plain-English — it surfaces to the user.
16
16
  changelog:
17
+ - version: 0.3.0
18
+ summary: Export your bill of materials as CSV and Excel on Run
19
+ changes:
20
+ - "Added: two export steps at the end that write your bill of materials to a .csv and a real .xlsx file when you Run the workflow — the same figures as the BOM table and the Export buttons, saved to your exports folder automatically. Freeze a step once it's right and Run won't overwrite the file."
17
21
  - version: 0.2.0
18
22
  summary: Filter your drawing down to the steel first
19
23
  changes:
@@ -34,6 +38,7 @@ inputs:
34
38
  requires:
35
39
  - html-report@0.2.x
36
40
  - viewer-3d@0.1.x
41
+ - file@1.0.x
37
42
  layout: linear
38
43
  nodes:
39
44
  # ── node 1: filter — isolate the steel linework (FIRST stage) ─────────────────
@@ -145,7 +150,45 @@ nodes:
145
150
  - { id: m1, profile: W10X33, role: beam, wp: [[0,0],[6000,0]] }
146
151
  - { id: m2, profile: HSS6X6X3/8, role: column, wp: [[0,0],[0,4500]] }
147
152
 
153
+ # ── node 5: csv-export — write the bill of materials to a CSV file ────────────
154
+ # Saves the bill of materials to a .csv file on disk every time you Run the
155
+ # workflow — the same figures and the same file you'd get from the "Export BOM
156
+ # (CSV)" button, but produced by the run so it lands automatically. When you
157
+ # Approve a takeoff, FloLess fills in the finished CSV and points this at your
158
+ # ~/.floless/exports folder; until then it ships a small sample so the step works
159
+ # out of the box. Freeze this node once you're happy and Run won't rewrite the file.
160
+ # (Uses the generic `file/write` verb with the CSV text FloLess generates — so the
161
+ # exported file is byte-for-byte the one the Export button produces, formula-injection
162
+ # guard and all — rather than re-serialising the rows in the runtime.)
163
+ - id: csv-export
164
+ agent: file
165
+ command: write
166
+ safety: { snapshot: false } # writing a file is a write-mode act; an un-frozen run needs a safety block (a frozen node skips the check)
167
+ config:
168
+ path: steel-model-bom.csv # baked to <home>/.floless/exports/steel-model/steel-model-bom.csv on Approve
169
+ encoding: text
170
+ bytes: "Profile,Qty,Length (ft),lb/ft,Weight (lb)\r\nW10X33,4,96,49.1,4714\r\nHSS6X6X3/8,6,54,27.48,1484\r\nTOTAL,10,150,,6198\r\n"
171
+
172
+ # ── node 6: xlsx-export — write the bill of materials to an Excel file ────────
173
+ # Saves the bill of materials to a real .xlsx Excel file every time you Run the
174
+ # workflow — the same figures as the BOM table and the "Export BOM (Excel)" button.
175
+ # Excel files are binary, so FloLess builds the spreadsheet and hands its bytes to
176
+ # this step to land on disk (base64-encoded). When you Approve a takeoff, FloLess
177
+ # fills in the real workbook and points this at your ~/.floless/exports folder; until
178
+ # then it ships a small sample so the step works out of the box. Freeze this node once
179
+ # you're happy and Run won't rewrite the file.
180
+ - id: xlsx-export
181
+ agent: file
182
+ command: write
183
+ safety: { snapshot: false } # writing a file is a write-mode act; an un-frozen run needs a safety block (a frozen node skips the check)
184
+ config:
185
+ path: steel-model-bom.xlsx # baked to <home>/.floless/exports/steel-model/steel-model-bom.xlsx on Approve
186
+ encoding: base64
187
+ bytes: "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"
188
+
148
189
  connections:
149
190
  - { from: filter, to: read }
150
191
  - { from: read, to: view }
151
192
  - { from: view, to: bom }
193
+ - { from: bom, to: csv-export }
194
+ - { from: csv-export, to: xlsx-export }
package/dist/web/aware.js CHANGED
@@ -2880,7 +2880,7 @@
2880
2880
  }
2881
2881
 
2882
2882
  // ── Workflow update (module-shaped) ─────────────────────────────────────────────
2883
- // A FloLess-published workflow (e.g. steel-takeoff) has a newer bundled template. Poll GET
2883
+ // A FloLess-published workflow (e.g. steel-model) has a newer bundled template. Poll GET
2884
2884
  // /api/workflows/updates; when the OPEN app has one, show a pill → modal with the changelog →
2885
2885
  // "Back up & update" POSTs /api/workflows/:id/update (backup → swap structure → re-bake the user's
2886
2886
  // data from the contract store → recompile) → reload the app. No auto-update. `_wfUpdates` + the
@@ -1,5 +1,5 @@
1
1
  /*
2
- * steel-3d-view.js — the 3D render/interaction layer for the steel-takeoff editor's 3D mode.
2
+ * steel-3d-view.js — the 3D render/interaction layer for the steel-model editor's 3D mode.
3
3
  *
4
4
  * Three.js lives ONLY here (the rest of the editor is import-free). It renders the SAME scene the
5
5
  * IFC/Tekla bake uses (fetched from /api/contract/:id/scene → contractToScene), so the editable 3D
@@ -1,5 +1,5 @@
1
1
  <!doctype html><html><head><meta charset=utf-8>
2
- <title>Steel takeoff — editor</title>
2
+ <title>Steel Model — editor</title>
3
3
  <style>
4
4
  :root{--bg:#0f172a;--panel:#1e293b;--line:#334155;--text:#e2e8f0;--mut:#94a3b8;--brand:#3b82f6}
5
5
  /* Theme EVERY scrollbar — no native white default may leak (applies to the canvas, panels, and every modal list). */
@@ -204,7 +204,7 @@
204
204
  <script type="module" src="./steel-3d-view.js"></script>
205
205
  </head><body>
206
206
  <header>
207
- <b>Steel takeoff</b>
207
+ <b>Steel Model</b>
208
208
  <div id=viewToggle role=group aria-label="Canvas view"><button id=vt2d class="seg on" aria-pressed=true title="Plan view (2D overlay)">2D</button><button id=vt3d class=seg aria-pressed=false title="3D model view">3D</button></div>
209
209
  <select id=planSel title="Switch plan view"></select>
210
210
  <span class=stat>Members <b id=mc>0</b></span><span class=stat>Weight <b id=wt>0</b> tons · <b id=wtlb>0</b> lb</span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {