@floless/app 0.34.1 → 0.35.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/floless-server.cjs +91 -32
- package/dist/schemas/steel.takeoff.v1.schema.json +1 -0
- package/dist/skills/floless-app-bridge/SKILL.md +4 -1
- package/dist/skills/floless-app-queue/SKILL.md +88 -0
- package/dist/skills/floless-app-steel-takeoff/SKILL.md +6 -3
- package/dist/web/package.json +5 -0
- package/dist/web/steel-3d-core.js +137 -0
- package/dist/web/steel-3d-view.js +422 -0
- package/dist/web/steel-editor.html +140 -6
- package/dist/web/vendor/OrbitControls.js +1963 -0
- package/dist/web/vendor/README.md +15 -0
- package/dist/web/vendor/three.core.js +59732 -0
- package/dist/web/vendor/three.module.js +19552 -0
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -50822,7 +50822,7 @@ function deriveSandboxLicenseDir() {
|
|
|
50822
50822
|
deriveSandboxLicenseDir();
|
|
50823
50823
|
|
|
50824
50824
|
// main.ts
|
|
50825
|
-
var
|
|
50825
|
+
var import_node_child_process9 = require("node:child_process");
|
|
50826
50826
|
var import_node_readline2 = require("node:readline");
|
|
50827
50827
|
|
|
50828
50828
|
// index.ts
|
|
@@ -50830,7 +50830,7 @@ var import_node_url4 = require("node:url");
|
|
|
50830
50830
|
var import_node_path26 = require("node:path");
|
|
50831
50831
|
var import_node_os18 = require("node:os");
|
|
50832
50832
|
var import_node_fs28 = require("node:fs");
|
|
50833
|
-
var
|
|
50833
|
+
var import_node_child_process7 = require("node:child_process");
|
|
50834
50834
|
|
|
50835
50835
|
// log.mjs
|
|
50836
50836
|
var import_node_path = require("node:path");
|
|
@@ -52856,7 +52856,7 @@ function appVersion() {
|
|
|
52856
52856
|
return resolveVersion({
|
|
52857
52857
|
isSea: isSea2(),
|
|
52858
52858
|
sqVersionXml: readSqVersionXml(),
|
|
52859
|
-
define: true ? "0.
|
|
52859
|
+
define: true ? "0.35.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.
|
|
52869
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.35.0" : void 0 });
|
|
52870
52870
|
}
|
|
52871
52871
|
|
|
52872
52872
|
// oauth-presets.ts
|
|
@@ -53948,6 +53948,42 @@ function writeTeklaApp(dir, appId, scene, teklaVersion = "2026.0") {
|
|
|
53948
53948
|
return flo;
|
|
53949
53949
|
}
|
|
53950
53950
|
|
|
53951
|
+
// tekla-version.ts
|
|
53952
|
+
var import_node_child_process3 = require("node:child_process");
|
|
53953
|
+
var import_node_util = require("node:util");
|
|
53954
|
+
var pexec = (0, import_node_util.promisify)(import_node_child_process3.execFile);
|
|
53955
|
+
var DEFAULT_TEKLA_VERSION = "2026.0";
|
|
53956
|
+
function parseTeklaVersionFromExePath(p) {
|
|
53957
|
+
const m = /Tekla Structures[\\/]+([^\\/]+)[\\/]+bin\b/i.exec(p);
|
|
53958
|
+
return m ? m[1].trim() : null;
|
|
53959
|
+
}
|
|
53960
|
+
async function detectRunningTeklaVersion() {
|
|
53961
|
+
if (process.platform !== "win32") return null;
|
|
53962
|
+
try {
|
|
53963
|
+
const { stdout } = await pexec(
|
|
53964
|
+
"powershell",
|
|
53965
|
+
[
|
|
53966
|
+
"-NoProfile",
|
|
53967
|
+
"-NonInteractive",
|
|
53968
|
+
"-Command",
|
|
53969
|
+
`Get-CimInstance Win32_Process -Filter "Name='TeklaStructures.exe'" | Select-Object -First 1 -ExpandProperty ExecutablePath`
|
|
53970
|
+
],
|
|
53971
|
+
{ timeout: 5e3, windowsHide: true }
|
|
53972
|
+
);
|
|
53973
|
+
const exePath = stdout.trim();
|
|
53974
|
+
if (!exePath) return null;
|
|
53975
|
+
const version = parseTeklaVersionFromExePath(exePath);
|
|
53976
|
+
if (!version) console.warn(`tekla-version: Tekla is running but no version parsed from "${exePath}" \u2014 baking ${DEFAULT_TEKLA_VERSION}`);
|
|
53977
|
+
return version;
|
|
53978
|
+
} catch (e) {
|
|
53979
|
+
console.warn(`tekla-version: could not detect the running Tekla version (${e instanceof Error ? e.message : String(e)}) \u2014 baking ${DEFAULT_TEKLA_VERSION}`);
|
|
53980
|
+
return null;
|
|
53981
|
+
}
|
|
53982
|
+
}
|
|
53983
|
+
async function teklaVersionToBake() {
|
|
53984
|
+
return await detectRunningTeklaVersion() ?? DEFAULT_TEKLA_VERSION;
|
|
53985
|
+
}
|
|
53986
|
+
|
|
53951
53987
|
// bom-format.ts
|
|
53952
53988
|
var round1 = (n) => Math.round(n * 10) / 10;
|
|
53953
53989
|
function contractToBom(contractInput) {
|
|
@@ -55187,7 +55223,7 @@ function isGatedAwareRoute(url, method) {
|
|
|
55187
55223
|
}
|
|
55188
55224
|
|
|
55189
55225
|
// autostart.mjs
|
|
55190
|
-
var
|
|
55226
|
+
var import_node_child_process4 = require("node:child_process");
|
|
55191
55227
|
var import_node_fs22 = require("node:fs");
|
|
55192
55228
|
var import_node_os14 = require("node:os");
|
|
55193
55229
|
var import_node_path19 = require("node:path");
|
|
@@ -55296,7 +55332,7 @@ function currentUserId() {
|
|
|
55296
55332
|
}
|
|
55297
55333
|
function removeLegacyRunKey() {
|
|
55298
55334
|
try {
|
|
55299
|
-
(0,
|
|
55335
|
+
(0, import_node_child_process4.execFileSync)("reg", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"], { stdio: "ignore", windowsHide: true });
|
|
55300
55336
|
} catch {
|
|
55301
55337
|
}
|
|
55302
55338
|
}
|
|
@@ -55314,7 +55350,7 @@ function registerAutostart(exePath) {
|
|
|
55314
55350
|
const tmp = (0, import_node_path19.join)((0, import_node_os14.tmpdir)(), `floless-autostart-${process.pid}-${Date.now()}.xml`);
|
|
55315
55351
|
(0, import_node_fs22.writeFileSync)(tmp, "\uFEFF" + xml, { encoding: "utf16le" });
|
|
55316
55352
|
try {
|
|
55317
|
-
(0,
|
|
55353
|
+
(0, import_node_child_process4.execFileSync)("schtasks", ["/Create", "/TN", TASK_NAME, "/XML", tmp, "/F"], {
|
|
55318
55354
|
stdio: ["ignore", "ignore", "ignore"],
|
|
55319
55355
|
windowsHide: true
|
|
55320
55356
|
});
|
|
@@ -55332,7 +55368,7 @@ function registerAutostart(exePath) {
|
|
|
55332
55368
|
function autostartPresent() {
|
|
55333
55369
|
if (!isWin) return false;
|
|
55334
55370
|
try {
|
|
55335
|
-
(0,
|
|
55371
|
+
(0, import_node_child_process4.execFileSync)("schtasks", ["/query", "/tn", TASK_NAME], { stdio: "ignore", windowsHide: true });
|
|
55336
55372
|
return true;
|
|
55337
55373
|
} catch {
|
|
55338
55374
|
return false;
|
|
@@ -55349,14 +55385,14 @@ function ensureAutostart(exePath) {
|
|
|
55349
55385
|
function unregisterAutostart() {
|
|
55350
55386
|
if (!isWin) return;
|
|
55351
55387
|
try {
|
|
55352
|
-
(0,
|
|
55388
|
+
(0, import_node_child_process4.execFileSync)("schtasks", ["/delete", "/tn", TASK_NAME, "/f"], { stdio: "ignore", windowsHide: true });
|
|
55353
55389
|
} catch {
|
|
55354
55390
|
}
|
|
55355
55391
|
removeLegacyRunKey();
|
|
55356
55392
|
}
|
|
55357
55393
|
|
|
55358
55394
|
// updater.ts
|
|
55359
|
-
var
|
|
55395
|
+
var import_node_child_process5 = require("node:child_process");
|
|
55360
55396
|
var import_node_crypto6 = require("node:crypto");
|
|
55361
55397
|
var import_node_fs24 = require("node:fs");
|
|
55362
55398
|
var import_node_stream = require("node:stream");
|
|
@@ -55547,7 +55583,7 @@ async function applyUpdate(check, opts) {
|
|
|
55547
55583
|
const pkg = await downloadPackage(check.asset);
|
|
55548
55584
|
if (opts?.onBeforeApply) await opts.onBeforeApply();
|
|
55549
55585
|
await new Promise((resolve6, reject) => {
|
|
55550
|
-
const child = (0,
|
|
55586
|
+
const child = (0, import_node_child_process5.spawn)(exe, ["apply", "--package", pkg, "--waitPid", String(process.pid)], {
|
|
55551
55587
|
cwd: installRoot(),
|
|
55552
55588
|
detached: true,
|
|
55553
55589
|
stdio: "ignore",
|
|
@@ -55780,7 +55816,7 @@ function isTraceCorrupt(events) {
|
|
|
55780
55816
|
}
|
|
55781
55817
|
|
|
55782
55818
|
// launch.mjs
|
|
55783
|
-
var
|
|
55819
|
+
var import_node_child_process6 = require("node:child_process");
|
|
55784
55820
|
var import_node_path22 = require("node:path");
|
|
55785
55821
|
var import_node_url2 = require("node:url");
|
|
55786
55822
|
var import_node_fs25 = require("node:fs");
|
|
@@ -55866,7 +55902,7 @@ function startServerDetached() {
|
|
|
55866
55902
|
const { cmd, args, shell } = resolveServerStart();
|
|
55867
55903
|
rotateLog();
|
|
55868
55904
|
const fd = openLogFd();
|
|
55869
|
-
const child = (0,
|
|
55905
|
+
const child = (0, import_node_child_process6.spawn)(cmd, args, {
|
|
55870
55906
|
cwd: __dirname2,
|
|
55871
55907
|
detached: true,
|
|
55872
55908
|
stdio: fd == null ? "ignore" : ["ignore", fd, fd],
|
|
@@ -55876,13 +55912,13 @@ function startServerDetached() {
|
|
|
55876
55912
|
child.unref();
|
|
55877
55913
|
}
|
|
55878
55914
|
function openBrowser2(url) {
|
|
55879
|
-
if (isWin2) (0,
|
|
55880
|
-
else (0,
|
|
55915
|
+
if (isWin2) (0, import_node_child_process6.spawn)("cmd", ["/c", "start", "", url], { windowsHide: true }).unref();
|
|
55916
|
+
else (0, import_node_child_process6.spawn)(process.platform === "darwin" ? "open" : "xdg-open", [url], { detached: true }).unref();
|
|
55881
55917
|
}
|
|
55882
55918
|
function stopServer() {
|
|
55883
55919
|
if (!isWin2) {
|
|
55884
55920
|
try {
|
|
55885
|
-
(0,
|
|
55921
|
+
(0, import_node_child_process6.execSync)(`bash -lc "fuser -k ${PORT}/tcp"`, { stdio: "ignore" });
|
|
55886
55922
|
return true;
|
|
55887
55923
|
} catch {
|
|
55888
55924
|
return false;
|
|
@@ -55890,7 +55926,7 @@ function stopServer() {
|
|
|
55890
55926
|
}
|
|
55891
55927
|
try {
|
|
55892
55928
|
const ps = `$p = Get-NetTCPConnection -LocalPort ${PORT} -State Listen -ErrorAction SilentlyContinue | Select-Object -Expand OwningProcess -Unique; if ($p) { $p | ForEach-Object { taskkill /PID $_ /T /F } } else { exit 9 }`;
|
|
55893
|
-
(0,
|
|
55929
|
+
(0, import_node_child_process6.execSync)(`powershell -NoProfile -Command "${ps}"`, { stdio: "ignore" });
|
|
55894
55930
|
return true;
|
|
55895
55931
|
} catch {
|
|
55896
55932
|
return false;
|
|
@@ -55942,7 +55978,7 @@ async function cmdSupervise() {
|
|
|
55942
55978
|
if (isSeaChannel && !process.env[SUPERVISE_RESPAWN_ENV]) {
|
|
55943
55979
|
rotateLog();
|
|
55944
55980
|
const fd = openLogFd();
|
|
55945
|
-
const child = (0,
|
|
55981
|
+
const child = (0, import_node_child_process6.spawn)(process.execPath, ["--supervise"], {
|
|
55946
55982
|
detached: true,
|
|
55947
55983
|
stdio: fd == null ? "ignore" : ["ignore", fd, fd],
|
|
55948
55984
|
windowsHide: true,
|
|
@@ -55997,7 +56033,7 @@ function enumerateProcesses() {
|
|
|
55997
56033
|
if (!isWin2) return [];
|
|
55998
56034
|
try {
|
|
55999
56035
|
const ps = "Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress";
|
|
56000
|
-
const out = (0,
|
|
56036
|
+
const out = (0, import_node_child_process6.execSync)(`powershell -NoProfile -Command "${ps}"`, {
|
|
56001
56037
|
encoding: "utf8",
|
|
56002
56038
|
windowsHide: true,
|
|
56003
56039
|
maxBuffer: 16 * 1024 * 1024
|
|
@@ -56020,7 +56056,7 @@ function killSupervisor({ tree = true } = {}) {
|
|
|
56020
56056
|
for (const pid of pids) {
|
|
56021
56057
|
log(`stopping supervisor (pid ${pid})\u2026`);
|
|
56022
56058
|
try {
|
|
56023
|
-
(0,
|
|
56059
|
+
(0, import_node_child_process6.execFileSync)("taskkill", taskkillArgs(pid, { tree }), { stdio: "ignore", windowsHide: true });
|
|
56024
56060
|
} catch {
|
|
56025
56061
|
}
|
|
56026
56062
|
}
|
|
@@ -56111,11 +56147,11 @@ async function cmdUpdate() {
|
|
|
56111
56147
|
function removeRegistryFootprint() {
|
|
56112
56148
|
if (!isWin2) return;
|
|
56113
56149
|
try {
|
|
56114
|
-
(0,
|
|
56150
|
+
(0, import_node_child_process6.execFileSync)("reg", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"], { stdio: "ignore", windowsHide: true });
|
|
56115
56151
|
} catch {
|
|
56116
56152
|
}
|
|
56117
56153
|
try {
|
|
56118
|
-
(0,
|
|
56154
|
+
(0, import_node_child_process6.execFileSync)("reg", ["delete", PROTOCOL_KEY, "/f"], { stdio: "ignore", windowsHide: true });
|
|
56119
56155
|
} catch {
|
|
56120
56156
|
}
|
|
56121
56157
|
}
|
|
@@ -56141,13 +56177,13 @@ async function cmdUninstall(flags = {}) {
|
|
|
56141
56177
|
if (removeAware) {
|
|
56142
56178
|
log("removing the AWARE runtime \u2014 this will affect ANY other tool that uses @aware-aeco/cli.");
|
|
56143
56179
|
try {
|
|
56144
|
-
(0,
|
|
56180
|
+
(0, import_node_child_process6.execSync)("npm uninstall -g @aware-aeco/cli", { stdio: "inherit", shell: isWin2 });
|
|
56145
56181
|
} catch {
|
|
56146
56182
|
log('warning: "npm uninstall -g @aware-aeco/cli" failed \u2014 see the output above.');
|
|
56147
56183
|
}
|
|
56148
56184
|
let lsJson = "";
|
|
56149
56185
|
try {
|
|
56150
|
-
lsJson = (0,
|
|
56186
|
+
lsJson = (0, import_node_child_process6.execSync)("npm ls -g @aware-aeco/cli --depth=0 --json", {
|
|
56151
56187
|
encoding: "utf8",
|
|
56152
56188
|
stdio: ["ignore", "pipe", "ignore"],
|
|
56153
56189
|
shell: isWin2
|
|
@@ -56264,6 +56300,8 @@ var PRODUCT_SKILLS = [
|
|
|
56264
56300
|
// drive the floless.app CLI / desktop bridge from the user's AI
|
|
56265
56301
|
"floless-app-onboarding",
|
|
56266
56302
|
// guided, re-runnable tour of AWARE + floless.app for new users
|
|
56303
|
+
"floless-app-queue",
|
|
56304
|
+
// list the queued Dashboard requests in plain English + let the user pick which to process
|
|
56267
56305
|
"floless-app-rebake",
|
|
56268
56306
|
// re-read & re-bake a baked Visual Input (B3) after the user swaps the drawing
|
|
56269
56307
|
"floless-app-report-issue",
|
|
@@ -58224,7 +58262,7 @@ async function startServer() {
|
|
|
58224
58262
|
const isWin3 = process.platform === "win32";
|
|
58225
58263
|
const has = (cmd) => {
|
|
58226
58264
|
try {
|
|
58227
|
-
(0,
|
|
58265
|
+
(0, import_node_child_process7.execFileSync)(isWin3 ? "where" : "which", [cmd], { stdio: "ignore", windowsHide: true, timeout: 5e3 });
|
|
58228
58266
|
return true;
|
|
58229
58267
|
} catch {
|
|
58230
58268
|
return false;
|
|
@@ -58233,7 +58271,7 @@ async function startServer() {
|
|
|
58233
58271
|
function installAwareGlobal(spec, onLine = () => {
|
|
58234
58272
|
}) {
|
|
58235
58273
|
return new Promise((resolve6, reject) => {
|
|
58236
|
-
const child = (0,
|
|
58274
|
+
const child = (0, import_node_child_process7.spawn)("npm", ["i", "-g", `@aware-aeco/cli@${spec}`], { shell: isWin3, windowsHide: true });
|
|
58237
58275
|
let stderrTail = "";
|
|
58238
58276
|
let combinedTail = "";
|
|
58239
58277
|
const tail = (buf, s) => (buf + s).slice(-4e3);
|
|
@@ -58623,7 +58661,14 @@ async function startServer() {
|
|
|
58623
58661
|
const first = v.errors[0];
|
|
58624
58662
|
return reply.status(400).send({ ok: false, error: `contract failed schema validation \u2014 ${first ? `${first.path}: ${first.message}` : "invalid"}` });
|
|
58625
58663
|
}
|
|
58626
|
-
const
|
|
58664
|
+
const docTarget = doc2 && typeof doc2 === "object" ? doc2.target_confidence : void 0;
|
|
58665
|
+
let appDefault = 70;
|
|
58666
|
+
try {
|
|
58667
|
+
const inp = readApp(req.params.appId).inputs.find((i) => i.name === "target_confidence");
|
|
58668
|
+
if (typeof inp?.default === "number") appDefault = inp.default;
|
|
58669
|
+
} catch {
|
|
58670
|
+
}
|
|
58671
|
+
const target = typeof req.body?.target === "number" ? req.body.target : typeof docTarget === "number" ? docTarget : appDefault;
|
|
58627
58672
|
const result = scoreContract2(doc2);
|
|
58628
58673
|
const meetsTarget = result.score != null && result.score >= target;
|
|
58629
58674
|
return {
|
|
@@ -58635,6 +58680,19 @@ async function startServer() {
|
|
|
58635
58680
|
};
|
|
58636
58681
|
}
|
|
58637
58682
|
);
|
|
58683
|
+
app.post(
|
|
58684
|
+
"/api/contract/:appId/scene",
|
|
58685
|
+
async (req, reply) => {
|
|
58686
|
+
const doc2 = req.body && "contract" in req.body ? req.body.contract : readContract(req.params.appId);
|
|
58687
|
+
if (doc2 == null) return reply.status(404).send({ ok: false, error: "no contract to render" });
|
|
58688
|
+
const v = validateSteelTakeoff(doc2);
|
|
58689
|
+
if (!v.valid) {
|
|
58690
|
+
const first = v.errors[0];
|
|
58691
|
+
return reply.status(400).send({ ok: false, error: `contract failed schema validation \u2014 ${first ? `${first.path}: ${first.message}` : "invalid"}` });
|
|
58692
|
+
}
|
|
58693
|
+
return { ok: true, ...contractToScene(doc2) };
|
|
58694
|
+
}
|
|
58695
|
+
);
|
|
58638
58696
|
function awareStderr(e) {
|
|
58639
58697
|
if (e instanceof AwareError && e.detail && typeof e.detail === "object") {
|
|
58640
58698
|
const s = e.detail.stderr;
|
|
@@ -58730,9 +58788,10 @@ async function startServer() {
|
|
|
58730
58788
|
return reply.status(422).send({ ok: false, error: "no resolvable members to bake \u2014 every member is an RFI (no AISC size yet)", skipped });
|
|
58731
58789
|
}
|
|
58732
58790
|
const companionId = `${req.params.appId}-tekla`;
|
|
58791
|
+
const teklaVersion = await teklaVersionToBake();
|
|
58733
58792
|
let flo;
|
|
58734
58793
|
try {
|
|
58735
|
-
flo = writeTeklaApp(appPath(companionId), companionId, scene);
|
|
58794
|
+
flo = writeTeklaApp(appPath(companionId), companionId, scene, teklaVersion);
|
|
58736
58795
|
} catch (e) {
|
|
58737
58796
|
app.log.error({ companionId, err: e instanceof Error ? e.message : e }, "export-tekla: companion app write failed");
|
|
58738
58797
|
return reply.status(500).send({ ok: false, error: `could not write the Tekla companion app: ${e instanceof Error ? e.message : "write error"}` });
|
|
@@ -59524,10 +59583,10 @@ async function startServer() {
|
|
|
59524
59583
|
}
|
|
59525
59584
|
|
|
59526
59585
|
// protocol.ts
|
|
59527
|
-
var
|
|
59586
|
+
var import_node_child_process8 = require("node:child_process");
|
|
59528
59587
|
var BASE = PROTOCOL_KEY;
|
|
59529
59588
|
function reg(args) {
|
|
59530
|
-
(0,
|
|
59589
|
+
(0, import_node_child_process8.execFileSync)("reg", args, { stdio: "ignore", windowsHide: true });
|
|
59531
59590
|
}
|
|
59532
59591
|
function registerProtocol(exePath) {
|
|
59533
59592
|
if (process.platform !== "win32") return;
|
|
@@ -59567,13 +59626,13 @@ async function removeAwarePerDecision(removeAwareFlag, prompt) {
|
|
|
59567
59626
|
);
|
|
59568
59627
|
const shell = process.platform === "win32" ? process.env.ComSpec || "cmd.exe" : "/bin/sh";
|
|
59569
59628
|
try {
|
|
59570
|
-
(0,
|
|
59629
|
+
(0, import_node_child_process9.execSync)("npm uninstall -g @aware-aeco/cli", { stdio: "inherit", shell });
|
|
59571
59630
|
} catch {
|
|
59572
59631
|
process.stdout.write('floless: warning: "npm uninstall -g @aware-aeco/cli" failed \u2014 see the output above.\n');
|
|
59573
59632
|
}
|
|
59574
59633
|
let lsJson = "";
|
|
59575
59634
|
try {
|
|
59576
|
-
lsJson = (0,
|
|
59635
|
+
lsJson = (0, import_node_child_process9.execSync)("npm ls -g @aware-aeco/cli --depth=0 --json", {
|
|
59577
59636
|
encoding: "utf8",
|
|
59578
59637
|
stdio: ["ignore", "pipe", "ignore"],
|
|
59579
59638
|
shell
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"additionalProperties": true,
|
|
9
9
|
"properties": {
|
|
10
10
|
"type": { "const": "steel.takeoff/v1", "description": "Renderer-registry discriminator." },
|
|
11
|
+
"target_confidence": { "type": "number", "minimum": 0, "maximum": 100, "description": "User's per-read confidence target (%); overrides the app's target_confidence input default. The editor chip and the /score endpoint use it." },
|
|
11
12
|
"active": { "type": "integer", "description": "Index into plans of the active sheet." },
|
|
12
13
|
"palette": { "type": "array", "items": { "type": "string" }, "description": "Default per-index profile colours." },
|
|
13
14
|
"weights": { "type": "object", "additionalProperties": { "type": ["number", "null"] }, "description": "profile -> plf (pounds per linear foot), from the AISC lookup. null for an unresolved profile (e.g. an unsized moment-frame mark) that has no weight yet." },
|
|
@@ -78,7 +78,10 @@ digraph { "GET /api/requests" -> "for each pending" -> "read app .flo + node" ->
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
1. **Pull**: `curl -s http://127.0.0.1:4317/api/requests` → take the `pending` ones (oldest first).
|
|
81
|
-
Summarize them for the user before acting on anything destructive.
|
|
81
|
+
Summarize them for the user before acting on anything destructive. **When several are queued and
|
|
82
|
+
the user wants to choose which to apply (not just the oldest), use the `floless-app-queue` skill** —
|
|
83
|
+
it lists the queue in plain English and asks which to process, then routes each pick to the owning
|
|
84
|
+
authoring skill (`floless-app-workflows` / `-tweak-contract` / `-ui` / `-rebake`).
|
|
82
85
|
2. **Locate the app source.** The editable `.flo` is the repo copy `demos/<appId>/<appId>.flo`
|
|
83
86
|
(preferred — it's version-controlled); the installed copy is `~/.aware/apps/<appId>/<appId>.flo`.
|
|
84
87
|
Edit the **repo** copy. `GET /api/app/<appId>` returns the parsed nodes + each node's
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: floless-app-queue
|
|
3
|
+
description: This skill should be used when the user has MULTIPLE floless.app requests queued from the Dashboard and wants to see them and choose which to process, rather than the AI just picking up the oldest one. Use it when the user says things like "what's in my floless queue", "list my queued floless requests", "show me the pending requests and let me pick", "I queued a few tweaks — which should I apply?", "let me choose what to process", or "pick from the floless queue". It lists the pending requests from GET /api/requests in plain language (type, which app/node, the instruction), asks the user which one(s) to apply, then hands each chosen request to the owning skill (floless-app-workflows / -tweak-contract / -ui / -rebake) which applies it and clears it. It is the interactive picker that sits in front of floless-app-bridge (which is the actual pull-apply-clear engine).
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# floless.app queue picker
|
|
9
|
+
|
|
10
|
+
floless.app is the **thin web UI**; the **terminal AI (you) is the brain**. The UI cannot edit
|
|
11
|
+
`.flo` files — it **queues requests** (Tweak / use-template / customize-UI / rebake / edit-contract)
|
|
12
|
+
to a local API. The **`floless-app-bridge`** skill pulls those and applies them, but it processes
|
|
13
|
+
the queue **oldest-first with no choice step**. This skill adds the missing affordance: **list the
|
|
14
|
+
whole queue in plain English and let the user pick which one(s) to process** — then route each pick
|
|
15
|
+
through the normal bridge flow.
|
|
16
|
+
|
|
17
|
+
Use this when there are **several** pending requests and the user wants to triage. For a single
|
|
18
|
+
"apply the tweak I just queued" or a pasted `[floless-request …]` marker, `floless-app-bridge` is
|
|
19
|
+
enough.
|
|
20
|
+
|
|
21
|
+
## 1. Fetch the queue
|
|
22
|
+
|
|
23
|
+
The server runs locally on **port 4317** (override: `$PORT`). Confirm it's up, then read the queue:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
curl -s http://127.0.0.1:4317/api/health # → {"ok":true,...}; if down, user runs `cd server && npm run dev`
|
|
27
|
+
curl -s http://127.0.0.1:4317/api/requests # → { "ok": true, "requests": [ ... ] }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`requests` is every **pending** request, oldest first. If it's empty, tell the user the queue is
|
|
31
|
+
empty and stop — there is nothing to pick.
|
|
32
|
+
|
|
33
|
+
## 2. Present the queue in plain language
|
|
34
|
+
|
|
35
|
+
Render a **numbered list** the user can choose from. For each request show the index, a short id
|
|
36
|
+
(first 8 chars), and a one-line plain-English summary by `type` — never raw JSON. The fields are
|
|
37
|
+
documented in `floless-app-bridge`; the five types map to:
|
|
38
|
+
|
|
39
|
+
| `type` | Plain-English line | Owning skill |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `tweak` | "Tweak node **`<nodeId>`** in **`<appId>`** — *<instruction>*" | `floless-app-workflows` |
|
|
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` |
|
|
44
|
+
| `ui-customize` | "Customize the Dashboard (optionally `panel <panelId>`) — *<instruction>*" | `floless-app-ui` |
|
|
45
|
+
| `rebake` | "Re-read the **<inputName>** drawing (<sourceName>) in **`<appId>`** — *<instruction>*" | `floless-app-rebake` |
|
|
46
|
+
|
|
47
|
+
Note the count of screenshots for requests that carry `snapshots`. If a request has an unfamiliar
|
|
48
|
+
`type`, show it verbatim and flag it rather than guessing what it does.
|
|
49
|
+
|
|
50
|
+
## 3. Ask which to process
|
|
51
|
+
|
|
52
|
+
Ask the user to choose: **one, several, all, or none** (e.g. "1 and 3", "all", "skip 2"). This skill
|
|
53
|
+
is read-and-choose: **never apply a request the user did not pick.** Wait for the answer before
|
|
54
|
+
touching any `.flo`, contract, or panel.
|
|
55
|
+
|
|
56
|
+
## 4. Process each chosen request — route to the owning skill
|
|
57
|
+
|
|
58
|
+
For each pick, hand it to the skill in the table above (the type→skill routing
|
|
59
|
+
`floless-app-bridge` uses for pasted markers, extended here to also cover `tweak-contract`).
|
|
60
|
+
That skill does the real work — read the app's
|
|
61
|
+
current `.flo`/contract/panel, apply the change, recompile if needed — **and clears the request**:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl -s -X DELETE http://127.0.0.1:4317/api/requests/<id> # the UI's "requests" badge updates live over SSE
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Process the picks in the order the user gave (or oldest-first if they said "all"). Re-run the
|
|
68
|
+
owning skill's normal verification before reporting each one done.
|
|
69
|
+
|
|
70
|
+
## 5. The un-picked rest
|
|
71
|
+
|
|
72
|
+
Leave any request the user did **not** pick in the queue — do nothing to it. If the user wants to
|
|
73
|
+
**discard** one without processing, `DELETE /api/requests/<id>`; to drop the whole queue at once
|
|
74
|
+
the UI's "Clear all" maps to `DELETE /api/requests`. Only delete on an explicit ask.
|
|
75
|
+
|
|
76
|
+
When you're done, tell the user what was processed and **what is still queued**, so the picker can
|
|
77
|
+
be re-run for the remainder.
|
|
78
|
+
|
|
79
|
+
## Guardrails
|
|
80
|
+
|
|
81
|
+
- **You are the brain; the UI only queues intent.** This skill never composes a `.flo` itself — it
|
|
82
|
+
triages the queue and delegates each pick to the owning authoring skill.
|
|
83
|
+
- **Choose, don't bulk-apply.** Listing is non-destructive; only act on the user's explicit picks.
|
|
84
|
+
Never silently process the whole queue without an explicit "all".
|
|
85
|
+
- **Always clear a processed request** (the owning skill does the `DELETE`) so the UI's count
|
|
86
|
+
reflects reality; only `DELETE` an *un*-processed request when the user asks to discard it.
|
|
87
|
+
- **Don't invent request types** — only the five real ones exist; surface anything else rather than
|
|
88
|
+
guessing. This is the picker; **`floless-app-bridge`** is the puller/applier it sits in front of.
|
|
@@ -117,9 +117,12 @@ Follow the methodology condensed here (full detail + every gotcha in
|
|
|
117
117
|
|
|
118
118
|
### 3. Honor `target_confidence` — score, then LOOP to the goal
|
|
119
119
|
|
|
120
|
-
Read the app's `target_confidence` input (default 70). The
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
Read the app's `target_confidence` input (default 70). The user may also set a **per-read override**
|
|
121
|
+
via the editor (stored as a top-level `target_confidence` on the contract); it wins over the input
|
|
122
|
+
default. On a **re-read / rebake**, preserve any `target_confidence` already on the existing contract
|
|
123
|
+
(carry it into the new draft) so the user's chosen target isn't reset. The confidence score is a
|
|
124
|
+
**deterministic function of observable evidence** — not a model self-report — so you measure it with
|
|
125
|
+
the server, never by eyeballing. Per-member bands (full method in
|
|
123
126
|
`docs/superpowers/specs/2026-06-20-steel-takeoff-confidence-report.md`):
|
|
124
127
|
|
|
125
128
|
- **Verified (1.0)** — human-confirmed. **High (0.9)** — resolved from drawing evidence, no contradiction.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* steel-3d-core.js — PURE, framework-free core for the steel 3D editor (no Three.js, no DOM, no
|
|
3
|
+
* window). Unit-tested in node (server/steel-3d-core.test.ts) AND loaded by web/steel-3d-view.js.
|
|
4
|
+
* This is the extraction-ready unit: the generic snap engine + the typed-candidate model are what
|
|
5
|
+
* later lift into AWARE viewer-3d; only `snapCandidates` (beam top-flange centerline, column
|
|
6
|
+
* plan-center) is steel-specific.
|
|
7
|
+
*
|
|
8
|
+
* Coordinate map MUST match server/contract-to-scene.ts (the bake) so editing-space == render-space:
|
|
9
|
+
* scene is mm, Z-up. mmX = dispX·k, mmY = -dispY·k (k = FT_TO_MM / pt_per_ft, Y flipped),
|
|
10
|
+
* mmZ = elevation_inches · IN_TO_MM. planPointToWp is the inverse (editing-only; the bake never
|
|
11
|
+
* needs it).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const FT_TO_MM = 304.8;
|
|
15
|
+
export const IN_TO_MM = 25.4;
|
|
16
|
+
|
|
17
|
+
const kOf = (ptPerFt) => FT_TO_MM / (ptPerFt > 0 ? ptPerFt : 1);
|
|
18
|
+
|
|
19
|
+
const nz = (n) => n + 0; // normalize -0 → 0 (negating 0 yields -0, which surprises equality checks)
|
|
20
|
+
|
|
21
|
+
/** display-space wp point → scene mm [x,y] (Y flipped, matching contractToScene.dispToMm). */
|
|
22
|
+
export function dispToMm(pt, ptPerFt) { const k = kOf(ptPerFt); return [nz(pt[0] * k), nz(-pt[1] * k)]; }
|
|
23
|
+
|
|
24
|
+
/** scene mm point [x,y,…] → display-space wp [px,py] (inverse of dispToMm). */
|
|
25
|
+
export function planPointToWp(p, ptPerFt) { const k = kOf(ptPerFt); return [nz(p[0] / k), nz(-p[1] / k)]; }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* member → its 3D centerline `{ kind, line:[[x,y,z],[x,y,z]] }` in mm, matching contractToScene's
|
|
29
|
+
* from/to. Beam: work-line at T.O.S. Column: vertical at the plan point, bos→tos.
|
|
30
|
+
*/
|
|
31
|
+
export function memberGeometry(m, ptPerFt, defaultTosMm = 0) {
|
|
32
|
+
const a = dispToMm(m.wp[0], ptPerFt), b = dispToMm(m.wp[1], ptPerFt);
|
|
33
|
+
if (m.role === 'column') {
|
|
34
|
+
const bos = m.col && m.col.bos != null ? m.col.bos * IN_TO_MM : 0;
|
|
35
|
+
const tos = m.col && m.col.tos != null ? m.col.tos * IN_TO_MM : defaultTosMm;
|
|
36
|
+
return { kind: 'column', line: [[a[0], a[1], bos], [a[0], a[1], tos]] };
|
|
37
|
+
}
|
|
38
|
+
const e = m.ends || [];
|
|
39
|
+
const z0 = e[0] && e[0].tos != null ? e[0].tos * IN_TO_MM : defaultTosMm;
|
|
40
|
+
const z1 = e[1] && e[1].tos != null ? e[1].tos * IN_TO_MM : defaultTosMm;
|
|
41
|
+
return { kind: 'beam', line: [[a[0], a[1], z0], [b[0], b[1], z1]] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** XY intersection point of segments a-b and c-d (within both), or null (parallel / outside). */
|
|
45
|
+
function segXY(a, b, c, d) {
|
|
46
|
+
const rx = b[0] - a[0], ry = b[1] - a[1], sx = d[0] - c[0], sy = d[1] - c[1];
|
|
47
|
+
const rxs = rx * sy - ry * sx;
|
|
48
|
+
if (Math.abs(rxs) < 1e-9) return null; // parallel/collinear
|
|
49
|
+
const qpx = c[0] - a[0], qpy = c[1] - a[1];
|
|
50
|
+
const t = (qpx * sy - qpy * sx) / rxs, u = (qpx * ry - qpy * rx) / rxs;
|
|
51
|
+
if (t < 0 || t > 1 || u < 0 || u > 1) return null; // crossing lies outside a segment
|
|
52
|
+
return [a[0] + rx * t, a[1] + ry * t];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Snap targets from the other members (the dragged one excluded). Beam → its top-flange centerline
|
|
57
|
+
* (the work-line at T.O.S) + endpoint vertices + a midpoint. Column → a vertical axis at its plan
|
|
58
|
+
* point + endpoint vertices. Plus member-member centerline INTERSECTIONS (Tekla-standard). Each
|
|
59
|
+
* tagged `fromId`. Generic `grid-line` targets (from plan grids) plug in here too.
|
|
60
|
+
*/
|
|
61
|
+
export function snapCandidates(members, ptPerFt, defaultTosMm, excludeId) {
|
|
62
|
+
const out = [];
|
|
63
|
+
const lines = []; // beam centerlines, for pairwise intersections
|
|
64
|
+
for (const m of members || []) {
|
|
65
|
+
if (!m || m.id === excludeId || !Array.isArray(m.wp) || m.wp.length < 2) continue;
|
|
66
|
+
const { kind, line } = memberGeometry(m, ptPerFt, defaultTosMm);
|
|
67
|
+
const [p0, p1] = line;
|
|
68
|
+
if (kind === 'column') { out.push({ type: 'vertical-axis', p: p0, fromId: m.id }); }
|
|
69
|
+
else {
|
|
70
|
+
out.push({ type: 'centerline', a: p0, b: p1, fromId: m.id });
|
|
71
|
+
out.push({ type: 'midpoint', p: [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, (p0[2] + p1[2]) / 2], fromId: m.id });
|
|
72
|
+
lines.push({ id: m.id, p0, p1 });
|
|
73
|
+
}
|
|
74
|
+
out.push({ type: 'vertex', p: p0, fromId: m.id });
|
|
75
|
+
out.push({ type: 'vertex', p: p1, fromId: m.id });
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < lines.length; i++) {
|
|
78
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
79
|
+
const x = segXY(lines[i].p0, lines[i].p1, lines[j].p0, lines[j].p1);
|
|
80
|
+
if (x) out.push({ type: 'intersection', p: [x[0], x[1], (lines[i].p0[2] + lines[j].p0[2]) / 2], fromIds: [lines[i].id, lines[j].id] });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Distinct elevation levels (mm, sorted) among the OTHER members' endpoints — the snap targets for a
|
|
88
|
+
* vertical (Alt) drag, so a member can be raised/lowered onto another's T.O.S level. The caller turns
|
|
89
|
+
* each level into a point candidate at the dragged member's plan X/Y and reuses snapPoint().
|
|
90
|
+
*/
|
|
91
|
+
export function elevationLevels(members, ptPerFt, defaultTosMm, excludeId) {
|
|
92
|
+
const set = new Set();
|
|
93
|
+
for (const m of members || []) {
|
|
94
|
+
if (!m || m.id === excludeId || !Array.isArray(m.wp) || m.wp.length < 2) continue;
|
|
95
|
+
const { line } = memberGeometry(m, ptPerFt, defaultTosMm);
|
|
96
|
+
set.add(Math.round(line[0][2])); set.add(Math.round(line[1][2]));
|
|
97
|
+
}
|
|
98
|
+
return [...set].sort((a, b) => a - b);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Lower wins when two candidates are both within tolerance.
|
|
102
|
+
const PRECEDENCE = { vertex: 0, intersection: 1, midpoint: 2, centerline: 3, 'vertical-axis': 4, 'grid-line': 5, level: 1 };
|
|
103
|
+
|
|
104
|
+
function closestOnSeg(p, a, b) {
|
|
105
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
106
|
+
const len2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2] || 1;
|
|
107
|
+
let t = ((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1] + (p[2] - a[2]) * ab[2]) / len2;
|
|
108
|
+
t = Math.max(0, Math.min(1, t));
|
|
109
|
+
return [a[0] + ab[0] * t, a[1] + ab[1] * t, a[2] + ab[2] * t];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The 3D point a candidate proposes for the dragged point. */
|
|
113
|
+
function candidatePoint(c, dragged) {
|
|
114
|
+
if (c.type === 'vertex' || c.type === 'intersection' || c.type === 'midpoint' || c.type === 'level') return c.p; // fixed points
|
|
115
|
+
if (c.type === 'vertical-axis') return [c.p[0], c.p[1], dragged[2]]; // lock plan X/Y to the axis, keep dragged Z
|
|
116
|
+
return closestOnSeg(dragged, c.a, c.b); // centerline / grid-line
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Snap `dragged` (3D mm) to the nearest candidate within `tolPx` SCREEN pixels. `toScreen` maps a 3D
|
|
121
|
+
* mm point → `{x,y}` screen px (injected — keeps this DOM-free). Ties within tolerance break by
|
|
122
|
+
* PRECEDENCE (endpoint > intersection > centerline > axis > grid). Returns `{ snapped, candidate }`;
|
|
123
|
+
* candidate is null (and snapped === dragged) when nothing is in range.
|
|
124
|
+
*/
|
|
125
|
+
export function snapPoint(dragged, candidates, toScreen, tolPx) {
|
|
126
|
+
const ds = toScreen(dragged);
|
|
127
|
+
let best = null, bestPt = null, bestPrec = Infinity, bestD = Infinity;
|
|
128
|
+
for (const c of candidates || []) {
|
|
129
|
+
const pt = candidatePoint(c, dragged);
|
|
130
|
+
const s = toScreen(pt);
|
|
131
|
+
const d = Math.hypot(s.x - ds.x, s.y - ds.y);
|
|
132
|
+
if (d > tolPx) continue;
|
|
133
|
+
const prec = PRECEDENCE[c.type] ?? 9;
|
|
134
|
+
if (prec < bestPrec || (prec === bestPrec && d < bestD)) { best = c; bestPt = pt; bestPrec = prec; bestD = d; }
|
|
135
|
+
}
|
|
136
|
+
return best ? { snapped: bestPt, candidate: best } : { snapped: dragged, candidate: null };
|
|
137
|
+
}
|