loopengine 0.3.9 → 0.3.11
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/official-abilities.d.ts +14 -0
- package/dist/core/official-abilities.js +58 -0
- package/dist/core/system-agents/templates.d.ts +13 -0
- package/dist/core/system-agents/templates.js +18 -380
- package/dist/core/system-agents/workflow-examples.d.ts +10 -0
- package/dist/core/system-agents/workflow-examples.js +432 -0
- package/dist/core/system-agents/workflow-planner.d.ts +10 -2
- package/dist/core/system-agents/workflow-planner.js +6 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/web/abilities-admin.js +18 -2
- package/dist/web/agents-config-page.js +12 -12
- package/dist/web/agents-list-page.js +2 -2
- package/dist/web/console-shell.js +6 -6
- package/dist/web/global-config-page.js +82 -6
- package/dist/web/templates-admin.js +20 -4
- package/dist/web/templates-page.d.ts +1 -0
- package/dist/web/templates-page.js +8 -149
- package/dist/web/workflow-admin.js +13 -0
- package/dist/web/workflow-page.js +77 -21
- package/package.json +3 -1
|
@@ -728,7 +728,7 @@ ${consoleShellScript}
|
|
|
728
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">' +
|
|
729
729
|
escapeHtml(x.describe.charAt(0).toUpperCase() + x.describe.slice(1)) + '</a></span>' + state +
|
|
730
730
|
(values ? '<span class="hint">' + values + '</span>' : '') +
|
|
731
|
-
(x.notify ? '<span class="hint">
|
|
731
|
+
(x.notify ? '<span class="hint">messages you the result when it finishes</span>' : '') +
|
|
732
732
|
'<span style="display:flex;gap:6px;margin-left:auto">' +
|
|
733
733
|
(x.status === 'paused' ? '<button type="button" class="workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="resume">Resume</button>'
|
|
734
734
|
: x.status === 'scheduled' ? '<button type="button" class="workflow-schedule-action" data-id="' + escapeHtml(x.taskId) + '" data-action="pause">Pause</button>' : '') +
|
|
@@ -1023,7 +1023,7 @@ ${consoleShellScript}
|
|
|
1023
1023
|
if (t.filter) rules.push('only when ' + escapeHtml(t.filter.path) + ' is ' + t.filter.values.map(escapeHtml).join(' or '));
|
|
1024
1024
|
if (t.mapping) rules.push(Object.keys(t.mapping).map(function (p) { return escapeHtml(p) + ' ' + String.fromCharCode(8592) + ' ' + escapeHtml(t.mapping[p]); }).join(', '));
|
|
1025
1025
|
if (t.payload_param) rules.push('the whole payload goes to ' + escapeHtml(t.payload_param));
|
|
1026
|
-
if (t.notify) rules.push('
|
|
1026
|
+
if (t.notify) rules.push('messages you the result when it finishes');
|
|
1027
1027
|
if (t.callback_url) rules.push('reports to ' + escapeHtml(t.callback_url));
|
|
1028
1028
|
// Made by the workflow planner without its secret: it turns on once
|
|
1029
1029
|
// the sender's signing secret is added.
|
|
@@ -1069,7 +1069,7 @@ ${consoleShellScript}
|
|
|
1069
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>' +
|
|
1070
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>'
|
|
1071
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>') +
|
|
1072
|
-
'<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>' +
|
|
1073
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>' +
|
|
1074
1074
|
'<div style="display:flex;gap:8px"><button type="submit" class="primary">Make the URL</button></div>' +
|
|
1075
1075
|
'<div class="hint hook-form-result"></div></form>';
|
|
@@ -2431,8 +2431,8 @@ ${consoleShellScript}
|
|
|
2431
2431
|
? '<div><span class="le-pill bad">Workflow removed</span> <span class="hint">' + escapeHtml(t.workflow_id) + '</span></div>'
|
|
2432
2432
|
: '<div class="schedule-result hint">' + escapeHtml(t.message) + '</div>') +
|
|
2433
2433
|
(t.status !== 'cancelled' && t.status !== 'done'
|
|
2434
|
-
? '<label class="schedule-notify hint" title="
|
|
2435
|
-
: (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>' +
|
|
2436
2436
|
'<td>' + scheduleText(t.schedule) + (t.next_run_at ? '<div class="hint">next ' + escapeHtml(fmtScheduleTime(t.next_run_at)) + '</div>' : '') + '</td>' +
|
|
2437
2437
|
'<td><span class="le-pill ' + (SCHEDULE_PILLS[t.status] || 'neutral') + '">' + escapeHtml(SCHEDULE_LABELS[t.status] || t.status) + '</span>' +
|
|
2438
2438
|
(t.status === 'awaiting_approval' ? '<div class="hint"><a href="#" class="schedule-to-approvals">Decide in Messaging</a></div>' : '') + '</td>' +
|
|
@@ -2474,7 +2474,7 @@ ${consoleShellScript}
|
|
|
2474
2474
|
'<input type="datetime-local" name="runAt" aria-label="Date and time"></div>' +
|
|
2475
2475
|
'</div>' +
|
|
2476
2476
|
'<label>Name <span class="hint">(optional)</span><input name="task_name" placeholder="Daily ticket summary"></label>' +
|
|
2477
|
-
'<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>' +
|
|
2478
2478
|
'<div style="display:flex;gap:8px"><button type="submit">Schedule</button></div>' +
|
|
2479
2479
|
'<div id="scheduleResult" class="hint"></div>' +
|
|
2480
2480
|
'</form></section>';
|
|
@@ -3040,7 +3040,7 @@ ${consoleShellScript}
|
|
|
3040
3040
|
}
|
|
3041
3041
|
|
|
3042
3042
|
// ---- Apps tab: what's already installed for this agent, a
|
|
3043
|
-
// search box over
|
|
3043
|
+
// search box over official (@loopengine-co on npm) abilities,
|
|
3044
3044
|
// and a plain spec field for a private ability (a git URL, a local
|
|
3045
3045
|
// file:../path, or a private registry name) — both installs go
|
|
3046
3046
|
// through the same POST .../abilities, since installAbility itself
|
|
@@ -3085,7 +3085,7 @@ ${consoleShellScript}
|
|
|
3085
3085
|
'</tr>';
|
|
3086
3086
|
}
|
|
3087
3087
|
|
|
3088
|
-
//
|
|
3088
|
+
// An official or private ability not installed here yet: its
|
|
3089
3089
|
// details show the permissions it ships with.
|
|
3090
3090
|
function renderAbilitySearchResultRow(pkg, installedNames, spec) {
|
|
3091
3091
|
var installSpec = spec || pkg.name;
|
|
@@ -3108,15 +3108,15 @@ ${consoleShellScript}
|
|
|
3108
3108
|
: '<p class="hint">Nothing added to this agent yet.</p>';
|
|
3109
3109
|
|
|
3110
3110
|
// What this agent has first; then the workspace's connected apps (each
|
|
3111
|
-
// with the tools this agent gets), then
|
|
3111
|
+
// with the tools this agent gets), then official and private abilities.
|
|
3112
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 +
|
|
3113
3113
|
// Installing by name or spec is on the console's Abilities page.
|
|
3114
3114
|
'<p id="abilityInstallStatus" class="hint"></p>' +
|
|
3115
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>' +
|
|
3116
3116
|
'<div id="agentApps" style="margin-bottom:32px"><p class="hint">Loading…</p></div>' +
|
|
3117
|
-
'<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>' +
|
|
3118
3118
|
'<form id="abilitySearchForm" style="display:flex;gap:6px;margin:0 0 10px;max-width:560px">' +
|
|
3119
|
-
'<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">' +
|
|
3120
3120
|
'<button type="submit">Search</button>' +
|
|
3121
3121
|
'</form>' +
|
|
3122
3122
|
'<div id="abilitySearchResults" style="margin-bottom:32px"></div>' +
|
|
@@ -3334,7 +3334,7 @@ ${consoleShellScript}
|
|
|
3334
3334
|
var results = result.body.results || [];
|
|
3335
3335
|
resultsEl.innerHTML = results.length
|
|
3336
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>'
|
|
3337
|
-
: '<p class="hint">No
|
|
3337
|
+
: '<p class="hint">No official abilities found.</p>';
|
|
3338
3338
|
var installBtns = resultsEl.querySelectorAll('.ability-install-btn');
|
|
3339
3339
|
for (var i = 0; i < installBtns.length; i++) {
|
|
3340
3340
|
installBtns[i].addEventListener('click', function (ev2) {
|
|
@@ -50,13 +50,13 @@ ${consoleSidebarHtml}
|
|
|
50
50
|
<div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>
|
|
51
51
|
<div><h1>Agents</h1><p>Every agent under agents/ in this project. Open one to chat with it or change its tools, permissions and settings.</p></div>
|
|
52
52
|
</div>
|
|
53
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap"><a href="/
|
|
53
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap"><a href="/workflows/new#examples" style="padding:6px 12px;border:1px solid var(--line);border-radius:6px;text-decoration:none;color:var(--ink);font-size:13px">Workflow examples</a><a href="/interview" style="padding:6px 12px;border:1px solid var(--line);border-radius:6px;text-decoration:none;color:var(--ink);font-size:13px">Find your first agent</a>
|
|
54
54
|
<button type="button" id="newAgentBtn" class="primary">New agent</button></div>
|
|
55
55
|
</div>
|
|
56
56
|
</div>
|
|
57
57
|
<div class="le-scroll"><div class="le-page">
|
|
58
58
|
<form id="newAgentForm" style="display:none">
|
|
59
|
-
<p class="hint" style="margin:0">Not sure what to build? <a href="/
|
|
59
|
+
<p class="hint" style="margin:0">Not sure what to build? <a href="/workflows/new#examples">Start from a workflow example</a>, or <a href="/interview">find your first agent</a>: a short interview that suggests one and creates it.</p>
|
|
60
60
|
<h2>New agent</h2>
|
|
61
61
|
<label>Name <span class="hint">(lowercase, hyphens allowed: becomes agents/<name>/)</span>
|
|
62
62
|
<input type="text" name="name" placeholder="weather-agent" pattern="[a-z0-9]+(-[a-z0-9]+)*" required>
|
|
@@ -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
|
|
@@ -28,8 +28,7 @@ export const consoleSidebarHtml = `<aside class="le-sidebar" aria-label="Console
|
|
|
28
28
|
<div class="le-nav-label"><span>Agents</span><a href="/agents?new=1">+ New</a></div>
|
|
29
29
|
<ul class="le-agent-list" id="leAgentList"></ul>
|
|
30
30
|
<a class="le-find" href="/interview" data-le-nav="interview">Find your first agent</a>
|
|
31
|
-
<a class="le-find" href="/
|
|
32
|
-
<a class="le-find" href="/workflows/new" data-le-nav="workflows">Describe a workflow</a>
|
|
31
|
+
<a class="le-find" href="/workflows/new" data-le-nav="workflows">New workflow</a>
|
|
33
32
|
<nav class="le-foot" aria-label="Settings">
|
|
34
33
|
<a href="/config?section=models" data-le-nav="models">Models</a>
|
|
35
34
|
<a href="/config?section=apps" data-le-nav="apps">Apps</a>
|
|
@@ -174,7 +173,7 @@ export const consoleShellScript = `<script>
|
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
var section = params.get('section');
|
|
177
|
-
var foot = page === 'config' ? (section === 'abilities' || section === 'gateways' ? 'abilities' : section === 'apps' ? 'apps' : section === 'settings' ? 'settings' : 'models') : page === 'interview' || page === '
|
|
176
|
+
var foot = page === 'config' ? (section === 'abilities' || section === 'gateways' ? 'abilities' : section === 'apps' ? 'apps' : section === 'settings' ? 'settings' : 'models') : page === 'interview' || page === 'workflows' ? page : '';
|
|
178
177
|
var footLinks = document.querySelectorAll('[data-le-nav]');
|
|
179
178
|
for (var i = 0; i < footLinks.length; i++) {
|
|
180
179
|
if (footLinks[i].getAttribute('data-le-nav') === foot) footLinks[i].setAttribute('aria-current', 'page');
|
|
@@ -360,11 +359,12 @@ export const consoleShellScript = `<script>
|
|
|
360
359
|
return decision ? '<span class="le-pill ' + decision + '">' + DECISION_LABELS[decision] + '</span>' : '<span class="hint">No rule</span>';
|
|
361
360
|
}
|
|
362
361
|
|
|
363
|
-
// Public (npm) or Private, with where
|
|
362
|
+
// Official (@loopengine-co), Public (other npm) or Private, with where
|
|
363
|
+
// it came from on hover.
|
|
364
364
|
function abilityOriginBadge(origin) {
|
|
365
365
|
if (!origin) return '';
|
|
366
366
|
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>';
|
|
367
|
+
(origin.kind === 'private' ? 'Private' : origin.kind === 'official' ? 'Official' : 'Public') + '</span> <span class="hint">' + esc(origin.label) + '</span>';
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
|
|
@@ -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
|
|
@@ -15,6 +15,8 @@ import { ActauthRuleExistsError, addActauthRule, applyPlannedRules, setDefaultDe
|
|
|
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 });
|
|
@@ -179,8 +192,11 @@ export async function createFromTemplateObject(template, deps) {
|
|
|
179
192
|
// none twice, a stricter one placed so it counts, none loosened.
|
|
180
193
|
applyPlannedRules(name, setup.rules.map((r) => ({ name: r.name, tool: r.tool, decision: r.decision })));
|
|
181
194
|
// 3. Its own skills and non-secret settings.
|
|
182
|
-
for (const skill of setup.skills ?? [])
|
|
195
|
+
for (const skill of setup.skills ?? []) {
|
|
183
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
|
+
}
|
|
184
200
|
for (const [variable, value] of Object.entries(setup.env ?? {}))
|
|
185
201
|
await setAgentEnvVar(name, variable, value);
|
|
186
202
|
// 4. Load it, now that everything is in place.
|
|
@@ -1,156 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
// (
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// script avoids backslashes and template placeholders, since it lives
|
|
6
|
-
// inside a template literal.
|
|
7
|
-
import { devUiCss } from './dev-ui-styles.js';
|
|
8
|
-
import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
|
|
1
|
+
// Templates are now workflow examples, on the new-workflow page
|
|
2
|
+
// (web/workflow-page.ts). This page only sends a project server from an
|
|
3
|
+
// older template, which still serves it at /templates, there.
|
|
4
|
+
/** @deprecated The console redirects /templates to the workflow examples. */
|
|
9
5
|
export const templatesPageHtml = `<!doctype html>
|
|
10
6
|
<html lang="en">
|
|
11
7
|
<head>
|
|
12
8
|
<meta charset="utf-8">
|
|
13
|
-
<meta
|
|
14
|
-
<title>
|
|
15
|
-
<style>${devUiCss}
|
|
16
|
-
.tp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; max-width: 1000px; }
|
|
17
|
-
.tp-card { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; display: grid; gap: 8px; align-content: start; }
|
|
18
|
-
.tp-card h3 { margin: 0; font-size: 15px; }
|
|
19
|
-
.tp-cat { justify-self: start; font-size: 11.5px; font-weight: 600; color: var(--ink-muted); background: var(--surface); padding: 2px 8px; border-radius: 999px; }
|
|
20
|
-
.tp-card .meta { font-size: 12.5px; color: var(--ink-muted); display: grid; gap: 4px; }
|
|
21
|
-
.tp-card .row { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
22
|
-
.tp-preview { border-top: 1px solid var(--line); padding-top: 10px; display: grid; gap: 8px; font-size: 13px; }
|
|
23
|
-
.tp-preview pre { white-space: pre-wrap; max-height: 220px; overflow: auto; margin: 0; }
|
|
24
|
-
.tp-btn { display: inline-block; padding: 6px 12px; border-radius: 6px; text-decoration: none; font-size: 13px; font-weight: 500; background: var(--accent); color: var(--on-accent); }
|
|
25
|
-
.tp-btn.plain { background: none; color: var(--ink); border: 1px solid var(--line); }
|
|
26
|
-
.hint { font-size: 12px; color: var(--ink-muted); }
|
|
27
|
-
.error { font-size: 13px; color: var(--bad); }
|
|
28
|
-
</style>
|
|
9
|
+
<meta http-equiv="refresh" content="0; url=/workflows/new#examples">
|
|
10
|
+
<title>Workflow examples</title>
|
|
29
11
|
</head>
|
|
30
|
-
<body
|
|
31
|
-
<
|
|
32
|
-
${consoleSidebarHtml}
|
|
33
|
-
<main class="le-main">
|
|
34
|
-
<div class="le-head plain">
|
|
35
|
-
<div class="le-head-row">
|
|
36
|
-
<div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>
|
|
37
|
-
<div><h1>Templates</h1><p>Ready-made agents to start from. Each one asks you before it sends, pays, changes or deletes anything.</p></div>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="le-scroll"><div class="le-page" id="tpPage"><p class="hint">Loading…</p></div></div>
|
|
42
|
-
</main>
|
|
43
|
-
</div>
|
|
44
|
-
${consoleShellScript}
|
|
45
|
-
<script>
|
|
46
|
-
(function () {
|
|
47
|
-
var page = document.getElementById('tpPage');
|
|
48
|
-
var ELLIPSIS = String.fromCharCode(8230);
|
|
49
|
-
|
|
50
|
-
function esc(s) {
|
|
51
|
-
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
52
|
-
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function api(method, path) {
|
|
56
|
-
return fetch(path, { method: method, cache: 'no-store' })
|
|
57
|
-
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); });
|
|
58
|
-
}
|
|
59
|
-
function checklistHtml(items) {
|
|
60
|
-
if (!items.length) return '<p>Nothing else to set up.</p>';
|
|
61
|
-
return '<ul style="margin:0;padding-left:18px">' + items.map(function (it) {
|
|
62
|
-
return '<li>' + (it.href ? '<a href="' + esc(it.href) + '">' + esc(it.label) + '</a>' : esc(it.label)) + '</li>';
|
|
63
|
-
}).join('') + '</ul>';
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
var canInstall = true;
|
|
67
|
-
var DECISIONS = { allow: 'runs on its own', ask: 'asks you first', deny: 'never' };
|
|
68
|
-
var CHECK = String.fromCharCode(10003);
|
|
69
|
-
|
|
70
|
-
function cardHtml(t) {
|
|
71
|
-
return '<div class="tp-card" data-id="' + esc(t.id) + '">' +
|
|
72
|
-
'<span class="tp-cat">' + esc(t.category) + '</span>' +
|
|
73
|
-
'<h3>' + esc(t.title) + '</h3>' +
|
|
74
|
-
'<div>' + esc(t.summary) + '</div>' +
|
|
75
|
-
'<div class="meta">' +
|
|
76
|
-
'<div><strong>Runs:</strong> ' + esc(t.trigger) + '</div>' +
|
|
77
|
-
(t.asksFirst.length ? '<div><strong>Asks you first:</strong> ' + esc(t.asksFirst.join(', ')) + '</div>' : '') +
|
|
78
|
-
'<div><strong>Installs:</strong> ' + t.abilities.map(function (a) { return '<code>' + esc(a) + '</code>'; }).join(' ') + '</div>' +
|
|
79
|
-
'</div>' +
|
|
80
|
-
'<div class="row"><button type="button" class="primary tp-use">Use this template</button></div>' +
|
|
81
|
-
'<div class="tp-slot"></div></div>';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function listHtml(items) {
|
|
85
|
-
return '<ul style="margin:4px 0 0;padding-left:18px">' + items.join('') + '</ul>';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function show() {
|
|
89
|
-
api('GET', '/starter-templates').then(function (r) {
|
|
90
|
-
if (!r.ok) { page.innerHTML = '<p class="error">' + esc(r.body.error || 'Could not load the templates') + '</p>'; return; }
|
|
91
|
-
canInstall = r.body.canInstallAbilities !== false;
|
|
92
|
-
page.innerHTML = '<div class="tp-grid">' + r.body.templates.map(cardHtml).join('') + '</div>' +
|
|
93
|
-
'<p class="hint" style="margin-top:16px">Not what you need? <a href="/interview">Find your first agent</a> suggests one from your own work, or <a href="/agents?new=1">start from a blank agent</a>.</p>';
|
|
94
|
-
Array.prototype.forEach.call(page.querySelectorAll('.tp-use'), function (btn) {
|
|
95
|
-
btn.addEventListener('click', function () { preview(btn.closest('.tp-card')); });
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function preview(card) {
|
|
101
|
-
var id = card.getAttribute('data-id');
|
|
102
|
-
var slot = card.querySelector('.tp-slot');
|
|
103
|
-
slot.innerHTML = '<p class="hint">Loading' + ELLIPSIS + '</p>';
|
|
104
|
-
api('GET', '/starter-templates/' + encodeURIComponent(id) + '/preview').then(function (r) {
|
|
105
|
-
if (!r.ok) { slot.innerHTML = '<p class="error">' + esc(r.body.error || 'Could not prepare it') + '</p>'; return; }
|
|
106
|
-
var p = r.body;
|
|
107
|
-
var settings = Object.keys(p.settings || {});
|
|
108
|
-
slot.innerHTML = '<div class="tp-preview">' +
|
|
109
|
-
'<div><strong>Agent:</strong> <code>' + esc(p.name) + '</code></div>' +
|
|
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>' +
|
|
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>' +
|
|
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
|
-
'<div><strong>Permissions</strong> <span class="hint">(anything not listed asks you first)</span>' +
|
|
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>Keys it needs</strong>' +
|
|
118
|
-
listHtml(p.keys.map(function (k) {
|
|
119
|
-
var names = k.names.length > 1 ? 'one of ' + k.names.join(', ') : k.names[0];
|
|
120
|
-
return '<li>' + esc(names) + (k.set ? ' <span class="hint">' + CHECK + ' already set</span>' : '') + '</li>';
|
|
121
|
-
})) + '</div>' +
|
|
122
|
-
(settings.length ? '<div><strong>Settings filled in:</strong> ' + settings.map(function (s) { return '<code>' + esc(s) + '=' + esc(p.settings[s]) + '</code>'; }).join(', ') + '</div>' : '') +
|
|
123
|
-
(p.schedule ? '<div><strong>Schedule:</strong> ' + esc(p.schedule) + '</div>' : '') +
|
|
124
|
-
(p.needsApprovals ? '<div><strong>Approvals:</strong> requests reach you wherever its Messaging tab sends them (Slack, email, Lark or a webhook).</div>' : '') +
|
|
125
|
-
(p.notes.length ? '<div><strong>You also</strong>' + listHtml(p.notes.map(function (n) { return '<li>' + esc(n) + '</li>'; })) + '</div>' : '') +
|
|
126
|
-
'<div class="row"><button type="button" class="primary tp-create">Create it</button><button type="button" class="tp-cancel">Cancel</button></div>' +
|
|
127
|
-
'<div class="hint tp-status"></div></div>';
|
|
128
|
-
slot.querySelector('.tp-cancel').addEventListener('click', function () { slot.innerHTML = ''; });
|
|
129
|
-
slot.querySelector('.tp-create').addEventListener('click', function (ev) {
|
|
130
|
-
var btn = ev.currentTarget;
|
|
131
|
-
btn.disabled = true;
|
|
132
|
-
slot.querySelector('.tp-status').textContent = 'Creating it and installing its abilities' + ELLIPSIS + ' (this can take a minute)';
|
|
133
|
-
api('POST', '/starter-templates/' + encodeURIComponent(id) + '/create').then(function (r2) {
|
|
134
|
-
if (!r2.ok) { btn.disabled = false; slot.querySelector('.tp-status').textContent = r2.body.error || 'Could not create it'; return; }
|
|
135
|
-
var c = r2.body;
|
|
136
|
-
var installed = c.abilities.installed.map(function (a) { return '<code>' + esc(a) + '</code>'; }).join(' ');
|
|
137
|
-
slot.innerHTML = '<div class="tp-preview">' +
|
|
138
|
-
'<div><strong>Created <code>' + esc(c.name) + '</code>.</strong> Anything its permissions don' + String.fromCharCode(8217) + 't cover asks you first.</div>' +
|
|
139
|
-
(installed ? '<div>' + CHECK + ' Installed ' + installed + '</div>' : '') +
|
|
140
|
-
(c.schedule ? '<div>' + CHECK + ' Schedule created, paused: ' + esc(c.schedule.description) + (c.schedule.next_run_at ? ' (first run ' + esc(new Date(c.schedule.next_run_at).toLocaleString(undefined, c.schedule.timeZone ? { timeZone: c.schedule.timeZone, timeZoneName: 'short' } : undefined)) + ' once turned on)' : '') + '</div>' : '') +
|
|
141
|
-
(c.loadError ? '<div class="error">It was written but couldn' + String.fromCharCode(8217) + 't be loaded: ' + esc(c.loadError) + '</div>' : '') +
|
|
142
|
-
(c.scheduleError ? '<div class="error">Its schedule wasn' + String.fromCharCode(8217) + 't created: ' + esc(c.scheduleError) + '. Add one in its Schedules tab.</div>' : '') +
|
|
143
|
-
'<div><strong>Finish setup:</strong>' + checklistHtml(c.checklist) + '</div>' +
|
|
144
|
-
(c.exampleMessage ? '<div><strong>Try it</strong> in Chat with: <em>' + esc(c.exampleMessage) + '</em></div>' : '') +
|
|
145
|
-
'<div class="row"><a class="tp-btn" href="/agents/config?agent=' + encodeURIComponent(c.name) + '">Open ' + esc(c.name) + '</a>' +
|
|
146
|
-
'<a class="tp-btn plain" href="' + esc(leShell.tabHref(c.name, 'chat')) + '">Try it in Chat</a></div></div>';
|
|
147
|
-
}).catch(function () { btn.disabled = false; slot.querySelector('.tp-status').textContent = 'Could not reach the server; try again.'; });
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
show();
|
|
153
|
-
})();
|
|
154
|
-
</script>
|
|
12
|
+
<body>
|
|
13
|
+
<p>Templates are now workflow examples: <a href="/workflows/new#examples">open them</a>.</p>
|
|
155
14
|
</body>
|
|
156
15
|
</html>`;
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
// agent it would go into, and creates it: as a new agent through the same
|
|
5
5
|
// path as a template (web/templates-admin.ts), or added to an existing
|
|
6
6
|
// one without changing anything that agent already does.
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
7
8
|
import { join } from 'node:path';
|
|
8
9
|
import { addGatewayTool, readGatewayToolsFromDir } from '../core/gateway-tools.js';
|
|
9
10
|
import { applyPlannedRules } from './actauth-admin.js';
|
|
10
11
|
import { writeSkill } from './skills-admin.js';
|
|
12
|
+
import { setAgentEnvVar } from './env-admin.js';
|
|
11
13
|
import { createFromTemplateObject, installFailureReason, templateChecklist } from './templates-admin.js';
|
|
12
14
|
import { templateScheduleStart } from '../core/system-agents/templates.js';
|
|
13
15
|
import { planParameters, planWebhook, planWorkflowWithModel, workflowTemplate } from '../core/system-agents/workflow-planner.js';
|
|
@@ -436,6 +438,17 @@ export async function createWorkflow(input, deps) {
|
|
|
436
438
|
const ruled = applyPlannedRules(agent.name, plan.rules);
|
|
437
439
|
// 3. Its steps, as a skill; then load the agent again with all of it.
|
|
438
440
|
writeSkill(agent.name, plan.id, { description: plan.skill.description, body: plan.skill.body });
|
|
441
|
+
// The skills and settings it comes with, where the agent has none:
|
|
442
|
+
// its own policies and values stay as they are.
|
|
443
|
+
for (const skill of plan.skills ?? []) {
|
|
444
|
+
if (skill.id === plan.id || existsSync(join(deps.agentDir(agent.name), 'skills', skill.id, 'SKILL.md')))
|
|
445
|
+
continue;
|
|
446
|
+
writeSkill(agent.name, skill.id, { description: skill.description, body: skill.body });
|
|
447
|
+
}
|
|
448
|
+
for (const [variable, value] of Object.entries(plan.env ?? {})) {
|
|
449
|
+
if (!deps.envValue(agent.name, variable))
|
|
450
|
+
await setAgentEnvVar(agent.name, variable, value);
|
|
451
|
+
}
|
|
439
452
|
let loadError;
|
|
440
453
|
try {
|
|
441
454
|
await deps.reloadAgent(agent.name);
|