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.
@@ -728,7 +728,7 @@ ${consoleShellScript}
728
728
  '<span><a href="?agent=' + encodeURIComponent(agentName) + '&amp;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">results go to your messaging app</span>' : '') +
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('sends each result to you');
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"> Send each run' + String.fromCharCode(8217) + 's result to you through the agent' + String.fromCharCode(8217) + 's messaging app</label>' +
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="Send each run&#39;s result through this agent&#39;s messaging connection (Messaging tab)"><input type="checkbox" class="schedule-notify-toggle" data-id="' + escapeHtml(t.task_id) + '"' + (t.notify ? ' checked' : '') + '> Send each result to you</label>'
2435
- : (t.notify ? '<div class="hint">Sent each result to you</div>' : '')) + '</td>' +
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"> Send each result through this agent&#39;s messaging connection (set in the Messaging tab)</label>' +
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 public (npm, tagged loopengine-ability) packages,
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
- // A public (npm) or private ability not installed here yet: its
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 public and private abilities.
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&hellip;</p></div>' +
3117
- '<h3 style="margin-top:24px;justify-content:flex-start;align-items:baseline;gap:8px">Public abilities <span class="hint" style="font-weight:400">npm packages tagged loopengine-ability</span></h3>' +
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 public abilities" style="flex:1">' +
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 public abilities found.</p>';
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">&#9776;</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="/templates" style="padding:6px 12px;border:1px solid var(--line);border-radius:6px;text-decoration:none;color:var(--ink);font-size:13px">Templates</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>
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="/templates">Start from a template</a>, or <a href="/interview">find your first agent</a>: a short interview that suggests one and creates it.</p>
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/&lt;name&gt;/)</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="/templates" data-le-nav="templates">Start from a template</a>
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 === 'templates' ? 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 it came from on hover.
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. Public abilities (npm) are browsed
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>Public <span class="hint">npm packages tagged loopengine-ability; add one to an agent from its Apps tab</span></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 public abilities" style="flex:1">' +
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&hellip;</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 public results don't push it down.
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 ? ' &middot; ' + escapeHtml(s.description) : '') + (s.header ? ' &middot; 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 the public ones on npm.</p><div class="source" id="abilitySourcesBox"><p class="hint">Loading' + String.fromCharCode(8230) + '</p></div></section>' +
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&#39;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 public ones
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
- if (template.setup.needsApprovals && !deps.messagingConfigured(name)) {
108
- items.push({ label: 'Choose where approval requests go: Slack, email, Lark or a webhook', href: tab('messaging') });
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 +1,2 @@
1
+ /** @deprecated The console redirects /templates to the workflow examples. */
1
2
  export declare const templatesPageHtml: string;
@@ -1,156 +1,15 @@
1
- // The console's Templates page (/templates): ready-made agents
2
- // (core/system-agents/templates.ts) a person can preview and create in
3
- // one click (web/templates-admin.ts behind /starter-templates/*). Same
4
- // shell and styles as the other console pages. Like them, its inline
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 name="viewport" content="width=device-width, initial-scale=1">
14
- <title>Templates</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 data-le-page="templates">
31
- <div class="le-app">
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">&#9776;</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&hellip;</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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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);