loopengine 0.1.94 → 0.1.97
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/README.md +21 -2
- package/dist/bin/ability-manager.d.ts +4 -0
- package/dist/bin/ability-manager.js +34 -2
- package/dist/bin/cli.d.ts +1 -1
- package/dist/bin/cli.js +3 -0
- package/dist/core/ability-details.d.ts +31 -0
- package/dist/core/ability-details.js +140 -0
- package/dist/core/ability-sources.d.ts +59 -0
- package/dist/core/ability-sources.js +312 -0
- package/dist/core/agent-config.d.ts +21 -1
- package/dist/core/discover-agents.js +11 -1
- package/dist/core/http-notifier.d.ts +7 -1
- package/dist/core/http-notifier.js +68 -9
- package/dist/core/http-notify-triggers/email.d.ts +2 -2
- package/dist/core/http-notify-triggers/email.js +2 -4
- package/dist/core/http-notify-triggers/lark.d.ts +2 -2
- package/dist/core/http-notify-triggers/lark.js +2 -4
- package/dist/core/http-notify-triggers/slack.d.ts +2 -2
- package/dist/core/http-notify-triggers/slack.js +2 -4
- package/dist/core/http-notify-triggers/telegram.d.ts +28 -0
- package/dist/core/http-notify-triggers/telegram.js +71 -0
- package/dist/core/http-notify-triggers/webhook.d.ts +2 -2
- package/dist/core/http-notify-triggers/webhook.js +2 -4
- package/dist/core/model-calls/meta-model-call.d.ts +15 -0
- package/dist/core/model-calls/meta-model-call.js +48 -0
- package/dist/core/model-calls/openai-model-call.d.ts +5 -0
- package/dist/core/model-calls/openai-model-call.js +47 -1
- package/dist/core/model-calls/xai-model-call.d.ts +15 -0
- package/dist/core/model-calls/xai-model-call.js +45 -0
- package/dist/core/notifier-file.d.ts +40 -6
- package/dist/core/notifier-file.js +119 -25
- package/dist/core/system-agents/workflow-planner.d.ts +100 -0
- package/dist/core/system-agents/workflow-planner.js +278 -0
- package/dist/core/workflows.d.ts +26 -0
- package/dist/core/workflows.js +39 -0
- package/dist/core/workspace-abilities.d.ts +31 -0
- package/dist/core/workspace-abilities.js +110 -0
- package/dist/index.d.ts +18 -4
- package/dist/index.js +17 -3
- package/dist/web/ability-details-admin.d.ts +17 -0
- package/dist/web/ability-details-admin.js +45 -0
- package/dist/web/ability-sources-admin.d.ts +33 -0
- package/dist/web/ability-sources-admin.js +65 -0
- package/dist/web/agent-file-admin.d.ts +1 -1
- package/dist/web/agent-file-admin.js +3 -0
- package/dist/web/agents-config-page.js +608 -224
- package/dist/web/agents-list-page.js +49 -20
- package/dist/web/console-shell.js +248 -7
- package/dist/web/dev-ui-styles.js +13 -1
- package/dist/web/global-config-page.js +403 -81
- package/dist/web/global-config.d.ts +44 -1
- package/dist/web/global-config.js +162 -2
- package/dist/web/interview-admin.js +2 -2
- package/dist/web/messaging-icons.d.ts +2 -0
- package/dist/web/messaging-icons.js +7 -0
- package/dist/web/notifier-admin.d.ts +33 -15
- package/dist/web/notifier-admin.js +109 -65
- package/dist/web/provider-icons.d.ts +2 -0
- package/dist/web/provider-icons.js +10 -0
- package/dist/web/templates-admin.d.ts +3 -0
- package/dist/web/templates-admin.js +5 -1
- package/dist/web/workflow-admin.d.ts +124 -0
- package/dist/web/workflow-admin.js +308 -0
- package/dist/web/workflow-page.d.ts +1 -0
- package/dist/web/workflow-page.js +226 -0
- package/dist/web/workspace-abilities-admin.d.ts +24 -0
- package/dist/web/workspace-abilities-admin.js +118 -0
- package/package.json +13 -1
|
@@ -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.
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// is built, once when a browser parses the served <script> as JS).
|
|
22
22
|
import { devUiCss } from './dev-ui-styles.js';
|
|
23
23
|
import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
|
|
24
|
+
import { PROVIDER_ICONS } from './provider-icons.js';
|
|
24
25
|
export const globalConfigPageHtml = `<!doctype html>
|
|
25
26
|
<html lang="en">
|
|
26
27
|
<head>
|
|
@@ -29,8 +30,12 @@ export const globalConfigPageHtml = `<!doctype html>
|
|
|
29
30
|
<title>LoopEngine Settings</title>
|
|
30
31
|
<style>${devUiCss}
|
|
31
32
|
section { margin-bottom: 28px; }
|
|
33
|
+
.provider-name { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }
|
|
34
|
+
.le-provider-icon { width: 18px; height: 18px; flex: none; }
|
|
32
35
|
section h3 { font-size: 15px; font-weight: 600; margin: 0 0 10px; }
|
|
33
36
|
section > table { border: 1px solid var(--line); border-radius: var(--radius); border-collapse: separate; border-spacing: 0; overflow: hidden; }
|
|
37
|
+
section h4 { font-size: 13px; font-weight: 600; margin: 18px 0 8px; }
|
|
38
|
+
section h4 .hint { font-weight: 400; margin-left: 4px; }
|
|
34
39
|
.source {
|
|
35
40
|
border: 1px solid var(--line);
|
|
36
41
|
border-radius: 8px;
|
|
@@ -38,6 +43,12 @@ export const globalConfigPageHtml = `<!doctype html>
|
|
|
38
43
|
margin-bottom: 10px;
|
|
39
44
|
}
|
|
40
45
|
.source.unsupported { opacity: 0.6; }
|
|
46
|
+
/* Settings → Private ability sources: one field per row, label above. */
|
|
47
|
+
.source-form { display: grid; gap: 12px; max-width: 520px; margin-top: 12px; }
|
|
48
|
+
.source-form label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; font-weight: 500; }
|
|
49
|
+
.source-form label .hint { font-weight: 400; }
|
|
50
|
+
.source-form input, .source-form select { width: 100%; box-sizing: border-box; }
|
|
51
|
+
.source-form button[type="submit"] { justify-self: start; }
|
|
41
52
|
.source-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
42
53
|
.source-head h4 { margin: 0; font-size: 14px; font-family: ui-monospace, monospace; }
|
|
43
54
|
.hint { font-size: 11px; color: var(--ink-muted); }
|
|
@@ -78,10 +89,10 @@ ${consoleSidebarHtml}
|
|
|
78
89
|
<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
90
|
<div class="le-scroll"><div class="le-page" style="display:block"><div id="modelsContent"><p class="hint">Loading…</p></div></div></div>
|
|
80
91
|
</div>
|
|
81
|
-
<div class="section-panel" data-section-panel="
|
|
92
|
+
<div class="section-panel" data-section-panel="abilities" style="display:none">
|
|
82
93
|
<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="
|
|
94
|
+
<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>
|
|
95
|
+
<div class="le-scroll"><div class="le-page" style="display:block"><div id="abilitiesContent"><p class="hint">Loading…</p></div></div></div>
|
|
85
96
|
</div>
|
|
86
97
|
<div class="section-panel" data-section-panel="settings" style="display:none">
|
|
87
98
|
<div class="le-head plain"><div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>
|
|
@@ -110,6 +121,12 @@ ${consoleShellScript}
|
|
|
110
121
|
|
|
111
122
|
// ---- Models section ----
|
|
112
123
|
|
|
124
|
+
// Each provider's logo (web/provider-icons.ts), shown beside its name.
|
|
125
|
+
var PROVIDER_ICONS = ${JSON.stringify(PROVIDER_ICONS)};
|
|
126
|
+
function providerName(provider) {
|
|
127
|
+
return '<span class="provider-name">' + (PROVIDER_ICONS[provider] || '') + '<code>' + escapeHtml(provider) + '</code></span>';
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
function renderModels(data) {
|
|
114
131
|
var providerRows = data.providers.map(function (p) {
|
|
115
132
|
// Write-only: the field is always empty; the server never sends a
|
|
@@ -121,7 +138,7 @@ ${consoleShellScript}
|
|
|
121
138
|
(p.configured ? '<button type="button" class="delete-btn" data-remove-key="' + escapeHtml(p.provider) + '">Remove</button>' : '') +
|
|
122
139
|
'</form><div class="key-msg" data-key-msg="' + escapeHtml(p.provider) + '"></div>'
|
|
123
140
|
: '<span class="hint">Set LOOPENGINE_ADMIN_AUTH to manage keys here, or put ' + escapeHtml(p.envVar) + ' in .env.</span>';
|
|
124
|
-
return '<tr><td
|
|
141
|
+
return '<tr><td>' + providerName(p.provider) + '</td>' +
|
|
125
142
|
'<td><code>' + escapeHtml(p.envVar) + '</code></td>' +
|
|
126
143
|
'<td>' + badge(p.configured, 'configured', 'not set') + '</td>' +
|
|
127
144
|
'<td>' + keyCell + '</td></tr>';
|
|
@@ -130,7 +147,7 @@ ${consoleShellScript}
|
|
|
130
147
|
var agentRows = data.agents.length
|
|
131
148
|
? data.agents.map(function (a) {
|
|
132
149
|
return '<tr><td><code>' + escapeHtml(a.agent) + '</code></td>' +
|
|
133
|
-
'<td
|
|
150
|
+
'<td>' + providerName(a.provider) + '</td>' +
|
|
134
151
|
'<td><code>' + escapeHtml(a.model) + '</code></td></tr>';
|
|
135
152
|
}).join('')
|
|
136
153
|
: '<tr><td colspan="3"><p class="hint">No agents with a resolved model config (a custom createModelCall has nothing to report here).</p></td></tr>';
|
|
@@ -202,86 +219,290 @@ ${consoleShellScript}
|
|
|
202
219
|
});
|
|
203
220
|
}
|
|
204
221
|
|
|
205
|
-
// ----
|
|
222
|
+
// ---- Abilities section ----
|
|
223
|
+
// How agents reach outside apps. Public abilities (npm) are browsed
|
|
224
|
+
// here and installed from an agent's own Abilities tab. A private one
|
|
225
|
+
// (from the sources in Settings) is installed here into the workspace
|
|
226
|
+
// (core/workspace-abilities.ts), then added to any agent from its tab.
|
|
206
227
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
'<div
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
'</
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
228
|
+
// An ability's details (tools with their permissions, skills, settings)
|
|
229
|
+
// come from the console shell, shared with each agent's Abilities tab.
|
|
230
|
+
var toggleAbilityDetails = leShell.toggleAbilityDetails;
|
|
231
|
+
|
|
232
|
+
function detailsButton(spec) {
|
|
233
|
+
return '<button type="button" class="ability-details-btn" data-spec="' + escapeHtml(spec) + '">Details</button>';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
var lastSearch = { q: null, results: [] };
|
|
237
|
+
var lastPrivate = null;
|
|
238
|
+
var workspaceAbilities = null;
|
|
239
|
+
|
|
240
|
+
function renderAbilities() {
|
|
241
|
+
var content = document.getElementById('abilitiesContent');
|
|
242
|
+
content.innerHTML = '<section>' +
|
|
243
|
+
'<h4>Public <span class="hint">npm packages tagged loopengine-ability; add one to an agent from its Abilities tab</span></h4>' +
|
|
244
|
+
'<form id="abilitySearchForm" style="display:flex;gap:6px;margin:0 0 10px;max-width:560px">' +
|
|
245
|
+
'<input type="search" name="q" placeholder="Search abilities, e.g. gmail" aria-label="Search public abilities" style="flex:1">' +
|
|
246
|
+
'<button type="submit">Search</button>' +
|
|
247
|
+
'</form>' +
|
|
248
|
+
'<div id="abilitySearchResults"><p class="hint">Loading…</p></div>' +
|
|
249
|
+
'<h4 id="private-abilities">Private <span class="hint">your own, from the sources in <a href="/config?section=settings">Settings</a>; install one into the workspace, then add it to agents from their Abilities tab</span></h4>' +
|
|
250
|
+
'<p id="abilityInstallStatus" class="hint"></p>' +
|
|
251
|
+
'<div id="workspaceAbilities"></div>' +
|
|
252
|
+
'<div id="privateAbilities"><p class="hint">Loading…</p></div>' +
|
|
253
|
+
'<form id="abilitySpecForm" style="display:flex;gap:6px;margin:10px 0 0;max-width:560px">' +
|
|
254
|
+
'<input name="spec" placeholder="Or install by name or spec: @company/lp-x, github:org/repo" aria-label="Ability name or spec" style="flex:1">' +
|
|
255
|
+
'<button type="submit">Install</button>' +
|
|
256
|
+
'</form>' +
|
|
257
|
+
'<div class="source" style="margin-top:12px;max-width:560px;box-sizing:border-box">' +
|
|
258
|
+
'<p style="margin:0 0 8px"><strong>Upload an ability</strong> <span class="hint">from your computer: a .zip or .tgz, or its folder (up to 25 MB)</span></p>' +
|
|
259
|
+
'<input type="file" id="abilityUploadFile" accept=".zip,.tgz,.gz" hidden>' +
|
|
260
|
+
'<input type="file" id="abilityUploadFolder" webkitdirectory hidden>' +
|
|
261
|
+
'<div style="display:flex;gap:6px;flex-wrap:wrap"><button type="button" id="abilityUploadFileBtn">Choose a file' + String.fromCharCode(8230) + '</button>' +
|
|
262
|
+
'<button type="button" id="abilityUploadFolderBtn">Choose a folder' + String.fromCharCode(8230) + '</button></div>' +
|
|
263
|
+
'</div></section>';
|
|
264
|
+
var form = document.getElementById('abilitySearchForm');
|
|
265
|
+
form.addEventListener('submit', function (ev) { ev.preventDefault(); searchAbilities(form.q.value.trim()); });
|
|
266
|
+
var specForm = document.getElementById('abilitySpecForm');
|
|
267
|
+
specForm.addEventListener('submit', function (ev) {
|
|
268
|
+
ev.preventDefault();
|
|
269
|
+
var spec = specForm.spec.value.trim();
|
|
270
|
+
if (spec) installIntoWorkspace(spec, specForm.querySelector('button'));
|
|
271
|
+
});
|
|
272
|
+
wireAbilityUpload();
|
|
273
|
+
searchAbilities('');
|
|
274
|
+
loadWorkspaceAbilities();
|
|
275
|
+
loadPrivateAbilities();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---- Uploading an ability from your computer ----
|
|
279
|
+
|
|
280
|
+
// A file's contents as base64 (the part after a data: URL's comma).
|
|
281
|
+
function readBase64(file) {
|
|
282
|
+
return new Promise(function (resolve, reject) {
|
|
283
|
+
var reader = new FileReader();
|
|
284
|
+
reader.onload = function () { resolve(String(reader.result).split(',')[1] || ''); };
|
|
285
|
+
reader.onerror = function () { reject(reader.error || new Error('Could not read ' + file.name)); };
|
|
286
|
+
reader.readAsDataURL(file);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function uploadAbility(body, label, buttons) {
|
|
291
|
+
var status = document.getElementById('abilityInstallStatus');
|
|
292
|
+
buttons.forEach(function (b) { b.disabled = true; });
|
|
293
|
+
status.className = 'hint';
|
|
294
|
+
status.textContent = 'Uploading ' + label + String.fromCharCode(8230);
|
|
295
|
+
return fetch('/abilities/workspace/upload', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
|
|
296
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
297
|
+
.then(function (r) {
|
|
298
|
+
if (!r.ok) throw new Error(r.body.error || 'upload failed');
|
|
299
|
+
status.textContent = 'Uploaded ' + label + ' into the workspace. Add it to an agent from the agent' + String.fromCharCode(8217) + 's Abilities tab.';
|
|
300
|
+
workspaceAbilities = r.body.abilities || [];
|
|
301
|
+
renderWorkspaceAbilities();
|
|
302
|
+
renderPrivateAbilities();
|
|
303
|
+
})
|
|
304
|
+
.catch(function (err) {
|
|
305
|
+
status.className = 'error';
|
|
306
|
+
status.textContent = 'Could not upload ' + label + ': ' + err.message;
|
|
307
|
+
})
|
|
308
|
+
.then(function () { buttons.forEach(function (b) { b.disabled = false; }); });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function wireAbilityUpload() {
|
|
312
|
+
var fileInput = document.getElementById('abilityUploadFile');
|
|
313
|
+
var folderInput = document.getElementById('abilityUploadFolder');
|
|
314
|
+
var fileBtn = document.getElementById('abilityUploadFileBtn');
|
|
315
|
+
var folderBtn = document.getElementById('abilityUploadFolderBtn');
|
|
316
|
+
var buttons = [fileBtn, folderBtn];
|
|
317
|
+
var MAX = 25 * 1024 * 1024;
|
|
318
|
+
fileBtn.addEventListener('click', function () { fileInput.value = ''; fileInput.click(); });
|
|
319
|
+
folderBtn.addEventListener('click', function () { folderInput.value = ''; folderInput.click(); });
|
|
320
|
+
fileInput.addEventListener('change', function () {
|
|
321
|
+
var file = fileInput.files[0];
|
|
322
|
+
if (!file) return;
|
|
323
|
+
if (file.size > MAX) { document.getElementById('abilityInstallStatus').textContent = file.name + ' is larger than 25 MB.'; return; }
|
|
324
|
+
readBase64(file).then(function (data) { return uploadAbility({ kind: 'archive', fileName: file.name, data: data }, file.name, buttons); });
|
|
325
|
+
});
|
|
326
|
+
folderInput.addEventListener('change', function () {
|
|
327
|
+
// Leaves out what an ability never needs: installed packages, git
|
|
328
|
+
// data and hidden files.
|
|
329
|
+
var files = Array.prototype.filter.call(folderInput.files, function (f) {
|
|
330
|
+
return !f.webkitRelativePath.split('/').some(function (part) { return part === 'node_modules' || part.charAt(0) === '.'; });
|
|
269
331
|
});
|
|
332
|
+
if (!files.length) return;
|
|
333
|
+
var folderName = files[0].webkitRelativePath.split('/')[0];
|
|
334
|
+
var total = files.reduce(function (sum, f) { return sum + f.size; }, 0);
|
|
335
|
+
if (total > MAX) { document.getElementById('abilityInstallStatus').textContent = folderName + ' is larger than 25 MB.'; return; }
|
|
336
|
+
Promise.all(files.map(function (f) { return readBase64(f).then(function (data) { return { path: f.webkitRelativePath, data: data }; }); }))
|
|
337
|
+
.then(function (list) { return uploadAbility({ kind: 'files', folderName: folderName, files: list }, folderName, buttons); });
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Details, Install and Remove buttons anywhere in the section.
|
|
342
|
+
document.getElementById('abilitiesContent').addEventListener('click', function (ev) {
|
|
343
|
+
var btn = ev.target.closest && ev.target.closest('button');
|
|
344
|
+
if (!btn) return;
|
|
345
|
+
if (btn.classList.contains('ability-details-btn')) toggleAbilityDetails(btn);
|
|
346
|
+
else if (btn.classList.contains('workspace-install-btn')) installIntoWorkspace(btn.dataset.spec, btn);
|
|
347
|
+
else if (btn.classList.contains('workspace-remove-btn')) removeFromWorkspace(btn);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
function renderSearchResults() {
|
|
351
|
+
var resultsEl = document.getElementById('abilitySearchResults');
|
|
352
|
+
var results = lastSearch.results;
|
|
353
|
+
resultsEl.innerHTML = results.length
|
|
354
|
+
? '<table><thead><tr><th>Ability</th><th>What it does</th><th></th></tr></thead><tbody>' + results.map(function (pkg) {
|
|
355
|
+
return '<tr><td><code>' + escapeHtml(pkg.name) + '</code> <span class="hint">' + escapeHtml(pkg.version) + '</span></td>' +
|
|
356
|
+
'<td>' + escapeHtml(pkg.description || '') + '</td>' +
|
|
357
|
+
'<td style="white-space:nowrap">' + detailsButton(pkg.name + '@' + pkg.version) + '</td></tr>';
|
|
358
|
+
}).join('') + '</tbody></table>'
|
|
359
|
+
: '<p class="hint">No published abilities match.</p>';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function workspaceCopy(name) {
|
|
363
|
+
return (workspaceAbilities || []).filter(function (a) { return a.name === name; })[0];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// What's already in the workspace, ready to add to agents.
|
|
367
|
+
function renderWorkspaceAbilities() {
|
|
368
|
+
var el = document.getElementById('workspaceAbilities');
|
|
369
|
+
if (!el || !workspaceAbilities) return;
|
|
370
|
+
el.innerHTML = workspaceAbilities.length
|
|
371
|
+
? '<div class="source"><p style="margin:0 0 8px"><strong>In this workspace</strong> <span class="hint">add these to an agent from its Abilities tab</span></p>' +
|
|
372
|
+
'<table><thead><tr><th>Ability</th><th>From</th><th></th></tr></thead><tbody>' + workspaceAbilities.map(function (a) {
|
|
373
|
+
return '<tr><td><code>' + escapeHtml(a.name) + '</code>' + (a.version ? ' <span class="hint">' + escapeHtml(a.version) + '</span>' : '') + '</td>' +
|
|
374
|
+
'<td><span class="hint">' + escapeHtml(a.source.indexOf('upload:') === 0 ? 'Uploaded ' + a.source.slice(7) : a.source) + '</span></td>' +
|
|
375
|
+
'<td style="white-space:nowrap">' + detailsButton(a.spec) + ' <button type="button" class="delete-btn workspace-remove-btn" data-name="' + escapeHtml(a.name) + '">Remove</button></td></tr>';
|
|
376
|
+
}).join('') + '</tbody></table></div>'
|
|
377
|
+
: '';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Each source's abilities: Install puts one into the workspace; one
|
|
381
|
+
// already there shows so, with Update when the source has another version.
|
|
382
|
+
function renderPrivateAbilities() {
|
|
383
|
+
var el = document.getElementById('privateAbilities');
|
|
384
|
+
if (!el || !lastPrivate) return;
|
|
385
|
+
var sources = lastPrivate.sources || [];
|
|
386
|
+
if (lastPrivate.error) { el.innerHTML = '<p class="error">' + escapeHtml(lastPrivate.error) + '</p>'; return; }
|
|
387
|
+
if (!sources.length) {
|
|
388
|
+
el.innerHTML = '<p class="hint">No private sources yet. <a href="/config?section=settings">Add one in Settings</a> (a GitHub org or user, or a private npm registry), or upload an ability below.</p>';
|
|
389
|
+
return;
|
|
270
390
|
}
|
|
391
|
+
el.innerHTML = sources.map(function (s) {
|
|
392
|
+
var body = s.error
|
|
393
|
+
? '<p class="error" style="margin:0">' + escapeHtml(s.error) + '</p>'
|
|
394
|
+
: s.abilities.length
|
|
395
|
+
? '<table><thead><tr><th>Ability</th><th>What it does</th><th></th></tr></thead><tbody>' + s.abilities.map(function (a) {
|
|
396
|
+
var copy = workspaceCopy(a.name);
|
|
397
|
+
var action = !copy
|
|
398
|
+
? '<button type="button" class="workspace-install-btn" data-spec="' + escapeHtml(a.spec) + '">Install</button>'
|
|
399
|
+
: copy.version && a.version && copy.version !== a.version
|
|
400
|
+
? '<span class="hint">In workspace (' + escapeHtml(copy.version) + ')</span> <button type="button" class="workspace-install-btn" data-spec="' + escapeHtml(a.spec) + '">Update to ' + escapeHtml(a.version) + '</button>'
|
|
401
|
+
: '<span class="hint">In workspace</span>';
|
|
402
|
+
return '<tr><td><code>' + escapeHtml(a.name) + '</code>' + (a.version ? ' <span class="hint">' + escapeHtml(a.version) + '</span>' : '') + '</td>' +
|
|
403
|
+
'<td>' + escapeHtml(a.description || '') + '</td>' +
|
|
404
|
+
'<td style="white-space:nowrap">' + detailsButton(a.spec) + ' ' + action + '</td></tr>';
|
|
405
|
+
}).join('') + '</tbody></table>'
|
|
406
|
+
: '<p class="hint" style="margin:0">No abilities found here.</p>';
|
|
407
|
+
return '<div class="source"><p style="margin:0 0 8px"><strong>' + escapeHtml(s.label) + '</strong></p>' + body + '</div>';
|
|
408
|
+
}).join('');
|
|
271
409
|
}
|
|
272
410
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
411
|
+
// Linked from an agent's Abilities tab (/config?section=abilities#private-abilities):
|
|
412
|
+
// once every list has loaded, so the public results don't push it down.
|
|
413
|
+
var scrolledToPrivate = false;
|
|
414
|
+
function scrollToPrivateIfLinked() {
|
|
415
|
+
if (scrolledToPrivate || location.hash !== '#private-abilities' || !lastPrivate || !workspaceAbilities || lastSearch.q === null) return;
|
|
416
|
+
var heading = document.getElementById('private-abilities');
|
|
417
|
+
if (!heading) return;
|
|
418
|
+
scrolledToPrivate = true;
|
|
419
|
+
heading.scrollIntoView({ block: 'start' });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function loadWorkspaceAbilities() {
|
|
423
|
+
return fetch('/abilities/workspace')
|
|
276
424
|
.then(function (r) { return r.json(); })
|
|
277
|
-
.then(function (
|
|
278
|
-
|
|
425
|
+
.then(function (d) {
|
|
426
|
+
workspaceAbilities = d.abilities || [];
|
|
427
|
+
renderWorkspaceAbilities();
|
|
428
|
+
renderPrivateAbilities();
|
|
429
|
+
scrollToPrivateIfLinked();
|
|
430
|
+
})
|
|
431
|
+
.catch(function (err) {
|
|
432
|
+
var el = document.getElementById('workspaceAbilities');
|
|
433
|
+
if (el) el.innerHTML = '<p class="error">Could not list the workspace' + String.fromCharCode(8217) + 's abilities: ' + escapeHtml(err.message) + '</p>';
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function loadPrivateAbilities() {
|
|
438
|
+
fetch('/abilities/private')
|
|
439
|
+
.then(function (r) { return r.json(); })
|
|
440
|
+
.then(function (d) {
|
|
441
|
+
lastPrivate = d;
|
|
442
|
+
renderPrivateAbilities();
|
|
443
|
+
scrollToPrivateIfLinked();
|
|
279
444
|
})
|
|
280
445
|
.catch(function (err) {
|
|
281
|
-
|
|
446
|
+
var el = document.getElementById('privateAbilities');
|
|
447
|
+
if (el) el.innerHTML = '<p class="error">Could not list private abilities: ' + escapeHtml(err.message) + '</p>';
|
|
282
448
|
});
|
|
283
449
|
}
|
|
284
450
|
|
|
451
|
+
function searchAbilities(q) {
|
|
452
|
+
var resultsEl = document.getElementById('abilitySearchResults');
|
|
453
|
+
if (lastSearch.q === q) { renderSearchResults(); return; }
|
|
454
|
+
resultsEl.innerHTML = '<p class="hint">Loading…</p>';
|
|
455
|
+
fetch('/abilities/search' + (q ? '?q=' + encodeURIComponent(q) : ''))
|
|
456
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
457
|
+
.then(function (r) {
|
|
458
|
+
if (!r.ok) throw new Error(r.body.error || 'search failed');
|
|
459
|
+
lastSearch = { q: q, results: r.body.results || [] };
|
|
460
|
+
renderSearchResults();
|
|
461
|
+
scrollToPrivateIfLinked();
|
|
462
|
+
})
|
|
463
|
+
.catch(function (err) { resultsEl.innerHTML = '<p class="error">Could not search abilities: ' + escapeHtml(err.message) + '</p>'; });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function installIntoWorkspace(spec, btn) {
|
|
467
|
+
var status = document.getElementById('abilityInstallStatus');
|
|
468
|
+
btn.disabled = true;
|
|
469
|
+
status.className = 'hint';
|
|
470
|
+
status.textContent = 'Installing ' + spec + ' into the workspace' + String.fromCharCode(8230);
|
|
471
|
+
fetch('/abilities/workspace', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ spec: spec }) })
|
|
472
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
473
|
+
.then(function (r) {
|
|
474
|
+
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
475
|
+
status.textContent = 'Installed ' + spec + ' into the workspace. Add it to an agent from the agent' + String.fromCharCode(8217) + 's Abilities tab.';
|
|
476
|
+
workspaceAbilities = r.body.abilities || [];
|
|
477
|
+
renderWorkspaceAbilities();
|
|
478
|
+
renderPrivateAbilities();
|
|
479
|
+
btn.disabled = false;
|
|
480
|
+
})
|
|
481
|
+
.catch(function (err) {
|
|
482
|
+
btn.disabled = false;
|
|
483
|
+
status.className = 'error';
|
|
484
|
+
status.textContent = 'Could not install: ' + err.message;
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// A second click confirms. Agents that added it keep their own copies.
|
|
489
|
+
function removeFromWorkspace(btn) {
|
|
490
|
+
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to remove'; return; }
|
|
491
|
+
btn.disabled = true;
|
|
492
|
+
fetch('/abilities/workspace?name=' + encodeURIComponent(btn.dataset.name), { method: 'DELETE' })
|
|
493
|
+
.then(function (r) { return r.json(); })
|
|
494
|
+
.then(function (d) {
|
|
495
|
+
workspaceAbilities = d.abilities || [];
|
|
496
|
+
renderWorkspaceAbilities();
|
|
497
|
+
renderPrivateAbilities();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function loadAbilities() {
|
|
502
|
+
renderAbilities();
|
|
503
|
+
loadedSections.abilities = true;
|
|
504
|
+
}
|
|
505
|
+
|
|
285
506
|
// ---- Settings section ----
|
|
286
507
|
|
|
287
508
|
function renderSettings(d) {
|
|
@@ -305,9 +526,107 @@ ${consoleShellScript}
|
|
|
305
526
|
rows.map(function (r) { return '<tr><td>' + r[0] + '</td><td>' + r[1] + '</td></tr>'; }).join('') +
|
|
306
527
|
'</tbody></table></section>' +
|
|
307
528
|
(d.timeZone ? '<section><h3>Time zone</h3><div class="source" id="timeZoneBox"><p class="hint">Loading' + String.fromCharCode(8230) + '</p></div></section>' : '') +
|
|
529
|
+
'<section><h3>Private ability sources</h3><p class="muted" style="margin:0 0 10px">Where your own abilities come from, listed under Private on the Abilities page next to the public ones on npm.</p><div class="source" id="abilitySourcesBox"><p class="hint">Loading' + String.fromCharCode(8230) + '</p></div></section>' +
|
|
308
530
|
'<section><h3>Updates</h3><div class="source" id="upgradeBox"><p class="hint">Checking for a newer LoopEngine' + String.fromCharCode(8230) + '</p></div></section>';
|
|
309
531
|
}
|
|
310
532
|
|
|
533
|
+
// ---- Private ability sources (Settings) ----
|
|
534
|
+
// Where this project's own abilities come from, next to the public ones
|
|
535
|
+
// on npm: a GitHub org or user, or a private npm registry
|
|
536
|
+
// (core/ability-sources.ts). There's no folder kind here: this server
|
|
537
|
+
// may run in the cloud, so local files are uploaded on the Abilities
|
|
538
|
+
// page instead. A token is write-only, like a model key.
|
|
539
|
+
|
|
540
|
+
var SOURCE_TYPES = [
|
|
541
|
+
['github', 'GitHub org or user', 'Each ability is a repo with loopengine.ability.json at its root.'],
|
|
542
|
+
['npm', 'Private npm registry', 'Packages in one scope, tagged loopengine-ability.']
|
|
543
|
+
];
|
|
544
|
+
|
|
545
|
+
function renderAbilitySources(d) {
|
|
546
|
+
var box = document.getElementById('abilitySourcesBox');
|
|
547
|
+
if (!box) return;
|
|
548
|
+
var rows = (d.sources || []).map(function (s) {
|
|
549
|
+
var token = s.tokenVar
|
|
550
|
+
? (s.tokenSet ? '<span class="le-pill good">Token saved</span>' : '<span class="le-pill warn">No token</span>') + ' <span class="hint">' + escapeHtml(s.tokenVar) + '</span>'
|
|
551
|
+
: '<span class="hint">No token needed</span>';
|
|
552
|
+
return '<tr><td>' + escapeHtml(s.label) + '</td><td>' + token + '</td>' +
|
|
553
|
+
'<td><button type="button" class="delete-btn source-remove" data-id="' + escapeHtml(s.source.id) + '"' + (d.canSave ? '' : ' disabled') + '>Remove</button></td></tr>';
|
|
554
|
+
}).join('');
|
|
555
|
+
box.innerHTML =
|
|
556
|
+
(d.error ? '<p class="error">' + escapeHtml(d.error) + '</p>' : '') +
|
|
557
|
+
(rows
|
|
558
|
+
? '<table><thead><tr><th>Source</th><th>Token</th><th></th></tr></thead><tbody>' + rows + '</tbody></table>'
|
|
559
|
+
: '<p class="hint" style="margin-top:0">No private sources yet. Abilities from npm are always listed; add a source to also list your own.</p>') +
|
|
560
|
+
(d.canSave
|
|
561
|
+
? '<form id="sourceForm" class="source-form">' +
|
|
562
|
+
'<label>Kind<select name="type" id="sourceType">' + SOURCE_TYPES.map(function (t) { return '<option value="' + t[0] + '">' + t[1] + '</option>'; }).join('') + '</select></label>' +
|
|
563
|
+
'<p class="hint" id="sourceTypeHint" style="margin:0"></p>' +
|
|
564
|
+
'<label data-source-field="github">Org or user<input name="owner" placeholder="loopengine-co"></label>' +
|
|
565
|
+
'<label data-source-field="npm">Registry URL<input name="registry" placeholder="https://npm.pkg.github.com"></label>' +
|
|
566
|
+
'<label data-source-field="npm">Scope<input name="scope" placeholder="@your-company"></label>' +
|
|
567
|
+
'<label data-source-field="github npm">Token <span class="hint" id="sourceTokenHint"></span><input type="password" name="token" autocomplete="off" spellcheck="false"></label>' +
|
|
568
|
+
'<button type="submit">Add source</button>' +
|
|
569
|
+
'<div class="error" id="sourceError"></div>' +
|
|
570
|
+
'</form>'
|
|
571
|
+
: '<p class="hint">Adding a source needs <code>LOOPENGINE_ADMIN_AUTH</code> set, because it stores a token. Or edit <code>ability-sources.yml</code> and <code>.env</code>.</p>');
|
|
572
|
+
var form = document.getElementById('sourceForm');
|
|
573
|
+
if (!form) return;
|
|
574
|
+
var TOKEN_HINTS = {
|
|
575
|
+
github: '(needed for private repos: a token that can read their contents)',
|
|
576
|
+
npm: '(the registry' + String.fromCharCode(8217) + 's auth token, if it needs one)'
|
|
577
|
+
};
|
|
578
|
+
function showFields() {
|
|
579
|
+
var type = form.type.value;
|
|
580
|
+
var fields = form.querySelectorAll('[data-source-field]');
|
|
581
|
+
for (var i = 0; i < fields.length; i++) fields[i].style.display = fields[i].dataset.sourceField.split(' ').indexOf(type) !== -1 ? '' : 'none';
|
|
582
|
+
SOURCE_TYPES.forEach(function (t) { if (t[0] === type) document.getElementById('sourceTypeHint').textContent = t[2]; });
|
|
583
|
+
document.getElementById('sourceTokenHint').textContent = TOKEN_HINTS[type] || '';
|
|
584
|
+
}
|
|
585
|
+
form.type.addEventListener('change', showFields);
|
|
586
|
+
showFields();
|
|
587
|
+
form.addEventListener('submit', function (ev) {
|
|
588
|
+
ev.preventDefault();
|
|
589
|
+
var type = form.type.value;
|
|
590
|
+
var body = { type: type };
|
|
591
|
+
if (type === 'github') body.owner = form.owner.value;
|
|
592
|
+
if (type === 'npm') { body.registry = form.registry.value; body.scope = form.scope.value; }
|
|
593
|
+
if (form.token.value) body.token = form.token.value;
|
|
594
|
+
sendAbilitySources('PUT', '/config/ability-sources', body, document.getElementById('sourceError'));
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function sendAbilitySources(method, url, body, errorEl) {
|
|
599
|
+
if (errorEl) errorEl.textContent = '';
|
|
600
|
+
return fetch(url, { method: method, headers: { 'content-type': 'application/json' }, body: body ? JSON.stringify(body) : undefined })
|
|
601
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
602
|
+
.then(function (r) {
|
|
603
|
+
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
604
|
+
renderAbilitySources(r.body);
|
|
605
|
+
// The Abilities page lists what the sources offer; reload it next time.
|
|
606
|
+
loadedSections.abilities = false;
|
|
607
|
+
})
|
|
608
|
+
.catch(function (err) { if (errorEl) errorEl.textContent = err.message; });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function loadAbilitySources() {
|
|
612
|
+
fetch('/config/ability-sources')
|
|
613
|
+
.then(function (r) { return r.json(); })
|
|
614
|
+
.then(renderAbilitySources)
|
|
615
|
+
.catch(function (err) {
|
|
616
|
+
var box = document.getElementById('abilitySourcesBox');
|
|
617
|
+
if (box) box.innerHTML = '<p class="error">Could not load the sources: ' + escapeHtml(err.message) + '</p>';
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Remove a source: a second click confirms. Installed abilities stay.
|
|
622
|
+
document.getElementById('settingsContent').addEventListener('click', function (ev) {
|
|
623
|
+
var btn = ev.target.closest && ev.target.closest('.source-remove');
|
|
624
|
+
if (!btn) return;
|
|
625
|
+
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to remove'; return; }
|
|
626
|
+
btn.disabled = true;
|
|
627
|
+
sendAbilitySources('DELETE', '/config/ability-sources/' + encodeURIComponent(btn.dataset.id), null, null);
|
|
628
|
+
});
|
|
629
|
+
|
|
311
630
|
// ---- Time zone ----
|
|
312
631
|
|
|
313
632
|
function renderTimeZone(t) {
|
|
@@ -501,6 +820,7 @@ ${consoleShellScript}
|
|
|
501
820
|
content.innerHTML = renderSettings(d);
|
|
502
821
|
loadedSections.settings = true;
|
|
503
822
|
loadTimeZone();
|
|
823
|
+
loadAbilitySources();
|
|
504
824
|
loadUpgrade();
|
|
505
825
|
})
|
|
506
826
|
.catch(function (err) {
|
|
@@ -520,7 +840,7 @@ ${consoleShellScript}
|
|
|
520
840
|
panels[j].style.display = panels[j].dataset.sectionPanel === section ? 'contents' : 'none';
|
|
521
841
|
}
|
|
522
842
|
if (section === 'models' && !loadedSections.models) loadModels();
|
|
523
|
-
if (section === '
|
|
843
|
+
if (section === 'abilities' && !loadedSections.abilities) loadAbilities();
|
|
524
844
|
if (section === 'settings' && !loadedSections.settings) loadSettings();
|
|
525
845
|
}
|
|
526
846
|
|
|
@@ -528,12 +848,14 @@ ${consoleShellScript}
|
|
|
528
848
|
sectionButtons[i].addEventListener('click', function (ev) { switchSection(ev.currentTarget.dataset.section); });
|
|
529
849
|
}
|
|
530
850
|
|
|
531
|
-
// Deep-linked from
|
|
532
|
-
// links (/config?section=
|
|
851
|
+
// Deep-linked from the console sidebar's Models/Abilities/Settings
|
|
852
|
+
// links (/config?section=abilities, say) — preselect it if it names a
|
|
533
853
|
// real section, same "trust the query param only if it's valid"
|
|
534
854
|
// caution playground.ts's own ?agent= handling already uses.
|
|
535
855
|
var requestedSection = new URLSearchParams(location.search).get('section');
|
|
536
|
-
|
|
856
|
+
// ?section=gateways is the old Integrations page's link.
|
|
857
|
+
if (requestedSection === 'gateways') requestedSection = 'abilities';
|
|
858
|
+
var initialSection = requestedSection === 'abilities' || requestedSection === 'models' || requestedSection === 'settings' ? requestedSection : currentSection;
|
|
537
859
|
switchSection(initialSection);
|
|
538
860
|
})();
|
|
539
861
|
</script>
|