loopengine 0.1.2 → 0.1.4

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.
@@ -0,0 +1 @@
1
+ export declare const agentsListPageHtml: string;
@@ -0,0 +1,246 @@
1
+ // The HTML face of GET /agents. adapters/http.ts content-negotiates that
2
+ // route: a browser navigating there (Accept: text/html, ...) gets this
3
+ // page; a client fetching it programmatically (fetch()'s default
4
+ // Accept: */*, same as playground.ts and agents-config-page.ts's own
5
+ // fetch('/agents') calls) still gets the plain {agents: [...]} JSON body
6
+ // that route always returned, unchanged. See web/dev-ui-styles.ts for
7
+ // why the look is a shared import rather than a third copy of the same CSS.
8
+ import { devUiCss } from './dev-ui-styles.js';
9
+ export const agentsListPageHtml = `<!doctype html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1">
14
+ <title>LoopEngine Agents</title>
15
+ <style>${devUiCss}
16
+ main { max-width: 640px; margin: 0 auto; padding: 24px 20px; }
17
+ h1 { font-size: 18px; margin: 0 0 4px; }
18
+ p.intro { color: light-dark(#666, #999); font-size: 13px; margin: 0 0 20px; }
19
+ ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
20
+ li {
21
+ border: 1px solid light-dark(#ddd, #333);
22
+ border-radius: 8px;
23
+ padding: 12px 14px;
24
+ background: light-dark(#fff, #202024);
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ gap: 12px;
29
+ }
30
+ li .name { font-family: ui-monospace, monospace; font-size: 14px; font-weight: 600; }
31
+ li .prompt {
32
+ font-size: 12px;
33
+ color: light-dark(#666, #999);
34
+ margin-top: 2px;
35
+ max-width: 380px;
36
+ overflow: hidden;
37
+ text-overflow: ellipsis;
38
+ white-space: nowrap;
39
+ }
40
+ li .links { display: flex; gap: 10px; font-size: 12px; flex-shrink: 0; }
41
+ li .links a {
42
+ text-decoration: none;
43
+ border: 1px solid light-dark(#ccc, #444);
44
+ border-radius: 6px;
45
+ padding: 4px 9px;
46
+ }
47
+ li .links a:hover { background: light-dark(#eee, #2a2a2e); }
48
+ #empty { color: light-dark(#666, #999); font-size: 13px; }
49
+ .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 4px; }
50
+ .page-head h1 { margin: 0; }
51
+ .hint { font-size: 12px; color: light-dark(#666, #999); }
52
+ .error { font-size: 12px; color: light-dark(#991b1b, #f87171); }
53
+ #newAgentForm {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 8px;
57
+ margin: 4px 0 20px;
58
+ max-width: 480px;
59
+ }
60
+ #newAgentForm .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
61
+ #newAgentForm .row input { flex: 1; min-width: 180px; max-width: 280px; }
62
+ #newAgentForm details { font-size: 12px; }
63
+ #newAgentForm details summary { cursor: pointer; color: light-dark(#666, #999); margin-bottom: 8px; }
64
+ #newAgentForm details label {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 4px;
68
+ margin-bottom: 8px;
69
+ font-size: 12px;
70
+ }
71
+ #newAgentForm details textarea { min-height: 60px; font-family: inherit; }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <nav class="topnav">
76
+ <a href="/agents" class="active">Agents</a>
77
+ <a href="/agents/config">Config</a>
78
+ <a href="/playground">Playground</a>
79
+ </nav>
80
+ <main>
81
+ <div class="page-head">
82
+ <h1>Registered agents</h1>
83
+ <button type="button" id="newAgentBtn">+ New agent</button>
84
+ </div>
85
+ <p class="intro">Every AgentConfig discovered under agents/ — open one in the playground to chat with it, or in the config viewer to see its tools, permissions, and hooks.</p>
86
+ <form id="newAgentForm" style="display:none">
87
+ <div class="row">
88
+ <input type="text" name="name" placeholder="weather-agent" pattern="[a-z0-9]+(-[a-z0-9]+)*" required>
89
+ <button type="submit">Create</button>
90
+ <button type="button" id="cancelNewAgentBtn">Cancel</button>
91
+ </div>
92
+ <details>
93
+ <summary>Advanced (optional)</summary>
94
+ <label>System prompt
95
+ <textarea name="systemPrompt" placeholder="You are ..."></textarea>
96
+ </label>
97
+ <label>Model provider
98
+ <select name="provider">
99
+ <option value="anthropic">anthropic</option>
100
+ <option value="openai">openai</option>
101
+ <option value="deepseek">deepseek</option>
102
+ </select>
103
+ </label>
104
+ <label>Model name <span class="hint">(required for openai/deepseek; defaults to claude-sonnet-5 for anthropic)</span>
105
+ <input type="text" name="modelName" placeholder="claude-sonnet-5">
106
+ </label>
107
+ </details>
108
+ </form>
109
+ <p class="error" id="newAgentError" style="display:none"></p>
110
+ <p class="hint" id="newAgentResult" style="display:none"></p>
111
+ <ul id="agentUl"></ul>
112
+ <p id="empty" style="display:none">No agents registered.</p>
113
+ </main>
114
+ <script>
115
+ (function () {
116
+ var agentUl = document.getElementById('agentUl');
117
+ var empty = document.getElementById('empty');
118
+ var newAgentBtn = document.getElementById('newAgentBtn');
119
+ var newAgentForm = document.getElementById('newAgentForm');
120
+ var newAgentError = document.getElementById('newAgentError');
121
+ var newAgentResult = document.getElementById('newAgentResult');
122
+ var cancelNewAgentBtn = document.getElementById('cancelNewAgentBtn');
123
+
124
+ function escapeHtml(s) {
125
+ return String(s).replace(/[&<>"']/g, function (c) {
126
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
127
+ });
128
+ }
129
+
130
+ // (Re)loads the list from GET /agents — factored out so a successful
131
+ // create can refresh it directly instead of duplicating this fetch.
132
+ function loadAgents() {
133
+ fetch('/agents')
134
+ .then(function (r) { return r.json(); })
135
+ .then(function (data) {
136
+ var agents = data.agents || [];
137
+ agentUl.textContent = '';
138
+ if (!agents.length) {
139
+ empty.style.display = 'block';
140
+ return;
141
+ }
142
+ empty.style.display = 'none';
143
+ for (var i = 0; i < agents.length; i++) {
144
+ var agent = agents[i];
145
+ var li = document.createElement('li');
146
+ var qs = '?agent=' + encodeURIComponent(agent.name);
147
+ li.innerHTML =
148
+ '<div><div class="name">' + escapeHtml(agent.name) + '</div>' +
149
+ '<div class="prompt">' + escapeHtml(agent.systemPrompt) + '</div></div>' +
150
+ '<div class="links">' +
151
+ '<a href="/agents/config' + qs + '">Config</a>' +
152
+ '<a href="/playground' + qs + '">Playground</a>' +
153
+ '</div>';
154
+ agentUl.appendChild(li);
155
+ }
156
+ })
157
+ .catch(function (err) {
158
+ empty.textContent = 'Could not load agents: ' + err.message;
159
+ empty.style.display = 'block';
160
+ });
161
+ }
162
+
163
+ // handleCreateAgent (adapters/http.ts) loads and registers the new
164
+ // agent into the *running* server immediately — see its own doc
165
+ // comment for why that's safe (a module that's never been imported
166
+ // before has nothing stale to invalidate, unlike hot-reloading an
167
+ // already-loaded agent's code would). registered: true means it's
168
+ // genuinely live already, so this just re-fetches the list rather than
169
+ // telling the operator to restart anything. registered: false is
170
+ // the rare fallback (the file was scaffolded, but loading it back
171
+ // failed for some reason) — that case still needs a real restart,
172
+ // same as before this existed; loopengine dev picks it up
173
+ // automatically via its own --include 'agents/*/index.ts' (see
174
+ // bin/cli.ts's own comment there).
175
+ newAgentBtn.addEventListener('click', function () {
176
+ newAgentBtn.style.display = 'none';
177
+ newAgentForm.style.display = 'flex';
178
+ newAgentResult.style.display = 'none';
179
+ newAgentForm.querySelector('input[name="name"]').focus();
180
+ });
181
+
182
+ cancelNewAgentBtn.addEventListener('click', function () {
183
+ newAgentForm.style.display = 'none';
184
+ newAgentBtn.style.display = '';
185
+ newAgentError.style.display = 'none';
186
+ });
187
+
188
+ newAgentForm.addEventListener('submit', function (ev) {
189
+ ev.preventDefault();
190
+ newAgentError.style.display = 'none';
191
+ var data = new FormData(newAgentForm);
192
+ var submitBtn = newAgentForm.querySelector('button[type="submit"]');
193
+ submitBtn.disabled = true;
194
+
195
+ // systemPrompt/model are both optional — see
196
+ // adapters/http.ts's parseAgentTemplateOptions for the defaults
197
+ // scaffoldAgent falls back to when either is left out of the body
198
+ // entirely (blank/untouched Advanced fields are treated the same
199
+ // as not sending them at all).
200
+ var body = { name: data.get('name') };
201
+ var systemPrompt = data.get('systemPrompt');
202
+ if (systemPrompt && systemPrompt.trim()) body.systemPrompt = systemPrompt;
203
+ var provider = data.get('provider');
204
+ if (provider) {
205
+ body.model = { provider: provider };
206
+ var modelName = data.get('modelName');
207
+ if (modelName && modelName.trim()) body.model.model = modelName;
208
+ }
209
+
210
+ fetch('/agents', {
211
+ method: 'POST',
212
+ headers: { 'content-type': 'application/json' },
213
+ body: JSON.stringify(body),
214
+ })
215
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
216
+ .then(function (result) {
217
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
218
+ newAgentForm.style.display = 'none';
219
+ newAgentBtn.style.display = '';
220
+ newAgentForm.reset();
221
+ newAgentResult.style.display = 'block';
222
+ if (result.body.registered) {
223
+ newAgentResult.textContent = 'Created ' + result.body.path + ' — it\\'s live now.';
224
+ loadAgents();
225
+ } else {
226
+ newAgentResult.textContent =
227
+ 'Created ' + result.body.path + ', but it could not be loaded into this running server' +
228
+ (result.body.error ? ' (' + result.body.error + ')' : '') +
229
+ '. Restart the server to pick it up — loopengine dev does this automatically.';
230
+ }
231
+ })
232
+ .catch(function (err) {
233
+ newAgentError.style.display = 'block';
234
+ newAgentError.textContent = err.message;
235
+ })
236
+ .finally(function () {
237
+ submitBtn.disabled = false;
238
+ });
239
+ });
240
+
241
+ loadAgents();
242
+ })();
243
+ </script>
244
+ </body>
245
+ </html>
246
+ `;
@@ -0,0 +1 @@
1
+ export declare const devUiCss: string;
@@ -0,0 +1,63 @@
1
+ // Shared look for every browser page adapters/http.ts serves — playground
2
+ // (playground.ts), the per-agent config viewer (agents-config-page.ts),
3
+ // and the agents index (agents-list-page.ts). Pulled out once these grew to
4
+ // three pages that all want the same fonts/colors/form controls: without
5
+ // this, harmonizing them meant hand-syncing the same CSS block in three
6
+ // separate template-literal strings every time one changed. Kept to the
7
+ // tokens actually shared across all three (base colors, form controls,
8
+ // badges, the cross-page nav strip) — each page still owns its own
9
+ // page-specific rules (chat bubbles, timeline events, permission tables, ...)
10
+ // in its own <style> block.
11
+ export const devUiCss = `
12
+ :root { color-scheme: light dark; }
13
+ * { box-sizing: border-box; }
14
+ body {
15
+ margin: 0;
16
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17
+ background: light-dark(#f7f7f8, #1a1a1e);
18
+ color: light-dark(#1a1a1e, #e8e8ea);
19
+ }
20
+ a { color: light-dark(#1d4ed8, #60a5fa); }
21
+ select, input, textarea, button {
22
+ font: inherit;
23
+ color: inherit;
24
+ background: light-dark(#fff, #26262b);
25
+ border: 1px solid light-dark(#ccc, #444);
26
+ border-radius: 6px;
27
+ padding: 6px 8px;
28
+ }
29
+ button {
30
+ cursor: pointer;
31
+ background: light-dark(#e8e8ea, #333338);
32
+ }
33
+ button:disabled { opacity: 0.5; cursor: default; }
34
+ code {
35
+ font-family: ui-monospace, monospace;
36
+ background: light-dark(#eee, #2a2a2e);
37
+ padding: 1px 5px;
38
+ border-radius: 4px;
39
+ font-size: 11px;
40
+ }
41
+ .badge {
42
+ display: inline-block;
43
+ font-size: 11px;
44
+ font-family: ui-monospace, monospace;
45
+ padding: 2px 8px;
46
+ border-radius: 10px;
47
+ background: light-dark(#eee, #2a2a2e);
48
+ }
49
+ .badge-custom { background: light-dark(#dcfce7, #14532d); color: light-dark(#166534, #86efac); }
50
+ .badge-default { background: light-dark(#eee, #2a2a2e); color: light-dark(#666, #999); }
51
+ .muted { color: light-dark(#666, #999); font-size: 12px; }
52
+ .topnav {
53
+ display: flex;
54
+ gap: 14px;
55
+ padding: 8px 16px;
56
+ font-size: 12px;
57
+ border-bottom: 1px solid light-dark(#ddd, #333);
58
+ background: light-dark(#fff, #202024);
59
+ }
60
+ .topnav a { text-decoration: none; }
61
+ .topnav a:hover { text-decoration: underline; }
62
+ .topnav a.active { color: light-dark(#1a1a1e, #e8e8ea); font-weight: 600; }
63
+ `;
@@ -0,0 +1 @@
1
+ export declare const globalConfigPageHtml: string;
@@ -0,0 +1,303 @@
1
+ // A local dev page: a single self-contained HTML page (no build step, no
2
+ // framework, no CDN) served by adapters/http.ts at GET /config — the
3
+ // account-wide counterpart to web/agents-config-page.ts's per-agent
4
+ // Overview/Skills/Tools/ActAuth tabs (served separately at
5
+ // /agents/config, deliberately left untouched by this page). A left
6
+ // sidebar picks between Models (which providers this deployment can
7
+ // call, and which agents use which) and Gateways (whether the composio
8
+ // CLI is authenticated on this machine) — see global-config.ts for where
9
+ // that data actually comes from. "Agents" is a plain link out to
10
+ // /agents/config, not a third client-rendered section here: that page
11
+ // already owns the full per-agent picture, duplicating or embedding it
12
+ // would just be two places for the same UI to drift apart.
13
+ //
14
+ // Embedded as a TS string (not a separate .html file), same reasoning as
15
+ // web/playground.ts: tsc's ordinary compile emits it straight into
16
+ // dist/adapters/ alongside http.js, no asset-copy step needed. Plain
17
+ // string concatenation, not a nested template literal, for the same
18
+ // backtick/${}-escaping reason playground.ts's own header comment
19
+ // explains — see also the memory note on this file's double-escaping
20
+ // hazard (template-literal escapes run twice: once when this constant
21
+ // is built, once when a browser parses the served <script> as JS).
22
+ import { devUiCss } from './dev-ui-styles.js';
23
+ export const globalConfigPageHtml = `<!doctype html>
24
+ <html lang="en">
25
+ <head>
26
+ <meta charset="utf-8">
27
+ <meta name="viewport" content="width=device-width, initial-scale=1">
28
+ <title>LoopEngine Config</title>
29
+ <style>${devUiCss}
30
+ body {
31
+ display: flex;
32
+ flex-direction: column;
33
+ height: 100vh;
34
+ }
35
+ .layout { flex: 1; display: flex; min-height: 0; }
36
+ nav.sidebar {
37
+ width: 180px;
38
+ flex-shrink: 0;
39
+ border-right: 1px solid light-dark(#ddd, #333);
40
+ padding: 6px;
41
+ }
42
+ nav.sidebar button, nav.sidebar a {
43
+ display: block;
44
+ width: 100%;
45
+ text-align: left;
46
+ background: none;
47
+ border: none;
48
+ border-radius: 6px;
49
+ padding: 8px 10px;
50
+ font-size: 13px;
51
+ cursor: pointer;
52
+ text-decoration: none;
53
+ color: inherit;
54
+ box-sizing: border-box;
55
+ }
56
+ nav.sidebar button:hover, nav.sidebar a:hover { background: light-dark(#eee, #26262b); }
57
+ nav.sidebar button.active { background: light-dark(#dbeafe, #1e3a5f); font-weight: 600; }
58
+ main {
59
+ flex: 1;
60
+ overflow-y: auto;
61
+ padding: 20px 28px;
62
+ }
63
+ h2 { font-size: 20px; margin: 0 0 4px; font-family: ui-monospace, monospace; }
64
+ section { margin-bottom: 22px; }
65
+ section h3 {
66
+ font-size: 12px;
67
+ text-transform: uppercase;
68
+ letter-spacing: 0.04em;
69
+ color: light-dark(#666, #999);
70
+ margin: 0 0 8px;
71
+ border-bottom: 1px solid light-dark(#eee, #2a2a2e);
72
+ padding-bottom: 4px;
73
+ }
74
+ table { width: 100%; border-collapse: collapse; font-size: 12px; }
75
+ th, td {
76
+ text-align: left;
77
+ padding: 6px 8px;
78
+ border-bottom: 1px solid light-dark(#eee, #2a2a2e);
79
+ vertical-align: top;
80
+ }
81
+ th { color: light-dark(#666, #999); font-weight: 600; }
82
+ .source {
83
+ border: 1px solid light-dark(#ddd, #3a3a3e);
84
+ border-radius: 8px;
85
+ padding: 12px 14px;
86
+ margin-bottom: 10px;
87
+ }
88
+ .source.unsupported { opacity: 0.6; }
89
+ .source-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
90
+ .source-head h4 { margin: 0; font-size: 14px; font-family: ui-monospace, monospace; }
91
+ .hint { font-size: 11px; color: light-dark(#666, #999); }
92
+ pre {
93
+ background: light-dark(#fff, #26262b);
94
+ border: 1px solid light-dark(#ddd, #3a3a3e);
95
+ border-radius: 6px;
96
+ padding: 8px 10px;
97
+ font-size: 12px;
98
+ white-space: pre-wrap;
99
+ word-break: break-word;
100
+ margin: 6px 0;
101
+ }
102
+ /* Same palette as agents-config-page.ts's own .delete-btn — kept in
103
+ sync by hand since each admin page owns its own <style> block (no
104
+ shared component CSS beyond devUiCss's base tokens), not because
105
+ this pink-for-destructive convention is meant to drift between them. */
106
+ .delete-btn {
107
+ font-size: 12px;
108
+ color: light-dark(#991b1b, #f87171);
109
+ background: light-dark(#fee2e2, #3a1f1f);
110
+ border-color: light-dark(#f3b4b4, #6b3232);
111
+ }
112
+ .delete-btn:hover { background: light-dark(#fecaca, #4a1f1f); }
113
+ </style>
114
+ </head>
115
+ <body>
116
+ <nav class="topnav">
117
+ <a href="/agents">Agents</a>
118
+ <a href="/agents/config" class="active">Config</a>
119
+ <a href="/playground">Playground</a>
120
+ </nav>
121
+ <div class="layout">
122
+ <nav class="sidebar">
123
+ <a href="/agents/config">Agents</a>
124
+ <button class="section-btn active" data-section="models">Models</button>
125
+ <button class="section-btn" data-section="gateways">Gateways</button>
126
+ </nav>
127
+ <main>
128
+ <div class="section-panel" data-section-panel="models">
129
+ <h2>Models</h2>
130
+ <div id="modelsContent"><p class="hint">Loading&hellip;</p></div>
131
+ </div>
132
+ <div class="section-panel" data-section-panel="gateways" style="display:none">
133
+ <h2>Gateways</h2>
134
+ <div id="gatewaysContent"><p class="hint">Loading&hellip;</p></div>
135
+ </div>
136
+ </main>
137
+ </div>
138
+ <script>
139
+ (function () {
140
+ var main = document.querySelector('main');
141
+ var sectionButtons = document.querySelectorAll('.section-btn');
142
+ var currentSection = 'models';
143
+ var loadedSections = {};
144
+
145
+ function escapeHtml(s) {
146
+ return String(s).replace(/[&<>"']/g, function (c) {
147
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
148
+ });
149
+ }
150
+
151
+ function badge(ok, yesLabel, noLabel) {
152
+ return '<span class="badge ' + (ok ? 'badge-custom' : 'badge-default') + '">' + escapeHtml(ok ? yesLabel : noLabel) + '</span>';
153
+ }
154
+
155
+ // ---- Models section ----
156
+
157
+ function renderModels(data) {
158
+ var providerRows = data.providers.map(function (p) {
159
+ return '<tr><td><code>' + escapeHtml(p.provider) + '</code></td>' +
160
+ '<td><code>' + escapeHtml(p.envVar) + '</code></td>' +
161
+ '<td>' + badge(p.configured, 'configured', 'not set') + '</td></tr>';
162
+ }).join('');
163
+
164
+ var agentRows = data.agents.length
165
+ ? data.agents.map(function (a) {
166
+ return '<tr><td><code>' + escapeHtml(a.agent) + '</code></td>' +
167
+ '<td><code>' + escapeHtml(a.provider) + '</code></td>' +
168
+ '<td><code>' + escapeHtml(a.model) + '</code></td></tr>';
169
+ }).join('')
170
+ : '<tr><td colspan="3"><p class="hint">No agents with a resolved model config (a custom createModelCall has nothing to report here).</p></td></tr>';
171
+
172
+ return '<section><h3>Providers</h3>' +
173
+ '<table><thead><tr><th>Provider</th><th>Env var</th><th>Status</th></tr></thead><tbody>' + providerRows + '</tbody></table>' +
174
+ '</section>' +
175
+ '<section><h3>Agents</h3>' +
176
+ '<table><thead><tr><th>Agent</th><th>Provider</th><th>Model</th></tr></thead><tbody>' + agentRows + '</tbody></table>' +
177
+ '</section>';
178
+ }
179
+
180
+ function loadModels() {
181
+ var content = document.getElementById('modelsContent');
182
+ fetch('/config/models')
183
+ .then(function (r) { return r.json(); })
184
+ .then(function (data) {
185
+ content.innerHTML = renderModels(data);
186
+ loadedSections.models = true;
187
+ })
188
+ .catch(function (err) {
189
+ content.innerHTML = '<p class="error">Could not load models: ' + escapeHtml(err.message) + '</p>';
190
+ });
191
+ }
192
+
193
+ // ---- Gateways section ----
194
+
195
+ function renderGatewayCard(g) {
196
+ var statusHtml;
197
+ var bodyHtml = '';
198
+ if (!g.supported) {
199
+ statusHtml = '<span class="hint">not yet supported</span>';
200
+ } else if (g.connected) {
201
+ statusHtml = badge(true, 'connected', 'not connected');
202
+ bodyHtml = '<p class="hint">' + escapeHtml(g.email || '') + (g.org ? ' &middot; ' + escapeHtml(g.org) : '') + '</p>' +
203
+ '<button type="button" class="delete-btn disconnect-btn" data-provider="' + escapeHtml(g.provider) + '">Disconnect</button>';
204
+ } else {
205
+ // No Connect button/form here — composio login's only
206
+ // non-interactive path (--user-api-key) doesn't accept the kind of
207
+ // key an operator actually has on hand (Composio hands out a
208
+ // separate x-consumer-api-key credential for MCP clients, which
209
+ // 401s against this flag); composio login itself opens a real
210
+ // browser, which a web request can't drive. These are the same
211
+ // two commands Composio's own docs give for getting started.
212
+ statusHtml = badge(false, 'connected', 'not connected');
213
+ bodyHtml = '<pre><code>curl -fsSL https://composio.dev/install | sh</code></pre>' +
214
+ '<pre><code>composio login</code></pre>' +
215
+ '<p class="hint">Run both on the machine running this server, then reload this page.</p>';
216
+ }
217
+ return '<div class="source' + (g.supported ? '' : ' unsupported') + '">' +
218
+ '<div class="source-head">' +
219
+ '<h4>' + escapeHtml(g.provider) + '</h4>' +
220
+ statusHtml +
221
+ '</div>' +
222
+ bodyHtml +
223
+ '</div>';
224
+ }
225
+
226
+ function applyGateways(gateways) {
227
+ var content = document.getElementById('gatewaysContent');
228
+ content.innerHTML = (gateways || []).map(renderGatewayCard).join('');
229
+ wireGatewayCards(content);
230
+ loadedSections.gateways = true;
231
+ }
232
+
233
+ // Disconnect is a machine-wide CLI-session action (see
234
+ // disconnectComposioAccount's own doc comment in gateway-tools.ts) —
235
+ // it gets a confirm() for that reason, same as this admin UI's other
236
+ // consequential-but-not-undoable actions (removing a skill, a rule, a
237
+ // gateway tool). No Connect counterpart to wire — see renderGatewayCard's
238
+ // own comment for why that's instructions, not a form.
239
+ function wireGatewayCards(content) {
240
+ var disconnectButtons = content.querySelectorAll('.disconnect-btn');
241
+ for (var j = 0; j < disconnectButtons.length; j++) {
242
+ disconnectButtons[j].addEventListener('click', function (ev) {
243
+ var btn = ev.currentTarget;
244
+ var provider = btn.getAttribute('data-provider');
245
+ if (!confirm('Disconnect ' + provider + '? This logs out the CLI on this machine for every agent’s gateway tools, not just this browser tab.')) return;
246
+ btn.disabled = true;
247
+ fetch('/config/gateways/' + encodeURIComponent(provider) + '/disconnect', { method: 'POST' })
248
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
249
+ .then(function (result) {
250
+ if (!result.ok) throw new Error(result.body.error || 'request failed');
251
+ applyGateways(result.body.gateways || []);
252
+ })
253
+ .catch(function (err) {
254
+ alert('Could not disconnect: ' + err.message);
255
+ btn.disabled = false;
256
+ });
257
+ });
258
+ }
259
+ }
260
+
261
+ function loadGateways() {
262
+ var content = document.getElementById('gatewaysContent');
263
+ fetch('/config/gateways')
264
+ .then(function (r) { return r.json(); })
265
+ .then(function (data) {
266
+ applyGateways(data.gateways || []);
267
+ })
268
+ .catch(function (err) {
269
+ content.innerHTML = '<p class="error">Could not load gateways: ' + escapeHtml(err.message) + '</p>';
270
+ });
271
+ }
272
+
273
+ // ---- Section switching ----
274
+
275
+ function switchSection(section) {
276
+ currentSection = section;
277
+ for (var i = 0; i < sectionButtons.length; i++) {
278
+ sectionButtons[i].classList.toggle('active', sectionButtons[i].dataset.section === section);
279
+ }
280
+ var panels = main.querySelectorAll('.section-panel');
281
+ for (var j = 0; j < panels.length; j++) {
282
+ panels[j].style.display = panels[j].dataset.sectionPanel === section ? 'block' : 'none';
283
+ }
284
+ if (section === 'models' && !loadedSections.models) loadModels();
285
+ if (section === 'gateways' && !loadedSections.gateways) loadGateways();
286
+ }
287
+
288
+ for (var i = 0; i < sectionButtons.length; i++) {
289
+ sectionButtons[i].addEventListener('click', function (ev) { switchSection(ev.currentTarget.dataset.section); });
290
+ }
291
+
292
+ // Deep-linked from agents-config-page.ts's own Models/Gateways sidebar
293
+ // links (/config?section=gateways, say) — preselect it if it names a
294
+ // real section, same "trust the query param only if it's valid"
295
+ // caution playground.ts's own ?agent= handling already uses.
296
+ var requestedSection = new URLSearchParams(location.search).get('section');
297
+ var initialSection = requestedSection === 'gateways' || requestedSection === 'models' ? requestedSection : currentSection;
298
+ switchSection(initialSection);
299
+ })();
300
+ </script>
301
+ </body>
302
+ </html>
303
+ `;
@@ -0,0 +1,57 @@
1
+ import type { AgentConfig } from '../core/agent-config.js';
2
+ export interface ModelProviderInfo {
3
+ provider: 'anthropic' | 'openai' | 'deepseek';
4
+ envVar: string;
5
+ configured: boolean;
6
+ }
7
+ export interface AgentModelUsage {
8
+ agent: string;
9
+ provider: string;
10
+ model: string;
11
+ }
12
+ export interface ModelsView {
13
+ providers: ModelProviderInfo[];
14
+ agents: AgentModelUsage[];
15
+ }
16
+ /** Every registered agent's resolved model, alongside whether each
17
+ * supported provider actually has credentials available in this
18
+ * process's environment — an operator's first question when a request
19
+ * fails isn't "what does this one agent request?" (Overview's own Model
20
+ * section already answers that) but "is this deployment even set up to
21
+ * call that provider at all?". Read-only: providers/env vars are wired
22
+ * into the actual model-call modules (model-calls/*.ts), not something
23
+ * this page can add support for by itself. An agent with a custom
24
+ * `createModelCall` (no `config.model`) is skipped — there's no
25
+ * provider/model pair to report for it, the same "custom" case
26
+ * describeAgent's own model field already carves out.
27
+ *
28
+ * Takes the registered agent names and a config lookup as parameters,
29
+ * rather than importing core/agent-registry.ts's listAgents/getEntry
30
+ * directly — that module is this repo's own reference app, not part of
31
+ * the published package (see index.ts's own header comment), and its
32
+ * top-level-await agent scan is resolved relative to *its own* file
33
+ * location, which would be meaningless run from inside node_modules.
34
+ * Every caller already has its own local registry's listAgents/getEntry
35
+ * on hand (adapters/http.ts, in this repo or a scaffolded project alike)
36
+ * to pass straight through. */
37
+ export declare function describeModelProviders(agentNames: string[], getConfig: (name: string) => AgentConfig | undefined): ModelsView;
38
+ export interface GatewayProviderInfo {
39
+ provider: string;
40
+ supported: boolean;
41
+ connected: boolean;
42
+ email?: string;
43
+ org?: string;
44
+ }
45
+ export interface GatewaysView {
46
+ gateways: GatewayProviderInfo[];
47
+ }
48
+ /** The provider-level counterpart to gateway-tools.ts's own registry
49
+ * (which is scoped to one agent's chosen tools within an already-
50
+ * connected provider) — this is "is Composio itself usable from this
51
+ * machine at all", not "what has agent X registered". Nango/Arcade/
52
+ * Scalekit are listed as not-yet-supported placeholders, matching
53
+ * gateway-tools.ts's own header comment about them slotting in later as
54
+ * thin mcpplug ToolSource adapters once this mechanism is proven —
55
+ * shown so the Gateways panel isn't silently missing them, not because
56
+ * there's anything to check for them yet. */
57
+ export declare function describeGateways(cliCommand?: string): Promise<GatewaysView>;