@floless/app 0.24.1 → 0.25.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.
@@ -51852,6 +51852,28 @@ var aware = {
51852
51852
  const { data } = await runJson(["agent", "describe", assertId(id)]);
51853
51853
  return data;
51854
51854
  },
51855
+ /**
51856
+ * GET /api/agents/available — every agent in the registry catalog (installed or
51857
+ * not), for the Agent Library's "Available" tab. `vendor`/`keywords` (CLI ≥ 0.72,
51858
+ * the catalog-enrichment release) let the UI group by category; older CLIs that
51859
+ * support `catalog` but predate the enrichment simply omit them → the UI falls
51860
+ * back to a flat list. A CLI too old to know `catalog` at all (pre-0.54) throws
51861
+ * AwareUnsupportedError so the caller can say "update AWARE".
51862
+ */
51863
+ async agentCatalog() {
51864
+ try {
51865
+ const { data } = await runJson(["agent", "catalog"]);
51866
+ return data.agents ?? [];
51867
+ } catch (err) {
51868
+ if (err instanceof AwareError && isInvokeUnsupported(err.detail)) {
51869
+ throw new AwareUnsupportedError(
51870
+ `aware CLI does not support \`agent catalog\` (needs \u2265 0.54) \u2014 update AWARE`,
51871
+ err.detail
51872
+ );
51873
+ }
51874
+ throw err;
51875
+ }
51876
+ },
51855
51877
  /**
51856
51878
  * Invoke a BUILTIN agent command directly (`aware agent invoke <agent> <command>
51857
51879
  * --inputs @file --json`, CLI ≥ 0.63) and return the envelope's `data` — e.g.
@@ -52755,7 +52777,7 @@ function appVersion() {
52755
52777
  return resolveVersion({
52756
52778
  isSea: isSea2(),
52757
52779
  sqVersionXml: readSqVersionXml(),
52758
- define: true ? "0.24.1" : void 0,
52780
+ define: true ? "0.25.0" : void 0,
52759
52781
  pkgVersion: readPkgVersion()
52760
52782
  });
52761
52783
  }
@@ -52765,7 +52787,7 @@ function resolveChannel(s) {
52765
52787
  return "dev";
52766
52788
  }
52767
52789
  function appChannel() {
52768
- return resolveChannel({ isSea: isSea2(), define: true ? "0.24.1" : void 0 });
52790
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.25.0" : void 0 });
52769
52791
  }
52770
52792
 
52771
52793
  // oauth-presets.ts
@@ -57603,6 +57625,21 @@ async function startServer() {
57603
57625
  const agent = await aware.agentDescribe(req.params.id);
57604
57626
  return { ok: true, agent };
57605
57627
  });
57628
+ app.get("/api/agents/available", async () => {
57629
+ try {
57630
+ const agents = await aware.agentCatalog();
57631
+ return { ok: true, agents };
57632
+ } catch (err) {
57633
+ if (err instanceof AwareUnsupportedError) return { ok: true, agents: [], unsupported: true };
57634
+ throw err;
57635
+ }
57636
+ });
57637
+ app.post("/api/agents/install", async (req, reply) => {
57638
+ const id = req.body?.id;
57639
+ if (!id || typeof id !== "string") return reply.status(400).send({ ok: false, error: "id is required" });
57640
+ const result = await aware.ensureAgentInstalled(id);
57641
+ return { ok: true, id: result.installed ?? id };
57642
+ });
57606
57643
  app.get("/api/integrations", async () => {
57607
57644
  let integrations = listIntegrations();
57608
57645
  try {
package/dist/web/app.css CHANGED
@@ -1174,6 +1174,38 @@
1174
1174
  }
1175
1175
  .lib-item .lib-fav.faved { color: var(--star); border-color: var(--star); }
1176
1176
 
1177
+ /* Two-tab Agent Library: the tab bar sits between title and panes; each pane is a
1178
+ flex column so its inner .lib-list keeps scrolling within the modal's max-height.
1179
+ The hidden pane uses [hidden] (display:none) so it's skipped by assistive tech. */
1180
+ .lib-tabs { margin: 4px 0 14px; }
1181
+ /* flex-basis:auto (not 0) so the pane's content height still drives the modal up to
1182
+ its max-height; min-height:0 lets the inner .lib-list (overflow:auto) scroll. With
1183
+ basis:0 the pane contributes no intrinsic height and the modal collapses. */
1184
+ .lib-pane { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
1185
+ .lib-pane[hidden] { display: none; }
1186
+
1187
+ /* Available tab: a category band spans both grid columns; the same .lib-item shell
1188
+ as Installed carries the vendor tag, the description blurb, and the Install action. */
1189
+ .lib-section { grid-column: 1 / -1; font-size: 10px; text-transform: uppercase;
1190
+ letter-spacing: 0.12em; color: var(--text-dim); padding: 10px 2px 2px; }
1191
+ .lib-section:first-child { padding-top: 2px; }
1192
+ .lib-vendor { color: var(--text-dim); font-weight: 400; font-size: 11px; }
1193
+ .lib-item .info .blurb {
1194
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
1195
+ }
1196
+ .lib-install {
1197
+ background: transparent; border: 1px solid var(--border-strong); color: var(--text-dim);
1198
+ padding: 4px 11px; font-size: 11px; cursor: pointer; border-radius: 3px; flex-shrink: 0;
1199
+ line-height: 1; transition: color 0.15s, border-color 0.15s;
1200
+ display: inline-flex; align-items: center; gap: 6px;
1201
+ }
1202
+ .lib-install:hover { color: var(--text); border-color: var(--accent-dim); }
1203
+ .lib-install[aria-busy="true"] { color: var(--text-dim); border-color: var(--border-strong); cursor: default; pointer-events: none; }
1204
+ .lib-install .rtn-spinner { width: 11px; height: 11px; border-width: 2px; }
1205
+ .lib-update-link { background: none; border: none; color: var(--accent); cursor: pointer;
1206
+ font-size: 13px; padding: 6px 0 0; font-family: var(--ui); }
1207
+ .lib-update-link:hover { text-decoration: underline; }
1208
+
1177
1209
  /* ========== INTEGRATIONS MODAL ========== */
1178
1210
  .modal.integrations { width: 760px; max-height: 86vh; display: flex; flex-direction: column; }
1179
1211
  .integrations-hint {
package/dist/web/aware.js CHANGED
@@ -3699,6 +3699,8 @@
3699
3699
 
3700
3700
  openLibrary = function openLibraryReal() {
3701
3701
  $libSearch.value = '';
3702
+ if ($libAvailSearch) $libAvailSearch.value = '';
3703
+ switchLibTab('installed'); // always open on Installed (the primary job)
3702
3704
  renderLibrary();
3703
3705
  $libModal.classList.add('show');
3704
3706
  if (!agentCatalog.length) loadAgentCatalog().then(renderLibrary).catch(reportErr);
@@ -3762,6 +3764,192 @@
3762
3764
  } catch (e) { reportErr(e); }
3763
3765
  }
3764
3766
 
3767
+ // ── Agents library: "Available" tab (browse the registry, install on click) ──
3768
+ // Functional categories derived from each agent's registry `keywords` (the CLI
3769
+ // surfaces them in `agent catalog --json` from ≥0.72). First matching category
3770
+ // wins; an agent matching none falls to "Other"; a catalog with NO keywords at
3771
+ // all (older CLI) renders as a flat list (no headers). Categories are a browse
3772
+ // aid only — search always cuts across every agent regardless of category.
3773
+ const AGENT_CATEGORIES = [
3774
+ { label: 'Structural & Analysis', kw: ['structural', 'analysis', 'engineering', 'fabrication', 'cnc'] },
3775
+ { label: 'Productivity & Communication', kw: ['messaging', 'chat', 'email', 'calendar', 'teams', 'outlook', 'gmail', 'm365', 'microsoft-365', 'sharepoint', 'onedrive', 'gworkspace'] },
3776
+ { label: 'Visualization & Viewers', kw: ['visualization', 'viewer', 'rendering', 'webgl', 'digital-twin', '3d'] },
3777
+ { label: 'Model Checking & Coordination', kw: ['model-checking', 'clash-detection', 'bim-coordination', 'federation', 'inspection', 'validation'] },
3778
+ { label: 'Construction & Document Control', kw: ['cde', 'document-control', 'construction', 'project-management', 'rfi', 'submittals', 'issues', 'correspondence', 'file-management', 'file-storage', 'drawings', 'specifications', 'spec', 'markup', 'masterformat', 'classification', 'ribadrm', 'sharing', 'redline'] },
3779
+ { label: 'Interoperability & Formats', kw: ['ifc', 'bcf', 'dstv', 'nc1', 'file-format', 'cobie', 'openbim', 'bcf-xml'] },
3780
+ { label: 'BIM Authoring & CAD', kw: ['architecture', 'bim', 'cad', 'drafting', 'parametric'] },
3781
+ { label: 'Web, Data & Automation', kw: ['rest', 'http', 'api', 'webhook', 'openapi', 'json', 'vision', 'extract', 'escape-hatch'] },
3782
+ { label: 'AWARE Tooling', kw: ['meta', 'builder', 'skill', 'descriptor', 'panels', 'blocks', 'introspection', 'uiautomation', 'codegen', 'agent'] },
3783
+ ];
3784
+ const OTHER_CATEGORY = 'Other';
3785
+
3786
+ function categorizeAgent(a) {
3787
+ const kws = a.keywords || [];
3788
+ for (const cat of AGENT_CATEGORIES) {
3789
+ if (kws.some((k) => cat.kw.includes(k))) return cat.label;
3790
+ }
3791
+ return OTHER_CATEGORY;
3792
+ }
3793
+
3794
+ let availableCatalog = []; // every registry agent (installed or not)
3795
+ let availableLoaded = false; // first-switch lazy-load guard (set once loaded)
3796
+ let availableLoading = false; // a fetch is in flight (prevents a double-fetch race)
3797
+ let availableUnsupported = false; // CLI too old to know `agent catalog`
3798
+ let libTab = 'installed';
3799
+
3800
+ async function loadAvailableAgents() {
3801
+ const { agents, unsupported } = await api('/api/agents/available');
3802
+ availableCatalog = agents || [];
3803
+ availableUnsupported = !!unsupported;
3804
+ availableLoaded = true;
3805
+ }
3806
+
3807
+ // "Available" means available-to-install, so already-installed agents live only
3808
+ // in the Installed tab. agentCatalog holds the installed set (id per item).
3809
+ function installedIdSet() { return new Set(agentCatalog.map((a) => a.id)); }
3810
+
3811
+ function availCardHtml(a) {
3812
+ const name = escapeHtml(a.display_name || a.id);
3813
+ const vendor = a.vendor ? ` <span class="lib-vendor">· ${escapeHtml(a.vendor)}</span>` : '';
3814
+ const desc = (a.description || '').trim();
3815
+ const blurb = desc ? `<div class="blurb">${escapeHtml(desc)}</div>` : '';
3816
+ // Counts come from the CLI; default to 0 so an older CLI omitting them never
3817
+ // renders "undefined command". (Numbers, so no escaping needed.)
3818
+ const cmds = Number(a.commands) || 0;
3819
+ const skills = Number(a.skills) || 0;
3820
+ return `
3821
+ <div class="lib-card" data-agent="${escapeAttr(a.id)}">
3822
+ <div class="lib-item">
3823
+ <div class="info">
3824
+ <div class="name">${name}${vendor}</div>
3825
+ <div class="meta">${cmds} command${cmds === 1 ? '' : 's'} · ${skills} skill${skills === 1 ? '' : 's'}</div>
3826
+ ${blurb}
3827
+ </div>
3828
+ <button class="lib-install" data-install="${escapeAttr(a.id)}" aria-label="Install ${escapeAttr(a.display_name || a.id)}">Install</button>
3829
+ </div>
3830
+ </div>`;
3831
+ }
3832
+
3833
+ function renderAvailable() {
3834
+ // Degradation (i): CLI predates `agent catalog` entirely — hide search, show notice.
3835
+ if (availableUnsupported) {
3836
+ $libAvailSearch.hidden = true;
3837
+ $libAvailList.innerHTML =
3838
+ '<div class="empty-state">Browsing available agents needs AWARE 0.54 or later.' +
3839
+ '<br><button class="lib-update-link" id="lib-avail-update">Update AWARE →</button></div>';
3840
+ const up = document.getElementById('lib-avail-update');
3841
+ if (up) up.onclick = () => {
3842
+ hideModal($libModal);
3843
+ const pill = document.getElementById('aware-update');
3844
+ if (pill && !pill.hidden) pill.click();
3845
+ else showToast('Update AWARE (@aware-aeco/cli) to 0.54+ to browse available agents', 'info');
3846
+ };
3847
+ $libAvailCount.textContent = '';
3848
+ return;
3849
+ }
3850
+ $libAvailSearch.hidden = false;
3851
+
3852
+ if (!availableLoaded) {
3853
+ $libAvailList.innerHTML = '<div class="empty-state"><span class="rtn-spinner"></span> Loading available agents…</div>';
3854
+ $libAvailCount.textContent = '';
3855
+ return;
3856
+ }
3857
+
3858
+ const installed = installedIdSet();
3859
+ const notInstalled = availableCatalog.filter((a) => !installed.has(a.id));
3860
+ const q = ($libAvailSearch.value || '').toLowerCase().trim();
3861
+ const match = (a) => !q
3862
+ || a.id.toLowerCase().includes(q)
3863
+ || (a.display_name || '').toLowerCase().includes(q)
3864
+ || (a.description || '').toLowerCase().includes(q)
3865
+ || (a.vendor || '').toLowerCase().includes(q)
3866
+ || (a.keywords || []).some((k) => k.toLowerCase().includes(q));
3867
+ const items = notInstalled.filter(match);
3868
+ $libAvailCount.textContent = `${items.length} agent${items.length === 1 ? '' : 's'}`;
3869
+
3870
+ if (!items.length) {
3871
+ $libAvailList.innerHTML = '<div class="empty-state">No agents match.</div>';
3872
+ return;
3873
+ }
3874
+
3875
+ const hasKeywords = items.some((a) => (a.keywords || []).length);
3876
+ let html;
3877
+ if (q || !hasKeywords) {
3878
+ // Searching, or no keyword data (older CLI) → one flat list, no headers.
3879
+ html = items.map(availCardHtml).join('');
3880
+ } else {
3881
+ // Browse mode: group by category in AGENT_CATEGORIES order, "Other" last.
3882
+ const order = [...AGENT_CATEGORIES.map((c) => c.label), OTHER_CATEGORY];
3883
+ const byCat = new Map();
3884
+ for (const a of items) {
3885
+ const c = categorizeAgent(a);
3886
+ if (!byCat.has(c)) byCat.set(c, []);
3887
+ byCat.get(c).push(a);
3888
+ }
3889
+ html = order.filter((c) => byCat.has(c)).map((c) =>
3890
+ `<div class="lib-section">${escapeHtml(c)}</div>` + byCat.get(c).map(availCardHtml).join('')
3891
+ ).join('');
3892
+ }
3893
+ $libAvailList.innerHTML = html;
3894
+ $libAvailList.querySelectorAll('[data-install]').forEach((btn) => {
3895
+ btn.onclick = () => installAgent(btn.dataset.install, btn);
3896
+ });
3897
+ }
3898
+
3899
+ async function installAgent(id, btn) {
3900
+ const label = btn.getAttribute('aria-label') || `Install ${id}`;
3901
+ const a = availableCatalog.find((x) => x.id === id);
3902
+ btn.setAttribute('aria-busy', 'true');
3903
+ btn.setAttribute('aria-label', label.replace(/^Install/, 'Installing') + '…');
3904
+ btn.innerHTML = '<span class="rtn-spinner"></span>';
3905
+ try {
3906
+ await api('/api/agents/install', { method: 'POST', body: JSON.stringify({ id }) });
3907
+ } catch (e) {
3908
+ btn.removeAttribute('aria-busy');
3909
+ btn.setAttribute('aria-label', label);
3910
+ btn.textContent = 'Install';
3911
+ reportErr(e);
3912
+ return;
3913
+ }
3914
+ // Installed now — signal success BEFORE the catalog refresh so a transient refresh
3915
+ // failure can't swallow the confirmation of an install that already succeeded.
3916
+ showToast(`Installed “${a && a.display_name ? a.display_name : id}” — find it in Installed`, 'ok');
3917
+ // Refresh the Installed catalog (footer count + Installed tab) best-effort, then
3918
+ // re-render Available so the now-installed agent drops out (= not-yet-installed).
3919
+ try { await loadAgentCatalog(); } catch (e) { console.warn('post-install catalog refresh failed', e); }
3920
+ renderAvailable();
3921
+ if (libTab === 'installed') renderLibrary();
3922
+ }
3923
+
3924
+ function switchLibTab(tab) {
3925
+ libTab = tab;
3926
+ $libTabs.querySelectorAll('[data-libtab]').forEach((b) => {
3927
+ const on = b.dataset.libtab === tab;
3928
+ b.classList.toggle('active', on);
3929
+ b.setAttribute('aria-selected', on ? 'true' : 'false');
3930
+ });
3931
+ $libPaneInstalled.hidden = tab !== 'installed';
3932
+ $libPaneAvailable.hidden = tab !== 'available';
3933
+ if (tab === 'available') {
3934
+ renderAvailable(); // shows the loading spinner first if not yet loaded
3935
+ if (!availableLoaded && !availableLoading && !availableUnsupported) {
3936
+ availableLoading = true;
3937
+ loadAvailableAgents()
3938
+ .then(renderAvailable)
3939
+ .catch((e) => {
3940
+ $libAvailList.innerHTML = '<div class="empty-state">Could not load available agents.</div>';
3941
+ $libAvailCount.textContent = '';
3942
+ reportErr(e);
3943
+ })
3944
+ .finally(() => { availableLoading = false; });
3945
+ }
3946
+ setTimeout(() => { if (!$libAvailSearch.hidden) $libAvailSearch.focus(); }, 0);
3947
+ } else {
3948
+ renderLibrary(); // repaint from agentCatalog — it may have grown via an Install on the other tab
3949
+ setTimeout(() => $libSearch.focus(), 0);
3950
+ }
3951
+ }
3952
+
3765
3953
  // ── Integrations: real connection state from the local vault ────────────────
3766
3954
  const VENDOR_ICON = { microsoft: '▦', google: 'G', trimble: '◬', autodesk: 'A' };
3767
3955
  let integrations = [];
@@ -4755,6 +4943,19 @@
4755
4943
  $browseBtn.onclick = () => openLibrary();
4756
4944
  $libSearch.oninput = () => renderLibrary();
4757
4945
 
4946
+ // Agent Library two-tab wiring (Installed | Available). The Available tab is
4947
+ // lazy-loaded on first switch (see switchLibTab).
4948
+ const $libTabs = document.getElementById('lib-tabs');
4949
+ const $libPaneInstalled = document.getElementById('lib-pane-installed');
4950
+ const $libPaneAvailable = document.getElementById('lib-pane-available');
4951
+ const $libAvailSearch = document.getElementById('lib-avail-search');
4952
+ const $libAvailList = document.getElementById('lib-avail-list');
4953
+ const $libAvailCount = document.getElementById('lib-avail-count');
4954
+ if ($libTabs) $libTabs.querySelectorAll('[data-libtab]').forEach((b) => {
4955
+ b.onclick = () => switchLibTab(b.dataset.libtab);
4956
+ });
4957
+ if ($libAvailSearch) $libAvailSearch.oninput = () => renderAvailable();
4958
+
4758
4959
  // Compile-notes strip: × collapses it to a faint one-line pill; clicking the
4759
4960
  // collapsed pill re-expands. Delegated (the strip's innerHTML is rebuilt each
4760
4961
  // load). The choice is remembered per app+note-signature (see notesSig).
@@ -416,9 +416,24 @@
416
416
  <div class="modal-backdrop" id="lib-modal">
417
417
  <div class="modal library">
418
418
  <div class="modal-title">Agent Library</div>
419
- <div class="modal-sub">All installed agents · star a command to save it as a Template.</div>
420
- <input id="lib-search" class="lib-search" type="text" placeholder="Search (tekla, slack, file, …)">
421
- <div class="lib-list" id="lib-list"></div>
419
+ <div class="tabs lib-tabs" id="lib-tabs" role="tablist" aria-label="Agent Library tabs">
420
+ <button data-libtab="installed" class="active" role="tab" aria-selected="true" aria-controls="lib-pane-installed">Installed</button>
421
+ <button data-libtab="available" role="tab" aria-selected="false" aria-controls="lib-pane-available">Available</button>
422
+ </div>
423
+
424
+ <div class="lib-pane" id="lib-pane-installed" role="tabpanel" aria-label="Installed agents">
425
+ <div class="modal-sub">All installed agents · star a command to save it as a Template.</div>
426
+ <input id="lib-search" class="lib-search" type="text" placeholder="Search (tekla, slack, file, …)" aria-label="Search installed agents">
427
+ <div class="lib-list" id="lib-list"></div>
428
+ </div>
429
+
430
+ <div class="lib-pane" id="lib-pane-available" role="tabpanel" aria-label="Available agents" hidden>
431
+ <div class="modal-sub">Every agent in the AWARE registry · install one to add it to your library.</div>
432
+ <input id="lib-avail-search" class="lib-search" type="text" placeholder="Search available agents (revit, ifc, slack, …)" aria-label="Search available agents">
433
+ <span class="sr-only" aria-live="polite" id="lib-avail-count"></span>
434
+ <div class="lib-list" id="lib-avail-list"></div>
435
+ </div>
436
+
422
437
  <div class="modal-actions">
423
438
  <button id="lib-graft" style="margin-right:auto;" data-tip="Build a new agent from a tool you own — a DLL, C# source, NuGet package, or API spec.">⊕ Graft new agent</button>
424
439
  <button id="lib-close">Close</button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.24.1",
3
+ "version": "0.25.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": {