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.
- package/dist/core/agent-config.d.ts +9 -5
- package/dist/core/durable-approvals.d.ts +106 -0
- package/dist/core/durable-approvals.js +158 -0
- package/dist/core/gateway-tools.js +10 -9
- package/dist/core/run-agent.js +20 -3
- package/dist/index.d.ts +24 -1
- package/dist/index.js +26 -1
- package/dist/web/actauth-admin.d.ts +36 -0
- package/dist/web/actauth-admin.js +108 -0
- package/dist/web/agent-file-admin.d.ts +30 -0
- package/dist/web/agent-file-admin.js +165 -0
- package/dist/web/agents-config-page.d.ts +1 -0
- package/dist/web/agents-config-page.js +1628 -0
- package/dist/web/agents-list-page.d.ts +1 -0
- package/dist/web/agents-list-page.js +246 -0
- package/dist/web/dev-ui-styles.d.ts +1 -0
- package/dist/web/dev-ui-styles.js +63 -0
- package/dist/web/global-config-page.d.ts +1 -0
- package/dist/web/global-config-page.js +303 -0
- package/dist/web/global-config.d.ts +57 -0
- package/dist/web/global-config.js +67 -0
- package/dist/web/playground.d.ts +1 -0
- package/dist/web/playground.js +1271 -0
- package/dist/web/skills-admin.d.ts +23 -0
- package/dist/web/skills-admin.js +67 -0
- package/dist/web/web-approver.d.ts +35 -0
- package/dist/web/web-approver.js +88 -0
- package/package.json +5 -2
|
@@ -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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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…</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…</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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 ? ' · ' + 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>;
|