loopengine 0.3.8 → 0.3.10
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.js +5 -0
- package/dist/core/ability-sources.d.ts +3 -1
- package/dist/core/ability-sources.js +1 -1
- package/dist/core/mcp-directory.d.ts +16 -0
- package/dist/core/mcp-directory.js +96 -3
- package/dist/core/messaging/connectors/telegram/index.d.ts +4 -0
- package/dist/core/messaging/connectors/telegram/index.js +10 -1
- package/dist/core/model-calls/openai-model-call.d.ts +4 -0
- package/dist/core/model-calls/openai-model-call.js +12 -2
- package/dist/core/official-abilities.d.ts +14 -0
- package/dist/core/official-abilities.js +58 -0
- package/dist/core/system-agents/templates.d.ts +9 -0
- package/dist/core/system-agents/templates.js +127 -61
- package/dist/core/system-agents/workflow-planner.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/web/abilities-admin.js +18 -2
- package/dist/web/actauth-admin.d.ts +33 -1
- package/dist/web/actauth-admin.js +39 -2
- package/dist/web/agents-config-page.js +41 -36
- package/dist/web/console-shell.js +6 -5
- package/dist/web/global-config-page.js +82 -6
- package/dist/web/notifier-admin.d.ts +2 -0
- package/dist/web/notifier-admin.js +3 -1
- package/dist/web/playground.js +1 -1
- package/dist/web/templates-admin.js +25 -16
- package/dist/web/templates-page.js +6 -5
- package/dist/web/workflow-admin.js +8 -15
- package/dist/web/workflow-page.js +1 -1
- package/package.json +3 -1
|
@@ -438,10 +438,6 @@ ${consoleShellScript}
|
|
|
438
438
|
return attempt().catch(function () { return attempt(); });
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
function badge(label) {
|
|
442
|
-
return '<span class="badge ' + (label === 'custom' ? 'badge-custom' : 'badge-default') + '">' + escapeHtml(label) + '</span>';
|
|
443
|
-
}
|
|
444
|
-
|
|
445
441
|
// A small, deliberately non-CommonMark markdown-to-HTML renderer for
|
|
446
442
|
// the skill Body field's Preview toggle (see wireSkillsHandlers) — this
|
|
447
443
|
// page stays a self-contained string, no build step, no CDN (same
|
|
@@ -732,7 +728,7 @@ ${consoleShellScript}
|
|
|
732
728
|
'<span><a href="?agent=' + encodeURIComponent(agentName) + '&tab=schedules#schedule-' + escapeHtml(x.taskId) + '" class="workflow-schedule-link" data-task="' + escapeHtml(x.taskId) + '" title="See it in the Schedules tab">' +
|
|
733
729
|
escapeHtml(x.describe.charAt(0).toUpperCase() + x.describe.slice(1)) + '</a></span>' + state +
|
|
734
730
|
(values ? '<span class="hint">' + values + '</span>' : '') +
|
|
735
|
-
(x.notify ? '<span class="hint">
|
|
731
|
+
(x.notify ? '<span class="hint">messages you the result when it finishes</span>' : '') +
|
|
736
732
|
'<span style="display:flex;gap:6px;margin-left:auto">' +
|
|
737
733
|
(x.status === 'paused' ? '<button type="button" class="workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="resume">Resume</button>'
|
|
738
734
|
: x.status === 'scheduled' ? '<button type="button" class="workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="pause">Pause</button>' : '') +
|
|
@@ -899,6 +895,7 @@ ${consoleShellScript}
|
|
|
899
895
|
if (!r.ok) { el.innerHTML = '<p class="error">Could not load workflows: ' + escapeHtml(r.body.error || 'unknown error') + '</p>'; return; }
|
|
900
896
|
var workflows = r.body.workflows || [];
|
|
901
897
|
fillWorkflowsCard(workflows);
|
|
898
|
+
leShell.setCount('workflows', workflows.length);
|
|
902
899
|
workflowsById = {};
|
|
903
900
|
workflows.forEach(function (w) { workflowsById[w.id] = w; });
|
|
904
901
|
el.innerHTML = renderWorkflowsHtml(name, workflows);
|
|
@@ -1026,7 +1023,7 @@ ${consoleShellScript}
|
|
|
1026
1023
|
if (t.filter) rules.push('only when ' + escapeHtml(t.filter.path) + ' is ' + t.filter.values.map(escapeHtml).join(' or '));
|
|
1027
1024
|
if (t.mapping) rules.push(Object.keys(t.mapping).map(function (p) { return escapeHtml(p) + ' ' + String.fromCharCode(8592) + ' ' + escapeHtml(t.mapping[p]); }).join(', '));
|
|
1028
1025
|
if (t.payload_param) rules.push('the whole payload goes to ' + escapeHtml(t.payload_param));
|
|
1029
|
-
if (t.notify) rules.push('
|
|
1026
|
+
if (t.notify) rules.push('messages you the result when it finishes');
|
|
1030
1027
|
if (t.callback_url) rules.push('reports to ' + escapeHtml(t.callback_url));
|
|
1031
1028
|
// Made by the workflow planner without its secret: it turns on once
|
|
1032
1029
|
// the sender's signing secret is added.
|
|
@@ -1072,7 +1069,7 @@ ${consoleShellScript}
|
|
|
1072
1069
|
params.map(function (p) { return '<div class="workflow-param-row"><code class="wp-name">' + escapeHtml(p) + '</code><input name="map:' + escapeHtml(p) + '" placeholder="data.object.id" style="flex:1"></div>'; }).join('') + '</div>' +
|
|
1073
1070
|
'<label>Or give the whole payload to <select name="payload_param"><option value="">(none)</option>' + params.map(function (p) { return '<option>' + escapeHtml(p) + '</option>'; }).join('') + '</select></label>'
|
|
1074
1071
|
: '<p class="hint" style="margin:0">This workflow takes no parameters, so the payload isn' + String.fromCharCode(8217) + 't passed on. Add one in Edit to use it.</p>') +
|
|
1075
|
-
'<label class="http-tool-inline-label"><input type="checkbox" name="notify">
|
|
1072
|
+
'<label class="http-tool-inline-label"><input type="checkbox" name="notify"> Message me the result when it finishes <span class="hint">(through its Messaging tab: Slack, email, Lark or a webhook)</span></label>' +
|
|
1076
1073
|
'<label>Post each run' + String.fromCharCode(8217) + 's result to <span class="hint">(optional, an https URL)</span><input name="callback_url" placeholder="https://example.com/hooks/loopengine"></label>' +
|
|
1077
1074
|
'<div style="display:flex;gap:8px"><button type="submit" class="primary">Make the URL</button></div>' +
|
|
1078
1075
|
'<div class="hint hook-form-result"></div></form>';
|
|
@@ -1224,7 +1221,11 @@ ${consoleShellScript}
|
|
|
1224
1221
|
var action = btn.dataset.action;
|
|
1225
1222
|
if (action === 'cancel' && btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to remove'; return; }
|
|
1226
1223
|
btn.disabled = true;
|
|
1227
|
-
fetchJsonRetryOnce('/schedules/' + encodeURIComponent(btn.dataset.id) + '/' + action, { method: 'POST' }).then(function () {
|
|
1224
|
+
fetchJsonRetryOnce('/schedules/' + encodeURIComponent(btn.dataset.id) + '/' + action, { method: 'POST' }).then(function () {
|
|
1225
|
+
loadWorkflowsSection(name);
|
|
1226
|
+
// The Schedules tab's count, and the Overview's tile.
|
|
1227
|
+
fillOverviewCards(name);
|
|
1228
|
+
});
|
|
1228
1229
|
});
|
|
1229
1230
|
}
|
|
1230
1231
|
card.querySelector('.workflow-run').addEventListener('click', function (ev) {
|
|
@@ -1277,6 +1278,7 @@ ${consoleShellScript}
|
|
|
1277
1278
|
var waiting = active.filter(function (t) { return t.status === 'awaiting_approval'; }).length;
|
|
1278
1279
|
var next = active.map(function (t) { return t.next_run_at; }).filter(Boolean).sort()[0];
|
|
1279
1280
|
set('ovSchedulesValue', String(active.length));
|
|
1281
|
+
leShell.setCount('schedules', active.length);
|
|
1280
1282
|
var schedulesDetail = [];
|
|
1281
1283
|
if (waiting) schedulesDetail.push(waiting + ' waiting for approval');
|
|
1282
1284
|
if (next) schedulesDetail.push('Next run ' + escapeHtml(new Date(next).toLocaleString()));
|
|
@@ -1310,7 +1312,10 @@ ${consoleShellScript}
|
|
|
1310
1312
|
'<dt>Provider</dt><dd>' + escapeHtml(cfg.model.provider) + '</dd>' +
|
|
1311
1313
|
'<dt>Model</dt><dd>' + escapeHtml(cfg.model.model || '(provider default)') + '</dd>' +
|
|
1312
1314
|
'<dt>Max tokens</dt><dd>' + escapeHtml(cfg.model.maxTokens != null ? cfg.model.maxTokens : '(default)') + '</dd>' +
|
|
1313
|
-
|
|
1315
|
+
// Unset is low for OpenAI's reasoning models (core/model-calls/openai-model-call.ts).
|
|
1316
|
+
'<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort ||
|
|
1317
|
+
(cfg.model.provider === 'openai' && /^(gpt-[5-9]|o[0-9])/i.test(cfg.model.model || '') ? 'low (default)'
|
|
1318
|
+
: REASONING_PROVIDERS.indexOf(cfg.model.provider) !== -1 ? '(model default)' : 'n/a')) + '</dd>' +
|
|
1314
1319
|
'</dl>' +
|
|
1315
1320
|
'<form class="add-source" id="modelForm" style="display:none">' +
|
|
1316
1321
|
'<label>Provider' +
|
|
@@ -1326,7 +1331,7 @@ ${consoleShellScript}
|
|
|
1326
1331
|
'<label>Max tokens <span class="hint">(blank for provider default)</span>' +
|
|
1327
1332
|
'<input type="number" name="maxTokens" min="1" step="1" value="' + (cfg.model.maxTokens != null ? escapeHtml(cfg.model.maxTokens) : '') + '">' +
|
|
1328
1333
|
'</label>' +
|
|
1329
|
-
'<label id="reasoningEffortField">Reasoning effort <span class="hint">(how much the model thinks before answering: none turns it off, unset uses the model\\\'s default)</span>' +
|
|
1334
|
+
'<label id="reasoningEffortField">Reasoning effort <span class="hint">(how much the model thinks before answering: none turns it off, unset uses the model\\\'s default, and low for OpenAI\\\'s reasoning models)</span>' +
|
|
1330
1335
|
'<select name="reasoningEffort">' +
|
|
1331
1336
|
['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].map(function (v) {
|
|
1332
1337
|
return '<option value="' + v + '"' + (v === (cfg.model.reasoningEffort || '') ? ' selected' : '') + '>' + (v || '(unset)') + '</option>';
|
|
@@ -1342,11 +1347,6 @@ ${consoleShellScript}
|
|
|
1342
1347
|
return renderOverviewCards(cfg) +
|
|
1343
1348
|
'<section>' + systemPromptSection + '</section>' +
|
|
1344
1349
|
'<section>' + modelSection + '</section>' +
|
|
1345
|
-
'<section><h3>Hooks</h3><dl class="kv">' +
|
|
1346
|
-
'<dt>sessionIdFor</dt><dd>' + badge(cfg.sessionIdFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.sessionIdFor) + '</span></dd>' +
|
|
1347
|
-
'<dt>tenantFor</dt><dd>' + badge(cfg.tenantFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.tenantFor) + '</span></dd>' +
|
|
1348
|
-
'<dt>isSafeTool</dt><dd>' + badge(cfg.isSafeTool === 'custom' ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.isSafeTool) + '</span></dd>' +
|
|
1349
|
-
'</dl></section>' +
|
|
1350
1350
|
'<section><h3>Limits & budgets <button type="button" class="edit-btn" id="editLimitsBtn">Edit</button></h3>' +
|
|
1351
1351
|
'<dl class="kv" id="limitsDisplay">' +
|
|
1352
1352
|
'<dt>maxTurns</dt><dd>' + escapeHtml(cfg.maxTurns) + '</dd>' +
|
|
@@ -1367,7 +1367,6 @@ ${consoleShellScript}
|
|
|
1367
1367
|
'<button type="button" id="cancelLimitsBtn">Cancel</button>' +
|
|
1368
1368
|
'<div class="error" id="limitsError"></div>' +
|
|
1369
1369
|
'</form>' +
|
|
1370
|
-
'<dl class="kv"><dt>skillsDirs</dt><dd>' + escapeHtml(cfg.skillsDirs.join(', ') || '(none)') + '</dd></dl>' +
|
|
1371
1370
|
'</section>';
|
|
1372
1371
|
}
|
|
1373
1372
|
|
|
@@ -1956,7 +1955,10 @@ ${consoleShellScript}
|
|
|
1956
1955
|
(saved ? '<button type="button" class="delete-btn notifier-remove"' + (locked ? ' disabled' : '') + '>Remove</button>' : '') + '</div>' +
|
|
1957
1956
|
(isWebhook ? '<p class="hint" style="margin:0">A webhook always sends once saved, alongside the primary messaging app.</p>' : '') +
|
|
1958
1957
|
'<div class="hint notifier-result"></div>' +
|
|
1959
|
-
'</form
|
|
1958
|
+
'</form>' +
|
|
1959
|
+
// Who can decide and give jobs here: part of this app's settings.
|
|
1960
|
+
(channel === 'slack' || channel === 'telegram' ? renderApproversHtml(n, channel) : '') +
|
|
1961
|
+
'</details>';
|
|
1960
1962
|
}
|
|
1961
1963
|
|
|
1962
1964
|
function renderConnectionsHtml(n) {
|
|
@@ -2011,7 +2013,7 @@ ${consoleShellScript}
|
|
|
2011
2013
|
'<td><button type="button" class="delete-btn approver-remove" data-user="' + escapeHtml(a.user) + '"' + (n.canSave === false ? ' disabled' : '') + '>Remove</button></td></tr>';
|
|
2012
2014
|
}).join('');
|
|
2013
2015
|
var where = app === 'telegram' ? 'by replying to it' : 'by replying in its thread';
|
|
2014
|
-
return '<section class="approvers-section" data-app="' + app + '"
|
|
2016
|
+
return '<section class="approvers-section" data-app="' + app + '" style="max-width:560px;margin-top:18px;padding-top:14px;border-top:1px solid var(--le-line, #e5e7eb)"><h4 style="margin:0 0 6px">Approvers</h4>' +
|
|
2015
2017
|
(n.primary !== app ? '<p class="hint" style="margin:0 0 8px">' + title + ' isn't primary, so requests aren't sent there until it is. Jobs started there still use this list.</p>' : '') +
|
|
2016
2018
|
'<p class="muted" style="margin:0 0 10px">People who can approve, deny, change or answer a request ' + where + ', like “yes, go ahead” or “make it $40”. A plain “yes” runs it; a reply that's less clear (“sure, send it”, “yes but check the amount”) shows exactly what will run with a Confirm button first, and a change is shown to approve first. Once anyone is listed, only approvers' button clicks count, and only approvers can give the agent jobs. Anyone can still ask about a request.</p>' +
|
|
2017
2019
|
(approvers.length
|
|
@@ -2101,8 +2103,7 @@ ${consoleShellScript}
|
|
|
2101
2103
|
'<p class="muted" style="margin:0 0 12px">' + renderNotifierStatus(n) + '</p>' +
|
|
2102
2104
|
(n.error ? '<p class="error">' + escapeHtml(n.error) + '</p>' : '') +
|
|
2103
2105
|
(n.source === 'code' ? '' : renderConnectionsHtml(n)) + '</section>' +
|
|
2104
|
-
|
|
2105
|
-
renderApproversHtml(n, 'telegram') +
|
|
2106
|
+
|
|
2106
2107
|
'<section><h3>Recent deliveries</h3>' + renderDeliveries(n.deliveries || []) + '</section>';
|
|
2107
2108
|
}
|
|
2108
2109
|
|
|
@@ -2251,8 +2252,16 @@ ${consoleShellScript}
|
|
|
2251
2252
|
fetchJsonRetryOnce(url).then(function (r) {
|
|
2252
2253
|
if (!r.ok) { out.textContent = r.body.error || 'Could not check'; return; }
|
|
2253
2254
|
var b = r.body;
|
|
2255
|
+
// Telegram keeps its last error until a newer one, so it says when.
|
|
2256
|
+
var when = '';
|
|
2257
|
+
if (b.lastErrorAt) {
|
|
2258
|
+
var mins = Math.max(0, Math.round((Date.now() - Date.parse(b.lastErrorAt)) / 60000));
|
|
2259
|
+
when = mins < 2 ? 'just now' : mins < 120 ? mins + ' minutes ago' : mins < 2880 ? Math.round(mins / 60) + ' hours ago' : Math.round(mins / 1440) + ' days ago';
|
|
2260
|
+
when = ' (' + when + ', ' + new Date(b.lastErrorAt).toLocaleString() + ')';
|
|
2261
|
+
}
|
|
2254
2262
|
out.textContent = (b.pointsHere ? 'Connected' + (b.botUsername ? ' (@' + b.botUsername + ')' : '') + '.' : b.url ? 'The bot sends its updates somewhere else: ' + b.url + '. Connect again to point it here.' : 'The bot isn' + String.fromCharCode(8217) + 't sending its updates anywhere. Connect again.') +
|
|
2255
|
-
(b.
|
|
2263
|
+
(b.pending ? ' ' + b.pending + ' update' + (b.pending === 1 ? '' : 's') + ' waiting to be delivered.' : '') +
|
|
2264
|
+
(b.lastError ? ' Telegram' + String.fromCharCode(8217) + 's last delivery error' + when + ': ' + b.lastError + (b.pointsHere && !b.pending ? ' Nothing is waiting, so it' + String.fromCharCode(8217) + 's likely from before and since fixed.' : '') : '');
|
|
2256
2265
|
});
|
|
2257
2266
|
});
|
|
2258
2267
|
}
|
|
@@ -2422,8 +2431,8 @@ ${consoleShellScript}
|
|
|
2422
2431
|
? '<div><span class="le-pill bad">Workflow removed</span> <span class="hint">' + escapeHtml(t.workflow_id) + '</span></div>'
|
|
2423
2432
|
: '<div class="schedule-result hint">' + escapeHtml(t.message) + '</div>') +
|
|
2424
2433
|
(t.status !== 'cancelled' && t.status !== 'done'
|
|
2425
|
-
? '<label class="schedule-notify hint" title="
|
|
2426
|
-
: (t.notify ? '<div class="hint">
|
|
2434
|
+
? '<label class="schedule-notify hint" title="Sent through its Messaging tab: Slack, email, Lark or a webhook"><input type="checkbox" class="schedule-notify-toggle" data-id="' + escapeHtml(t.task_id) + '"' + (t.notify ? ' checked' : '') + '> Message me the result when it finishes</label>'
|
|
2435
|
+
: (t.notify ? '<div class="hint">Messaged you the result when it finished</div>' : '')) + '</td>' +
|
|
2427
2436
|
'<td>' + scheduleText(t.schedule) + (t.next_run_at ? '<div class="hint">next ' + escapeHtml(fmtScheduleTime(t.next_run_at)) + '</div>' : '') + '</td>' +
|
|
2428
2437
|
'<td><span class="le-pill ' + (SCHEDULE_PILLS[t.status] || 'neutral') + '">' + escapeHtml(SCHEDULE_LABELS[t.status] || t.status) + '</span>' +
|
|
2429
2438
|
(t.status === 'awaiting_approval' ? '<div class="hint"><a href="#" class="schedule-to-approvals">Decide in Messaging</a></div>' : '') + '</td>' +
|
|
@@ -2465,7 +2474,7 @@ ${consoleShellScript}
|
|
|
2465
2474
|
'<input type="datetime-local" name="runAt" aria-label="Date and time"></div>' +
|
|
2466
2475
|
'</div>' +
|
|
2467
2476
|
'<label>Name <span class="hint">(optional)</span><input name="task_name" placeholder="Daily ticket summary"></label>' +
|
|
2468
|
-
'<label class="http-tool-inline-label"><input type="checkbox" name="notify">
|
|
2477
|
+
'<label class="http-tool-inline-label"><input type="checkbox" name="notify"> Message me the result when it finishes <span class="hint">(through its Messaging tab: Slack, email, Lark or a webhook)</span></label>' +
|
|
2469
2478
|
'<div style="display:flex;gap:8px"><button type="submit">Schedule</button></div>' +
|
|
2470
2479
|
'<div id="scheduleResult" class="hint"></div>' +
|
|
2471
2480
|
'</form></section>';
|
|
@@ -3031,7 +3040,7 @@ ${consoleShellScript}
|
|
|
3031
3040
|
}
|
|
3032
3041
|
|
|
3033
3042
|
// ---- Apps tab: what's already installed for this agent, a
|
|
3034
|
-
// search box over
|
|
3043
|
+
// search box over official (@loopengine-co on npm) abilities,
|
|
3035
3044
|
// and a plain spec field for a private ability (a git URL, a local
|
|
3036
3045
|
// file:../path, or a private registry name) — both installs go
|
|
3037
3046
|
// through the same POST .../abilities, since installAbility itself
|
|
@@ -3076,7 +3085,7 @@ ${consoleShellScript}
|
|
|
3076
3085
|
'</tr>';
|
|
3077
3086
|
}
|
|
3078
3087
|
|
|
3079
|
-
//
|
|
3088
|
+
// An official or private ability not installed here yet: its
|
|
3080
3089
|
// details show the permissions it ships with.
|
|
3081
3090
|
function renderAbilitySearchResultRow(pkg, installedNames, spec) {
|
|
3082
3091
|
var installSpec = spec || pkg.name;
|
|
@@ -3099,15 +3108,15 @@ ${consoleShellScript}
|
|
|
3099
3108
|
: '<p class="hint">Nothing added to this agent yet.</p>';
|
|
3100
3109
|
|
|
3101
3110
|
// What this agent has first; then the workspace's connected apps (each
|
|
3102
|
-
// with the tools this agent gets), then
|
|
3111
|
+
// with the tools this agent gets), then official and private abilities.
|
|
3103
3112
|
return '<h3 style="justify-content:flex-start;align-items:baseline;gap:8px">Installed <span class="hint" style="font-weight:400">the apps and abilities this agent has</span></h3>' + installedHtml +
|
|
3104
3113
|
// Installing by name or spec is on the console's Abilities page.
|
|
3105
3114
|
'<p id="abilityInstallStatus" class="hint"></p>' +
|
|
3106
3115
|
'<h3 style="margin-top:24px;justify-content:flex-start;align-items:baseline;gap:8px">Apps <span class="hint" style="font-weight:400">connected in this workspace; choose which of their tools this agent gets, and each one' + String.fromCharCode(8217) + 's permission</span></h3>' +
|
|
3107
3116
|
'<div id="agentApps" style="margin-bottom:32px"><p class="hint">Loading…</p></div>' +
|
|
3108
|
-
'<h3 style="margin-top:24px;justify-content:flex-start;align-items:baseline;gap:8px">
|
|
3117
|
+
'<h3 style="margin-top:24px;justify-content:flex-start;align-items:baseline;gap:8px">Official abilities <span class="hint" style="font-weight:400">published by LoopEngine</span></h3>' +
|
|
3109
3118
|
'<form id="abilitySearchForm" style="display:flex;gap:6px;margin:0 0 10px;max-width:560px">' +
|
|
3110
|
-
'<input type="search" name="q" placeholder="Search abilities, e.g. gmail" aria-label="Search
|
|
3119
|
+
'<input type="search" name="q" placeholder="Search abilities, e.g. gmail" aria-label="Search official abilities" style="flex:1">' +
|
|
3111
3120
|
'<button type="submit">Search</button>' +
|
|
3112
3121
|
'</form>' +
|
|
3113
3122
|
'<div id="abilitySearchResults" style="margin-bottom:32px"></div>' +
|
|
@@ -3325,7 +3334,7 @@ ${consoleShellScript}
|
|
|
3325
3334
|
var results = result.body.results || [];
|
|
3326
3335
|
resultsEl.innerHTML = results.length
|
|
3327
3336
|
? '<table><thead><tr><th>Package</th><th>Description</th><th></th></tr></thead><tbody>' + results.map(function (pkg) { return renderAbilitySearchResultRow(pkg, installedNames); }).join('') + '</tbody></table>'
|
|
3328
|
-
: '<p class="hint">No
|
|
3337
|
+
: '<p class="hint">No official abilities found.</p>';
|
|
3329
3338
|
var installBtns = resultsEl.querySelectorAll('.ability-install-btn');
|
|
3330
3339
|
for (var i = 0; i < installBtns.length; i++) {
|
|
3331
3340
|
installBtns[i].addEventListener('click', function (ev2) {
|
|
@@ -3610,11 +3619,7 @@ ${consoleShellScript}
|
|
|
3610
3619
|
|
|
3611
3620
|
function renderSkillsTabHtml(cfg) {
|
|
3612
3621
|
var listHtml = cfg.skills.length ? cfg.skills.map(renderSkillCard).join('') : '<p class="hint">No skills yet.</p>';
|
|
3613
|
-
return '<section><h3>Skills
|
|
3614
|
-
'<dt>skillsDirs</dt><dd>' + escapeHtml(cfg.skillsDirs.join(', ') || '(none)') + '</dd>' +
|
|
3615
|
-
'<dt>skillIndexBudgetTokens</dt><dd>' + escapeHtml(cfg.skillIndexBudgetTokens) + '</dd>' +
|
|
3616
|
-
'</dl></section>' +
|
|
3617
|
-
'<section><h3>Skills (' + cfg.skills.length + ')</h3><div id="skillList">' + listHtml + '</div></section>' +
|
|
3622
|
+
return '<section><h3>Skills (' + cfg.skills.length + ')</h3><div id="skillList">' + listHtml + '</div></section>' +
|
|
3618
3623
|
'<section><h3 id="skillFormHeading">Add a skill</h3>' +
|
|
3619
3624
|
'<form class="add-source" id="skillForm">' +
|
|
3620
3625
|
'<label>Id <span class="hint">(lowercase, hyphen-separated — becomes the SKILL.md folder name; nested skills aren\\'t editable here)</span>' +
|
|
@@ -4501,7 +4506,7 @@ ${consoleShellScript}
|
|
|
4501
4506
|
prompt: cfg.systemPrompt,
|
|
4502
4507
|
chips: [modelChip(cfg), { text: 'unmatched calls: ' + cfg.permissions.defaultDecision }],
|
|
4503
4508
|
tab: currentTab,
|
|
4504
|
-
counts: { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount,
|
|
4509
|
+
counts: { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount, workflows: cfg.skills.filter(function (s) { return s.workflow; }).length },
|
|
4505
4510
|
}) +
|
|
4506
4511
|
'<div class="le-scroll"><div class="le-page" style="display:block">' +
|
|
4507
4512
|
'<div class="tab-panel" data-tab-panel="overview">' + renderOverviewHtml(cfg) + '</div>' +
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// leShell.wireModelPicker(form) model dropdown for a provider select
|
|
16
16
|
// leShell.loadProviderKeys() Promise of {provider: key saved?}, or null
|
|
17
17
|
// leShell.applyProviderKeys(form, keys, keep) only providers with a key
|
|
18
|
-
// leShell.abilityOriginBadge(origin) Public/Private badge for an ability
|
|
18
|
+
// leShell.abilityOriginBadge(origin) Official/Public/Private badge for an ability
|
|
19
19
|
// leShell.toggleAbilityDetails(btn) an ability's tools, skills and settings
|
|
20
20
|
// Same embedded-string conventions as the pages themselves: plain
|
|
21
21
|
// string concatenation and no backslashes inside the script, so nothing
|
|
@@ -56,8 +56,8 @@ export const consoleShellScript = `<script>
|
|
|
56
56
|
// ability on the Apps tab (id 'abilities', so older links still work).
|
|
57
57
|
// ?tab=tools and ?tab=skills still open Advanced.
|
|
58
58
|
var TABS = [
|
|
59
|
-
['overview', 'Overview'], ['workflows', 'Workflows'], ['abilities', 'Apps'], ['
|
|
60
|
-
['messaging', 'Messaging'], ['schedules', 'Schedules'], ['
|
|
59
|
+
['overview', 'Overview'], ['workflows', 'Workflows'], ['abilities', 'Apps'], ['chat', 'Chat'],
|
|
60
|
+
['messaging', 'Messaging'], ['schedules', 'Schedules'], ['actauth', 'Permissions'], ['env', 'Environment'], ['advanced', 'Advanced']
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
function esc(s) {
|
|
@@ -360,11 +360,12 @@ export const consoleShellScript = `<script>
|
|
|
360
360
|
return decision ? '<span class="le-pill ' + decision + '">' + DECISION_LABELS[decision] + '</span>' : '<span class="hint">No rule</span>';
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
// Public (npm) or Private, with where
|
|
363
|
+
// Official (@loopengine-co), Public (other npm) or Private, with where
|
|
364
|
+
// it came from on hover.
|
|
364
365
|
function abilityOriginBadge(origin) {
|
|
365
366
|
if (!origin) return '';
|
|
366
367
|
return '<span class="le-pill ' + (origin.kind === 'private' ? 'accent' : '') + '" title="' + esc(origin.label) + '">' +
|
|
367
|
-
(origin.kind === 'private' ? 'Private' : 'Public') + '</span> <span class="hint">' + esc(origin.label) + '</span>';
|
|
368
|
+
(origin.kind === 'private' ? 'Private' : origin.kind === 'official' ? 'Official' : 'Public') + '</span> <span class="hint">' + esc(origin.label) + '</span>';
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
|
|
@@ -251,7 +251,7 @@ ${consoleShellScript}
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// ---- Abilities section ----
|
|
254
|
-
// How agents reach outside apps.
|
|
254
|
+
// How agents reach outside apps. Official abilities (npm) are browsed
|
|
255
255
|
// here and installed from an agent's own Apps tab. A private one
|
|
256
256
|
// (from the sources in Settings) is installed here into the workspace
|
|
257
257
|
// (core/workspace-abilities.ts), then added to any agent from its tab.
|
|
@@ -271,9 +271,9 @@ ${consoleShellScript}
|
|
|
271
271
|
function renderAbilities() {
|
|
272
272
|
var content = document.getElementById('abilitiesContent');
|
|
273
273
|
content.innerHTML = '<section>' +
|
|
274
|
-
'<h4>
|
|
274
|
+
'<h4>Official <span class="hint">published by LoopEngine; add one to an agent from its Apps tab</span></h4>' +
|
|
275
275
|
'<form id="abilitySearchForm" style="display:flex;gap:6px;margin:0 0 10px;max-width:560px">' +
|
|
276
|
-
'<input type="search" name="q" placeholder="Search abilities, e.g. gmail" aria-label="Search
|
|
276
|
+
'<input type="search" name="q" placeholder="Search abilities, e.g. gmail" aria-label="Search official abilities" style="flex:1">' +
|
|
277
277
|
'<button type="submit">Search</button>' +
|
|
278
278
|
'</form>' +
|
|
279
279
|
'<div id="abilitySearchResults"><p class="hint">Loading…</p></div>' +
|
|
@@ -443,7 +443,7 @@ ${consoleShellScript}
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
// Linked from an agent's Apps tab (/config?section=abilities#private-abilities):
|
|
446
|
-
// once every list has loaded, so the
|
|
446
|
+
// once every list has loaded, so the official results don't push it down.
|
|
447
447
|
var scrolledToPrivate = false;
|
|
448
448
|
function scrollToPrivateIfLinked() {
|
|
449
449
|
if (scrolledToPrivate || location.hash !== '#private-abilities' || !lastPrivate || !workspaceAbilities || lastSearch.q === null) return;
|
|
@@ -547,6 +547,8 @@ ${consoleShellScript}
|
|
|
547
547
|
// Open tool panels, by ability: { tools, filter, error, busy }.
|
|
548
548
|
var openTools = {};
|
|
549
549
|
var appSearch = { q: '', servers: null, error: '' };
|
|
550
|
+
// The "Add your own server" form: open, and what's typed in it.
|
|
551
|
+
var ownForm = { open: false };
|
|
550
552
|
|
|
551
553
|
function appLetter(title) {
|
|
552
554
|
return '<span class="app-letter" aria-hidden="true">' + escapeHtml((title || '?').charAt(0).toUpperCase()) + '</span>';
|
|
@@ -572,6 +574,37 @@ ${consoleShellScript}
|
|
|
572
574
|
'<p class="hint app-msg" style="margin:0"></p></form>';
|
|
573
575
|
}
|
|
574
576
|
|
|
577
|
+
// Your company's own servers (mcp-servers.yml): each connects like any
|
|
578
|
+
// app; a saved one not connected can be connected again or removed.
|
|
579
|
+
function renderOwnServers() {
|
|
580
|
+
var saved = apps.saved || [];
|
|
581
|
+
var rows = saved.map(function (s) {
|
|
582
|
+
var connected = apps.connected.some(function (c) { return c.url === s.url; });
|
|
583
|
+
return '<div class="app-row" data-app="' + escapeHtml(s.id) + '"><div class="app-head">' + appLetter(s.title) + '</div>' +
|
|
584
|
+
'<div class="app-row-main"><div><strong>' + escapeHtml(s.title) + '</strong> <span class="badge badge-default">Private</span></div>' +
|
|
585
|
+
'<div class="hint">' + escapeHtml(s.url) + (s.description ? ' · ' + escapeHtml(s.description) : '') + (s.header ? ' · key sent in ' + escapeHtml(s.header) : '') + '</div></div>' +
|
|
586
|
+
'<div class="app-slot">' + (connected
|
|
587
|
+
? '<span class="badge badge-custom">Connected</span>'
|
|
588
|
+
: '<button type="button" class="app-connect" data-id="' + escapeHtml(s.id) + '"' + (apps.canConnect ? '' : ' disabled') + '>Connect</button> <button type="button" class="delete-btn app-forget" data-id="' + escapeHtml(s.id) + '"' + (apps.canConnect ? '' : ' disabled') + '>Remove</button>') + '</div></div>';
|
|
589
|
+
}).join('');
|
|
590
|
+
var form = ownForm.open
|
|
591
|
+
? '<form id="ownServerForm" class="app-form" style="max-width:560px">' +
|
|
592
|
+
'<label>Name<input name="title" required maxlength="60" placeholder="Acme CRM"></label>' +
|
|
593
|
+
'<label>Address<input name="url" type="url" required placeholder="https://mcp.yourcompany.com/mcp"></label>' +
|
|
594
|
+
'<label>What it does <span class="hint">(optional)</span><input name="description" maxlength="200" placeholder="Our customers, orders and support tickets"></label>' +
|
|
595
|
+
'<label>API key <span class="hint">(if it needs one; saved in this workspace' + String.fromCharCode(8217) + 's settings)</span><input name="key" type="password" autocomplete="off"></label>' +
|
|
596
|
+
'<label>Header for the key <span class="hint">(optional; empty sends it as Authorization: Bearer ' + String.fromCharCode(8230) + ')</span><input name="header" placeholder="X-Api-Key" pattern="[A-Za-z0-9-]+"></label>' +
|
|
597
|
+
'<div class="app-foot"><button type="submit">Connect</button><button type="button" class="own-cancel">Cancel</button></div>' +
|
|
598
|
+
'<p class="hint app-msg" style="margin:0"></p></form>'
|
|
599
|
+
: '<button type="button" class="own-add"' + (apps.canConnect ? '' : ' disabled') + '>Add your own server</button>';
|
|
600
|
+
return '<section id="own-servers"><h3>Your own servers</h3>' +
|
|
601
|
+
'<p class="hint">Your company' + String.fromCharCode(8217) + 's own MCP server, by its address: it connects like any app, and only this workspace sees it. Servers that only offer a sign-in (OAuth) can' + String.fromCharCode(8217) + 't be connected yet.</p>' +
|
|
602
|
+
(rows ? '<div class="app-list" style="margin-bottom:10px">' + rows + '</div>' : '') + form + '</section>';
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// By name: form.title is the form's own title attribute.
|
|
606
|
+
function ownField(form, name) { return form.querySelector('[name=' + name + ']').value; }
|
|
607
|
+
|
|
575
608
|
function renderApps() {
|
|
576
609
|
var el = document.getElementById('appsContent');
|
|
577
610
|
if (!apps) return;
|
|
@@ -601,6 +634,7 @@ ${consoleShellScript}
|
|
|
601
634
|
}).join('') + '</div>' +
|
|
602
635
|
'<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
636
|
'</section>' +
|
|
637
|
+
renderOwnServers() +
|
|
604
638
|
'<section><h3>More apps</h3>' +
|
|
605
639
|
'<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
640
|
'<div id="appSearchResults">' + renderAppSearch() + '</div>' +
|
|
@@ -716,6 +750,26 @@ ${consoleShellScript}
|
|
|
716
750
|
return;
|
|
717
751
|
}
|
|
718
752
|
if (t.classList.contains('app-cancel')) { renderApps(); return; }
|
|
753
|
+
if (t.classList.contains('own-add')) {
|
|
754
|
+
ownForm.open = true;
|
|
755
|
+
renderApps();
|
|
756
|
+
document.querySelector('#ownServerForm input[name=title]').focus();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (t.classList.contains('own-cancel')) { ownForm.open = false; renderApps(); return; }
|
|
760
|
+
if (t.classList.contains('app-forget')) {
|
|
761
|
+
if (t.dataset.confirming !== '1') { t.dataset.confirming = '1'; t.textContent = 'Click again to remove'; return; }
|
|
762
|
+
t.disabled = true;
|
|
763
|
+
fetch('/config/apps/servers?id=' + encodeURIComponent(t.dataset.id), { method: 'DELETE' })
|
|
764
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
765
|
+
.then(function (r) {
|
|
766
|
+
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
767
|
+
apps = r.body;
|
|
768
|
+
renderApps();
|
|
769
|
+
})
|
|
770
|
+
.catch(function (err) { t.disabled = false; t.textContent = 'Could not remove: ' + err.message; });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
719
773
|
if (t.classList.contains('app-disconnect')) {
|
|
720
774
|
if (t.dataset.confirming !== '1') { t.dataset.confirming = '1'; t.textContent = 'Click again to disconnect'; return; }
|
|
721
775
|
t.disabled = true;
|
|
@@ -760,6 +814,28 @@ ${consoleShellScript}
|
|
|
760
814
|
.catch(function (err) { appSearch.error = 'Could not search: ' + err.message; document.getElementById('appSearchResults').innerHTML = renderAppSearch(); });
|
|
761
815
|
return;
|
|
762
816
|
}
|
|
817
|
+
if (form.id === 'ownServerForm') {
|
|
818
|
+
var ownBtn = form.querySelector('button[type=submit]');
|
|
819
|
+
var ownMsg = form.querySelector('.app-msg');
|
|
820
|
+
ownBtn.disabled = true;
|
|
821
|
+
ownMsg.className = 'hint app-msg';
|
|
822
|
+
ownMsg.textContent = 'Connecting' + String.fromCharCode(8230) + ' it asks the server for its tools and sets their permissions, which can take a minute.';
|
|
823
|
+
fetch('/config/apps/servers', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: ownField(form, 'title'), url: ownField(form, 'url'), description: ownField(form, 'description'), key: ownField(form, 'key'), header: ownField(form, 'header') }) })
|
|
824
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
825
|
+
.then(function (r) {
|
|
826
|
+
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
827
|
+
apps = r.body;
|
|
828
|
+
ownForm.open = false;
|
|
829
|
+
if (r.body.connectedAbility) loadTools(r.body.connectedAbility, false);
|
|
830
|
+
else renderApps();
|
|
831
|
+
})
|
|
832
|
+
.catch(function (err) {
|
|
833
|
+
ownBtn.disabled = false;
|
|
834
|
+
ownMsg.className = 'error app-msg';
|
|
835
|
+
ownMsg.textContent = err.message;
|
|
836
|
+
});
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
763
839
|
if (!form.dataset.connect) return;
|
|
764
840
|
var btn = form.querySelector('button[type=submit]');
|
|
765
841
|
var msg = form.querySelector('.app-msg');
|
|
@@ -805,12 +881,12 @@ ${consoleShellScript}
|
|
|
805
881
|
rows.map(function (r) { return '<tr><td>' + r[0] + '</td><td>' + r[1] + '</td></tr>'; }).join('') +
|
|
806
882
|
'</tbody></table></section>' +
|
|
807
883
|
(d.timeZone ? '<section><h3>Time zone</h3><div class="source" id="timeZoneBox"><p class="hint">Loading' + String.fromCharCode(8230) + '</p></div></section>' : '') +
|
|
808
|
-
'<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
|
|
884
|
+
'<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 LoopEngine's official ones.</p><div class="source" id="abilitySourcesBox"><p class="hint">Loading' + String.fromCharCode(8230) + '</p></div></section>' +
|
|
809
885
|
'<section><h3>Updates</h3><div class="source" id="upgradeBox"><p class="hint">Checking for a newer LoopEngine' + String.fromCharCode(8230) + '</p></div></section>';
|
|
810
886
|
}
|
|
811
887
|
|
|
812
888
|
// ---- Private ability sources (Settings) ----
|
|
813
|
-
// Where this project's own abilities come from, next to the
|
|
889
|
+
// Where this project's own abilities come from, next to the official ones
|
|
814
890
|
// on npm: a GitHub org or user, or a private npm registry
|
|
815
891
|
// (core/ability-sources.ts). There's no folder kind here: this server
|
|
816
892
|
// may run in the cloud, so local files are uploaded on the Abilities
|
|
@@ -127,6 +127,8 @@ export declare function setupAgentTelegramWebhook(config: AgentConfig, baseUrl:
|
|
|
127
127
|
export declare function agentTelegramWebhookInfo(config: AgentConfig): Promise<{
|
|
128
128
|
url?: string;
|
|
129
129
|
lastError?: string;
|
|
130
|
+
lastErrorAt?: string;
|
|
131
|
+
pending?: number;
|
|
130
132
|
pointsHere: boolean;
|
|
131
133
|
botUsername?: string;
|
|
132
134
|
}>;
|
|
@@ -161,7 +161,7 @@ export function jobAllowed(config, app, message) {
|
|
|
161
161
|
const home = app === 'slack' ? file?.channelId : file?.chatId;
|
|
162
162
|
if (!message.direct && home && home === message.conversation)
|
|
163
163
|
return { allowed: true };
|
|
164
|
-
return { allowed: false, message: `Jobs by direct message need approvers:
|
|
164
|
+
return { allowed: false, message: `Jobs by direct message need approvers: in LoopEngine, open this agent's Messaging tab, then ${APP_NAMES[app]} → Approvers, and add yourself.` };
|
|
165
165
|
}
|
|
166
166
|
/** Replaces the approver list on the agent's saved connection for `app`,
|
|
167
167
|
* primary or not. Throws NotifierFileError when there's no connection
|
|
@@ -224,6 +224,8 @@ export async function agentTelegramWebhookInfo(config) {
|
|
|
224
224
|
return {
|
|
225
225
|
...(info.url ? { url: info.url } : {}),
|
|
226
226
|
...(info.lastError ? { lastError: info.lastError } : {}),
|
|
227
|
+
...(info.lastErrorAt ? { lastErrorAt: info.lastErrorAt } : {}),
|
|
228
|
+
...(info.pending ? { pending: info.pending } : {}),
|
|
227
229
|
pointsHere: Boolean(info.url?.endsWith(`/hooks/telegram/${encodeURIComponent(config.name)}`)),
|
|
228
230
|
...(botUsername ? { botUsername } : {}),
|
|
229
231
|
};
|
package/dist/web/playground.js
CHANGED
|
@@ -1433,7 +1433,7 @@ ${consoleShellScript}
|
|
|
1433
1433
|
var counts = {};
|
|
1434
1434
|
if (cfg) {
|
|
1435
1435
|
chips.push({ text: typeof cfg.model === 'string' ? cfg.model : cfg.model.provider + ' \u00b7 ' + (cfg.model.model || 'default model'), mono: true });
|
|
1436
|
-
counts = { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount
|
|
1436
|
+
counts = { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount };
|
|
1437
1437
|
}
|
|
1438
1438
|
head.innerHTML = leShell.renderHead({ name: name, prompt: agent.systemPrompt, chips: chips, tab: 'chat', counts: counts });
|
|
1439
1439
|
}
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
import { createScheduledTask, pauseScheduledTask } from '../core/scheduler.js';
|
|
12
12
|
import { workspaceTimeZone } from '../core/time-zone.js';
|
|
13
13
|
import { STARTER_TEMPLATES, starterTemplate, templateScheduleStart } from '../core/system-agents/templates.js';
|
|
14
|
-
import { ActauthRuleExistsError, addActauthRule, setDefaultDecision } from './actauth-admin.js';
|
|
14
|
+
import { ActauthRuleExistsError, addActauthRule, applyPlannedRules, setDefaultDecision } from './actauth-admin.js';
|
|
15
15
|
import { setAgentEnvVar } from './env-admin.js';
|
|
16
16
|
import { freeAgentName } from './interview-admin.js';
|
|
17
17
|
import { writeSkill } from './skills-admin.js';
|
|
18
|
+
import { agentDir } from '../core/gateway-tools.js';
|
|
19
|
+
import { setWorkflowFields } from '../core/workflows.js';
|
|
18
20
|
/** What a template's schedule runs (its workflow, when it comes from one,
|
|
19
21
|
* or its message), and whether it sends each result. */
|
|
20
22
|
function scheduleRuns(schedule) {
|
|
@@ -84,7 +86,7 @@ export function previewTemplate(id, deps) {
|
|
|
84
86
|
// A key set for the whole project already counts for a new agent.
|
|
85
87
|
keys: keyGroups(template).map((names) => ({ names, set: names.some((n) => Boolean(envValue(name, n))) })),
|
|
86
88
|
settings: setup.env ?? {},
|
|
87
|
-
schedule: setup.schedule ? `${describeTemplateSchedule(setup.schedule)} (created paused)` : null,
|
|
89
|
+
schedule: setup.schedule ? `${describeTemplateSchedule(setup.schedule)} (created paused)${setup.schedule.notify ? '; it messages you the result when it finishes, through its Messaging tab: Slack, email, Lark or a webhook' : ''}` : null,
|
|
88
90
|
needsApprovals: setup.needsApprovals,
|
|
89
91
|
notes: setup.notes ?? [],
|
|
90
92
|
};
|
|
@@ -104,8 +106,19 @@ export function templateChecklist(template, name, deps, failed = [], appsToConne
|
|
|
104
106
|
for (const group of missing.filter((g) => g.length > 1)) {
|
|
105
107
|
items.push({ label: `Add one of ${group.slice(0, -1).join(', ')} or ${group.at(-1)}`, href: tab('env') });
|
|
106
108
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
// A schedule or webhook that sends each result needs the same connection.
|
|
110
|
+
const webhookWorkflows = (template.setup.skills ?? []).filter((s) => s.workflow?.webhook);
|
|
111
|
+
const sendsResults = Boolean(template.setup.schedule?.notify) || webhookWorkflows.length > 0;
|
|
112
|
+
if ((template.setup.needsApprovals || sendsResults) && !deps.messagingConfigured(name)) {
|
|
113
|
+
items.push({
|
|
114
|
+
label: template.setup.needsApprovals
|
|
115
|
+
? `Choose where approval requests${sendsResults ? ' and results' : ''} go: Slack, email, Lark or a webhook`
|
|
116
|
+
: 'Choose where its results go: Slack, email, Lark or a webhook',
|
|
117
|
+
href: tab('messaging'),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
for (const skill of webhookWorkflows) {
|
|
121
|
+
items.push({ label: `Add a webhook to the "${skill.workflow.title}" workflow with "Message me the result when it finishes" on, then paste its URL into the app that sends the alerts`, href: tab('workflows') });
|
|
109
122
|
}
|
|
110
123
|
for (const note of template.setup.notes ?? [])
|
|
111
124
|
items.push({ label: note });
|
|
@@ -126,19 +139,9 @@ export async function createFromTemplate(id, deps) {
|
|
|
126
139
|
export async function createFromTemplateObject(template, deps) {
|
|
127
140
|
const { candidate, setup } = template;
|
|
128
141
|
const name = freeAgentName(candidate.agent.name, deps.agentExists);
|
|
129
|
-
// 1. The agent
|
|
130
|
-
// otherwise, and the template's rules come first.
|
|
142
|
+
// 1. The agent: everything asks unless a rule says otherwise.
|
|
131
143
|
await deps.scaffoldAgent(name, candidate.agent.systemPrompt);
|
|
132
144
|
setDefaultDecision(name, 'ask');
|
|
133
|
-
for (const rule of setup.rules) {
|
|
134
|
-
try {
|
|
135
|
-
addActauthRule(name, { name: rule.name, scope: '*/*', tool: rule.tool, decision: rule.decision });
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
if (!(err instanceof ActauthRuleExistsError))
|
|
139
|
-
throw err;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
145
|
// 2. Abilities: their tools, skills and rules. One failing doesn't stop
|
|
143
146
|
// the rest; it heads the checklist.
|
|
144
147
|
const installed = [];
|
|
@@ -185,9 +188,15 @@ export async function createFromTemplateObject(template, deps) {
|
|
|
185
188
|
failed.push({ name: app.title, error: installFailureReason(err) });
|
|
186
189
|
}
|
|
187
190
|
}
|
|
191
|
+
// The template's rules, now that abilities and apps have added theirs:
|
|
192
|
+
// none twice, a stricter one placed so it counts, none loosened.
|
|
193
|
+
applyPlannedRules(name, setup.rules.map((r) => ({ name: r.name, tool: r.tool, decision: r.decision })));
|
|
188
194
|
// 3. Its own skills and non-secret settings.
|
|
189
|
-
for (const skill of setup.skills ?? [])
|
|
195
|
+
for (const skill of setup.skills ?? []) {
|
|
190
196
|
writeSkill(name, skill.id, { description: skill.description, body: skill.body });
|
|
197
|
+
if (skill.workflow)
|
|
198
|
+
setWorkflowFields(agentDir(name), skill.id, { title: skill.workflow.title, instructions: skill.body, ...(skill.workflow.parameters ? { parameters: skill.workflow.parameters } : {}) });
|
|
199
|
+
}
|
|
191
200
|
for (const [variable, value] of Object.entries(setup.env ?? {}))
|
|
192
201
|
await setAgentEnvVar(name, variable, value);
|
|
193
202
|
// 4. Load it, now that everything is in place.
|
|
@@ -75,7 +75,7 @@ ${consoleShellScript}
|
|
|
75
75
|
'<div class="meta">' +
|
|
76
76
|
'<div><strong>Runs:</strong> ' + esc(t.trigger) + '</div>' +
|
|
77
77
|
(t.asksFirst.length ? '<div><strong>Asks you first:</strong> ' + esc(t.asksFirst.join(', ')) + '</div>' : '') +
|
|
78
|
-
'<div><strong>
|
|
78
|
+
(t.abilities.length || (t.apps || []).length ? '<div><strong>Uses:</strong> ' + t.abilities.map(function (a) { return '<code>' + esc(a) + '</code>'; }).concat((t.apps || []).map(function (a) { return esc(a); })).join(' ') + '</div>' : '') +
|
|
79
79
|
'</div>' +
|
|
80
80
|
'<div class="row"><button type="button" class="primary tp-use">Use this template</button></div>' +
|
|
81
81
|
'<div class="tp-slot"></div></div>';
|
|
@@ -108,17 +108,18 @@ ${consoleShellScript}
|
|
|
108
108
|
slot.innerHTML = '<div class="tp-preview">' +
|
|
109
109
|
'<div><strong>Agent:</strong> <code>' + esc(p.name) + '</code></div>' +
|
|
110
110
|
'<div><strong>Instructions</strong> <span class="hint">(edit them later in its Overview tab)</span><pre>' + esc(p.systemPrompt) + '</pre></div>' +
|
|
111
|
-
'<div><strong>Installs</strong> <span class="hint">(abilities: their tools, skills and permission rules)</span>' +
|
|
111
|
+
(p.abilities.length ? '<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
|
-
(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>' +
|
|
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
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>' : '') +
|
|
115
115
|
'<div><strong>Permissions</strong> <span class="hint">(anything not listed asks you first)</span>' +
|
|
116
116
|
listHtml(p.rules.map(function (rule) { return '<li><code>' + esc(rule.tool) + '</code>: ' + esc(DECISIONS[rule.decision] || rule.decision) + '</li>'; })) + '</div>' +
|
|
117
|
-
'<div><strong>
|
|
117
|
+
(p.skills && p.skills.length ? '<div><strong>Skills it starts with</strong> <span class="hint">(edit them later in Advanced ' + String.fromCharCode(8594) + ' Skills)</span>' + listHtml(p.skills.map(function (sk) { return '<li><code>' + esc(sk) + '</code></li>'; })) + '</div>' : '') +
|
|
118
|
+
(p.keys.length ? '<div><strong>Keys it needs</strong>' +
|
|
118
119
|
listHtml(p.keys.map(function (k) {
|
|
119
120
|
var names = k.names.length > 1 ? 'one of ' + k.names.join(', ') : k.names[0];
|
|
120
121
|
return '<li>' + esc(names) + (k.set ? ' <span class="hint">' + CHECK + ' already set</span>' : '') + '</li>';
|
|
121
|
-
})) + '</div>' +
|
|
122
|
+
})) + '</div>' : '') +
|
|
122
123
|
(settings.length ? '<div><strong>Settings filled in:</strong> ' + settings.map(function (s) { return '<code>' + esc(s) + '=' + esc(p.settings[s]) + '</code>'; }).join(', ') + '</div>' : '') +
|
|
123
124
|
(p.schedule ? '<div><strong>Schedule:</strong> ' + esc(p.schedule) + '</div>' : '') +
|
|
124
125
|
(p.needsApprovals ? '<div><strong>Approvals:</strong> requests reach you wherever its Messaging tab sends them (Slack, email, Lark or a webhook).</div>' : '') +
|