loopengine 0.1.95 → 0.1.98
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 +275 -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 +600 -220
- package/dist/web/agents-list-page.js +49 -20
- package/dist/web/console-shell.js +244 -3
- package/dist/web/dev-ui-styles.js +12 -0
- package/dist/web/global-config-page.js +348 -95
- 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
|
@@ -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); }
|
|
@@ -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>';
|
|
@@ -203,121 +220,263 @@ ${consoleShellScript}
|
|
|
203
220
|
}
|
|
204
221
|
|
|
205
222
|
// ---- Abilities section ----
|
|
206
|
-
// How agents reach outside apps
|
|
207
|
-
// installed
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
var
|
|
214
|
-
|
|
215
|
-
function
|
|
216
|
-
return '
|
|
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>';
|
|
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.
|
|
227
|
+
|
|
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>';
|
|
237
234
|
}
|
|
238
235
|
|
|
236
|
+
var lastSearch = { q: null, results: [] };
|
|
237
|
+
var lastPrivate = null;
|
|
238
|
+
var workspaceAbilities = null;
|
|
239
|
+
|
|
239
240
|
function renderAbilities() {
|
|
240
241
|
var content = document.getElementById('abilitiesContent');
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
'
|
|
256
|
-
|
|
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>';
|
|
257
264
|
var form = document.getElementById('abilitySearchForm');
|
|
258
265
|
form.addEventListener('submit', function (ev) { ev.preventDefault(); searchAbilities(form.q.value.trim()); });
|
|
259
|
-
document.getElementById('
|
|
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();
|
|
260
273
|
searchAbilities('');
|
|
274
|
+
loadWorkspaceAbilities();
|
|
275
|
+
loadPrivateAbilities();
|
|
261
276
|
}
|
|
262
277
|
|
|
263
|
-
|
|
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) === '.'; });
|
|
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
|
+
});
|
|
264
349
|
|
|
265
350
|
function renderSearchResults() {
|
|
266
351
|
var resultsEl = document.getElementById('abilitySearchResults');
|
|
267
|
-
var agent = document.getElementById('abilityAgent').value;
|
|
268
|
-
var installed = (abilitiesByAgent[agent] || []).map(function (a) { return a.name; });
|
|
269
352
|
var results = lastSearch.results;
|
|
270
353
|
resultsEl.innerHTML = results.length
|
|
271
354
|
? '<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
355
|
return '<tr><td><code>' + escapeHtml(pkg.name) + '</code> <span class="hint">' + escapeHtml(pkg.version) + '</span></td>' +
|
|
276
|
-
'<td>' + escapeHtml(pkg.description || '') + '</td
|
|
356
|
+
'<td>' + escapeHtml(pkg.description || '') + '</td>' +
|
|
357
|
+
'<td style="white-space:nowrap">' + detailsButton(pkg.name + '@' + pkg.version) + '</td></tr>';
|
|
277
358
|
}).join('') + '</tbody></table>'
|
|
278
359
|
: '<p class="hint">No published abilities match.</p>';
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
282
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('');
|
|
409
|
+
}
|
|
410
|
+
|
|
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')
|
|
424
|
+
.then(function (r) { return r.json(); })
|
|
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();
|
|
444
|
+
})
|
|
445
|
+
.catch(function (err) {
|
|
446
|
+
var el = document.getElementById('privateAbilities');
|
|
447
|
+
if (el) el.innerHTML = '<p class="error">Could not list private abilities: ' + escapeHtml(err.message) + '</p>';
|
|
448
|
+
});
|
|
283
449
|
}
|
|
284
450
|
|
|
285
451
|
function searchAbilities(q) {
|
|
286
452
|
var resultsEl = document.getElementById('abilitySearchResults');
|
|
287
453
|
if (lastSearch.q === q) { renderSearchResults(); return; }
|
|
288
454
|
resultsEl.innerHTML = '<p class="hint">Loading…</p>';
|
|
289
|
-
|
|
290
|
-
fetch('/agents/' + encodeURIComponent(abilityAgents[0]) + '/abilities/search' + (q ? '?q=' + encodeURIComponent(q) : ''))
|
|
455
|
+
fetch('/abilities/search' + (q ? '?q=' + encodeURIComponent(q) : ''))
|
|
291
456
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
292
457
|
.then(function (r) {
|
|
293
458
|
if (!r.ok) throw new Error(r.body.error || 'search failed');
|
|
294
459
|
lastSearch = { q: q, results: r.body.results || [] };
|
|
295
460
|
renderSearchResults();
|
|
461
|
+
scrollToPrivateIfLinked();
|
|
296
462
|
})
|
|
297
463
|
.catch(function (err) { resultsEl.innerHTML = '<p class="error">Could not search abilities: ' + escapeHtml(err.message) + '</p>'; });
|
|
298
464
|
}
|
|
299
465
|
|
|
300
|
-
function
|
|
466
|
+
function installIntoWorkspace(spec, btn) {
|
|
301
467
|
var status = document.getElementById('abilityInstallStatus');
|
|
302
468
|
btn.disabled = true;
|
|
303
469
|
status.className = 'hint';
|
|
304
|
-
status.textContent = 'Installing ' + spec + ' into ' +
|
|
305
|
-
fetch('/
|
|
306
|
-
method: 'POST',
|
|
307
|
-
headers: { 'content-type': 'application/json' },
|
|
308
|
-
body: JSON.stringify({ spec: spec }),
|
|
309
|
-
})
|
|
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 }) })
|
|
310
472
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
311
473
|
.then(function (r) {
|
|
312
474
|
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
document.getElementById('abilitiesInstalled').innerHTML = renderInstalledAbilities();
|
|
319
|
-
renderSearchResults();
|
|
320
|
-
});
|
|
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;
|
|
321
480
|
})
|
|
322
481
|
.catch(function (err) {
|
|
323
482
|
btn.disabled = false;
|
|
@@ -326,27 +485,22 @@ ${consoleShellScript}
|
|
|
326
485
|
});
|
|
327
486
|
}
|
|
328
487
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
});
|
|
334
499
|
}
|
|
335
500
|
|
|
336
501
|
function loadAbilities() {
|
|
337
|
-
|
|
338
|
-
|
|
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;
|
|
346
|
-
})
|
|
347
|
-
.catch(function (err) {
|
|
348
|
-
content.innerHTML = '<p class="error">Could not load abilities: ' + escapeHtml(err.message) + '</p>';
|
|
349
|
-
});
|
|
502
|
+
renderAbilities();
|
|
503
|
+
loadedSections.abilities = true;
|
|
350
504
|
}
|
|
351
505
|
|
|
352
506
|
// ---- Settings section ----
|
|
@@ -372,9 +526,107 @@ ${consoleShellScript}
|
|
|
372
526
|
rows.map(function (r) { return '<tr><td>' + r[0] + '</td><td>' + r[1] + '</td></tr>'; }).join('') +
|
|
373
527
|
'</tbody></table></section>' +
|
|
374
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>' +
|
|
375
530
|
'<section><h3>Updates</h3><div class="source" id="upgradeBox"><p class="hint">Checking for a newer LoopEngine' + String.fromCharCode(8230) + '</p></div></section>';
|
|
376
531
|
}
|
|
377
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
|
+
|
|
378
630
|
// ---- Time zone ----
|
|
379
631
|
|
|
380
632
|
function renderTimeZone(t) {
|
|
@@ -568,6 +820,7 @@ ${consoleShellScript}
|
|
|
568
820
|
content.innerHTML = renderSettings(d);
|
|
569
821
|
loadedSections.settings = true;
|
|
570
822
|
loadTimeZone();
|
|
823
|
+
loadAbilitySources();
|
|
571
824
|
loadUpgrade();
|
|
572
825
|
})
|
|
573
826
|
.catch(function (err) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentConfig } from '../core/agent-config.js';
|
|
2
2
|
export interface ModelProviderInfo {
|
|
3
|
-
provider: 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini';
|
|
3
|
+
provider: 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini' | 'xai' | 'meta';
|
|
4
4
|
envVar: string;
|
|
5
5
|
configured: boolean;
|
|
6
6
|
}
|
|
@@ -69,6 +69,49 @@ export declare function modelKeyEnvVar(provider: string): string | undefined;
|
|
|
69
69
|
*/
|
|
70
70
|
export declare function setModelProviderKey(provider: string, key: unknown): Promise<void>;
|
|
71
71
|
export declare function clearModelProviderKey(provider: string): Promise<void>;
|
|
72
|
+
export interface ProviderModelsView {
|
|
73
|
+
provider: string;
|
|
74
|
+
/** Model ids to offer, newest first where the provider says when each
|
|
75
|
+
* was released. Empty when they couldn't be listed (see `error`). */
|
|
76
|
+
models: string[];
|
|
77
|
+
/** Why the list is short or empty: no key saved (only common models,
|
|
78
|
+
* if any, are listed), or the provider refused or timed out. Shown
|
|
79
|
+
* under the dropdown. */
|
|
80
|
+
error?: string;
|
|
81
|
+
}
|
|
82
|
+
export declare const MODELS_FILE_ENV = "LOOPENGINE_MODELS_FILE";
|
|
83
|
+
/** models.yaml in the project root (where .env is), or the file
|
|
84
|
+
* LOOPENGINE_MODELS_FILE names (absolute, or relative to the root). */
|
|
85
|
+
export declare function modelsFilePath(): string;
|
|
86
|
+
/**
|
|
87
|
+
* The hand-edited model lists in models.yaml: provider name to the models
|
|
88
|
+
* its dropdown offers, in order.
|
|
89
|
+
*
|
|
90
|
+
* anthropic:
|
|
91
|
+
* - claude-opus-5-5
|
|
92
|
+
* - claude-sonnet-5-5
|
|
93
|
+
*
|
|
94
|
+
* Read fresh on every call (it's a few lines), so an edit shows the next
|
|
95
|
+
* time a dropdown loads. A missing file is no lists; a malformed one is
|
|
96
|
+
* reported in `error` and ignored as a whole, so a typo can't hide one
|
|
97
|
+
* provider's models while the rest still load.
|
|
98
|
+
*/
|
|
99
|
+
export declare function readModelsFile(): {
|
|
100
|
+
models: Partial<Record<ModelProviderInfo['provider'], string[]>>;
|
|
101
|
+
error?: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The models a provider offers, for the model dropdown in the New agent
|
|
105
|
+
* and Model forms. A provider listed in models.yaml (readModelsFile)
|
|
106
|
+
* offers exactly that list, with no key needed. Otherwise this asks the
|
|
107
|
+
* provider itself with the saved key (cached for ten minutes per key),
|
|
108
|
+
* so new models show up without a release. Never throws for a provider
|
|
109
|
+
* problem: the view carries an `error` instead, and the form lets the
|
|
110
|
+
* model name be typed. Throws ModelKeyError for an unknown provider.
|
|
111
|
+
*/
|
|
112
|
+
export declare function listProviderModels(provider: string, options?: {
|
|
113
|
+
fetch?: typeof fetch;
|
|
114
|
+
}): Promise<ProviderModelsView>;
|
|
72
115
|
export declare class TimeZoneError extends Error {
|
|
73
116
|
}
|
|
74
117
|
export interface TimeZoneView {
|