@floless/app 0.7.1 → 0.9.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 +262 -23
- package/dist/skills/floless-app-report-issue/SKILL.md +127 -0
- package/dist/web/app.css +221 -0
- package/dist/web/app.js +1 -0
- package/dist/web/aware.js +542 -62
- package/dist/web/index.html +59 -0
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -72,7 +72,7 @@ var require_queue = __commonJS({
|
|
|
72
72
|
if (!(_concurrency >= 1)) {
|
|
73
73
|
throw new Error("fastqueue concurrency must be equal to or greater than 1");
|
|
74
74
|
}
|
|
75
|
-
var
|
|
75
|
+
var cache2 = reusify(Task);
|
|
76
76
|
var queueHead = null;
|
|
77
77
|
var queueTail = null;
|
|
78
78
|
var _running = 0;
|
|
@@ -151,7 +151,7 @@ var require_queue = __commonJS({
|
|
|
151
151
|
return _running === 0 && self.length() === 0;
|
|
152
152
|
}
|
|
153
153
|
function push(value, done) {
|
|
154
|
-
var current =
|
|
154
|
+
var current = cache2.get();
|
|
155
155
|
current.context = context;
|
|
156
156
|
current.release = release;
|
|
157
157
|
current.value = value;
|
|
@@ -172,7 +172,7 @@ var require_queue = __commonJS({
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
function unshift(value, done) {
|
|
175
|
-
var current =
|
|
175
|
+
var current = cache2.get();
|
|
176
176
|
current.context = context;
|
|
177
177
|
current.release = release;
|
|
178
178
|
current.value = value;
|
|
@@ -194,7 +194,7 @@ var require_queue = __commonJS({
|
|
|
194
194
|
}
|
|
195
195
|
function release(holder) {
|
|
196
196
|
if (holder) {
|
|
197
|
-
|
|
197
|
+
cache2.release(holder);
|
|
198
198
|
}
|
|
199
199
|
var next = queueHead;
|
|
200
200
|
if (next && _running <= _concurrency) {
|
|
@@ -6621,12 +6621,12 @@ var require_levels = __commonJS({
|
|
|
6621
6621
|
function genLsCache(instance) {
|
|
6622
6622
|
const formatter = instance[formattersSym].level;
|
|
6623
6623
|
const { labels } = instance.levels;
|
|
6624
|
-
const
|
|
6624
|
+
const cache2 = {};
|
|
6625
6625
|
for (const label in labels) {
|
|
6626
6626
|
const level = formatter(labels[label], Number(label));
|
|
6627
|
-
|
|
6627
|
+
cache2[label] = JSON.stringify(level).slice(0, -1);
|
|
6628
6628
|
}
|
|
6629
|
-
instance[lsCacheSym] =
|
|
6629
|
+
instance[lsCacheSym] = cache2;
|
|
6630
6630
|
return instance;
|
|
6631
6631
|
}
|
|
6632
6632
|
function isStandardLevel(level, useOnlyCustomLevels) {
|
|
@@ -25430,7 +25430,7 @@ var require_range = __commonJS({
|
|
|
25430
25430
|
parseRange(range) {
|
|
25431
25431
|
const memoOpts = (this.options.includePrerelease && FLAG_INCLUDE_PRERELEASE) | (this.options.loose && FLAG_LOOSE);
|
|
25432
25432
|
const memoKey = memoOpts + ":" + range;
|
|
25433
|
-
const cached =
|
|
25433
|
+
const cached = cache2.get(memoKey);
|
|
25434
25434
|
if (cached) {
|
|
25435
25435
|
return cached;
|
|
25436
25436
|
}
|
|
@@ -25464,7 +25464,7 @@ var require_range = __commonJS({
|
|
|
25464
25464
|
rangeMap.delete("");
|
|
25465
25465
|
}
|
|
25466
25466
|
const result = [...rangeMap.values()];
|
|
25467
|
-
|
|
25467
|
+
cache2.set(memoKey, result);
|
|
25468
25468
|
return result;
|
|
25469
25469
|
}
|
|
25470
25470
|
intersects(range, options) {
|
|
@@ -25503,7 +25503,7 @@ var require_range = __commonJS({
|
|
|
25503
25503
|
};
|
|
25504
25504
|
module2.exports = Range;
|
|
25505
25505
|
var LRU = require_lrucache();
|
|
25506
|
-
var
|
|
25506
|
+
var cache2 = new LRU();
|
|
25507
25507
|
var parseOptions = require_parse_options();
|
|
25508
25508
|
var Comparator = require_comparator();
|
|
25509
25509
|
var debug = require_debug2();
|
|
@@ -26445,12 +26445,12 @@ var require_plugin_utils = __commonJS({
|
|
|
26445
26445
|
if (display) {
|
|
26446
26446
|
return display;
|
|
26447
26447
|
}
|
|
26448
|
-
const
|
|
26449
|
-
if (
|
|
26450
|
-
const keys = Object.keys(
|
|
26448
|
+
const cache2 = require.cache;
|
|
26449
|
+
if (cache2) {
|
|
26450
|
+
const keys = Object.keys(cache2);
|
|
26451
26451
|
for (let i = 0; i < keys.length; i++) {
|
|
26452
26452
|
const key = keys[i];
|
|
26453
|
-
if (
|
|
26453
|
+
if (cache2[key].exports === func) {
|
|
26454
26454
|
return key;
|
|
26455
26455
|
}
|
|
26456
26456
|
}
|
|
@@ -42463,10 +42463,10 @@ var require_accept_negotiator = __commonJS({
|
|
|
42463
42463
|
}
|
|
42464
42464
|
const {
|
|
42465
42465
|
supportedValues = [],
|
|
42466
|
-
cache
|
|
42466
|
+
cache: cache2
|
|
42467
42467
|
} = options && typeof options === "object" && options || {};
|
|
42468
42468
|
this.supportedValues = supportedValues;
|
|
42469
|
-
this.cache =
|
|
42469
|
+
this.cache = cache2;
|
|
42470
42470
|
}
|
|
42471
42471
|
Negotiator.prototype.negotiate = function(header) {
|
|
42472
42472
|
if (typeof header !== "string") {
|
|
@@ -51392,6 +51392,49 @@ function parseRunHandle(stdout) {
|
|
|
51392
51392
|
function traceLogPath(id, instance, runId, logsRoot = LOGS_DIR) {
|
|
51393
51393
|
return (0, import_node_path3.join)(logsRoot, id, instance, `${runId}.jsonl`);
|
|
51394
51394
|
}
|
|
51395
|
+
function latestTracePath(id, logsRoot = LOGS_DIR) {
|
|
51396
|
+
const appDir2 = (0, import_node_path3.join)(logsRoot, id);
|
|
51397
|
+
if (!(0, import_node_fs4.existsSync)(appDir2)) return null;
|
|
51398
|
+
let best = null;
|
|
51399
|
+
let instances;
|
|
51400
|
+
try {
|
|
51401
|
+
instances = (0, import_node_fs4.readdirSync)(appDir2);
|
|
51402
|
+
} catch {
|
|
51403
|
+
return null;
|
|
51404
|
+
}
|
|
51405
|
+
for (const instance of instances) {
|
|
51406
|
+
const instDir = (0, import_node_path3.join)(appDir2, instance);
|
|
51407
|
+
let entries;
|
|
51408
|
+
try {
|
|
51409
|
+
if (!(0, import_node_fs4.statSync)(instDir).isDirectory()) continue;
|
|
51410
|
+
entries = (0, import_node_fs4.readdirSync)(instDir);
|
|
51411
|
+
} catch {
|
|
51412
|
+
continue;
|
|
51413
|
+
}
|
|
51414
|
+
for (const f of entries) {
|
|
51415
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
51416
|
+
const p = (0, import_node_path3.join)(instDir, f);
|
|
51417
|
+
try {
|
|
51418
|
+
const m = (0, import_node_fs4.statSync)(p).mtimeMs;
|
|
51419
|
+
if (!best || m > best.mtimeMs) best = { path: p, mtimeMs: m };
|
|
51420
|
+
} catch {
|
|
51421
|
+
}
|
|
51422
|
+
}
|
|
51423
|
+
}
|
|
51424
|
+
return best ? best.path : null;
|
|
51425
|
+
}
|
|
51426
|
+
function readLatestTrace(id, logsRoot = LOGS_DIR) {
|
|
51427
|
+
const p = latestTracePath(id, logsRoot);
|
|
51428
|
+
if (!p) return null;
|
|
51429
|
+
let text;
|
|
51430
|
+
try {
|
|
51431
|
+
text = (0, import_node_fs4.readFileSync)(p, "utf8");
|
|
51432
|
+
} catch {
|
|
51433
|
+
return null;
|
|
51434
|
+
}
|
|
51435
|
+
const runId = p.split(/[\\/]/).pop()?.replace(/\.jsonl$/, "") ?? "";
|
|
51436
|
+
return { runId, path: p, text };
|
|
51437
|
+
}
|
|
51395
51438
|
function startTrigger(id, opts = {}) {
|
|
51396
51439
|
assertId(id);
|
|
51397
51440
|
const invoker = currentInvoker();
|
|
@@ -52050,9 +52093,15 @@ function mapUserStatus(u) {
|
|
|
52050
52093
|
function signInUrl() {
|
|
52051
52094
|
return `${env().webBase}/login?app=floless`;
|
|
52052
52095
|
}
|
|
52096
|
+
function webBaseUrl() {
|
|
52097
|
+
return env().webBase;
|
|
52098
|
+
}
|
|
52053
52099
|
function updateApiBase() {
|
|
52054
52100
|
return `${env().apiBase}/updates/releases`;
|
|
52055
52101
|
}
|
|
52102
|
+
function apiBaseUrl() {
|
|
52103
|
+
return env().apiBase;
|
|
52104
|
+
}
|
|
52056
52105
|
async function accessToken() {
|
|
52057
52106
|
return ensureFreshToken();
|
|
52058
52107
|
}
|
|
@@ -52308,7 +52357,7 @@ function appVersion() {
|
|
|
52308
52357
|
return resolveVersion({
|
|
52309
52358
|
isSea: isSea2(),
|
|
52310
52359
|
sqVersionXml: readSqVersionXml(),
|
|
52311
|
-
define: true ? "0.
|
|
52360
|
+
define: true ? "0.9.0" : void 0,
|
|
52312
52361
|
pkgVersion: readPkgVersion()
|
|
52313
52362
|
});
|
|
52314
52363
|
}
|
|
@@ -52318,7 +52367,7 @@ function resolveChannel(s) {
|
|
|
52318
52367
|
return "dev";
|
|
52319
52368
|
}
|
|
52320
52369
|
function appChannel() {
|
|
52321
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52370
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.9.0" : void 0 });
|
|
52322
52371
|
}
|
|
52323
52372
|
|
|
52324
52373
|
// oauth-presets.ts
|
|
@@ -53844,6 +53893,150 @@ async function applyUpdate(check, opts) {
|
|
|
53844
53893
|
return { applied: true, message: `updating to v${check.targetVersion}\u2026 the app will relaunch` };
|
|
53845
53894
|
}
|
|
53846
53895
|
|
|
53896
|
+
// report-relay.ts
|
|
53897
|
+
var REPORT_TIMEOUT_MS = 15e3;
|
|
53898
|
+
async function reportIssue(input) {
|
|
53899
|
+
let token;
|
|
53900
|
+
try {
|
|
53901
|
+
token = await accessToken();
|
|
53902
|
+
} catch {
|
|
53903
|
+
return { ok: false, error: "offline" };
|
|
53904
|
+
}
|
|
53905
|
+
if (!token) return { ok: false, error: "signed_out" };
|
|
53906
|
+
const url = `${apiBaseUrl()}/issues`;
|
|
53907
|
+
let res;
|
|
53908
|
+
try {
|
|
53909
|
+
res = await authedFetch(url, {
|
|
53910
|
+
method: "POST",
|
|
53911
|
+
headers: { "Content-Type": "application/json" },
|
|
53912
|
+
body: JSON.stringify(input),
|
|
53913
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
53914
|
+
});
|
|
53915
|
+
} catch {
|
|
53916
|
+
return { ok: false, error: "offline" };
|
|
53917
|
+
}
|
|
53918
|
+
if (res.status === 401) return { ok: false, error: "signed_out" };
|
|
53919
|
+
if (res.status === 429) return { ok: false, error: "rate_limited" };
|
|
53920
|
+
if (!res.ok) return { ok: false, error: "offline" };
|
|
53921
|
+
let ref = "";
|
|
53922
|
+
try {
|
|
53923
|
+
const b = await res.json();
|
|
53924
|
+
if (typeof b.ref === "string") ref = b.ref;
|
|
53925
|
+
} catch {
|
|
53926
|
+
}
|
|
53927
|
+
return { ok: true, ref };
|
|
53928
|
+
}
|
|
53929
|
+
|
|
53930
|
+
// release-notes.ts
|
|
53931
|
+
var FETCH_TIMEOUT_MS = 8e3;
|
|
53932
|
+
var AWARE_REPO = "aware-aeco/aware";
|
|
53933
|
+
var CHANGE_RE = /^[-*]\s+\*\*?(Added|Changed|Fixed|Removed|Security)\*\*?:\s+(.+)$/gim;
|
|
53934
|
+
function parseBulletChanges(body) {
|
|
53935
|
+
const out = [];
|
|
53936
|
+
let m;
|
|
53937
|
+
CHANGE_RE.lastIndex = 0;
|
|
53938
|
+
while ((m = CHANGE_RE.exec(body)) !== null) out.push({ type: m[1].toLowerCase(), description: m[2].trim() });
|
|
53939
|
+
return out;
|
|
53940
|
+
}
|
|
53941
|
+
var cache = /* @__PURE__ */ new Map();
|
|
53942
|
+
var CACHE_TTL_MS = 5 * 6e4;
|
|
53943
|
+
async function getJson(url, headers = {}) {
|
|
53944
|
+
try {
|
|
53945
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
53946
|
+
let json = null;
|
|
53947
|
+
try {
|
|
53948
|
+
json = await res.json();
|
|
53949
|
+
} catch {
|
|
53950
|
+
}
|
|
53951
|
+
return { status: res.status, json };
|
|
53952
|
+
} catch {
|
|
53953
|
+
return null;
|
|
53954
|
+
}
|
|
53955
|
+
}
|
|
53956
|
+
async function getAppReleaseNotes(version, deps = {}) {
|
|
53957
|
+
const useCache = !deps.changelogUrl;
|
|
53958
|
+
const key = `app:${version}`;
|
|
53959
|
+
if (useCache) {
|
|
53960
|
+
const hit = cache.get(key);
|
|
53961
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.notes;
|
|
53962
|
+
}
|
|
53963
|
+
const url = deps.changelogUrl ?? `${webBaseUrl()}/changelog.json`;
|
|
53964
|
+
const r = await getJson(url);
|
|
53965
|
+
let notes;
|
|
53966
|
+
if (!r || r.status >= 400 || !Array.isArray(r.json)) {
|
|
53967
|
+
notes = { ok: false, reason: "unavailable" };
|
|
53968
|
+
} else {
|
|
53969
|
+
const e = r.json.find(
|
|
53970
|
+
(x) => typeof x === "object" && x !== null && x.version === version
|
|
53971
|
+
);
|
|
53972
|
+
notes = e ? {
|
|
53973
|
+
ok: true,
|
|
53974
|
+
component: "app",
|
|
53975
|
+
version,
|
|
53976
|
+
title: String(e.title ?? `v${version}`),
|
|
53977
|
+
summary: String(e.description ?? ""),
|
|
53978
|
+
changes: Array.isArray(e.changes) ? e.changes.filter((c) => typeof c === "object" && c !== null).map((c) => ({ type: String(c.type ?? ""), description: String(c.description ?? "") })) : [],
|
|
53979
|
+
url: typeof e.url === "string" ? e.url : `${webBaseUrl()}/changelog#v${version}`
|
|
53980
|
+
} : { ok: false, reason: "not-found" };
|
|
53981
|
+
}
|
|
53982
|
+
if (useCache && (notes.ok || notes.reason === "not-found")) cache.set(key, { at: Date.now(), notes });
|
|
53983
|
+
return notes;
|
|
53984
|
+
}
|
|
53985
|
+
async function getAwareReleaseNotes(version, deps = {}) {
|
|
53986
|
+
const useCache = !deps.apiBase;
|
|
53987
|
+
const key = `aware:${version}`;
|
|
53988
|
+
if (useCache) {
|
|
53989
|
+
const hit = cache.get(key);
|
|
53990
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.notes;
|
|
53991
|
+
}
|
|
53992
|
+
const base = deps.apiBase ?? "https://api.github.com";
|
|
53993
|
+
const r = await getJson(`${base}/repos/${AWARE_REPO}/releases/tags/v${version}`, {
|
|
53994
|
+
Accept: "application/vnd.github+json",
|
|
53995
|
+
"User-Agent": "floless.app"
|
|
53996
|
+
});
|
|
53997
|
+
let notes;
|
|
53998
|
+
if (!r || r.status >= 400 || typeof r.json !== "object" || r.json === null) {
|
|
53999
|
+
notes = r && r.status === 404 ? { ok: false, reason: "not-found" } : { ok: false, reason: "unavailable" };
|
|
54000
|
+
} else {
|
|
54001
|
+
const rel = r.json;
|
|
54002
|
+
const body = typeof rel.body === "string" ? rel.body : "";
|
|
54003
|
+
const changes = parseBulletChanges(body);
|
|
54004
|
+
const summary = body.split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("#") && !l.startsWith("-") && !l.startsWith("*")) ?? "";
|
|
54005
|
+
notes = { ok: true, component: "aware", version, title: rel.name ?? `AWARE ${version}`, summary, changes, url: rel.html_url ?? `https://github.com/${AWARE_REPO}/releases/tag/v${version}` };
|
|
54006
|
+
}
|
|
54007
|
+
if (useCache && (notes.ok || notes.reason === "not-found")) cache.set(key, { at: Date.now(), notes });
|
|
54008
|
+
return notes;
|
|
54009
|
+
}
|
|
54010
|
+
|
|
54011
|
+
// run-summary.ts
|
|
54012
|
+
var clampMsg = (s) => {
|
|
54013
|
+
const t = s.trim();
|
|
54014
|
+
return t.length > 1e3 ? t.slice(0, 1e3) : t;
|
|
54015
|
+
};
|
|
54016
|
+
function summarizeRun(events) {
|
|
54017
|
+
const runEnd = [...events].reverse().find((e) => e.kind === "run-end");
|
|
54018
|
+
const status = runEnd && typeof runEnd.status === "string" ? runEnd.status : events.length ? "unknown" : "none";
|
|
54019
|
+
let errorNodeId = null;
|
|
54020
|
+
let errorMessage = null;
|
|
54021
|
+
for (const e of events) {
|
|
54022
|
+
if (e.kind === "node-error" && typeof e.node === "string") {
|
|
54023
|
+
errorNodeId = e.node;
|
|
54024
|
+
const err = e.error;
|
|
54025
|
+
errorMessage = typeof err === "string" && err.trim() ? clampMsg(err) : null;
|
|
54026
|
+
break;
|
|
54027
|
+
}
|
|
54028
|
+
const data = e.data;
|
|
54029
|
+
const result = data?.result ?? data;
|
|
54030
|
+
if (result && result.ok === false && typeof e.node === "string") {
|
|
54031
|
+
errorNodeId = e.node;
|
|
54032
|
+
const err = result.error;
|
|
54033
|
+
errorMessage = typeof err === "string" && err.trim() ? clampMsg(err) : null;
|
|
54034
|
+
break;
|
|
54035
|
+
}
|
|
54036
|
+
}
|
|
54037
|
+
return { status, errorNodeId, errorMessage };
|
|
54038
|
+
}
|
|
54039
|
+
|
|
53847
54040
|
// launch.mjs
|
|
53848
54041
|
var import_node_child_process5 = require("node:child_process");
|
|
53849
54042
|
var import_node_path14 = require("node:path");
|
|
@@ -54321,13 +54514,15 @@ var import_yaml5 = __toESM(require_dist6(), 1);
|
|
|
54321
54514
|
// build/ship-skills.mjs
|
|
54322
54515
|
var PRODUCT_SKILLS = [
|
|
54323
54516
|
"floless-app-bridge",
|
|
54324
|
-
//
|
|
54517
|
+
// drive the floless.app CLI / desktop bridge from the user's AI
|
|
54325
54518
|
"floless-app-onboarding",
|
|
54326
|
-
//
|
|
54519
|
+
// guided, re-runnable tour of AWARE + floless.app for new users
|
|
54520
|
+
"floless-app-report-issue",
|
|
54521
|
+
// file a diagnosed bug/idea/question to the private log via the floless.io relay
|
|
54327
54522
|
"floless-app-routines",
|
|
54328
|
-
//
|
|
54523
|
+
// author event-driven ("on trigger") routines
|
|
54329
54524
|
"floless-app-workflows"
|
|
54330
|
-
//
|
|
54525
|
+
// author/run .flo workflows
|
|
54331
54526
|
];
|
|
54332
54527
|
function selectShippedSkillNames(names) {
|
|
54333
54528
|
const present = new Set(names);
|
|
@@ -56318,7 +56513,12 @@ async function startServer() {
|
|
|
56318
56513
|
});
|
|
56319
56514
|
app.addHook("onRequest", async (req, reply) => {
|
|
56320
56515
|
if (!req.url.startsWith("/api/")) return;
|
|
56321
|
-
if (req.url.startsWith("/api/health") || req.url.startsWith("/api/license/") || req.url.startsWith("/api/bootstrap/") || req.url.startsWith("/api/autostart") ||
|
|
56516
|
+
if (req.url.startsWith("/api/health") || req.url.startsWith("/api/license/") || req.url.startsWith("/api/bootstrap/") || req.url.startsWith("/api/autostart") || // Report-an-issue is a support lifeline: a user whose subscription lapsed must still be
|
|
56517
|
+
// able to report "I got locked out". It touches no workspace data and the relay still
|
|
56518
|
+
// requires a valid SESSION token (so it's not open abuse — see report-relay.ts).
|
|
56519
|
+
req.url.startsWith("/api/report-issue") || req.url.startsWith("/api/update") || req.url.startsWith("/api/aware/update") || // Release notes are a read-only display surface for the update pills — a local control,
|
|
56520
|
+
// no workspace data, never spawns `aware`. Exempt like the other update routes.
|
|
56521
|
+
req.url.startsWith("/api/release-notes")) return;
|
|
56322
56522
|
const { state: state2, signInUrl: signInUrl2 } = await getLicenseStatus();
|
|
56323
56523
|
if (state2 !== "valid" && state2 !== "offline-grace") {
|
|
56324
56524
|
return reply.status(402).send({ ok: false, error: "subscription required", state: state2, signInUrl: signInUrl2 });
|
|
@@ -56337,6 +56537,8 @@ async function startServer() {
|
|
|
56337
56537
|
ok: true,
|
|
56338
56538
|
appVersion: appVersion(),
|
|
56339
56539
|
// the installed build (sq.version), so it's scriptable
|
|
56540
|
+
webBase: webBaseUrl(),
|
|
56541
|
+
// channel-correct public site base, for the changelog deep-link
|
|
56340
56542
|
awareVersion: awareInstalledVersion() ?? bs.awareVersion,
|
|
56341
56543
|
// fresh (TTL-cached) install version; the boot snapshot is only a fallback
|
|
56342
56544
|
awareReady: bs.awareReady,
|
|
@@ -56355,6 +56557,24 @@ async function startServer() {
|
|
|
56355
56557
|
logout();
|
|
56356
56558
|
return { ok: true };
|
|
56357
56559
|
});
|
|
56560
|
+
app.post(
|
|
56561
|
+
"/api/report-issue",
|
|
56562
|
+
async (req, reply) => {
|
|
56563
|
+
const b = req.body ?? {};
|
|
56564
|
+
const category = b.category === "idea" || b.category === "question" ? b.category : "bug";
|
|
56565
|
+
const title = typeof b.title === "string" ? b.title.trim() : "";
|
|
56566
|
+
const body = typeof b.body === "string" ? b.body.trim() : "";
|
|
56567
|
+
if (!title || !body) {
|
|
56568
|
+
return reply.status(400).send({ ok: false, error: "title and body are required" });
|
|
56569
|
+
}
|
|
56570
|
+
const clamp = (s, n) => s.length > n ? s.slice(0, n) : s;
|
|
56571
|
+
const context = b.context && typeof b.context === "object" && !Array.isArray(b.context) && JSON.stringify(b.context).length <= 16e3 ? b.context : void 0;
|
|
56572
|
+
const result = await reportIssue({ category, title: clamp(title, 200), body: clamp(body, 12e3), context });
|
|
56573
|
+
if (result.ok) return { ok: true, ref: result.ref };
|
|
56574
|
+
const status = result.error === "signed_out" ? 401 : result.error === "rate_limited" ? 429 : 503;
|
|
56575
|
+
return reply.status(status).send({ ok: false, error: result.error });
|
|
56576
|
+
}
|
|
56577
|
+
);
|
|
56358
56578
|
app.get("/api/autostart", async () => {
|
|
56359
56579
|
const supported = autostartSupported();
|
|
56360
56580
|
return { ok: true, supported, enabled: supported ? autostartPresent() : false };
|
|
@@ -56408,6 +56628,13 @@ async function startServer() {
|
|
|
56408
56628
|
awareInstallInFlight = false;
|
|
56409
56629
|
}
|
|
56410
56630
|
});
|
|
56631
|
+
app.get("/api/release-notes", async (req, reply) => {
|
|
56632
|
+
const component = req.query.component === "aware" ? "aware" : "app";
|
|
56633
|
+
const version = typeof req.query.version === "string" ? req.query.version.trim().replace(/^v/, "") : "";
|
|
56634
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][\w.]+)?$/.test(version)) return reply.status(400).send({ ok: false, reason: "unavailable" });
|
|
56635
|
+
const notes = component === "aware" ? await getAwareReleaseNotes(version) : await getAppReleaseNotes(version);
|
|
56636
|
+
return notes;
|
|
56637
|
+
});
|
|
56411
56638
|
app.post("/api/bootstrap/retry", async () => {
|
|
56412
56639
|
const st = getBootstrapState().status;
|
|
56413
56640
|
if (st === "failed" || st === "idle") {
|
|
@@ -56675,6 +56902,18 @@ async function startServer() {
|
|
|
56675
56902
|
const running = cancelActiveRun();
|
|
56676
56903
|
return { ok: true, running };
|
|
56677
56904
|
});
|
|
56905
|
+
app.get("/api/latest-run/:id", async (req, reply) => {
|
|
56906
|
+
const id = req.params.id;
|
|
56907
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
56908
|
+
return reply.status(400).send({ ok: false, error: "invalid app id" });
|
|
56909
|
+
}
|
|
56910
|
+
const latest = readLatestTrace(id);
|
|
56911
|
+
if (!latest) {
|
|
56912
|
+
return { ok: true, id, runId: null, status: "none", errorNodeId: null, errorMessage: null, events: [] };
|
|
56913
|
+
}
|
|
56914
|
+
const events = parseTrace(latest.text);
|
|
56915
|
+
return { ok: true, id, runId: latest.runId, ...summarizeRun(events), events };
|
|
56916
|
+
});
|
|
56678
56917
|
function resolveArgs(configArgs, inputs) {
|
|
56679
56918
|
const out = {};
|
|
56680
56919
|
for (const [k, v] of Object.entries(configArgs)) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: floless-app-report-issue
|
|
3
|
+
description: File a diagnosed bug / idea / question from floless.app into the PRIVATE product log via the floless.io relay. Use when the user invokes /floless-app-report-issue, clicks "Report an issue" in the floless.app UI (a queued report or a copied prompt), or says "report this", "file a bug", "something broke in floless", "send feedback / a feature idea". It READS the live run evidence first (latest run trace, failing node + error, the .flo, exec code), offers a workaround when one exists, then composes a maintainer-ready report and sends it — always showing the exact text and confirming first. Read-only: it never re-runs a workflow node.
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# floless.app — report an issue
|
|
9
|
+
|
|
10
|
+
You are not a feedback form. You are the **on-device incident responder**: a capable AI already
|
|
11
|
+
running on the user's machine, with its hands on the exact thing that broke — the workflow, the
|
|
12
|
+
run trace, the `.flo`, the exec code. A report you file should arrive **diagnosed, not described**.
|
|
13
|
+
|
|
14
|
+
The destination is floless.app's **private** product log. End users can't file there directly
|
|
15
|
+
(it's a private repo), so the local server **relays** the report to floless.io, which files it
|
|
16
|
+
on their behalf under the signed-in session. You compose + send; you never touch GitHub directly.
|
|
17
|
+
|
|
18
|
+
## Preconditions
|
|
19
|
+
|
|
20
|
+
- The local server is up: `curl -s http://127.0.0.1:4317/api/health` → `{"ok":true,…}` (fixed port
|
|
21
|
+
4317; override `$PORT`). It returns `appVersion`, `awareVersion`, and `license`. If it's down,
|
|
22
|
+
the user starts it from the repo (`cd server && npm run dev`).
|
|
23
|
+
- A signed-in session is required to file (the relay authenticates with it). If a send returns
|
|
24
|
+
`signed_out`, tell the user to sign in (menu → the license state in the footer) and retry — do
|
|
25
|
+
not try to work around it.
|
|
26
|
+
|
|
27
|
+
## Safety contract — READ-ONLY (non-negotiable)
|
|
28
|
+
|
|
29
|
+
Diagnosing must never change the user's model or data.
|
|
30
|
+
|
|
31
|
+
- **Never re-run an individual exec node.** Exec nodes run Roslyn C# against a **live Tekla/Trimble
|
|
32
|
+
host** and can mutate the model. There is no "re-run just this node" — don't invent one.
|
|
33
|
+
- **Only** offer a *full-workflow* re-run when **all** hold: the user explicitly asks, the app is
|
|
34
|
+
`runnable`, and the latest trace shows the previous run was **non-mutating** (read-only nodes).
|
|
35
|
+
Otherwise diagnose from the evidence already on disk.
|
|
36
|
+
- Everything below (`/api/health`, `/api/latest-run/:id`, `/api/app/:id`, reading the `.flo`/exec
|
|
37
|
+
source) is read-only. Stay there.
|
|
38
|
+
|
|
39
|
+
## Discriminator + routing — keep the trackers clean
|
|
40
|
+
|
|
41
|
+
Everything from a user funnels into the **one private floless log**; the maintainer triages and
|
|
42
|
+
escalates from there. The user never has to decide "is this AWARE or floless?".
|
|
43
|
+
|
|
44
|
+
- File here for: anything in the floless.app experience — the UI, a workflow/app behaving wrong, a
|
|
45
|
+
confusing result, a feature idea, a question.
|
|
46
|
+
- The one exception (maintainers only): if it is *plainly* an AWARE **runtime** defect (a compiler
|
|
47
|
+
panic, a CLI bug) **and** you're operating with repo access, prefer `/aware-log-issue`
|
|
48
|
+
(→ `aware-aeco`). For an ordinary user, still file here — the maintainer escalates.
|
|
49
|
+
|
|
50
|
+
## Workflow — read, diagnose, confirm, send
|
|
51
|
+
|
|
52
|
+
```dot
|
|
53
|
+
digraph { "health + identify app" -> "read evidence (READ-ONLY)" -> "root-cause hypothesis" ->
|
|
54
|
+
"offer workaround" -> "compose report" -> "privacy scrub + SHOW + confirm" ->
|
|
55
|
+
"POST /api/report-issue" -> "report the ref" }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
1. **Health + identify the app.** `GET /api/health` for versions + license. Establish which app/
|
|
59
|
+
workflow the report is about (ask if unclear, or take it from a queued UI request).
|
|
60
|
+
2. **Read the evidence (the killer step — READ-ONLY):**
|
|
61
|
+
- `GET /api/latest-run/<appId>` → structured trace: `runId`, `status`, `errorNodeId`,
|
|
62
|
+
`errorMessage`, `events`. This is the spine of the diagnosis.
|
|
63
|
+
- `GET /api/app/<appId>` → the normalized topology + `.flo` source + each node's `config`
|
|
64
|
+
(incl. exec `code`). Read the failing node's code; pin `file:line`.
|
|
65
|
+
- Form a concrete **root-cause hypothesis** — not "it failed", but *why*.
|
|
66
|
+
3. **Offer a fix/workaround in the moment** when the evidence supports one. The user often leaves
|
|
67
|
+
already unstuck — that's the point.
|
|
68
|
+
4. **Compose** the report from the template below: a diagnosed body, problem-first title.
|
|
69
|
+
5. **Privacy scrub + confirm (ALWAYS).** The private log is still real: include versions + a
|
|
70
|
+
sanitized summary by default. **Ask before** attaching the `.flo`, raw logs, a screenshot, or
|
|
71
|
+
any model/company names. **Show the user the exact title + body**, then wait for an explicit
|
|
72
|
+
yes. Nothing sends without it.
|
|
73
|
+
6. **Send** via the local relay:
|
|
74
|
+
```bash
|
|
75
|
+
curl -s -X POST http://127.0.0.1:4317/api/report-issue \
|
|
76
|
+
-H 'Content-Type: application/json' \
|
|
77
|
+
-d '{"category":"bug","title":"…","body":"…","context":{ "appId":"…","appVersion":"…","awareVersion":"…","runId":"…","errorNodeId":"…" }}'
|
|
78
|
+
```
|
|
79
|
+
→ `{ "ok": true, "ref": "#123" }`. Errors: `signed_out` (sign in), `rate_limited` (wait), or
|
|
80
|
+
`offline` (check connection) — relay them plainly; the user's text is not lost.
|
|
81
|
+
7. **Report** the `ref` to the user, plus any workaround you already gave. Because the repo is
|
|
82
|
+
private, there is no link to open — say so; the maintainer follows up.
|
|
83
|
+
|
|
84
|
+
### Local API
|
|
85
|
+
|
|
86
|
+
| Call | Purpose |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `GET /api/health` | versions + license state |
|
|
89
|
+
| `GET /api/latest-run/:id` | **read-only** structured trace of the app's most recent run |
|
|
90
|
+
| `GET /api/app/:id` | normalized topology + `.flo` source + node config/exec code |
|
|
91
|
+
| `POST /api/report-issue` | send the composed report through the relay → `{ ok, ref }` |
|
|
92
|
+
|
|
93
|
+
## Report body template
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
**Summary** — <one line, problem-first>
|
|
97
|
+
|
|
98
|
+
**Observed** (with evidence)
|
|
99
|
+
- <what happened> — node `<errorNodeId>` · `<errorMessage>`
|
|
100
|
+
- <file:line in the exec code, quoted, when relevant>
|
|
101
|
+
|
|
102
|
+
**Root-cause hypothesis**
|
|
103
|
+
- <why it happened — your diagnosis, marked as hypothesis if unconfirmed>
|
|
104
|
+
|
|
105
|
+
**Repro**
|
|
106
|
+
- app `<appId>` (FloLess v<appVersion>, AWARE v<awareVersion>), run `<runId>`
|
|
107
|
+
- <steps / inputs> ← attach the .flo only with consent
|
|
108
|
+
|
|
109
|
+
**Expected**
|
|
110
|
+
- <what AWARE/floless should have done>
|
|
111
|
+
|
|
112
|
+
**Workaround given** (if any)
|
|
113
|
+
- <what you told the user to do right now>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The local route stamps category → label and the relay adds `user-report` + the reporter id.
|
|
117
|
+
|
|
118
|
+
## Guardrails
|
|
119
|
+
|
|
120
|
+
- **Read-only.** Diagnose from evidence on disk; never re-run a node (live-host mutation risk).
|
|
121
|
+
- **Relay only.** This skill never calls `gh` — the user has no access to the private repo; the
|
|
122
|
+
relay files on their behalf. (The maintainer `gh` runbook is the separate `floless-app-log-issue`.)
|
|
123
|
+
- **Always confirm** the exact text before sending; default to versions + sanitized summary, and
|
|
124
|
+
ask before attaching anything richer.
|
|
125
|
+
- **Don't invent** node ids, errors, or a fix you can't support from the evidence. A wrong-but-
|
|
126
|
+
confident diagnosis is worse than an honest "couldn't reproduce — here's what I see."
|
|
127
|
+
- **One report per concern.** Don't bundle a bug and a feature idea into one.
|