@floless/app 0.26.0 → 0.27.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.
@@ -51710,8 +51710,11 @@ function parseBuildOutput(stdout) {
51710
51710
  function agentInstallArgv(id) {
51711
51711
  return ["agent", "install", assertId(id)];
51712
51712
  }
51713
+ function agentUpdateArgv(id) {
51714
+ return ["agent", "update", assertId(id)];
51715
+ }
51713
51716
  function parseAgentInstallOutput(stdout) {
51714
- const m = stdout.match(/✓\s+installed\s+(\S+)/);
51717
+ const m = stdout.match(/✓\s+(?:installed|updated)\s+(\S+)/);
51715
51718
  return { installed: m?.[1] ?? null };
51716
51719
  }
51717
51720
  function isAgentInstalled(agents, id) {
@@ -52023,6 +52026,18 @@ var aware = {
52023
52026
  if (isAgentInstalled(agents, id)) return { installed: id, output: "already installed" };
52024
52027
  return this.agentInstall(id);
52025
52028
  },
52029
+ /**
52030
+ * Update an installed agent to the latest registry version (`aware agent update
52031
+ * <id>`). Unlike `ensureAgentInstalled`, this always re-pulls — it is the Agent
52032
+ * Library's in-place "Update" action for a card whose installed version trails the
52033
+ * catalog. Like install, the pull fetches a tarball (slow), so use the generous
52034
+ * timeout. The token is NOT involved — only the agent manifest is fetched.
52035
+ */
52036
+ async agentUpdate(id) {
52037
+ const { code, stdout, stderr } = await runRawWithEnv(agentUpdateArgv(id), void 0, 6e5);
52038
+ if (code !== 0) throw new AwareError(`aware agent update ${id} failed (exit ${code})`, { stdout, stderr });
52039
+ return { ...parseAgentInstallOutput(stdout), output: stdout.trim() };
52040
+ },
52026
52041
  /**
52027
52042
  * POST /api/run — installed ID. `run` ignores --json and prints
52028
52043
  * "✓ run complete; trace at <path>"; we parse the path and read the JSONL.
@@ -52777,7 +52792,7 @@ function appVersion() {
52777
52792
  return resolveVersion({
52778
52793
  isSea: isSea2(),
52779
52794
  sqVersionXml: readSqVersionXml(),
52780
- define: true ? "0.26.0" : void 0,
52795
+ define: true ? "0.27.0" : void 0,
52781
52796
  pkgVersion: readPkgVersion()
52782
52797
  });
52783
52798
  }
@@ -52787,7 +52802,7 @@ function resolveChannel(s) {
52787
52802
  return "dev";
52788
52803
  }
52789
52804
  function appChannel() {
52790
- return resolveChannel({ isSea: isSea2(), define: true ? "0.26.0" : void 0 });
52805
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.27.0" : void 0 });
52791
52806
  }
52792
52807
 
52793
52808
  // oauth-presets.ts
@@ -57673,28 +57688,38 @@ async function startServer() {
57673
57688
  throw err;
57674
57689
  }
57675
57690
  });
57676
- const installingAgents = /* @__PURE__ */ new Set();
57677
- let installQueue = Promise.resolve();
57678
- app.post("/api/agents/install", async (req, reply) => {
57679
- const id = req.body?.id;
57680
- if (!id || typeof id !== "string") return reply.status(400).send({ ok: false, error: "id is required" });
57681
- if (installingAgents.has(id)) return reply.status(202).send({ ok: true, started: true });
57682
- installingAgents.add(id);
57683
- installQueue = installQueue.then(async () => {
57691
+ const agentJobsInFlight = /* @__PURE__ */ new Set();
57692
+ let agentJobQueue = Promise.resolve();
57693
+ function startAgentJob(id, action, run) {
57694
+ agentJobsInFlight.add(id);
57695
+ agentJobQueue = agentJobQueue.then(async () => {
57684
57696
  let result;
57685
57697
  try {
57686
- const res = await aware.ensureAgentInstalled(id);
57687
- result = { type: "agent-install-result", id, ok: true, installed: res.installed ?? id };
57698
+ const res = await run();
57699
+ result = { type: "agent-install-result", action, id, ok: true, installed: res.installed ?? id };
57688
57700
  } catch (err) {
57689
- result = { type: "agent-install-result", id, ok: false, error: err instanceof Error ? err.message : String(err) };
57701
+ result = { type: "agent-install-result", action, id, ok: false, error: err instanceof Error ? err.message : String(err) };
57690
57702
  } finally {
57691
- installingAgents.delete(id);
57703
+ agentJobsInFlight.delete(id);
57692
57704
  }
57693
57705
  try {
57694
57706
  broadcast(result);
57695
57707
  } catch {
57696
57708
  }
57697
57709
  });
57710
+ }
57711
+ app.post("/api/agents/install", async (req, reply) => {
57712
+ const id = req.body?.id;
57713
+ if (!id || typeof id !== "string") return reply.status(400).send({ ok: false, error: "id is required" });
57714
+ if (agentJobsInFlight.has(id)) return reply.status(202).send({ ok: true, started: true });
57715
+ startAgentJob(id, "install", () => aware.ensureAgentInstalled(id));
57716
+ return reply.status(202).send({ ok: true, started: true });
57717
+ });
57718
+ app.post("/api/agents/update", async (req, reply) => {
57719
+ const id = req.body?.id;
57720
+ if (!id || typeof id !== "string") return reply.status(400).send({ ok: false, error: "id is required" });
57721
+ if (agentJobsInFlight.has(id)) return reply.status(202).send({ ok: true, started: true });
57722
+ startAgentJob(id, "update", () => aware.agentUpdate(id));
57698
57723
  return reply.status(202).send({ ok: true, started: true });
57699
57724
  });
57700
57725
  app.get("/api/integrations", async () => {
package/dist/web/app.css CHANGED
@@ -1165,6 +1165,17 @@
1165
1165
  text-overflow: ellipsis;
1166
1166
  white-space: nowrap;
1167
1167
  }
1168
+ /* Version as a small dim pill on the meta line — matches the canvas node-card
1169
+ `.ver` token, so the version no longer competes with the agent name (#119). */
1170
+ .lib-item .info .meta .lib-ver {
1171
+ font-family: var(--mono);
1172
+ font-size: 9px;
1173
+ color: var(--text-dim);
1174
+ background: var(--surface-2);
1175
+ padding: 1px 5px;
1176
+ border-radius: 3px;
1177
+ margin-right: 4px;
1178
+ }
1168
1179
  .lib-item .info .blurb { font-size: 11px; color: var(--text-muted); line-height: 1.4; }
1169
1180
  .lib-item .lib-fav {
1170
1181
  background: transparent;
@@ -1180,6 +1191,22 @@
1180
1191
  }
1181
1192
  .lib-item .lib-fav.faved { color: var(--star); border-color: var(--star); }
1182
1193
 
1194
+ /* Installed-tab action cluster: the transient "Update" action sits LEFT of the
1195
+ persistent ⌄ "show commands" caret (#119). */
1196
+ .lib-item .lib-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
1197
+ /* "Update" affordance — outline-accent-dim: signals "an action is available / newer
1198
+ version" without the full-accent weight reserved for primary/selected. Shown only
1199
+ when the registry is strictly ahead of the installed version; hidden when current. */
1200
+ .lib-update {
1201
+ background: transparent; border: 1px solid var(--accent-dim); color: var(--accent-dim);
1202
+ padding: 4px 9px; font-size: 11px; cursor: pointer; border-radius: 3px; flex-shrink: 0;
1203
+ line-height: 1; transition: color 0.15s, border-color 0.15s; font-family: var(--ui);
1204
+ display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
1205
+ }
1206
+ .lib-update:hover { color: var(--accent); border-color: var(--accent); }
1207
+ .lib-update[aria-busy="true"] { color: var(--text-dim); border-color: var(--border-strong); cursor: default; pointer-events: none; }
1208
+ .lib-update .rtn-spinner { width: 11px; height: 11px; border-width: 2px; }
1209
+
1183
1210
  /* Two-tab Agent Library: the tab bar sits between title and panes; each pane is a
1184
1211
  flex column so its inner .lib-list keeps scrolling within the modal's max-height.
1185
1212
  The hidden pane uses [hidden] (display:none) so it's skipped by assistive tech. */
package/dist/web/aware.js CHANGED
@@ -3296,21 +3296,31 @@
3296
3296
  if ($libModal && $libModal.classList.contains('show')) renderLibrary();
3297
3297
  }).catch(() => {});
3298
3298
  } else if (m.type === 'agent-install-result') {
3299
- // A background `aware agent install` (from the Available tab) finished. Clear
3300
- // its in-flight state, then on success refresh so it drops out of Available and
3301
- // appears under Installed; on failure restore its Install button + surface why.
3299
+ // A background `aware agent install` (Available tab) or `aware agent update`
3300
+ // (Installed tab) finished `m.action` says which. Clear its in-flight state,
3301
+ // then on success refresh the catalog (install: drops out of Available, appears
3302
+ // under Installed; update: the new version replaces the old → its Update button
3303
+ // disappears). On failure restore the card + surface why.
3304
+ const isUpdate = m.action === 'update';
3302
3305
  installingIds.delete(m.id);
3303
3306
  if (m.ok) {
3304
- const a = availableCatalog.find((x) => x.id === m.id);
3305
- showToast(`Installed “${a && a.display_name ? a.display_name : m.id} — find it in Installed`, 'ok');
3307
+ if (isUpdate) {
3308
+ showToast(`Updated “${m.id}”`, 'ok');
3309
+ } else {
3310
+ const a = availableCatalog.find((x) => x.id === m.id);
3311
+ showToast(`Installed “${a && a.display_name ? a.display_name : m.id}” — find it in Installed`, 'ok');
3312
+ }
3306
3313
  loadAgentCatalog().then(() => {
3307
3314
  if (!($libModal && $libModal.classList.contains('show'))) return;
3308
3315
  renderAvailable();
3309
3316
  if (libTab === 'installed') renderLibrary();
3310
3317
  }).catch(() => {});
3311
3318
  } else {
3312
- showToast(`Install failed: ${String(m.error || '').slice(0, 80)}`, 'err');
3313
- if ($libModal && $libModal.classList.contains('show')) renderAvailable();
3319
+ showToast(`${isUpdate ? 'Update' : 'Install'} failed: ${String(m.error || '').slice(0, 80)}`, 'err');
3320
+ if ($libModal && $libModal.classList.contains('show')) {
3321
+ if (isUpdate) renderLibrary();
3322
+ else renderAvailable();
3323
+ }
3314
3324
  }
3315
3325
  } else if (m.type === 'request-added' || m.type === 'requests-changed') {
3316
3326
  loadRequests();
@@ -3726,29 +3736,85 @@
3726
3736
  renderLibrary();
3727
3737
  $libModal.classList.add('show');
3728
3738
  if (!agentCatalog.length) loadAgentCatalog().then(renderLibrary).catch(reportErr);
3739
+ // Load the registry catalog too, so the Installed tab can flag outdated agents
3740
+ // with an "Update" button (and the Available tab is instant when switched to).
3741
+ ensureAvailableLoaded(() => {
3742
+ renderLibrary();
3743
+ if (libTab === 'available') renderAvailable();
3744
+ });
3729
3745
  };
3730
3746
 
3747
+ // Compare two dotted version strings numerically, segment by segment (missing
3748
+ // segments = 0). Returns 1 if a>b, -1 if a<b, 0 if equal — so the Installed tab
3749
+ // only offers an "Update" when the registry is STRICTLY ahead (never a downgrade).
3750
+ // Handles the registry's mixed schemes (e.g. "0.2.0" vs "1.0.0", "0.1.0" vs
3751
+ // "2025.0.1"). ponytail: numeric-only compare — registry versions are pure dotted
3752
+ // integers; a pre-release/build suffix (none today) compares by its leading int.
3753
+ function cmpVer(a, b) {
3754
+ const pa = String(a).split('.');
3755
+ const pb = String(b).split('.');
3756
+ const n = Math.max(pa.length, pb.length);
3757
+ for (let i = 0; i < n; i++) {
3758
+ const x = parseInt(pa[i], 10) || 0;
3759
+ const y = parseInt(pb[i], 10) || 0;
3760
+ if (x !== y) return x > y ? 1 : -1;
3761
+ }
3762
+ return 0;
3763
+ }
3764
+
3765
+ // Latest installable version for an installed id, from availableCatalog (lazy-loaded
3766
+ // on library open). Uses the catalog's `manifest_version` — the agent's manifest
3767
+ // version for the latest registry entry, i.e. the version an in-place `aware agent
3768
+ // update` actually pulls. This is the SAME axis as the installed agent's version
3769
+ // (agentCatalog/`agent list` also reports manifest.version), so the comparison is
3770
+ // valid — the catalog's `version` field is the registry INDEX key (the install
3771
+ // spec, a different axis) and must NOT be compared. null when the registry hasn't
3772
+ // loaded, the agent isn't in it (e.g. a grafted/local agent), or the CLI predates
3773
+ // manifest_version (aware < the catalog-manifest-version release) → no Update shown.
3774
+ function latestVersionFor(id) {
3775
+ const a = availableCatalog.find((x) => x.id === id);
3776
+ return a && a.manifest_version ? a.manifest_version : null;
3777
+ }
3778
+
3731
3779
  renderLibrary = function renderLibraryReal() {
3732
3780
  const q = ($libSearch.value || '').toLowerCase().trim();
3733
3781
  const items = agentCatalog.filter((a) => !q || a.id.toLowerCase().includes(q) || (a.kind || '').toLowerCase().includes(q));
3734
3782
  // Each agent is ONE grid cell: the card header + its (lazy) command list live
3735
3783
  // in the same `.lib-card`, so expanding nests the commands directly under the
3736
3784
  // picked card instead of scattering them into a neighbouring grid slot.
3737
- $libList.innerHTML = items.map((a) => `
3738
- <div class="lib-card" data-agent="${escapeAttr(a.id)}">
3785
+ $libList.innerHTML = items.map((a) => {
3786
+ const id = escapeAttr(a.id);
3787
+ // "Update" shows only when the registry is strictly ahead of the installed
3788
+ // version AND the registry catalog has loaded (else we can't know) — hidden,
3789
+ // not disabled, when up to date. A pull in flight shows a spinner instead.
3790
+ const latest = availableLoaded ? latestVersionFor(a.id) : null;
3791
+ const canUpdate = !!latest && cmpVer(latest, a.version) > 0;
3792
+ const updateBtn = installingIds.has(a.id)
3793
+ ? `<button class="lib-update" aria-busy="true" aria-label="Updating ${escapeAttr(a.id)}…" data-tip="Downloading update from the registry — this can take a minute"><span class="rtn-spinner"></span></button>`
3794
+ : canUpdate
3795
+ ? `<button class="lib-update" data-update="${id}" aria-label="Update ${escapeAttr(a.id)} to v${escapeAttr(String(latest))}" data-tip="Update to v${escapeAttr(String(latest))}">↑ v${escapeHtml(String(latest))}</button>`
3796
+ : '';
3797
+ return `
3798
+ <div class="lib-card" data-agent="${id}">
3739
3799
  <div class="lib-item">
3740
3800
  <div class="info">
3741
- <div class="name">${escapeHtml(a.id)} <span style="color:var(--text-dim);font-weight:400">v${escapeHtml(String(a.version))}</span></div>
3742
- <div class="meta">${escapeHtml(a.kind || 'agent')} · ${a.commands} command${a.commands === 1 ? '' : 's'} · ${a.skills} skill${a.skills === 1 ? '' : 's'}</div>
3801
+ <div class="name">${escapeHtml(a.id)}</div>
3802
+ <div class="meta"><span class="lib-ver">v${escapeHtml(String(a.version))}</span> ${escapeHtml(a.kind || 'agent')} · ${a.commands} command${a.commands === 1 ? '' : 's'} · ${a.skills} skill${a.skills === 1 ? '' : 's'}</div>
3803
+ </div>
3804
+ <div class="lib-actions">
3805
+ ${updateBtn}
3806
+ <button class="lib-fav" data-expand="${id}" data-tip="Show commands">⌄</button>
3743
3807
  </div>
3744
- <button class="lib-fav" data-expand="${escapeAttr(a.id)}" data-tip="Show commands">⌄</button>
3745
3808
  </div>
3746
- <div class="lib-commands" id="cmds-${escapeAttr(a.id)}" hidden></div>
3747
- </div>
3748
- `).join('') || '<div class="empty-state">No agents match.</div>';
3809
+ <div class="lib-commands" id="cmds-${id}" hidden></div>
3810
+ </div>`;
3811
+ }).join('') || '<div class="empty-state">No agents match.</div>';
3749
3812
  $libList.querySelectorAll('[data-expand]').forEach((btn) => {
3750
3813
  btn.onclick = () => expandAgent(btn.dataset.expand);
3751
3814
  });
3815
+ $libList.querySelectorAll('[data-update]').forEach((btn) => {
3816
+ btn.onclick = () => updateAgent(btn.dataset.update, btn);
3817
+ });
3752
3818
  };
3753
3819
 
3754
3820
  async function expandAgent(id) {
@@ -3948,6 +4014,49 @@
3948
4014
  });
3949
4015
  }
3950
4016
 
4017
+ // Update an installed agent in place to the latest registry version (Installed tab
4018
+ // "Update" button). Mirrors installAgent: the server runs `aware agent update`
4019
+ // asynchronously (a registry pull, ~a minute) and reports completion over SSE
4020
+ // (`agent-install-result`, action:'update'), so we flip the card to a busy spinner
4021
+ // and let the SSE handler refresh the catalog + re-render. `installingIds` is the
4022
+ // shared in-flight set (an id can't be installing and updating at once).
4023
+ function updateAgent(id, btn) {
4024
+ if (installingIds.has(id)) return; // already in flight (server dedupes too)
4025
+ installingIds.add(id);
4026
+ if (btn) { // immediate feedback before the next render
4027
+ btn.setAttribute('aria-busy', 'true');
4028
+ btn.setAttribute('aria-label', `Updating ${id}…`);
4029
+ btn.removeAttribute('data-update'); // a stray re-wire can't re-trigger it
4030
+ btn.innerHTML = '<span class="rtn-spinner"></span>';
4031
+ }
4032
+ showToast(`Updating “${id}” — this can take a minute or two`, 'info');
4033
+ api('/api/agents/update', { method: 'POST', body: JSON.stringify({ id }) })
4034
+ .catch((e) => { // the START failed (bad id / server) — completion errors arrive via SSE
4035
+ installingIds.delete(id);
4036
+ if ($libModal && $libModal.classList.contains('show')) renderLibrary();
4037
+ reportErr(e);
4038
+ });
4039
+ }
4040
+
4041
+ // Lazy-load the registry catalog once. Used by the Available tab (to list agents)
4042
+ // AND the Installed tab (to detect outdated agents → show "Update"). `onLoaded`
4043
+ // fires after the data is available (immediately if already loaded), so the caller
4044
+ // can re-render whichever view needs it. No-op on a pre-0.54 CLI (no catalog).
4045
+ function ensureAvailableLoaded(onLoaded) {
4046
+ if (availableUnsupported) return;
4047
+ if (availableLoaded) { if (onLoaded) onLoaded(); return; }
4048
+ if (availableLoading) return; // a load is already in flight; its onLoaded re-renders
4049
+ availableLoading = true;
4050
+ loadAvailableAgents()
4051
+ .then(() => { if (onLoaded) onLoaded(); })
4052
+ .catch((e) => {
4053
+ $libAvailList.innerHTML = '<div class="empty-state">Could not load available agents.</div>';
4054
+ $libAvailCount.textContent = '';
4055
+ reportErr(e);
4056
+ })
4057
+ .finally(() => { availableLoading = false; });
4058
+ }
4059
+
3951
4060
  function switchLibTab(tab) {
3952
4061
  libTab = tab;
3953
4062
  $libTabs.querySelectorAll('[data-libtab]').forEach((b) => {
@@ -3959,17 +4068,7 @@
3959
4068
  $libPaneAvailable.hidden = tab !== 'available';
3960
4069
  if (tab === 'available') {
3961
4070
  renderAvailable(); // shows the loading spinner first if not yet loaded
3962
- if (!availableLoaded && !availableLoading && !availableUnsupported) {
3963
- availableLoading = true;
3964
- loadAvailableAgents()
3965
- .then(renderAvailable)
3966
- .catch((e) => {
3967
- $libAvailList.innerHTML = '<div class="empty-state">Could not load available agents.</div>';
3968
- $libAvailCount.textContent = '';
3969
- reportErr(e);
3970
- })
3971
- .finally(() => { availableLoading = false; });
3972
- }
4071
+ ensureAvailableLoaded(() => { renderAvailable(); renderLibrary(); });
3973
4072
  setTimeout(() => { if (!$libAvailSearch.hidden) $libAvailSearch.focus(); }, 0);
3974
4073
  } else {
3975
4074
  renderLibrary(); // repaint from agentCatalog — it may have grown via an Install on the other tab
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.26.0",
3
+ "version": "0.27.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": {