loopengine 0.1.39 → 0.1.42

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.
@@ -63,6 +63,14 @@ export interface InstalledAbilityRecord {
63
63
  skills: string[];
64
64
  actauthRules: string[];
65
65
  env: AbilityEnvDecl[];
66
+ /** Real npm package names (dependencies + optionalDependencies) the
67
+ * ability's own tool files import, as of the last install/upgrade —
68
+ * same values AbilityManifest.dependencies carries, persisted here so
69
+ * the Admin UI's "Install dependencies" button can read them back
70
+ * without refetching the ability from its spec. Optional only because
71
+ * an ability installed before this field existed has no recorded
72
+ * value. */
73
+ dependencies?: string[];
66
74
  /** relative path (e.g. "tools/foo.ts") -> sha256 hex, as of the last
67
75
  * install/upgrade — remove-ability's dirty-check compares against
68
76
  * this rather than storing/refetching full content to diff. */
@@ -454,6 +454,7 @@ export async function installAbility(agentName, spec, options = {}) {
454
454
  skills: skillIds,
455
455
  actauthRules: ruleNames,
456
456
  env: manifest.env ?? [],
457
+ dependencies: manifest.dependencies ?? [],
457
458
  contentHashes,
458
459
  };
459
460
  writeProvenance(agentName, provenance);
@@ -633,7 +634,14 @@ export async function upgradeAbility(agentName, abilityName, options = {}) {
633
634
  updateActauthRule(agentName, ruleName, { scope: newRule.scope, tool: newRule.tool, decision: newRule.decision });
634
635
  results.push({ path, status: 'updated' });
635
636
  }
636
- provenance[abilityName] = { ...record, version: newManifest.version, spec, contentHashes: newContentHashes, env: newManifest.env ?? record.env };
637
+ provenance[abilityName] = {
638
+ ...record,
639
+ version: newManifest.version,
640
+ spec,
641
+ contentHashes: newContentHashes,
642
+ env: newManifest.env ?? record.env,
643
+ dependencies: newManifest.dependencies ?? record.dependencies,
644
+ };
637
645
  writeProvenance(agentName, provenance);
638
646
  return { files: results };
639
647
  }
package/dist/index.d.ts CHANGED
@@ -59,7 +59,7 @@ export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule
59
59
  export type { ActauthRuleInput, ActauthConfigView } from './web/actauth-admin.js';
60
60
  export { installAbility, upgradeAbility, listInstalledAbilities, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
61
61
  export type { InstalledAbilityRecord, AbilityEnvDecl, UpgradeFileResult } from './bin/ability-manager.js';
62
- export { searchPublicAbilities } from './web/abilities-admin.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
@@ -52,7 +52,7 @@ export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundE
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
54
  export { installAbility, upgradeAbility, listInstalledAbilities, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
55
- export { searchPublicAbilities } from './web/abilities-admin.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,19 +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>';
1300
+ // Same reasoning as upToDate above — missingDependencies (see
1301
+ // hasNodeModule) only ever hides this once the server actually
1302
+ // found every dependency already present in node_modules; anything
1303
+ // it can't confirm still shows the button.
1304
+ var deps = a.missingDependencies || [];
1305
+ if (deps.length) {
1306
+ actionHtml += ' <button type="button" class="ability-install-deps-btn" data-name="' + escapeHtml(a.name) + '" title="npm install ' + escapeHtml(deps.join(' ')) + '">Install deps</button>';
1307
+ }
1291
1308
  return '<tr>' +
1292
1309
  '<td><code>' + escapeHtml(a.name) + '</code></td>' +
1293
1310
  '<td>' + escapeHtml(a.version) + '</td>' +
1294
1311
  '<td class="hint">' + escapeHtml((a.tools || []).join(', ')) + '</td>' +
1295
- '<td><button type="button" class="ability-upgrade-btn" data-name="' + escapeHtml(a.name) + '">Upgrade</button></td>' +
1312
+ '<td>' + actionHtml + '</td>' +
1296
1313
  '</tr>';
1297
1314
  }
1298
1315
 
1299
- function renderAbilitySearchResultRow(pkg) {
1316
+ function renderAbilitySearchResultRow(pkg, installedNames) {
1317
+ var actionHtml = installedNames.indexOf(pkg.name) !== -1
1318
+ ? '<span class="hint">Installed</span>'
1319
+ : '<button type="button" class="ability-install-btn" data-spec="' + escapeHtml(pkg.name) + '">Install</button>';
1300
1320
  return '<tr>' +
1301
1321
  '<td><code>' + escapeHtml(pkg.name) + '</code> <span class="hint">' + escapeHtml(pkg.version) + '</span></td>' +
1302
1322
  '<td>' + escapeHtml(pkg.description || '') + '</td>' +
1303
- '<td><button type="button" class="ability-install-btn" data-spec="' + escapeHtml(pkg.name) + '">Install</button></td>' +
1323
+ '<td>' + actionHtml + '</td>' +
1304
1324
  '</tr>';
1305
1325
  }
1306
1326
 
@@ -1360,8 +1380,10 @@ export const agentsConfigPageHtml = `<!doctype html>
1360
1380
  // has to be installed separately, or the tool fails at runtime
1361
1381
  // with an opaque "Cannot find package" the very next time this
1362
1382
  // agent's config is loaded. Surfaced here, loudly, instead of
1363
- // leaving that as a silent trap discovered later.
1364
- message += ' <strong>This ability also needs: <code>npm install ' + escapeHtml(deps.join(' ')) + '</code></strong> in your own project before it will actually work.';
1383
+ // leaving that as a silent trap discovered later — the
1384
+ // Installed table below now has its own "Install deps" button
1385
+ // for exactly this (see handleAbilityInstallDepsPost).
1386
+ message += ' <strong>This ability also needs: <code>' + escapeHtml(deps.join(', ')) + '</code></strong> — use the "Install deps" button below, or run <code>npm install ' + escapeHtml(deps.join(' ')) + '</code> yourself.';
1365
1387
  }
1366
1388
  statusEl.innerHTML = message;
1367
1389
  loadAbilitiesTab(name);
@@ -1415,12 +1437,45 @@ export const agentsConfigPageHtml = `<!doctype html>
1415
1437
  });
1416
1438
  }
1417
1439
 
1440
+ // Runs npm install for an ability's own missing dependencies
1441
+ // (handleAbilityInstallDepsPost) — the server reads the dependency
1442
+ // list back off this ability's own installed record, not anything
1443
+ // this form could tamper with, so there's nothing to pass except
1444
+ // which ability. Can take a while (a native module like sharp
1445
+ // especially), so the button stays disabled and the status line says
1446
+ // so rather than looking hung.
1447
+ function installAbilityDeps(name, abilityName, statusEl, btn) {
1448
+ if (btn) { btn.disabled = true; }
1449
+ statusEl.className = 'hint';
1450
+ statusEl.textContent = 'Running npm install for ' + abilityName + '’s dependencies… this can take a minute.';
1451
+ fetch('/agents/' + encodeURIComponent(name) + '/abilities/' + encodeURIComponent(abilityName) + '/install-deps', {
1452
+ method: 'POST',
1453
+ headers: { 'content-type': 'application/json' },
1454
+ body: JSON.stringify({}),
1455
+ })
1456
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
1457
+ .then(function (result) {
1458
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
1459
+ var installed = result.body.installed || [];
1460
+ statusEl.className = 'hint';
1461
+ statusEl.textContent = installed.length
1462
+ ? 'Installed: ' + installed.join(', ') + '. Restart the server to pick it up.'
1463
+ : abilityName + ' had no dependencies to install.';
1464
+ loadAbilitiesTab(name);
1465
+ })
1466
+ .catch(function (err) {
1467
+ statusEl.className = 'error';
1468
+ statusEl.textContent = 'Could not install dependencies: ' + err.message;
1469
+ if (btn) { btn.disabled = false; }
1470
+ });
1471
+ }
1472
+
1418
1473
  // Shared by the initial (empty-query) load right after the tab opens
1419
1474
  // and the search form's own submit — the empty-query case is just
1420
1475
  // "list every published ability" (the keyword filter alone, no extra
1421
1476
  // search text), so an operator sees what's out there immediately
1422
1477
  // instead of having to search for something before anything shows up.
1423
- function runAbilitySearch(name, q, content, resultsEl) {
1478
+ function runAbilitySearch(name, q, content, resultsEl, installedNames) {
1424
1479
  resultsEl.innerHTML = '<p class="hint">Loading&hellip;</p>';
1425
1480
  fetch('/agents/' + encodeURIComponent(name) + '/abilities/search' + (q ? '?q=' + encodeURIComponent(q) : ''))
1426
1481
  .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
@@ -1428,7 +1483,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1428
1483
  if (!result.ok) throw new Error(result.body.error || 'search failed');
1429
1484
  var results = result.body.results || [];
1430
1485
  resultsEl.innerHTML = results.length
1431
- ? '<table><thead><tr><th>Package</th><th>Description</th><th></th></tr></thead><tbody>' + results.map(renderAbilitySearchResultRow).join('') + '</tbody></table>'
1486
+ ? '<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>'
1432
1487
  : '<p class="hint">No public abilities found.</p>';
1433
1488
  var installBtns = resultsEl.querySelectorAll('.ability-install-btn');
1434
1489
  for (var i = 0; i < installBtns.length; i++) {
@@ -1443,7 +1498,7 @@ export const agentsConfigPageHtml = `<!doctype html>
1443
1498
  });
1444
1499
  }
1445
1500
 
1446
- function wireAbilitiesHandlers(name) {
1501
+ function wireAbilitiesHandlers(name, installedNames) {
1447
1502
  var content = abilitiesContentEl();
1448
1503
  if (!content) return;
1449
1504
 
@@ -1456,17 +1511,25 @@ export const agentsConfigPageHtml = `<!doctype html>
1456
1511
  });
1457
1512
  }
1458
1513
 
1514
+ var installDepsBtns = content.querySelectorAll('.ability-install-deps-btn');
1515
+ for (var d = 0; d < installDepsBtns.length; d++) {
1516
+ installDepsBtns[d].addEventListener('click', function (ev) {
1517
+ var btn = ev.currentTarget;
1518
+ installAbilityDeps(name, btn.dataset.name, upgradeStatusEl, btn);
1519
+ });
1520
+ }
1521
+
1459
1522
  var searchForm = content.querySelector('#abilitySearchForm');
1460
1523
  var resultsEl = content.querySelector('#abilitySearchResults');
1461
1524
  if (searchForm && resultsEl) {
1462
1525
  searchForm.addEventListener('submit', function (ev) {
1463
1526
  ev.preventDefault();
1464
1527
  var q = new FormData(searchForm).get('q');
1465
- runAbilitySearch(name, q, content, resultsEl);
1528
+ runAbilitySearch(name, q, content, resultsEl, installedNames);
1466
1529
  });
1467
1530
  // List every published ability immediately, not just after the
1468
1531
  // operator explicitly searches for something.
1469
- runAbilitySearch(name, '', content, resultsEl);
1532
+ runAbilitySearch(name, '', content, resultsEl, installedNames);
1470
1533
  }
1471
1534
 
1472
1535
  var installForm = content.querySelector('#abilityInstallForm');
@@ -1485,7 +1548,8 @@ export const agentsConfigPageHtml = `<!doctype html>
1485
1548
  var content = abilitiesContentEl();
1486
1549
  if (!content) return;
1487
1550
  content.innerHTML = renderAbilitiesConfigHtml(data);
1488
- wireAbilitiesHandlers(name);
1551
+ var installedNames = (data.abilities || []).map(function (a) { return a.name; });
1552
+ wireAbilitiesHandlers(name, installedNames);
1489
1553
  abilitiesLoadedFor = name;
1490
1554
  }
1491
1555
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.39",
3
+ "version": "0.1.42",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",