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.
|
|
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
|
-
|
|
2541
|
-
|
|
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…</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
|
|
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=
|
|
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' ? '
|
|
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/
|
|
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)
|
|
8
|
-
//
|
|
9
|
-
// that data
|
|
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…</p></div></div></div>
|
|
80
80
|
</div>
|
|
81
|
-
<div class="section-panel" data-section-panel="
|
|
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">☰</button>
|
|
83
|
-
<div><h1>
|
|
84
|
-
<div class="le-scroll"><div class="le-page" style="display:block"><div id="
|
|
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…</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">☰</button>
|
|
@@ -202,83 +202,150 @@ ${consoleShellScript}
|
|
|
202
202
|
});
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
// ----
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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…</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
|
|
239
|
-
var
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
285
|
+
function searchAbilities(q) {
|
|
286
|
+
var resultsEl = document.getElementById('abilitySearchResults');
|
|
287
|
+
if (lastSearch.q === q) { renderSearchResults(); return; }
|
|
288
|
+
resultsEl.innerHTML = '<p class="hint">Loading…</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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (!
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
.then(function (
|
|
277
|
-
.
|
|
278
|
-
|
|
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
|
|
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 === '
|
|
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
|
|
532
|
-
// links (/config?section=
|
|
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
|
-
|
|
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>
|