loopengine 0.1.97 → 0.1.99

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