loopengine 0.1.94 → 0.1.95

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.
@@ -533,7 +533,7 @@ ${consoleShellScript}
533
533
  // has no spec to repopulate an edit form from, so it just gets a blank
534
534
  // cell — not a button that would 404 the moment it's clicked.
535
535
  function renderLocalTools(tools) {
536
- if (!tools.length) return '<p class="muted">No tools yet. Add one below, install an ability, or connect an integration.</p>';
536
+ if (!tools.length) return '<p class="muted">No tools yet. Install an ability, or add one below.</p>';
537
537
  var rows = tools.map(function (t) {
538
538
  var actions = t.httpToolEditable
539
539
  ? '<button type="button" class="edit-http-tool-btn" data-name="' + escapeHtml(t.name) + '">Edit</button>'
@@ -2054,7 +2054,7 @@ ${consoleShellScript}
2054
2054
  '<input type="text" name="q" placeholder="Search by name or keyword (optional)">' +
2055
2055
  '<button type="submit">Search</button>' +
2056
2056
  '</form>' +
2057
- '<div id="abilitySearchResults"></div>' +
2057
+ '<div id="abilitySearchResults" style="margin-bottom:32px"></div>' +
2058
2058
  '<h3 style="margin-top:24px">Install from a spec</h3>' +
2059
2059
  '<p class="hint">A git URL, a local <code>file:../path</code>, or a private npm package name/version — for anything not published publicly.</p>' +
2060
2060
  '<form id="abilityInstallForm" class="add-source">' +
@@ -2537,8 +2537,10 @@ ${consoleShellScript}
2537
2537
  '<section><h3>Your tools (' + cfg.localTools.length + ')</h3>' + renderLocalTools(cfg.localTools) +
2538
2538
  '<div class="admin-subsection"><h4 id="httpToolFormHeading">Create an HTTP tool</h4>' + renderHttpToolFormHtml() + '</div>' +
2539
2539
  '</section>' +
2540
- '<section id="gatewayToolsSection">' +
2541
- '<h3>From integrations <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
2540
+ // Shown only for an agent that already has Composio tools
2541
+ // (gateway-tools.yml): see applyGatewaySources.
2542
+ '<section id="gatewayToolsSection" style="display:none">' +
2543
+ '<h3>From Composio <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
2542
2544
  '<div id="gatewayToolsContent"><p class="hint">Loading&hellip;</p></div>' +
2543
2545
  '</section>' +
2544
2546
  '<section><h3>Subagents (' + cfg.agentAsTools.length + ')</h3>' + renderTools(cfg.agentAsTools) + '</section>';
@@ -3082,6 +3084,8 @@ ${consoleShellScript}
3082
3084
  function applyGatewaySources(name, sources) {
3083
3085
  var content = gatewayContentEl();
3084
3086
  if (!content) return;
3087
+ var section = detail.querySelector('#gatewayToolsSection');
3088
+ if (section) section.style.display = sources.length ? '' : 'none';
3085
3089
  content.innerHTML = renderGatewayHtml(sources);
3086
3090
  wireGatewayHandlers(name);
3087
3091
  gatewayLoadedFor = name;
@@ -1,5 +1,5 @@
1
1
  // The app shell every console page renders inside: a sidebar listing the
2
- // agents (plus Models and Integrations), and, for a page about one agent,
2
+ // agents (plus Models, Abilities and Settings), and, for a page about one agent,
3
3
  // a header with its name, prompt and tabs. The tabs span two pages —
4
4
  // Chat is playground.ts, every other tab is agents-config-page.ts — so
5
5
  // both render the header through this one script and read as one app.
@@ -22,11 +22,11 @@ export const consoleSidebarHtml = `<aside class="le-sidebar" aria-label="Console
22
22
  <a class="le-brand" href="/agents"><span class="le-brand-mark">${BRAND_MARK}</span><span class="le-brand-name">LoopEngine</span></a>
23
23
  <div class="le-nav-label"><span>Agents</span><a href="/agents?new=1">+ New</a></div>
24
24
  <ul class="le-agent-list" id="leAgentList"></ul>
25
- <a class="le-find" href="/templates" data-le-nav="templates">Start from a template</a>
26
25
  <a class="le-find" href="/interview" data-le-nav="interview">Find your first agent</a>
26
+ <a class="le-find" href="/templates" data-le-nav="templates">Start from a template</a>
27
27
  <nav class="le-foot" aria-label="Settings">
28
28
  <a href="/config?section=models" data-le-nav="models">Models</a>
29
- <a href="/config?section=gateways" data-le-nav="integrations">Integrations</a>
29
+ <a href="/config?section=abilities" data-le-nav="abilities">Abilities</a>
30
30
  <a href="/config?section=settings" data-le-nav="settings">Settings</a>
31
31
  <div class="le-who" id="leWho" hidden></div>
32
32
  </nav>
@@ -161,7 +161,7 @@ export const consoleShellScript = `<script>
161
161
  }
162
162
 
163
163
  var section = params.get('section');
164
- var foot = page === 'config' ? (section === 'gateways' ? 'integrations' : section === 'settings' ? 'settings' : 'models') : page === 'interview' || page === 'templates' ? page : '';
164
+ var foot = page === 'config' ? (section === 'abilities' || section === 'gateways' ? 'abilities' : section === 'settings' ? 'settings' : 'models') : page === 'interview' || page === 'templates' ? page : '';
165
165
  var footLinks = document.querySelectorAll('[data-le-nav]');
166
166
  for (var i = 0; i < footLinks.length; i++) {
167
167
  if (footLinks[i].getAttribute('data-le-nav') === foot) footLinks[i].setAttribute('aria-current', 'page');
@@ -1,6 +1,6 @@
1
1
  // Shared look for every browser page adapters/http.ts serves — the agent
2
2
  // console (agents-config-page.ts), its Chat tab (playground.ts), the
3
- // agents home (agents-list-page.ts) and the Models/Integrations pages
3
+ // agents home (agents-list-page.ts) and the Models/Abilities pages
4
4
  // (global-config-page.ts). All four render inside the same app shell
5
5
  // (web/console-shell.ts), so the shell's own classes (.le-*) live here
6
6
  // too, alongside the shared base: color tokens, type, form controls,
@@ -4,9 +4,9 @@
4
4
  // Overview/Skills/Tools/ActAuth tabs (served separately at
5
5
  // /agents/config, deliberately left untouched by this page). A left
6
6
  // sidebar picks between Models (which providers this deployment can
7
- // call, and which agents use which) and Gateways (whether the composio
8
- // CLI is authenticated on this machine) — see global-config.ts for where
9
- // that data actually comes from. "Agents" is a plain link out to
7
+ // call, and which agents use which), Abilities (what each agent has
8
+ // installed, and adding more) and Settings — see global-config.ts for
9
+ // where most of that data comes from. "Agents" is a plain link out to
10
10
  // /agents/config, not a third client-rendered section here: that page
11
11
  // already owns the full per-agent picture, duplicating or embedding it
12
12
  // would just be two places for the same UI to drift apart.
@@ -78,10 +78,10 @@ ${consoleSidebarHtml}
78
78
  <div><h1>Models</h1><p>API keys for each model provider, and which agents use each one. A saved key is stored on this server and never shown again.</p></div></div></div>
79
79
  <div class="le-scroll"><div class="le-page" style="display:block"><div id="modelsContent"><p class="hint">Loading&hellip;</p></div></div></div>
80
80
  </div>
81
- <div class="section-panel" data-section-panel="gateways" style="display:none">
81
+ <div class="section-panel" data-section-panel="abilities" style="display:none">
82
82
  <div class="le-head plain"><div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">&#9776;</button>
83
- <div><h1>Integrations</h1><p>Outside tool sources (such as Composio) that agents can add tools from.</p></div></div></div>
84
- <div class="le-scroll"><div class="le-page" style="display:block"><div id="gatewaysContent"><p class="hint">Loading&hellip;</p></div></div></div>
83
+ <div><h1>Abilities</h1><p>How agents reach your apps, such as Gmail, Shopify or GitHub. An ability installed into an agent adds its tools, skills and permission rules.</p></div></div></div>
84
+ <div class="le-scroll"><div class="le-page" style="display:block"><div id="abilitiesContent"><p class="hint">Loading&hellip;</p></div></div></div>
85
85
  </div>
86
86
  <div class="section-panel" data-section-panel="settings" style="display:none">
87
87
  <div class="le-head plain"><div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">&#9776;</button>
@@ -202,83 +202,150 @@ ${consoleShellScript}
202
202
  });
203
203
  }
204
204
 
205
- // ---- Gateways section ----
206
-
207
- function renderGatewayCard(g) {
208
- var statusHtml;
209
- var bodyHtml = '';
210
- if (!g.supported) {
211
- statusHtml = '<span class="hint">not yet supported</span>';
212
- } else if (g.connected) {
213
- statusHtml = badge(true, 'connected', 'not connected');
214
- bodyHtml = '<p class="hint">' + escapeHtml(g.email || '') + (g.org ? ' &middot; ' + escapeHtml(g.org) : '') + '</p>' +
215
- '<button type="button" class="delete-btn disconnect-btn" data-provider="' + escapeHtml(g.provider) + '">Disconnect</button>';
216
- } else {
217
- // No Connect button/form here — composio login's only
218
- // non-interactive path (--user-api-key) doesn't accept the kind of
219
- // key an operator actually has on hand (Composio hands out a
220
- // separate x-consumer-api-key credential for MCP clients, which
221
- // 401s against this flag); composio login itself opens a real
222
- // browser, which a web request can't drive. These are the same
223
- // two commands Composio's own docs give for getting started.
224
- statusHtml = badge(false, 'connected', 'not connected');
225
- bodyHtml = '<pre><code>curl -fsSL https://composio.dev/install | sh</code></pre>' +
226
- '<pre><code>composio login</code></pre>' +
227
- '<p class="hint">Run both on the machine running this server, then reload this page.</p>';
205
+ // ---- Abilities section ----
206
+ // How agents reach outside apps: an ability (lp-gmail, lp-shopify...)
207
+ // installed into an agent brings its tools, skills and permission
208
+ // rules. Abilities live in each agent, so this page gathers them from
209
+ // every agent's own Abilities routes and installs through the same
210
+ // route, which also keeps it working with any project template.
211
+
212
+ var abilityAgents = [];
213
+ var abilitiesByAgent = {};
214
+
215
+ function agentAbilitiesHref(agent) {
216
+ return '/agents/config?agent=' + encodeURIComponent(agent) + '&tab=abilities';
217
+ }
218
+
219
+ function renderInstalledAbilities() {
220
+ var byName = {};
221
+ abilityAgents.forEach(function (agent) {
222
+ (abilitiesByAgent[agent] || []).forEach(function (a) {
223
+ if (!byName[a.name]) byName[a.name] = { name: a.name, versions: [], agents: [] };
224
+ if (byName[a.name].versions.indexOf(a.version) === -1) byName[a.name].versions.push(a.version);
225
+ byName[a.name].agents.push(agent);
226
+ });
227
+ });
228
+ var names = Object.keys(byName).sort();
229
+ if (!names.length) return '<p class="hint">No agent has an ability yet. Add one below, or start from a template, which installs the ones it needs.</p>';
230
+ return '<table><thead><tr><th>Ability</th><th>Version</th><th>Agents</th></tr></thead><tbody>' +
231
+ names.map(function (n) {
232
+ var a = byName[n];
233
+ return '<tr><td><code>' + escapeHtml(a.name) + '</code></td>' +
234
+ '<td>' + escapeHtml(a.versions.join(', ')) + '</td>' +
235
+ '<td>' + a.agents.map(function (agent) { return '<a href="' + agentAbilitiesHref(agent) + '">' + escapeHtml(agent) + '</a>'; }).join(', ') + '</td></tr>';
236
+ }).join('') + '</tbody></table>';
237
+ }
238
+
239
+ function renderAbilities() {
240
+ var content = document.getElementById('abilitiesContent');
241
+ var add = abilityAgents.length
242
+ ? '<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px">' +
243
+ '<label for="abilityAgent">Install into</label>' +
244
+ '<select id="abilityAgent">' + abilityAgents.map(function (a) { return '<option value="' + escapeHtml(a) + '">' + escapeHtml(a) + '</option>'; }).join('') + '</select>' +
245
+ '<form id="abilitySearchForm" style="display:flex;gap:6px;flex:1;min-width:220px;margin:0">' +
246
+ '<input type="search" name="q" placeholder="Search abilities, e.g. gmail" aria-label="Search abilities" style="flex:1">' +
247
+ '<button type="submit">Search</button>' +
248
+ '</form>' +
249
+ '</div>' +
250
+ '<p id="abilityInstallStatus" class="hint"></p>' +
251
+ '<div id="abilitySearchResults"><p class="hint">Loading&hellip;</p></div>'
252
+ : '<p>Create an agent first: abilities are installed into one. <a href="/templates">Start from a template</a> or <a href="/agents?new=1">create an agent</a>.</p>';
253
+ content.innerHTML =
254
+ '<section><h3>Installed</h3><div id="abilitiesInstalled">' + renderInstalledAbilities() + '</div></section>' +
255
+ '<section><h3>Add an ability</h3>' + add + '</section>';
256
+ if (!abilityAgents.length) return;
257
+ var form = document.getElementById('abilitySearchForm');
258
+ form.addEventListener('submit', function (ev) { ev.preventDefault(); searchAbilities(form.q.value.trim()); });
259
+ document.getElementById('abilityAgent').addEventListener('change', function () { searchAbilities(form.q.value.trim()); });
260
+ searchAbilities('');
261
+ }
262
+
263
+ var lastSearch = { q: null, results: [] };
264
+
265
+ function renderSearchResults() {
266
+ var resultsEl = document.getElementById('abilitySearchResults');
267
+ var agent = document.getElementById('abilityAgent').value;
268
+ var installed = (abilitiesByAgent[agent] || []).map(function (a) { return a.name; });
269
+ var results = lastSearch.results;
270
+ resultsEl.innerHTML = results.length
271
+ ? '<table><thead><tr><th>Ability</th><th>What it does</th><th></th></tr></thead><tbody>' + results.map(function (pkg) {
272
+ var action = installed.indexOf(pkg.name) !== -1
273
+ ? '<span class="hint">In ' + escapeHtml(agent) + '</span>'
274
+ : '<button type="button" class="ability-install-btn" data-spec="' + escapeHtml(pkg.name) + '">Install</button>';
275
+ return '<tr><td><code>' + escapeHtml(pkg.name) + '</code> <span class="hint">' + escapeHtml(pkg.version) + '</span></td>' +
276
+ '<td>' + escapeHtml(pkg.description || '') + '</td><td>' + action + '</td></tr>';
277
+ }).join('') + '</tbody></table>'
278
+ : '<p class="hint">No published abilities match.</p>';
279
+ var buttons = resultsEl.querySelectorAll('.ability-install-btn');
280
+ for (var i = 0; i < buttons.length; i++) {
281
+ buttons[i].addEventListener('click', function (ev) { installAbility(agent, ev.currentTarget.dataset.spec, ev.currentTarget); });
228
282
  }
229
- return '<div class="source' + (g.supported ? '' : ' unsupported') + '">' +
230
- '<div class="source-head">' +
231
- '<h4>' + escapeHtml(g.provider) + '</h4>' +
232
- statusHtml +
233
- '</div>' +
234
- bodyHtml +
235
- '</div>';
236
283
  }
237
284
 
238
- function applyGateways(gateways) {
239
- var content = document.getElementById('gatewaysContent');
240
- content.innerHTML = (gateways || []).map(renderGatewayCard).join('');
241
- wireGatewayCards(content);
242
- loadedSections.gateways = true;
285
+ function searchAbilities(q) {
286
+ var resultsEl = document.getElementById('abilitySearchResults');
287
+ if (lastSearch.q === q) { renderSearchResults(); return; }
288
+ resultsEl.innerHTML = '<p class="hint">Loading&hellip;</p>';
289
+ // Search doesn't depend on the agent; the route just lives under one.
290
+ fetch('/agents/' + encodeURIComponent(abilityAgents[0]) + '/abilities/search' + (q ? '?q=' + encodeURIComponent(q) : ''))
291
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
292
+ .then(function (r) {
293
+ if (!r.ok) throw new Error(r.body.error || 'search failed');
294
+ lastSearch = { q: q, results: r.body.results || [] };
295
+ renderSearchResults();
296
+ })
297
+ .catch(function (err) { resultsEl.innerHTML = '<p class="error">Could not search abilities: ' + escapeHtml(err.message) + '</p>'; });
243
298
  }
244
299
 
245
- // Disconnect is a machine-wide CLI-session action (see
246
- // disconnectComposioAccount's own doc comment in gateway-tools.ts) —
247
- // it gets a confirm() for that reason, same as this admin UI's other
248
- // consequential-but-not-undoable actions (removing a skill, a rule, a
249
- // gateway tool). No Connect counterpart to wire — see renderGatewayCard's
250
- // own comment for why that's instructions, not a form.
251
- function wireGatewayCards(content) {
252
- var disconnectButtons = content.querySelectorAll('.disconnect-btn');
253
- for (var j = 0; j < disconnectButtons.length; j++) {
254
- disconnectButtons[j].addEventListener('click', function (ev) {
255
- var btn = ev.currentTarget;
256
- var provider = btn.getAttribute('data-provider');
257
- if (!confirm('Disconnect ' + provider + '? This logs out the CLI on this machine for every agent’s gateway tools, not just this browser tab.')) return;
258
- btn.disabled = true;
259
- fetch('/config/gateways/' + encodeURIComponent(provider) + '/disconnect', { method: 'POST' })
260
- .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
261
- .then(function (result) {
262
- if (!result.ok) throw new Error(result.body.error || 'request failed');
263
- applyGateways(result.body.gateways || []);
264
- })
265
- .catch(function (err) {
266
- alert('Could not disconnect: ' + err.message);
267
- btn.disabled = false;
268
- });
300
+ function installAbility(agent, spec, btn) {
301
+ var status = document.getElementById('abilityInstallStatus');
302
+ btn.disabled = true;
303
+ status.className = 'hint';
304
+ status.textContent = 'Installing ' + spec + ' into ' + agent + String.fromCharCode(8230);
305
+ fetch('/agents/' + encodeURIComponent(agent) + '/abilities', {
306
+ method: 'POST',
307
+ headers: { 'content-type': 'application/json' },
308
+ body: JSON.stringify({ spec: spec }),
309
+ })
310
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
311
+ .then(function (r) {
312
+ if (!r.ok) throw new Error(r.body.error || 'request failed');
313
+ var deps = r.body.dependencies || [];
314
+ status.innerHTML = 'Installed ' + escapeHtml(spec) + ' into ' + escapeHtml(agent) + '. ' +
315
+ '<a href="' + agentAbilitiesHref(agent) + '">Add its keys in ' + escapeHtml(agent) + String.fromCharCode(8217) + 's Abilities tab</a>.' +
316
+ (deps.length ? ' <strong>It also needs <code>' + escapeHtml(deps.join(', ')) + '</code>:</strong> use Install deps there.' : '');
317
+ return loadAgentAbilities(agent).then(function () {
318
+ document.getElementById('abilitiesInstalled').innerHTML = renderInstalledAbilities();
319
+ renderSearchResults();
320
+ });
321
+ })
322
+ .catch(function (err) {
323
+ btn.disabled = false;
324
+ status.className = 'error';
325
+ status.textContent = 'Could not install: ' + err.message;
269
326
  });
270
- }
271
327
  }
272
328
 
273
- function loadGateways() {
274
- var content = document.getElementById('gatewaysContent');
275
- fetch('/config/gateways')
276
- .then(function (r) { return r.json(); })
277
- .then(function (data) {
278
- applyGateways(data.gateways || []);
329
+ function loadAgentAbilities(agent) {
330
+ return fetch('/agents/' + encodeURIComponent(agent) + '/abilities')
331
+ .then(function (r) { return r.ok ? r.json() : { abilities: [] }; })
332
+ .then(function (d) { abilitiesByAgent[agent] = d.abilities || []; })
333
+ .catch(function () { abilitiesByAgent[agent] = []; });
334
+ }
335
+
336
+ function loadAbilities() {
337
+ var content = document.getElementById('abilitiesContent');
338
+ leShell.ready
339
+ .then(function (agents) {
340
+ abilityAgents = agents.map(function (a) { return a.name; });
341
+ return Promise.all(abilityAgents.map(loadAgentAbilities));
342
+ })
343
+ .then(function () {
344
+ renderAbilities();
345
+ loadedSections.abilities = true;
279
346
  })
280
347
  .catch(function (err) {
281
- content.innerHTML = '<p class="error">Could not load gateways: ' + escapeHtml(err.message) + '</p>';
348
+ content.innerHTML = '<p class="error">Could not load abilities: ' + escapeHtml(err.message) + '</p>';
282
349
  });
283
350
  }
284
351
 
@@ -520,7 +587,7 @@ ${consoleShellScript}
520
587
  panels[j].style.display = panels[j].dataset.sectionPanel === section ? 'contents' : 'none';
521
588
  }
522
589
  if (section === 'models' && !loadedSections.models) loadModels();
523
- if (section === 'gateways' && !loadedSections.gateways) loadGateways();
590
+ if (section === 'abilities' && !loadedSections.abilities) loadAbilities();
524
591
  if (section === 'settings' && !loadedSections.settings) loadSettings();
525
592
  }
526
593
 
@@ -528,12 +595,14 @@ ${consoleShellScript}
528
595
  sectionButtons[i].addEventListener('click', function (ev) { switchSection(ev.currentTarget.dataset.section); });
529
596
  }
530
597
 
531
- // Deep-linked from agents-config-page.ts's own Models/Gateways sidebar
532
- // links (/config?section=gateways, say) — preselect it if it names a
598
+ // Deep-linked from the console sidebar's Models/Abilities/Settings
599
+ // links (/config?section=abilities, say) — preselect it if it names a
533
600
  // real section, same "trust the query param only if it's valid"
534
601
  // caution playground.ts's own ?agent= handling already uses.
535
602
  var requestedSection = new URLSearchParams(location.search).get('section');
536
- var initialSection = requestedSection === 'gateways' || requestedSection === 'models' || requestedSection === 'settings' ? requestedSection : currentSection;
603
+ // ?section=gateways is the old Integrations page's link.
604
+ if (requestedSection === 'gateways') requestedSection = 'abilities';
605
+ var initialSection = requestedSection === 'abilities' || requestedSection === 'models' || requestedSection === 'settings' ? requestedSection : currentSection;
537
606
  switchSection(initialSection);
538
607
  })();
539
608
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",