loopengine 0.1.38 → 0.1.41

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/index.d.ts CHANGED
@@ -57,9 +57,9 @@ export { listDeclaredEnvVars, setEnvVar, EnvVarNameError } from './web/env-admin
57
57
  export type { DeclaredEnvVar } from './web/env-admin.js';
58
58
  export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
59
59
  export type { ActauthRuleInput, ActauthConfigView } from './web/actauth-admin.js';
60
- export { installAbility, listInstalledAbilities, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, } from './bin/ability-manager.js';
61
- export type { InstalledAbilityRecord, AbilityEnvDecl } from './bin/ability-manager.js';
62
- export { searchPublicAbilities } from './web/abilities-admin.js';
60
+ export { installAbility, upgradeAbility, listInstalledAbilities, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
61
+ export type { InstalledAbilityRecord, AbilityEnvDecl, UpgradeFileResult } from './bin/ability-manager.js';
62
+ export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
63
63
  export type { PublicAbilitySearchResult } from './web/abilities-admin.js';
64
64
  export { describeModelProviders, describeGateways } from './web/global-config.js';
65
65
  export type { ModelProviderInfo, AgentModelUsage, ModelsView, GatewayProviderInfo, GatewaysView } from './web/global-config.js';
package/dist/index.js CHANGED
@@ -51,8 +51,8 @@ export { playgroundHtml } from './web/playground.js';
51
51
  export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
52
52
  export { listDeclaredEnvVars, setEnvVar, EnvVarNameError } from './web/env-admin.js';
53
53
  export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
54
- export { installAbility, listInstalledAbilities, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, } from './bin/ability-manager.js';
55
- export { searchPublicAbilities } from './web/abilities-admin.js';
54
+ export { installAbility, upgradeAbility, listInstalledAbilities, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
55
+ export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
56
56
  export { describeModelProviders, describeGateways } from './web/global-config.js';
57
57
  export { createTrackedApprover, listApprovals, decideApproval, findApproval } from './web/web-approver.js';
58
58
  export { scaffoldAgent, scaffoldSubagent, AgentNameError, AgentExistsError, AgentNotFoundError, AgentModelError } from './bin/cli.js';
@@ -4,3 +4,12 @@ export interface PublicAbilitySearchResult {
4
4
  version: string;
5
5
  }
6
6
  export declare function searchPublicAbilities(query?: string): Promise<PublicAbilitySearchResult[]>;
7
+ /** The latest version currently published on the public npm registry for
8
+ * `packageName`, or null if it can't be determined — not published
9
+ * there at all (a private/file:/git-installed ability, the common case
10
+ * during local development), a network hiccup, or a timeout. Best-effort
11
+ * only: the Abilities tab uses this to decide whether an installed
12
+ * ability's own "Upgrade" button is worth showing, not to block
13
+ * anything, so a failure here surfaces as "can't tell, show the button
14
+ * anyway" rather than an error the operator has to deal with. */
15
+ export declare function fetchLatestAbilityVersion(packageName: string): Promise<string | null>;
@@ -38,3 +38,29 @@ export async function searchPublicAbilities(query) {
38
38
  const needle = query.toLowerCase();
39
39
  return results.filter((r) => r.name.toLowerCase().includes(needle) || r.description.toLowerCase().includes(needle));
40
40
  }
41
+ const NPM_LATEST_VERSION_TIMEOUT_MS = 5000;
42
+ /** The latest version currently published on the public npm registry for
43
+ * `packageName`, or null if it can't be determined — not published
44
+ * there at all (a private/file:/git-installed ability, the common case
45
+ * during local development), a network hiccup, or a timeout. Best-effort
46
+ * only: the Abilities tab uses this to decide whether an installed
47
+ * ability's own "Upgrade" button is worth showing, not to block
48
+ * anything, so a failure here surfaces as "can't tell, show the button
49
+ * anyway" rather than an error the operator has to deal with. */
50
+ export async function fetchLatestAbilityVersion(packageName) {
51
+ const controller = new AbortController();
52
+ const timeout = setTimeout(() => controller.abort(), NPM_LATEST_VERSION_TIMEOUT_MS);
53
+ try {
54
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: controller.signal });
55
+ if (!res.ok)
56
+ return null;
57
+ const data = (await res.json());
58
+ return typeof data.version === 'string' ? data.version : null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ finally {
64
+ clearTimeout(timeout);
65
+ }
66
+ }
@@ -1288,25 +1288,39 @@ export const agentsConfigPageHtml = `<!doctype html>
1288
1288
  }
1289
1289
 
1290
1290
  function renderInstalledAbilityRow(a) {
1291
+ // a.upToDate only ever comes back true when the npm registry lookup
1292
+ // (handleAbilitiesGet's own fetchLatestAbilityVersion) actually
1293
+ // succeeded and matched — a private/file:/git-installed ability, or
1294
+ // any registry hiccup, leaves it false, so the button still shows
1295
+ // rather than silently hiding on an unknown instead of a genuine
1296
+ // "nothing to upgrade".
1297
+ var actionHtml = a.upToDate
1298
+ ? '<span class="hint">up to date</span>'
1299
+ : '<button type="button" class="ability-upgrade-btn" data-name="' + escapeHtml(a.name) + '">Upgrade</button>';
1291
1300
  return '<tr>' +
1292
1301
  '<td><code>' + escapeHtml(a.name) + '</code></td>' +
1293
1302
  '<td>' + escapeHtml(a.version) + '</td>' +
1294
1303
  '<td class="hint">' + escapeHtml((a.tools || []).join(', ')) + '</td>' +
1304
+ '<td>' + actionHtml + '</td>' +
1295
1305
  '</tr>';
1296
1306
  }
1297
1307
 
1298
- function renderAbilitySearchResultRow(pkg) {
1308
+ function renderAbilitySearchResultRow(pkg, installedNames) {
1309
+ var actionHtml = installedNames.indexOf(pkg.name) !== -1
1310
+ ? '<span class="hint">Installed</span>'
1311
+ : '<button type="button" class="ability-install-btn" data-spec="' + escapeHtml(pkg.name) + '">Install</button>';
1299
1312
  return '<tr>' +
1300
1313
  '<td><code>' + escapeHtml(pkg.name) + '</code> <span class="hint">' + escapeHtml(pkg.version) + '</span></td>' +
1301
1314
  '<td>' + escapeHtml(pkg.description || '') + '</td>' +
1302
- '<td><button type="button" class="ability-install-btn" data-spec="' + escapeHtml(pkg.name) + '">Install</button></td>' +
1315
+ '<td>' + actionHtml + '</td>' +
1303
1316
  '</tr>';
1304
1317
  }
1305
1318
 
1306
1319
  function renderAbilitiesConfigHtml(data) {
1307
1320
  var installed = data.abilities || [];
1308
1321
  var installedHtml = installed.length
1309
- ? '<table><thead><tr><th>Name</th><th>Version</th><th>Tools</th></tr></thead><tbody>' + installed.map(renderInstalledAbilityRow).join('') + '</tbody></table>'
1322
+ ? '<table><thead><tr><th>Name</th><th>Version</th><th>Tools</th><th></th></tr></thead><tbody>' + installed.map(renderInstalledAbilityRow).join('') + '</tbody></table>' +
1323
+ '<p id="abilityUpgradeStatus" class="hint"></p>'
1310
1324
  : '<p class="hint">No abilities installed for this agent yet.</p>';
1311
1325
 
1312
1326
  return '<h3>Installed</h3>' + installedHtml +
@@ -1371,12 +1385,54 @@ export const agentsConfigPageHtml = `<!doctype html>
1371
1385
  });
1372
1386
  }
1373
1387
 
1388
+ // Upgrades an already-installed ability to whatever it currently
1389
+ // resolves to — same endpoint upgradeAbility (bin/ability-manager.ts)
1390
+ // backs, which three-way merges tool/skill files instead of blindly
1391
+ // overwriting a hand-edited one, so a merge conflict is a real,
1392
+ // expected outcome here, not an error — surfaced distinctly from a
1393
+ // request actually failing.
1394
+ function upgradeAbility(name, abilityName, statusEl, btn) {
1395
+ if (btn) { btn.disabled = true; }
1396
+ statusEl.className = 'hint';
1397
+ statusEl.textContent = 'Upgrading ' + abilityName + '…';
1398
+ fetch('/agents/' + encodeURIComponent(name) + '/abilities/' + encodeURIComponent(abilityName) + '/upgrade', {
1399
+ method: 'POST',
1400
+ headers: { 'content-type': 'application/json' },
1401
+ body: JSON.stringify({}),
1402
+ })
1403
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
1404
+ .then(function (result) {
1405
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
1406
+ var files = result.body.files || [];
1407
+ var updated = files.filter(function (f) { return f.status === 'updated'; });
1408
+ var conflicts = files.filter(function (f) { return f.status === 'conflict'; });
1409
+ var message;
1410
+ if (!files.length || (!updated.length && !conflicts.length)) {
1411
+ message = escapeHtml(abilityName) + ' is already up to date.';
1412
+ } else {
1413
+ message = 'Upgraded ' + escapeHtml(abilityName) + ': ' + escapeHtml(updated.map(function (f) { return f.path; }).join(', ') || 'no files changed') +
1414
+ '. Already running under npx loopengine dev? It becomes active automatically. Running under serve (or nothing yet)? Restart the server to pick it up.';
1415
+ if (conflicts.length) {
1416
+ message += ' <strong>' + conflicts.length + ' file(s) need resolving by hand: ' + escapeHtml(conflicts.map(function (f) { return f.path; }).join(', ')) + '</strong> — a hand-edited file with upstream changes too, left alone rather than overwritten.';
1417
+ }
1418
+ }
1419
+ statusEl.className = conflicts.length ? 'error' : 'hint';
1420
+ statusEl.innerHTML = message;
1421
+ loadAbilitiesTab(name);
1422
+ })
1423
+ .catch(function (err) {
1424
+ statusEl.className = 'error';
1425
+ statusEl.textContent = 'Could not upgrade: ' + err.message;
1426
+ if (btn) { btn.disabled = false; }
1427
+ });
1428
+ }
1429
+
1374
1430
  // Shared by the initial (empty-query) load right after the tab opens
1375
1431
  // and the search form's own submit — the empty-query case is just
1376
1432
  // "list every published ability" (the keyword filter alone, no extra
1377
1433
  // search text), so an operator sees what's out there immediately
1378
1434
  // instead of having to search for something before anything shows up.
1379
- function runAbilitySearch(name, q, content, resultsEl) {
1435
+ function runAbilitySearch(name, q, content, resultsEl, installedNames) {
1380
1436
  resultsEl.innerHTML = '<p class="hint">Loading&hellip;</p>';
1381
1437
  fetch('/agents/' + encodeURIComponent(name) + '/abilities/search' + (q ? '?q=' + encodeURIComponent(q) : ''))
1382
1438
  .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
@@ -1384,7 +1440,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1384
1440
  if (!result.ok) throw new Error(result.body.error || 'search failed');
1385
1441
  var results = result.body.results || [];
1386
1442
  resultsEl.innerHTML = results.length
1387
- ? '<table><thead><tr><th>Package</th><th>Description</th><th></th></tr></thead><tbody>' + results.map(renderAbilitySearchResultRow).join('') + '</tbody></table>'
1443
+ ? '<table><thead><tr><th>Package</th><th>Description</th><th></th></tr></thead><tbody>' + results.map(function (pkg) { return renderAbilitySearchResultRow(pkg, installedNames); }).join('') + '</tbody></table>'
1388
1444
  : '<p class="hint">No public abilities found.</p>';
1389
1445
  var installBtns = resultsEl.querySelectorAll('.ability-install-btn');
1390
1446
  for (var i = 0; i < installBtns.length; i++) {
@@ -1399,21 +1455,30 @@ export const agentsConfigPageHtml = `<!doctype html>
1399
1455
  });
1400
1456
  }
1401
1457
 
1402
- function wireAbilitiesHandlers(name) {
1458
+ function wireAbilitiesHandlers(name, installedNames) {
1403
1459
  var content = abilitiesContentEl();
1404
1460
  if (!content) return;
1405
1461
 
1462
+ var upgradeStatusEl = content.querySelector('#abilityUpgradeStatus');
1463
+ var upgradeBtns = content.querySelectorAll('.ability-upgrade-btn');
1464
+ for (var u = 0; u < upgradeBtns.length; u++) {
1465
+ upgradeBtns[u].addEventListener('click', function (ev) {
1466
+ var btn = ev.currentTarget;
1467
+ upgradeAbility(name, btn.dataset.name, upgradeStatusEl, btn);
1468
+ });
1469
+ }
1470
+
1406
1471
  var searchForm = content.querySelector('#abilitySearchForm');
1407
1472
  var resultsEl = content.querySelector('#abilitySearchResults');
1408
1473
  if (searchForm && resultsEl) {
1409
1474
  searchForm.addEventListener('submit', function (ev) {
1410
1475
  ev.preventDefault();
1411
1476
  var q = new FormData(searchForm).get('q');
1412
- runAbilitySearch(name, q, content, resultsEl);
1477
+ runAbilitySearch(name, q, content, resultsEl, installedNames);
1413
1478
  });
1414
1479
  // List every published ability immediately, not just after the
1415
1480
  // operator explicitly searches for something.
1416
- runAbilitySearch(name, '', content, resultsEl);
1481
+ runAbilitySearch(name, '', content, resultsEl, installedNames);
1417
1482
  }
1418
1483
 
1419
1484
  var installForm = content.querySelector('#abilityInstallForm');
@@ -1432,7 +1497,8 @@ export const agentsConfigPageHtml = `<!doctype html>
1432
1497
  var content = abilitiesContentEl();
1433
1498
  if (!content) return;
1434
1499
  content.innerHTML = renderAbilitiesConfigHtml(data);
1435
- wireAbilitiesHandlers(name);
1500
+ var installedNames = (data.abilities || []).map(function (a) { return a.name; });
1501
+ wireAbilitiesHandlers(name, installedNames);
1436
1502
  abilitiesLoadedFor = name;
1437
1503
  }
1438
1504
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.38",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",