loopengine 0.1.98 → 0.1.99
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/dist/bin/ability-manager.d.ts +35 -2
- package/dist/bin/ability-manager.js +168 -35
- package/dist/core/ability-details.js +11 -0
- package/dist/core/mcp-abilities.d.ts +91 -0
- package/dist/core/mcp-abilities.js +400 -0
- package/dist/core/mcp-ability-generator.d.ts +59 -0
- package/dist/core/mcp-ability-generator.js +193 -0
- package/dist/core/mcp-apps.d.ts +95 -0
- package/dist/core/mcp-apps.js +254 -0
- package/dist/core/mcp-directory.d.ts +32 -0
- package/dist/core/mcp-directory.js +191 -0
- package/dist/core/run-agent.js +5 -2
- package/dist/core/system-agents/interviewer.d.ts +1 -1
- package/dist/core/system-agents/interviewer.js +1 -1
- package/dist/core/system-agents/templates.d.ts +11 -0
- package/dist/core/system-agents/templates.js +22 -16
- package/dist/core/system-agents/workflow-planner.d.ts +36 -3
- package/dist/core/system-agents/workflow-planner.js +23 -6
- package/dist/core/workspace-abilities.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/web/abilities-admin.d.ts +1 -1
- package/dist/web/abilities-admin.js +1 -1
- package/dist/web/agents-config-page.js +187 -11
- package/dist/web/console-shell.js +7 -5
- package/dist/web/dev-ui-styles.js +1 -1
- package/dist/web/global-config-page.js +293 -13
- package/dist/web/interview-admin.js +2 -0
- package/dist/web/templates-admin.d.ts +10 -2
- package/dist/web/templates-admin.js +36 -3
- package/dist/web/templates-page.js +1 -0
- package/dist/web/workflow-admin.d.ts +46 -0
- package/dist/web/workflow-admin.js +195 -3
- package/dist/web/workflow-page.js +33 -3
- package/package.json +10 -1
|
@@ -32,6 +32,7 @@ export const consoleSidebarHtml = `<aside class="le-sidebar" aria-label="Console
|
|
|
32
32
|
<a class="le-find" href="/workflows/new" data-le-nav="workflows">Describe a workflow</a>
|
|
33
33
|
<nav class="le-foot" aria-label="Settings">
|
|
34
34
|
<a href="/config?section=models" data-le-nav="models">Models</a>
|
|
35
|
+
<a href="/config?section=apps" data-le-nav="apps">Apps</a>
|
|
35
36
|
<a href="/config?section=abilities" data-le-nav="abilities">Abilities</a>
|
|
36
37
|
<a href="/config?section=settings" data-le-nav="settings">Settings</a>
|
|
37
38
|
<div class="le-who" id="leWho" hidden></div>
|
|
@@ -51,10 +52,11 @@ export const consoleShellScript = `<script>
|
|
|
51
52
|
// after the list loads.
|
|
52
53
|
var toSet = {};
|
|
53
54
|
// Tools and Skills live under Advanced (an agent's own tools and skills,
|
|
54
|
-
// Composio, subagents): what an agent can do is shown per
|
|
55
|
-
//
|
|
55
|
+
// Composio, subagents): what an agent can do is shown per app and
|
|
56
|
+
// ability on the Apps tab (id 'abilities', so older links still work).
|
|
57
|
+
// ?tab=tools and ?tab=skills still open Advanced.
|
|
56
58
|
var TABS = [
|
|
57
|
-
['overview', 'Overview'], ['chat', 'Chat'], ['abilities', '
|
|
59
|
+
['overview', 'Overview'], ['chat', 'Chat'], ['abilities', 'Apps'], ['actauth', 'Permissions'],
|
|
58
60
|
['messaging', 'Messaging'], ['schedules', 'Schedules'], ['env', 'Environment'], ['advanced', 'Advanced']
|
|
59
61
|
];
|
|
60
62
|
|
|
@@ -170,7 +172,7 @@ export const consoleShellScript = `<script>
|
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
var section = params.get('section');
|
|
173
|
-
var foot = page === 'config' ? (section === 'abilities' || section === 'gateways' ? 'abilities' : section === 'settings' ? 'settings' : 'models') : page === 'interview' || page === 'templates' ? page : '';
|
|
175
|
+
var foot = page === 'config' ? (section === 'abilities' || section === 'gateways' ? 'abilities' : section === 'apps' ? 'apps' : section === 'settings' ? 'settings' : 'models') : page === 'interview' || page === 'templates' ? page : '';
|
|
174
176
|
var footLinks = document.querySelectorAll('[data-le-nav]');
|
|
175
177
|
for (var i = 0; i < footLinks.length; i++) {
|
|
176
178
|
if (footLinks[i].getAttribute('data-le-nav') === foot) footLinks[i].setAttribute('aria-current', 'page');
|
|
@@ -348,7 +350,7 @@ export const consoleShellScript = `<script>
|
|
|
348
350
|
}
|
|
349
351
|
|
|
350
352
|
// ---- Abilities: where one came from, and what it can do ----
|
|
351
|
-
// Used by the Abilities page and each agent's
|
|
353
|
+
// Used by the Abilities page and each agent's Apps tab.
|
|
352
354
|
|
|
353
355
|
var DECISION_LABELS = { allow: 'Allow', ask: 'Ask me', deny: 'Never' };
|
|
354
356
|
|
|
@@ -108,7 +108,7 @@ export const devUiCss = `
|
|
|
108
108
|
.le-pill.ask, .le-pill.warn { background: var(--warn-soft); color: var(--warn); }
|
|
109
109
|
.le-pill.deny, .le-pill.bad { background: var(--bad-soft); color: var(--bad); }
|
|
110
110
|
/* An ability's details (leShell.toggleAbilityDetails), on the Abilities
|
|
111
|
-
page and each agent's
|
|
111
|
+
page and each agent's Apps tab. */
|
|
112
112
|
.ability-details-row > td { padding: 0 10px 14px; border-top: 0; background: transparent; }
|
|
113
113
|
.ability-details { border: 1px solid var(--accent); border-left-width: 4px; border-radius: var(--radius); background: var(--accent-soft); padding: 12px 16px 14px; }
|
|
114
114
|
.ability-details-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 16px; margin: 0 0 8px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
|
|
@@ -78,6 +78,32 @@ export const globalConfigPageHtml = `<!doctype html>
|
|
|
78
78
|
form.key-form button { font-size: 12px; }
|
|
79
79
|
.key-msg { font-size: 11px; margin-top: 4px; }
|
|
80
80
|
.key-msg.bad { color: var(--bad); }
|
|
81
|
+
.app-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
|
|
82
|
+
.app-tile { border: 1px solid var(--line); border-radius: var(--radius); padding: 12px; display: grid; gap: 8px; align-content: start; background: var(--bg); }
|
|
83
|
+
.app-head { display: flex; gap: 10px; align-items: center; min-width: 0; }
|
|
84
|
+
.app-head strong { overflow-wrap: anywhere; }
|
|
85
|
+
.app-icon { width: 32px; height: 32px; border-radius: 8px; flex: none; object-fit: contain; background: var(--surface); border: 1px solid var(--line); }
|
|
86
|
+
.app-letter { width: 32px; height: 32px; border-radius: 8px; flex: none; display: grid; place-items: center; font-weight: 700; font-size: 15px; background: var(--accent-soft, var(--surface)); color: var(--accent); border: 1px solid var(--line); }
|
|
87
|
+
.app-desc { font-size: 12.5px; color: var(--ink-muted); margin: 0; }
|
|
88
|
+
.app-foot { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
89
|
+
.app-form { display: grid; gap: 6px; }
|
|
90
|
+
.app-form input { font: inherit; font-size: 12px; font-family: ui-monospace, monospace; padding: 5px 7px; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); width: 100%; box-sizing: border-box; }
|
|
91
|
+
.app-row { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; padding: 10px 12px; border-top: 1px solid var(--line); }
|
|
92
|
+
.app-row:first-child { border-top: 0; }
|
|
93
|
+
.app-row-main { flex: 1; min-width: 200px; display: grid; gap: 3px; }
|
|
94
|
+
.app-list { border: 1px solid var(--line); border-radius: var(--radius); }
|
|
95
|
+
.app-tools { display: grid; gap: 8px; margin-top: 8px; font-size: 12.5px; }
|
|
96
|
+
.app-tools input[type=search] { font: inherit; font-size: 12px; padding: 5px 7px; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); max-width: 280px; }
|
|
97
|
+
.app-tool-list { display: grid; gap: 2px; max-height: 360px; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius); padding: 4px; }
|
|
98
|
+
.app-tool { display: flex; gap: 8px; align-items: flex-start; padding: 5px 6px; border-radius: 6px; cursor: pointer; }
|
|
99
|
+
.app-tool:hover { background: var(--surface); }
|
|
100
|
+
.app-tool input { margin-top: 3px; }
|
|
101
|
+
.app-tool code { overflow-wrap: anywhere; }
|
|
102
|
+
.app-tool-desc { display: block; }
|
|
103
|
+
.app-decision { font: inherit; font-size: 11.5px; padding: 1px 4px; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); }
|
|
104
|
+
.app-cats { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 10px; }
|
|
105
|
+
.app-cats button { font-size: 12px; }
|
|
106
|
+
.app-cats button.active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
|
81
107
|
</style>
|
|
82
108
|
</head>
|
|
83
109
|
<body data-le-page="config">
|
|
@@ -89,9 +115,14 @@ ${consoleSidebarHtml}
|
|
|
89
115
|
<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>
|
|
90
116
|
<div class="le-scroll"><div class="le-page" style="display:block"><div id="modelsContent"><p class="hint">Loading…</p></div></div></div>
|
|
91
117
|
</div>
|
|
118
|
+
<div class="section-panel" data-section-panel="apps" style="display:none">
|
|
119
|
+
<div class="le-head plain"><div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>
|
|
120
|
+
<div><h1>Apps</h1><p>Connect the apps your agents work in. All of a connected app’s tools become available to the workspace, each with a default permission; each agent then chooses which tools it gets, from its Apps tab.</p></div></div></div>
|
|
121
|
+
<div class="le-scroll"><div class="le-page" style="display:block"><div id="appsContent"><p class="hint">Loading…</p></div></div></div>
|
|
122
|
+
</div>
|
|
92
123
|
<div class="section-panel" data-section-panel="abilities" style="display:none">
|
|
93
124
|
<div class="le-head plain"><div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>
|
|
94
|
-
<div><h1>Abilities</h1><p>
|
|
125
|
+
<div><h1>Abilities</h1><p>What agents can do beyond your connected apps: search the web, read documents, schedule work, or your own code. An ability added to an agent brings its tools, skills and permission rules.</p></div></div></div>
|
|
95
126
|
<div class="le-scroll"><div class="le-page" style="display:block"><div id="abilitiesContent"><p class="hint">Loading…</p></div></div></div>
|
|
96
127
|
</div>
|
|
97
128
|
<div class="section-panel" data-section-panel="settings" style="display:none">
|
|
@@ -221,12 +252,12 @@ ${consoleShellScript}
|
|
|
221
252
|
|
|
222
253
|
// ---- Abilities section ----
|
|
223
254
|
// How agents reach outside apps. Public abilities (npm) are browsed
|
|
224
|
-
// here and installed from an agent's own
|
|
255
|
+
// here and installed from an agent's own Apps tab. A private one
|
|
225
256
|
// (from the sources in Settings) is installed here into the workspace
|
|
226
257
|
// (core/workspace-abilities.ts), then added to any agent from its tab.
|
|
227
258
|
|
|
228
259
|
// An ability's details (tools with their permissions, skills, settings)
|
|
229
|
-
// come from the console shell, shared with each agent's
|
|
260
|
+
// come from the console shell, shared with each agent's Apps tab.
|
|
230
261
|
var toggleAbilityDetails = leShell.toggleAbilityDetails;
|
|
231
262
|
|
|
232
263
|
function detailsButton(spec) {
|
|
@@ -240,13 +271,13 @@ ${consoleShellScript}
|
|
|
240
271
|
function renderAbilities() {
|
|
241
272
|
var content = document.getElementById('abilitiesContent');
|
|
242
273
|
content.innerHTML = '<section>' +
|
|
243
|
-
'<h4>Public <span class="hint">npm packages tagged loopengine-ability; add one to an agent from its
|
|
274
|
+
'<h4>Public <span class="hint">npm packages tagged loopengine-ability; add one to an agent from its Apps tab</span></h4>' +
|
|
244
275
|
'<form id="abilitySearchForm" style="display:flex;gap:6px;margin:0 0 10px;max-width:560px">' +
|
|
245
276
|
'<input type="search" name="q" placeholder="Search abilities, e.g. gmail" aria-label="Search public abilities" style="flex:1">' +
|
|
246
277
|
'<button type="submit">Search</button>' +
|
|
247
278
|
'</form>' +
|
|
248
279
|
'<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
|
|
280
|
+
'<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 Apps tab</span></h4>' +
|
|
250
281
|
'<p id="abilityInstallStatus" class="hint"></p>' +
|
|
251
282
|
'<div id="workspaceAbilities"></div>' +
|
|
252
283
|
'<div id="privateAbilities"><p class="hint">Loading…</p></div>' +
|
|
@@ -296,7 +327,7 @@ ${consoleShellScript}
|
|
|
296
327
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
297
328
|
.then(function (r) {
|
|
298
329
|
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
|
|
330
|
+
status.textContent = 'Uploaded ' + label + ' into the workspace. Add it to an agent from the agent' + String.fromCharCode(8217) + 's Apps tab.';
|
|
300
331
|
workspaceAbilities = r.body.abilities || [];
|
|
301
332
|
renderWorkspaceAbilities();
|
|
302
333
|
renderPrivateAbilities();
|
|
@@ -367,14 +398,17 @@ ${consoleShellScript}
|
|
|
367
398
|
function renderWorkspaceAbilities() {
|
|
368
399
|
var el = document.getElementById('workspaceAbilities');
|
|
369
400
|
if (!el || !workspaceAbilities) return;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
401
|
+
// Abilities made from a connected app are listed under Apps.
|
|
402
|
+
var fromApps = workspaceAbilities.filter(function (a) { return a.app || a.source.indexOf('mcp:') === 0; }).length;
|
|
403
|
+
var own = workspaceAbilities.filter(function (a) { return !a.app && a.source.indexOf('mcp:') !== 0; });
|
|
404
|
+
el.innerHTML = (fromApps ? '<p class="hint" style="margin:0 0 8px">' + fromApps + ' connected app' + (fromApps === 1 ? '' : 's') + ' also in this workspace: see <a href="/config?section=apps">Apps</a>.</p>' : '') + (own.length
|
|
405
|
+
? '<div class="source"><p style="margin:0 0 8px"><strong>In this workspace</strong> <span class="hint">add these to an agent from its Apps tab</span></p>' +
|
|
406
|
+
'<table><thead><tr><th>Ability</th><th>From</th><th></th></tr></thead><tbody>' + own.map(function (a) {
|
|
373
407
|
return '<tr><td><code>' + escapeHtml(a.name) + '</code>' + (a.version ? ' <span class="hint">' + escapeHtml(a.version) + '</span>' : '') + '</td>' +
|
|
374
408
|
'<td><span class="hint">' + escapeHtml(a.source.indexOf('upload:') === 0 ? 'Uploaded ' + a.source.slice(7) : a.source) + '</span></td>' +
|
|
375
409
|
'<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
410
|
}).join('') + '</tbody></table></div>'
|
|
377
|
-
: '';
|
|
411
|
+
: '');
|
|
378
412
|
}
|
|
379
413
|
|
|
380
414
|
// Each source's abilities: Install puts one into the workspace; one
|
|
@@ -408,7 +442,7 @@ ${consoleShellScript}
|
|
|
408
442
|
}).join('');
|
|
409
443
|
}
|
|
410
444
|
|
|
411
|
-
// Linked from an agent's
|
|
445
|
+
// Linked from an agent's Apps tab (/config?section=abilities#private-abilities):
|
|
412
446
|
// once every list has loaded, so the public results don't push it down.
|
|
413
447
|
var scrolledToPrivate = false;
|
|
414
448
|
function scrollToPrivateIfLinked() {
|
|
@@ -472,7 +506,7 @@ ${consoleShellScript}
|
|
|
472
506
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
473
507
|
.then(function (r) {
|
|
474
508
|
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
|
|
509
|
+
status.textContent = 'Installed ' + spec + ' into the workspace. Add it to an agent from the agent' + String.fromCharCode(8217) + 's Apps tab.';
|
|
476
510
|
workspaceAbilities = r.body.abilities || [];
|
|
477
511
|
renderWorkspaceAbilities();
|
|
478
512
|
renderPrivateAbilities();
|
|
@@ -503,6 +537,251 @@ ${consoleShellScript}
|
|
|
503
537
|
loadedSections.abilities = true;
|
|
504
538
|
}
|
|
505
539
|
|
|
540
|
+
// ---- Apps section ----
|
|
541
|
+
// Popular apps' own MCP servers (core/mcp-apps.ts) and the connected
|
|
542
|
+
// ones. Connecting makes a workspace ability from the server; agents
|
|
543
|
+
// add it from their Apps tab.
|
|
544
|
+
|
|
545
|
+
var apps = null;
|
|
546
|
+
var appCategory = 'All';
|
|
547
|
+
// Open tool panels, by ability: { tools, filter, error, busy }.
|
|
548
|
+
var openTools = {};
|
|
549
|
+
var appSearch = { q: '', servers: null, error: '' };
|
|
550
|
+
|
|
551
|
+
function appLetter(title) {
|
|
552
|
+
return '<span class="app-letter" aria-hidden="true">' + escapeHtml((title || '?').charAt(0).toUpperCase()) + '</span>';
|
|
553
|
+
}
|
|
554
|
+
function appIcon(app) {
|
|
555
|
+
return app.icon
|
|
556
|
+
? '<img class="app-icon" src="' + escapeHtml(app.icon) + '" alt="" data-title="' + escapeHtml(app.title || '') + '" loading="lazy" referrerpolicy="no-referrer">'
|
|
557
|
+
: appLetter(app.title);
|
|
558
|
+
}
|
|
559
|
+
// An icon that doesn't load becomes the app's first letter.
|
|
560
|
+
document.getElementById('appsContent').addEventListener('error', function (ev) {
|
|
561
|
+
var img = ev.target;
|
|
562
|
+
if (!img.classList || !img.classList.contains('app-icon')) return;
|
|
563
|
+
var holder = document.createElement('span');
|
|
564
|
+
holder.innerHTML = appLetter(img.dataset.title);
|
|
565
|
+
img.replaceWith(holder.firstChild);
|
|
566
|
+
}, true);
|
|
567
|
+
|
|
568
|
+
function connectForm(id) {
|
|
569
|
+
return '<form class="app-form" data-connect="' + escapeHtml(id) + '">' +
|
|
570
|
+
'<input type="password" name="key" autocomplete="off" placeholder="API key, if it needs one" aria-label="API key">' +
|
|
571
|
+
'<div class="app-foot"><button type="submit">Connect</button><button type="button" class="app-cancel">Cancel</button></div>' +
|
|
572
|
+
'<p class="hint app-msg" style="margin:0"></p></form>';
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function renderApps() {
|
|
576
|
+
var el = document.getElementById('appsContent');
|
|
577
|
+
if (!apps) return;
|
|
578
|
+
var categories = ['All'];
|
|
579
|
+
apps.apps.forEach(function (a) { if (categories.indexOf(a.category) === -1) categories.push(a.category); });
|
|
580
|
+
var shown = apps.apps.filter(function (a) { return appCategory === 'All' || a.category === appCategory; });
|
|
581
|
+
el.innerHTML =
|
|
582
|
+
(apps.canConnect ? '' : '<p class="error">Connecting an app saves its key on this server, so it needs LOOPENGINE_ADMIN_AUTH set first.</p>') +
|
|
583
|
+
'<section><h3>Connected</h3>' + (apps.connected.length
|
|
584
|
+
? '<div class="app-list">' + apps.connected.map(function (c) {
|
|
585
|
+
var used = c.agents.length ? 'Used by ' + c.agents.map(escapeHtml).join(', ') : 'Not added to an agent yet: add <code>' + escapeHtml(c.ability) + '</code> from an agent' + String.fromCharCode(8217) + 's Apps tab';
|
|
586
|
+
return '<div class="app-row"><div class="app-head">' + appIcon(findApp(c) || c) + '</div>' +
|
|
587
|
+
'<div class="app-row-main"><div><strong>' + escapeHtml(c.title) + '</strong> <span class="hint">' + c.tools.length + ' tool' + (c.tools.length === 1 ? '' : 's') + ' · <code>' + escapeHtml(c.ability) + '</code></span></div>' +
|
|
588
|
+
'<div class="hint">' + used + '</div>' +
|
|
589
|
+
(c.keyEnv && !c.keySet ? '<div class="error" style="font-size:12px">Its key, ' + escapeHtml(c.keyEnv) + ', isn' + String.fromCharCode(8217) + 't set.</div>' : '') +
|
|
590
|
+
(openTools[c.ability] ? toolsPanel(c.ability) : '') +
|
|
591
|
+
'</div>' +
|
|
592
|
+
'<div class="app-foot"><button type="button" class="app-tools-btn" data-ability="' + escapeHtml(c.ability) + '">' + (openTools[c.ability] ? 'Hide tools' : 'Tools') + '</button> <button type="button" class="delete-btn app-disconnect" data-ability="' + escapeHtml(c.ability) + '">Disconnect</button></div></div>';
|
|
593
|
+
}).join('') + '</div>'
|
|
594
|
+
: '<p class="hint">No apps connected yet. Pick one below.</p>') + '</section>' +
|
|
595
|
+
'<section><h3>Popular apps</h3>' +
|
|
596
|
+
'<div class="app-cats">' + categories.map(function (c) { return '<button type="button" class="' + (c === appCategory ? 'active' : '') + '" data-app-cat="' + escapeHtml(c) + '">' + escapeHtml(c) + '</button>'; }).join('') + '</div>' +
|
|
597
|
+
'<div class="app-grid">' + shown.map(function (a) {
|
|
598
|
+
return '<div class="app-tile" data-app="' + escapeHtml(a.id) + '"><div class="app-head">' + appIcon(a) + '<div><strong>' + escapeHtml(a.title) + '</strong><div class="hint">' + escapeHtml(a.category) + '</div></div></div>' +
|
|
599
|
+
'<p class="app-desc">' + escapeHtml(a.description) + '</p>' +
|
|
600
|
+
'<div class="app-slot">' + (a.connected ? '<span class="badge badge-custom">Connected</span>' : '<button type="button" class="app-connect" data-id="' + escapeHtml(a.id) + '"' + (apps.canConnect ? '' : ' disabled') + '>Connect</button>') + '</div></div>';
|
|
601
|
+
}).join('') + '</div>' +
|
|
602
|
+
'<p class="hint">Each is the app' + String.fromCharCode(8217) + 's own MCP server. Apps that only offer a sign-in (OAuth) can' + String.fromCharCode(8217) + 't be connected yet; their Connect says so.</p>' +
|
|
603
|
+
'</section>' +
|
|
604
|
+
'<section><h3>More apps</h3>' +
|
|
605
|
+
'<form id="appSearchForm" style="display:flex;gap:6px;margin:0 0 10px;max-width:560px"><input type="search" name="q" placeholder="Search the MCP Registry, e.g. calendly" aria-label="Search apps" style="flex:1" value="' + escapeHtml(appSearch.q) + '"><button type="submit">Search</button></form>' +
|
|
606
|
+
'<div id="appSearchResults">' + renderAppSearch() + '</div>' +
|
|
607
|
+
'</section>';
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
var DECISION_LABEL = { allow: 'Allow', ask: 'Ask me', deny: 'Never' };
|
|
611
|
+
|
|
612
|
+
function toolsPanel(ability) {
|
|
613
|
+
var panel = openTools[ability];
|
|
614
|
+
if (panel.error) return '<div class="app-tools"><p class="error" style="margin:0">' + escapeHtml(panel.error) + '</p></div>';
|
|
615
|
+
if (!panel.tools) return '<div class="app-tools"><p class="hint" style="margin:0">Asking the app for its tools' + String.fromCharCode(8230) + '</p></div>';
|
|
616
|
+
var used = panel.tools.filter(function (t) { return t.usedBy.length; }).length;
|
|
617
|
+
var f = (panel.filter || '').toLowerCase();
|
|
618
|
+
var shown = panel.tools.filter(function (t) { return !f || (t.remote + ' ' + t.description).toLowerCase().indexOf(f) !== -1; });
|
|
619
|
+
return '<div class="app-tools">' +
|
|
620
|
+
'<p class="hint" style="margin:0">All ' + panel.tools.length + ' tools are available to this workspace; ' + used + ' are used by an agent. Agents choose their own tools and permissions when you add the app to them (their Apps tab). The permission here is the default an agent starts from: Allow runs it, Ask me waits for your OK, Never blocks it.</p>' +
|
|
621
|
+
'<div class="app-foot">' + (panel.tools.length > 12 ? '<input type="search" class="app-tools-filter" data-ability="' + escapeHtml(ability) + '" placeholder="Filter tools" value="' + escapeHtml(panel.filter || '') + '" aria-label="Filter tools">' : '') +
|
|
622
|
+
'<button type="button" class="app-refresh" data-ability="' + escapeHtml(ability) + '"' + (apps.canConnect && !panel.busy ? '' : ' disabled') + '>Check for new tools</button></div>' +
|
|
623
|
+
(panel.message ? '<p class="' + (panel.messageBad ? 'error' : 'hint') + '" style="margin:0">' + escapeHtml(panel.message) + '</p>' : '') +
|
|
624
|
+
'<div class="app-tool-list">' + shown.map(function (t) {
|
|
625
|
+
var who = t.usedBy.length
|
|
626
|
+
? 'Used by ' + t.usedBy.map(function (u) { return escapeHtml(u.agent) + ' (' + escapeHtml(DECISION_LABEL[u.decision]) + ')'; }).join(', ')
|
|
627
|
+
: 'Not used by an agent';
|
|
628
|
+
return '<div class="app-tool"><span><code>' + escapeHtml(t.name) + '</code> ' +
|
|
629
|
+
'<select class="app-decision" data-tool="' + escapeHtml(t.remote) + '" data-ability="' + escapeHtml(ability) + '" aria-label="Default permission for ' + escapeHtml(t.name) + '"' + (apps.canConnect && !panel.busy ? '' : ' disabled') + '>' +
|
|
630
|
+
['allow', 'ask', 'deny'].map(function (d) { return '<option value="' + d + '"' + (d === t.decision ? ' selected' : '') + '>' + DECISION_LABEL[d] + '</option>'; }).join('') + '</select>' +
|
|
631
|
+
(t.destructive ? ' <span class="hint">destructive</span>' : t.readOnly ? ' <span class="hint">read-only</span>' : '') +
|
|
632
|
+
(t.description ? '<span class="hint app-tool-desc">' + escapeHtml(t.description) + '</span>' : '') +
|
|
633
|
+
'<span class="hint app-tool-desc">' + who + '</span></span></div>';
|
|
634
|
+
}).join('') + '</div></div>';
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function loadTools(ability, fresh) {
|
|
638
|
+
openTools[ability] = { tools: null, filter: openTools[ability] ? openTools[ability].filter : '' };
|
|
639
|
+
renderApps();
|
|
640
|
+
fetch('/config/apps/tools?ability=' + encodeURIComponent(ability) + (fresh ? '&fresh=1' : ''), { cache: 'no-store' })
|
|
641
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
642
|
+
.then(function (r) {
|
|
643
|
+
if (!openTools[ability]) return;
|
|
644
|
+
if (!r.ok) openTools[ability].error = r.body.error || 'Could not list its tools.';
|
|
645
|
+
else openTools[ability].tools = r.body.tools;
|
|
646
|
+
renderApps();
|
|
647
|
+
})
|
|
648
|
+
.catch(function (err) { if (openTools[ability]) { openTools[ability].error = err.message; renderApps(); } });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Sets a tool's default permission, or (tool null) checks the server
|
|
652
|
+
// for tools it added since.
|
|
653
|
+
function setDefault(ability, tool, decision) {
|
|
654
|
+
var panel = openTools[ability];
|
|
655
|
+
panel.busy = true;
|
|
656
|
+
panel.message = (tool ? 'Setting ' + tool + String.fromCharCode(8217) + 's default to ' + DECISION_LABEL[decision] : 'Checking the app for new tools') + String.fromCharCode(8230);
|
|
657
|
+
panel.messageBad = false;
|
|
658
|
+
renderApps();
|
|
659
|
+
var path = tool ? '/config/apps/tools' : '/config/apps/refresh';
|
|
660
|
+
fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ability: ability, tool: tool, decision: decision }) })
|
|
661
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
662
|
+
.then(function (r) {
|
|
663
|
+
panel.busy = false;
|
|
664
|
+
if (!r.ok) { panel.message = r.body.error || 'Could not change it.'; panel.messageBad = true; renderApps(); return; }
|
|
665
|
+
panel.tools = r.body.tools;
|
|
666
|
+
if (r.body.connected) apps = { apps: r.body.apps, connected: r.body.connected, canConnect: r.body.canConnect };
|
|
667
|
+
panel.message = tool
|
|
668
|
+
? tool + ' now starts at ' + DECISION_LABEL[decision] + ' for agents that add it; agents that have it keep theirs.'
|
|
669
|
+
: (r.body.added.length || r.body.removed.length
|
|
670
|
+
? (r.body.added.length ? 'New: ' + r.body.added.join(', ') + '. ' : '') + (r.body.removed.length ? 'No longer offered: ' + r.body.removed.join(', ') + '.' : '')
|
|
671
|
+
: 'No new tools.');
|
|
672
|
+
renderApps();
|
|
673
|
+
})
|
|
674
|
+
.catch(function (err) { panel.busy = false; panel.message = err.message; panel.messageBad = true; renderApps(); });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function findApp(c) {
|
|
678
|
+
for (var i = 0; i < apps.apps.length; i++) if (apps.apps[i].id === c.id || apps.apps[i].url === c.url) return apps.apps[i];
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function renderAppSearch() {
|
|
683
|
+
if (appSearch.error) return '<p class="error">' + escapeHtml(appSearch.error) + '</p>';
|
|
684
|
+
if (!appSearch.servers) return '<p class="hint">Over a thousand apps publish an MCP server. Verified means it' + String.fromCharCode(8217) + 's on its publisher' + String.fromCharCode(8217) + 's own domain; only add a key to one you trust.</p>';
|
|
685
|
+
if (!appSearch.servers.length) return '<p class="hint">Nothing found for ' + escapeHtml(appSearch.q) + '.</p>';
|
|
686
|
+
return '<div class="app-list">' + appSearch.servers.map(function (s) {
|
|
687
|
+
var connected = apps.connected.some(function (c) { return c.url === s.url; });
|
|
688
|
+
return '<div class="app-row" data-app="' + escapeHtml(s.id) + '"><div class="app-head">' + appIcon(s) + '</div>' +
|
|
689
|
+
'<div class="app-row-main"><div><strong>' + escapeHtml(s.title) + '</strong> ' + (s.verified ? '<span class="badge badge-custom">Verified</span>' : '<span class="badge badge-default">Unverified</span>') + '</div>' +
|
|
690
|
+
'<div class="hint">' + escapeHtml(s.url) + (s.description ? ' · ' + escapeHtml(s.description) : '') + '</div></div>' +
|
|
691
|
+
'<div class="app-slot">' + (connected ? '<span class="badge badge-custom">Connected</span>' : '<button type="button" class="app-connect" data-id="' + escapeHtml(s.id) + '"' + (apps.canConnect ? '' : ' disabled') + '>Connect</button>') + '</div></div>';
|
|
692
|
+
}).join('') + '</div>';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function loadApps() {
|
|
696
|
+
loadedSections.apps = true;
|
|
697
|
+
fetch('/config/apps', { cache: 'no-store' })
|
|
698
|
+
.then(function (r) { return r.json(); })
|
|
699
|
+
.then(function (d) { apps = d; renderApps(); })
|
|
700
|
+
.catch(function (err) { document.getElementById('appsContent').innerHTML = '<p class="error">Could not load apps: ' + escapeHtml(err.message) + '</p>'; });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
document.getElementById('appsContent').addEventListener('click', function (ev) {
|
|
704
|
+
var t = ev.target;
|
|
705
|
+
if (t.dataset && t.dataset.appCat) { appCategory = t.dataset.appCat; renderApps(); return; }
|
|
706
|
+
if (t.classList.contains('app-refresh')) { setDefault(t.dataset.ability, null, null); return; }
|
|
707
|
+
if (t.classList.contains('app-tools-btn')) {
|
|
708
|
+
if (openTools[t.dataset.ability]) { delete openTools[t.dataset.ability]; renderApps(); }
|
|
709
|
+
else loadTools(t.dataset.ability, false);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (t.classList.contains('app-connect')) {
|
|
713
|
+
var slot = t.parentNode;
|
|
714
|
+
slot.innerHTML = connectForm(t.dataset.id);
|
|
715
|
+
slot.querySelector('input').focus();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (t.classList.contains('app-cancel')) { renderApps(); return; }
|
|
719
|
+
if (t.classList.contains('app-disconnect')) {
|
|
720
|
+
if (t.dataset.confirming !== '1') { t.dataset.confirming = '1'; t.textContent = 'Click again to disconnect'; return; }
|
|
721
|
+
t.disabled = true;
|
|
722
|
+
fetch('/config/apps/connect?ability=' + encodeURIComponent(t.dataset.ability), { method: 'DELETE' })
|
|
723
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
724
|
+
.then(function (r) {
|
|
725
|
+
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
726
|
+
apps = r.body;
|
|
727
|
+
renderApps();
|
|
728
|
+
})
|
|
729
|
+
.catch(function (err) { t.disabled = false; t.textContent = 'Could not disconnect: ' + err.message; });
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
document.getElementById('appsContent').addEventListener('change', function (ev) {
|
|
734
|
+
var t = ev.target;
|
|
735
|
+
if (t.classList.contains('app-decision')) setDefault(t.dataset.ability, t.dataset.tool, t.value);
|
|
736
|
+
});
|
|
737
|
+
document.getElementById('appsContent').addEventListener('input', function (ev) {
|
|
738
|
+
var t = ev.target;
|
|
739
|
+
if (!t.classList.contains('app-tools-filter')) return;
|
|
740
|
+
openTools[t.dataset.ability].filter = t.value;
|
|
741
|
+
renderApps();
|
|
742
|
+
var again = document.querySelector('.app-tools-filter[data-ability="' + t.dataset.ability + '"]');
|
|
743
|
+
if (again) { again.focus(); again.setSelectionRange(again.value.length, again.value.length); }
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
document.getElementById('appsContent').addEventListener('submit', function (ev) {
|
|
747
|
+
ev.preventDefault();
|
|
748
|
+
var form = ev.target;
|
|
749
|
+
if (form.id === 'appSearchForm') {
|
|
750
|
+
var q = form.q.value.trim();
|
|
751
|
+
appSearch = { q: q, servers: null, error: '' };
|
|
752
|
+
document.getElementById('appSearchResults').innerHTML = '<p class="hint">Searching' + String.fromCharCode(8230) + '</p>';
|
|
753
|
+
fetch('/config/apps/search?q=' + encodeURIComponent(q))
|
|
754
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
755
|
+
.then(function (r) {
|
|
756
|
+
if (!r.ok) throw new Error(r.body.error || 'search failed');
|
|
757
|
+
appSearch.servers = r.body.servers || [];
|
|
758
|
+
document.getElementById('appSearchResults').innerHTML = renderAppSearch();
|
|
759
|
+
})
|
|
760
|
+
.catch(function (err) { appSearch.error = 'Could not search: ' + err.message; document.getElementById('appSearchResults').innerHTML = renderAppSearch(); });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!form.dataset.connect) return;
|
|
764
|
+
var btn = form.querySelector('button[type=submit]');
|
|
765
|
+
var msg = form.querySelector('.app-msg');
|
|
766
|
+
btn.disabled = true;
|
|
767
|
+
msg.className = 'hint app-msg';
|
|
768
|
+
msg.textContent = 'Connecting' + String.fromCharCode(8230) + ' it looks at the app' + String.fromCharCode(8217) + 's tools and sets their permissions, which can take a minute.';
|
|
769
|
+
fetch('/config/apps/connect', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: form.dataset.connect, key: form.key.value }) })
|
|
770
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
771
|
+
.then(function (r) {
|
|
772
|
+
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
773
|
+
apps = r.body;
|
|
774
|
+
// Straight to its tools, to choose what agents may use.
|
|
775
|
+
if (r.body.connectedAbility) loadTools(r.body.connectedAbility, false);
|
|
776
|
+
else renderApps();
|
|
777
|
+
})
|
|
778
|
+
.catch(function (err) {
|
|
779
|
+
btn.disabled = false;
|
|
780
|
+
msg.className = 'error app-msg';
|
|
781
|
+
msg.textContent = err.message;
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
506
785
|
// ---- Settings section ----
|
|
507
786
|
|
|
508
787
|
function renderSettings(d) {
|
|
@@ -841,6 +1120,7 @@ ${consoleShellScript}
|
|
|
841
1120
|
}
|
|
842
1121
|
if (section === 'models' && !loadedSections.models) loadModels();
|
|
843
1122
|
if (section === 'abilities' && !loadedSections.abilities) loadAbilities();
|
|
1123
|
+
if (section === 'apps' && !loadedSections.apps) loadApps();
|
|
844
1124
|
if (section === 'settings' && !loadedSections.settings) loadSettings();
|
|
845
1125
|
}
|
|
846
1126
|
|
|
@@ -855,7 +1135,7 @@ ${consoleShellScript}
|
|
|
855
1135
|
var requestedSection = new URLSearchParams(location.search).get('section');
|
|
856
1136
|
// ?section=gateways is the old Integrations page's link.
|
|
857
1137
|
if (requestedSection === 'gateways') requestedSection = 'abilities';
|
|
858
|
-
var initialSection = requestedSection === 'abilities' || requestedSection === 'models' || requestedSection === 'settings' ? requestedSection : currentSection;
|
|
1138
|
+
var initialSection = requestedSection === 'abilities' || requestedSection === 'apps' || requestedSection === 'models' || requestedSection === 'settings' ? requestedSection : currentSection;
|
|
859
1139
|
switchSection(initialSection);
|
|
860
1140
|
})();
|
|
861
1141
|
</script>
|
|
@@ -125,6 +125,8 @@ export function setupChecklist(candidate, agentName) {
|
|
|
125
125
|
switch (need.kind) {
|
|
126
126
|
case 'ability':
|
|
127
127
|
return { label: `Install the ${need.name ?? need.description} ability`, href: tab('abilities') };
|
|
128
|
+
case 'app':
|
|
129
|
+
return { label: `Connect ${need.name ?? need.description} on the Apps page, then add it from the agent's Apps tab`, href: '/config?section=apps' };
|
|
128
130
|
case 'api':
|
|
129
131
|
return { label: `Connect ${need.description} (an HTTP tool, or an MCP server)`, href: tab('advanced') };
|
|
130
132
|
case 'key':
|
|
@@ -10,7 +10,12 @@ export interface TemplateCreateDeps {
|
|
|
10
10
|
/** Installs an ability (an npm spec) into the agent. Absent where
|
|
11
11
|
* installing isn't allowed (no admin login): the abilities then head the
|
|
12
12
|
* checklist instead. */
|
|
13
|
-
installAbility?: (name: string, spec: string
|
|
13
|
+
installAbility?: (name: string, spec: string, options?: {
|
|
14
|
+
mcpTools?: string[];
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
/** The workspace ability of a connected app (by catalog id), if it's
|
|
17
|
+
* connected. */
|
|
18
|
+
prepareApp?: (id: string) => Promise<string | undefined>;
|
|
14
19
|
/** A setting as the agent sees it (its own, else the shared one). */
|
|
15
20
|
envValue(name: string, variable: string): string | undefined;
|
|
16
21
|
/** Whether approval requests from the agent reach anyone. */
|
|
@@ -37,12 +42,15 @@ export interface TemplateSummary {
|
|
|
37
42
|
trigger: string;
|
|
38
43
|
asksFirst: string[];
|
|
39
44
|
abilities: string[];
|
|
45
|
+
apps: string[];
|
|
40
46
|
schedule: string | null;
|
|
41
47
|
}
|
|
42
48
|
export interface TemplatePreview {
|
|
43
49
|
name: string;
|
|
44
50
|
systemPrompt: string;
|
|
45
51
|
abilities: string[];
|
|
52
|
+
/** Apps it uses, by title. */
|
|
53
|
+
apps: string[];
|
|
46
54
|
skills: string[];
|
|
47
55
|
rules: {
|
|
48
56
|
tool: string;
|
|
@@ -88,7 +96,7 @@ export declare function previewTemplate(id: string, deps: Pick<TemplateCreateDep
|
|
|
88
96
|
export declare function templateChecklist(template: StarterTemplate, name: string, deps: Pick<TemplateCreateDeps, 'envValue' | 'messagingConfigured'>, failed?: {
|
|
89
97
|
name: string;
|
|
90
98
|
error: string;
|
|
91
|
-
}[]): TemplateChecklistItem[];
|
|
99
|
+
}[], appsToConnect?: string[]): TemplateChecklistItem[];
|
|
92
100
|
/** Creates a new agent from the template, with its whole setup, under the
|
|
93
101
|
* first free name, so the same template can be used more than once. */
|
|
94
102
|
export declare function createFromTemplate(id: string, deps: TemplateCreateDeps | LegacyTemplateCreateDeps): Promise<CreatedTemplateAgent>;
|
|
@@ -24,7 +24,7 @@ export function installFailureReason(err) {
|
|
|
24
24
|
if (/E404|404 Not Found|is not in this registry/i.test(text))
|
|
25
25
|
return "it isn't published on npm yet";
|
|
26
26
|
if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN/.test(text))
|
|
27
|
-
return 'npm could not be reached; try again from the
|
|
27
|
+
return 'npm could not be reached; try again from the Apps tab';
|
|
28
28
|
const line = text.split('\n').map((l) => l.trim()).find((l) => l && !/^Command failed:/.test(l)) ?? text.split('\n')[0];
|
|
29
29
|
return line.length > 200 ? `${line.slice(0, 200)}…` : line;
|
|
30
30
|
}
|
|
@@ -57,6 +57,7 @@ export function listTemplates() {
|
|
|
57
57
|
trigger: setup.schedule ? describeTemplateSchedule(setup.schedule) : c.trigger.description,
|
|
58
58
|
asksFirst: c.asksFirst,
|
|
59
59
|
abilities: setup.abilities,
|
|
60
|
+
apps: (setup.apps ?? []).map((a) => a.title),
|
|
60
61
|
schedule: setup.schedule ? describeTemplateSchedule(setup.schedule) : null,
|
|
61
62
|
}));
|
|
62
63
|
}
|
|
@@ -72,6 +73,7 @@ export function previewTemplate(id, deps) {
|
|
|
72
73
|
name,
|
|
73
74
|
systemPrompt: template.candidate.agent.systemPrompt,
|
|
74
75
|
abilities: setup.abilities,
|
|
76
|
+
apps: (setup.apps ?? []).map((a) => a.title),
|
|
75
77
|
skills: (setup.skills ?? []).map((s) => s.id),
|
|
76
78
|
rules: setup.rules.map((r) => ({ tool: r.tool, decision: r.decision })),
|
|
77
79
|
// A key set for the whole project already counts for a new agent.
|
|
@@ -83,9 +85,11 @@ export function previewTemplate(id, deps) {
|
|
|
83
85
|
};
|
|
84
86
|
}
|
|
85
87
|
/** What's left once the agent exists, each linked to where it's done. */
|
|
86
|
-
export function templateChecklist(template, name, deps, failed = []) {
|
|
88
|
+
export function templateChecklist(template, name, deps, failed = [], appsToConnect = []) {
|
|
87
89
|
const tab = (t) => `/agents/config?agent=${encodeURIComponent(name)}&tab=${t}`;
|
|
88
90
|
const items = [];
|
|
91
|
+
for (const title of appsToConnect)
|
|
92
|
+
items.push({ label: `Connect ${title} on the Apps page, then add it to ${name} from its Apps tab`, href: '/config?section=apps' });
|
|
89
93
|
for (const f of failed)
|
|
90
94
|
items.push({ label: `Install the ${f.name} ability (it couldn't be installed: ${f.error})`, href: tab('abilities') });
|
|
91
95
|
const missing = keyGroups(template).filter((names) => !names.some((n) => deps.envValue(name, n)));
|
|
@@ -147,6 +151,35 @@ export async function createFromTemplateObject(template, deps) {
|
|
|
147
151
|
failed.push({ name: spec, error: installFailureReason(err) });
|
|
148
152
|
}
|
|
149
153
|
}
|
|
154
|
+
// Apps: a connected one is added with just the tools the template relies
|
|
155
|
+
// on; one not connected heads the checklist.
|
|
156
|
+
const appsToConnect = [];
|
|
157
|
+
for (const app of setup.apps ?? []) {
|
|
158
|
+
let spec;
|
|
159
|
+
try {
|
|
160
|
+
spec = deps.prepareApp ? await deps.prepareApp(app.id) : undefined;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
failed.push({ name: app.title, error: err instanceof Error ? err.message : String(err) });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!spec) {
|
|
167
|
+
appsToConnect.push(app.title);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!deps.installAbility) {
|
|
171
|
+
failed.push({ name: app.title, error: 'adding apps needs LOOPENGINE_ADMIN_AUTH set on the server' });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
// Just the tools the template relies on.
|
|
176
|
+
await deps.installAbility(name, spec, { mcpTools: app.tools });
|
|
177
|
+
installed.push(app.title);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
failed.push({ name: app.title, error: installFailureReason(err) });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
150
183
|
// 3. Its own skills and non-secret settings.
|
|
151
184
|
for (const skill of setup.skills ?? [])
|
|
152
185
|
writeSkill(name, skill.id, { description: skill.description, body: skill.body });
|
|
@@ -182,7 +215,7 @@ export async function createFromTemplateObject(template, deps) {
|
|
|
182
215
|
return {
|
|
183
216
|
name,
|
|
184
217
|
abilities: { installed, failed },
|
|
185
|
-
checklist: templateChecklist(template, name, deps, failed),
|
|
218
|
+
checklist: templateChecklist(template, name, deps, failed, appsToConnect),
|
|
186
219
|
...(candidate.agent.exampleMessage ? { exampleMessage: candidate.agent.exampleMessage } : {}),
|
|
187
220
|
...(schedule ? { schedule } : {}),
|
|
188
221
|
...(scheduleError ? { scheduleError } : {}),
|
|
@@ -111,6 +111,7 @@ ${consoleShellScript}
|
|
|
111
111
|
'<div><strong>Installs</strong> <span class="hint">(abilities: their tools, skills and permission rules)</span>' +
|
|
112
112
|
listHtml(p.abilities.map(function (a) { return '<li><code>' + esc(a) + '</code></li>'; })) +
|
|
113
113
|
(canInstall ? '' : '<p class="error" style="margin:4px 0 0">Installing abilities needs <code>LOOPENGINE_ADMIN_AUTH</code> set on the server. Without it, the agent is created and its abilities head the checklist.</p>') + '</div>' +
|
|
114
|
+
(p.apps && p.apps.length ? '<div><strong>Apps it uses</strong> <span class="hint">(connected on the <a href="/config?section=apps">Apps</a> page; one that isn' + String.fromCharCode(8217) + 't connected yet heads the checklist)</span>' + listHtml(p.apps.map(function (a) { return '<li>' + esc(a) + '</li>'; })) + '</div>' : '') +
|
|
114
115
|
'<div><strong>Permissions</strong> <span class="hint">(anything not listed asks you first)</span>' +
|
|
115
116
|
listHtml(p.rules.map(function (rule) { return '<li><code>' + esc(rule.tool) + '</code>: ' + esc(DECISIONS[rule.decision] || rule.decision) + '</li>'; })) + '</div>' +
|
|
116
117
|
'<div><strong>Keys it needs</strong>' +
|