clementine-agent 1.18.80 → 1.18.82
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/agent/assistant.d.ts +15 -0
- package/dist/agent/assistant.js +15 -0
- package/dist/cli/dashboard.js +421 -5
- package/dist/gateway/router.d.ts +9 -0
- package/dist/gateway/router.js +4 -0
- package/package.json +1 -1
|
@@ -100,6 +100,21 @@ export declare class PersonalAssistant {
|
|
|
100
100
|
}>;
|
|
101
101
|
updatedAt: string;
|
|
102
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* PRD Phase 2.1: clear the cached status for one server so the next query
|
|
105
|
+
* repopulates it from a fresh handshake. The SDK manages connections
|
|
106
|
+
* internally; we don't have a direct "reconnect now" hook, but invalidating
|
|
107
|
+
* the cached entry tells the dashboard to render 'pending' and resets any
|
|
108
|
+
* stale error/auth state. Returns the post-clear cached snapshot.
|
|
109
|
+
*/
|
|
110
|
+
invalidateMcpStatus(serverName: string): {
|
|
111
|
+
servers: Array<{
|
|
112
|
+
name: string;
|
|
113
|
+
status: string;
|
|
114
|
+
}>;
|
|
115
|
+
updatedAt: string;
|
|
116
|
+
cleared: boolean;
|
|
117
|
+
};
|
|
103
118
|
/** Inject a background work result into the session as silent follow-up context. */
|
|
104
119
|
injectPendingContext(sessionKey: string, userPrompt: string, result: string): void;
|
|
105
120
|
private initMemoryStore;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -810,6 +810,21 @@ export class PersonalAssistant {
|
|
|
810
810
|
getMcpStatus() {
|
|
811
811
|
return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime };
|
|
812
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* PRD Phase 2.1: clear the cached status for one server so the next query
|
|
815
|
+
* repopulates it from a fresh handshake. The SDK manages connections
|
|
816
|
+
* internally; we don't have a direct "reconnect now" hook, but invalidating
|
|
817
|
+
* the cached entry tells the dashboard to render 'pending' and resets any
|
|
818
|
+
* stale error/auth state. Returns the post-clear cached snapshot.
|
|
819
|
+
*/
|
|
820
|
+
invalidateMcpStatus(serverName) {
|
|
821
|
+
const beforeLen = this._lastMcpStatus.length;
|
|
822
|
+
this._lastMcpStatus = this._lastMcpStatus.filter((s) => s.name !== serverName);
|
|
823
|
+
const cleared = this._lastMcpStatus.length < beforeLen;
|
|
824
|
+
if (cleared)
|
|
825
|
+
this._lastMcpStatusTime = new Date().toISOString();
|
|
826
|
+
return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime, cleared };
|
|
827
|
+
}
|
|
813
828
|
/** Inject a background work result into the session as silent follow-up context. */
|
|
814
829
|
injectPendingContext(sessionKey, userPrompt, result) {
|
|
815
830
|
const pending = this.pendingContext.get(sessionKey) ?? [];
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -9266,6 +9266,31 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
9266
9266
|
app.get('/api/mcp-status', gwHandler(async (gw, _req, res) => {
|
|
9267
9267
|
res.json(gw.getMcpStatus());
|
|
9268
9268
|
}));
|
|
9269
|
+
// PRD Phase 2.1: Reconnect a single MCP server. Clears the cached status so
|
|
9270
|
+
// the next query handshake repopulates it. The SDK doesn't expose a direct
|
|
9271
|
+
// reconnect call, so this is the closest equivalent: kick the cache.
|
|
9272
|
+
app.post('/api/mcp-servers/:name/reconnect', gwHandler(async (gw, req, res) => {
|
|
9273
|
+
const rawName = req.params.name;
|
|
9274
|
+
const name = Array.isArray(rawName) ? rawName[0] : rawName;
|
|
9275
|
+
if (!name) {
|
|
9276
|
+
res.status(400).json({ ok: false, error: 'name required' });
|
|
9277
|
+
return;
|
|
9278
|
+
}
|
|
9279
|
+
try {
|
|
9280
|
+
const result = gw.invalidateMcpStatus(String(name));
|
|
9281
|
+
res.json({
|
|
9282
|
+
ok: true,
|
|
9283
|
+
cleared: result.cleared,
|
|
9284
|
+
message: result.cleared
|
|
9285
|
+
? `Reconnect queued for "${name}" — status will refresh on the next query.`
|
|
9286
|
+
: `"${name}" had no cached status to clear; next query will populate it.`,
|
|
9287
|
+
status: result,
|
|
9288
|
+
});
|
|
9289
|
+
}
|
|
9290
|
+
catch (err) {
|
|
9291
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
9292
|
+
}
|
|
9293
|
+
}));
|
|
9269
9294
|
// ── Self-Improvement API ─────────────────────────────────────────
|
|
9270
9295
|
// ── MCP Server Management API ───────────────────────────────────────
|
|
9271
9296
|
app.get('/api/mcp-servers', (_req, res) => {
|
|
@@ -16356,14 +16381,18 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16356
16381
|
|
|
16357
16382
|
<!-- ═══ Tasks Page — single unified surface ═══ -->
|
|
16358
16383
|
<div class="page" id="page-build">
|
|
16359
|
-
<!--
|
|
16360
|
-
|
|
16361
|
-
?tab=workflows
|
|
16362
|
-
|
|
16384
|
+
<!-- PRD Phase 2: top-level tab strip within the Tasks domain.
|
|
16385
|
+
Tasks (default) + Tools & MCP catalog. Workflows still reachable
|
|
16386
|
+
via deep-link ?tab=workflows for power users with existing
|
|
16387
|
+
multi-step workflows. -->
|
|
16388
|
+
<div id="build-tabs" style="display:flex;gap:4px;padding:8px 18px 0;background:var(--bg-secondary);border-bottom:1px solid var(--border);flex-shrink:0">
|
|
16363
16389
|
<button class="build-tab-btn active" data-build-tab="crons" onclick="switchBuildTab('crons')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-primary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
|
|
16364
16390
|
<span style="margin-right:6px">📅</span>Tasks <span id="build-tab-cron-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
|
|
16365
16391
|
</button>
|
|
16366
|
-
<button class="build-tab-btn" data-build-tab="
|
|
16392
|
+
<button class="build-tab-btn" data-build-tab="toolsmcp" onclick="switchBuildTab('toolsmcp')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
|
|
16393
|
+
<span style="margin-right:6px">🧰</span>Tools & MCP <span id="build-tab-toolsmcp-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
|
|
16394
|
+
</button>
|
|
16395
|
+
<button class="build-tab-btn" data-build-tab="workflows" onclick="switchBuildTab('workflows')" style="display:none;padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
|
|
16367
16396
|
<span style="margin-right:6px">🔧</span>Workflows <span id="build-tab-workflows-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
|
|
16368
16397
|
</button>
|
|
16369
16398
|
</div>
|
|
@@ -16375,6 +16404,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16375
16404
|
<div id="build-tab-crons" style="display:none;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
|
|
16376
16405
|
<div id="panel-cron"><div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading scheduled tasks…</div></div>
|
|
16377
16406
|
</div>
|
|
16407
|
+
<!-- ── PRD Phase 2: Tools & MCP catalog ────────────────────────────────
|
|
16408
|
+
Read-only foundation in 1.18.81. Future slices: per-tool bindings,
|
|
16409
|
+
Reconnect/Toggle/Edit actions, Approval Mode + Max-auto-runs config. -->
|
|
16410
|
+
<div id="build-tab-toolsmcp" style="display:none;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
|
|
16411
|
+
<div id="panel-toolsmcp"><div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading Tools & MCP catalog…</div></div>
|
|
16412
|
+
</div>
|
|
16378
16413
|
<!-- Tricks (workflows) tab — existing RoutinesUI surface ─────────────────── -->
|
|
16379
16414
|
<div id="build-tab-workflows" style="display:none;flex:1;min-height:0;display:flex;flex-direction:column">
|
|
16380
16415
|
<!-- Toolbar -->
|
|
@@ -20309,6 +20344,77 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20309
20344
|
|
|
20310
20345
|
<!-- (legacy standalone Preview modal removed in 1.18.70 — preview now lives as a tab inside the cron modal) -->
|
|
20311
20346
|
|
|
20347
|
+
<!-- ═══ MCP Server Edit Modal — PRD Phase 2.1 ═══ -->
|
|
20348
|
+
<div class="modal-overlay" id="mcp-edit-modal">
|
|
20349
|
+
<div class="modal" style="max-width:640px;width:96vw">
|
|
20350
|
+
<div class="modal-header">
|
|
20351
|
+
<h3 id="mcp-edit-title">Edit MCP Server</h3>
|
|
20352
|
+
<button class="btn-ghost btn-sm" onclick="closeMcpServerEditModal()">×</button>
|
|
20353
|
+
</div>
|
|
20354
|
+
<div class="modal-body" style="padding:18px">
|
|
20355
|
+
<div id="mcp-edit-readonly-note" style="display:none;margin-bottom:14px;padding:10px 12px;border-radius:6px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.30);color:var(--yellow);font-size:12px">
|
|
20356
|
+
⚠ Auto-detected server. Edits to this config aren't persisted by Clementine — change it in the source file (Claude Desktop config, Claude Code settings, or extensions). Use the toggle on the catalog card to enable/disable.
|
|
20357
|
+
</div>
|
|
20358
|
+
<div class="form-group">
|
|
20359
|
+
<label class="form-label">Name</label>
|
|
20360
|
+
<input type="text" id="mcp-edit-name">
|
|
20361
|
+
<div class="form-hint">Identifier — cannot be renamed via this dialog.</div>
|
|
20362
|
+
</div>
|
|
20363
|
+
<div class="form-row">
|
|
20364
|
+
<div class="form-group">
|
|
20365
|
+
<label class="form-label">Transport</label>
|
|
20366
|
+
<select id="mcp-edit-type" onchange="syncMcpEditTransportRows()">
|
|
20367
|
+
<option value="stdio">stdio (local process)</option>
|
|
20368
|
+
<option value="http">http</option>
|
|
20369
|
+
<option value="sse">sse</option>
|
|
20370
|
+
</select>
|
|
20371
|
+
</div>
|
|
20372
|
+
<div class="form-group">
|
|
20373
|
+
<label class="form-label" style="display:flex;align-items:center;gap:8px">
|
|
20374
|
+
<input type="checkbox" id="mcp-edit-enabled"> Enabled
|
|
20375
|
+
</label>
|
|
20376
|
+
<div class="form-hint">Disable to remove this server from every task without deleting it.</div>
|
|
20377
|
+
</div>
|
|
20378
|
+
</div>
|
|
20379
|
+
<div class="form-group">
|
|
20380
|
+
<label class="form-label">Description</label>
|
|
20381
|
+
<input type="text" id="mcp-edit-description" placeholder="What this server does">
|
|
20382
|
+
</div>
|
|
20383
|
+
<!-- stdio-only fields -->
|
|
20384
|
+
<div id="mcp-edit-stdio-rows">
|
|
20385
|
+
<div class="form-group">
|
|
20386
|
+
<label class="form-label">Command</label>
|
|
20387
|
+
<input type="text" id="mcp-edit-command" placeholder="e.g. npx, python, ./bin/server">
|
|
20388
|
+
</div>
|
|
20389
|
+
<div class="form-group">
|
|
20390
|
+
<label class="form-label">Args <span style="color:var(--text-muted);font-weight:normal">(one per line)</span></label>
|
|
20391
|
+
<textarea id="mcp-edit-args" rows="3" placeholder="--port 3001" style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
|
|
20392
|
+
</div>
|
|
20393
|
+
<div class="form-group">
|
|
20394
|
+
<label class="form-label">Env <span style="color:var(--text-muted);font-weight:normal">(JSON object, optional)</span></label>
|
|
20395
|
+
<textarea id="mcp-edit-env" rows="3" placeholder='{ "API_KEY": "..." }' style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
|
|
20396
|
+
</div>
|
|
20397
|
+
</div>
|
|
20398
|
+
<!-- http/sse fields -->
|
|
20399
|
+
<div id="mcp-edit-http-rows" style="display:none">
|
|
20400
|
+
<div class="form-group">
|
|
20401
|
+
<label class="form-label">URL</label>
|
|
20402
|
+
<input type="text" id="mcp-edit-url" placeholder="https://example.com/mcp">
|
|
20403
|
+
</div>
|
|
20404
|
+
<div class="form-group">
|
|
20405
|
+
<label class="form-label">Headers <span style="color:var(--text-muted);font-weight:normal">(JSON object, optional)</span></label>
|
|
20406
|
+
<textarea id="mcp-edit-headers" rows="3" placeholder='{ "Authorization": "Bearer ..." }' style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
|
|
20407
|
+
</div>
|
|
20408
|
+
</div>
|
|
20409
|
+
</div>
|
|
20410
|
+
<div class="modal-footer">
|
|
20411
|
+
<span style="flex:1"></span>
|
|
20412
|
+
<button onclick="closeMcpServerEditModal()">Close</button>
|
|
20413
|
+
<button class="btn-primary" id="mcp-edit-save" onclick="saveMcpServerEdit()">Save</button>
|
|
20414
|
+
</div>
|
|
20415
|
+
</div>
|
|
20416
|
+
</div>
|
|
20417
|
+
|
|
20312
20418
|
<!-- ═══ Goal Modal ═══ -->
|
|
20313
20419
|
<div class="modal-overlay" id="goal-modal">
|
|
20314
20420
|
<div class="modal" style="width:520px">
|
|
@@ -21148,12 +21254,27 @@ function switchBuildTab(tab) {
|
|
|
21148
21254
|
var workPane = document.getElementById('build-tab-workflows');
|
|
21149
21255
|
var cronPane = document.getElementById('build-tab-crons');
|
|
21150
21256
|
var tplPane = document.getElementById('build-tab-templates');
|
|
21257
|
+
var toolsmcpPane = document.getElementById('build-tab-toolsmcp');
|
|
21151
21258
|
var headerStrip = document.getElementById('build-header-strip');
|
|
21152
21259
|
var usagePanel = document.getElementById('build-usage-panel');
|
|
21153
21260
|
var newBtn = document.getElementById('builder-new-btn');
|
|
21154
21261
|
// Always close any open workflow when changing tabs — switching context
|
|
21155
21262
|
// is a clean slate, not a stale node hanging on the canvas.
|
|
21156
21263
|
if (typeof closeBuilderCanvas === 'function') closeBuilderCanvas();
|
|
21264
|
+
// Default: hide the Tools & MCP pane unless we're explicitly on it.
|
|
21265
|
+
if (toolsmcpPane && tab !== 'toolsmcp') toolsmcpPane.style.display = 'none';
|
|
21266
|
+
if (tab === 'toolsmcp') {
|
|
21267
|
+
// PRD Phase 2: Tools & MCP catalog. Read-only foundation in 1.18.81.
|
|
21268
|
+
if (workPane) workPane.style.display = 'none';
|
|
21269
|
+
if (cronPane) cronPane.style.display = 'none';
|
|
21270
|
+
if (tplPane) tplPane.style.display = 'none';
|
|
21271
|
+
if (toolsmcpPane) toolsmcpPane.style.display = 'block';
|
|
21272
|
+
if (headerStrip) headerStrip.style.display = 'none';
|
|
21273
|
+
if (usagePanel) usagePanel.style.display = 'none';
|
|
21274
|
+
if (newBtn) newBtn.style.display = 'none';
|
|
21275
|
+
if (typeof refreshToolsMcpCatalog === 'function') refreshToolsMcpCatalog();
|
|
21276
|
+
return;
|
|
21277
|
+
}
|
|
21157
21278
|
if (tab === 'templates') {
|
|
21158
21279
|
if (workPane) workPane.style.display = 'none';
|
|
21159
21280
|
if (cronPane) cronPane.style.display = 'none';
|
|
@@ -23532,6 +23653,301 @@ function renderRunningCard(item) {
|
|
|
23532
23653
|
+ '</div></div>';
|
|
23533
23654
|
}
|
|
23534
23655
|
|
|
23656
|
+
// ── PRD Phase 2: Tools & MCP catalog ──────────────────────────────────
|
|
23657
|
+
// Read-only foundation in 1.18.81. Renders the four-card taxonomy:
|
|
23658
|
+
// • Built-in — Claude SDK native tools (Read/Write/Bash/etc.)
|
|
23659
|
+
// • Custom MCP — in-process SDK MCP servers
|
|
23660
|
+
// • Shell command — CLI wrappers
|
|
23661
|
+
// • External MCP — stdio / sse / http MCP servers
|
|
23662
|
+
// Pulls from /api/mcp-status (live status) + /api/mcp-servers (config).
|
|
23663
|
+
// Future slices wire Reconnect/Toggle/Edit + the McpToolBinding modal.
|
|
23664
|
+
async function refreshToolsMcpCatalog() {
|
|
23665
|
+
var panel = document.getElementById('panel-toolsmcp');
|
|
23666
|
+
if (!panel) return;
|
|
23667
|
+
panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading Tools & MCP catalog…</div>';
|
|
23668
|
+
var statusMap = {};
|
|
23669
|
+
var servers = [];
|
|
23670
|
+
try {
|
|
23671
|
+
var sR = await apiFetch('/api/mcp-status');
|
|
23672
|
+
var statusJson = await sR.json();
|
|
23673
|
+
statusMap = statusJson || {};
|
|
23674
|
+
} catch (e) { /* status is optional — servers still render without it */ }
|
|
23675
|
+
try {
|
|
23676
|
+
var lR = await apiFetch('/api/mcp-servers');
|
|
23677
|
+
var lJson = await lR.json();
|
|
23678
|
+
servers = (lJson && lJson.servers) || [];
|
|
23679
|
+
} catch (e) {
|
|
23680
|
+
panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--red)">Failed to load MCP servers: ' + esc(String(e)) + '</div>';
|
|
23681
|
+
return;
|
|
23682
|
+
}
|
|
23683
|
+
var tabCount = document.getElementById('build-tab-toolsmcp-count');
|
|
23684
|
+
if (tabCount) {
|
|
23685
|
+
tabCount.textContent = servers.length;
|
|
23686
|
+
tabCount.style.display = servers.length > 0 ? '' : 'none';
|
|
23687
|
+
}
|
|
23688
|
+
// Bucket servers into the four PRD categories. The existing
|
|
23689
|
+
// ManagedMcpServer type doesn't have an explicit "kind" field, so we
|
|
23690
|
+
// infer: stdio with a known shell binary → 'shell', stdio bundled with
|
|
23691
|
+
// clementine → 'builtin', stdio external command → 'external_stdio',
|
|
23692
|
+
// http/sse → 'external_remote'. The bucket keys map to the PRD's four
|
|
23693
|
+
// taxonomy cards.
|
|
23694
|
+
var buckets = { builtin: [], custom: [], shell: [], external: [] };
|
|
23695
|
+
for (var i = 0; i < servers.length; i++) {
|
|
23696
|
+
var s = servers[i];
|
|
23697
|
+
var name = s.name || '';
|
|
23698
|
+
var type = s.type || 'stdio';
|
|
23699
|
+
var cmd = s.command || '';
|
|
23700
|
+
var kind;
|
|
23701
|
+
// The clementine-tools server is an in-process bundle
|
|
23702
|
+
if (name === 'clementine-tools' || name === 'kernel') kind = 'builtin';
|
|
23703
|
+
else if (type === 'http' || type === 'sse') kind = 'external';
|
|
23704
|
+
else if (/^(sf|gh|gcloud|kubectl|docker|aws|az|terraform)$/.test(cmd) || /\\b(sf|gh|gcloud|kubectl)$/.test(cmd)) kind = 'shell';
|
|
23705
|
+
else kind = 'external'; // default for stdio external MCP
|
|
23706
|
+
buckets[kind].push(s);
|
|
23707
|
+
}
|
|
23708
|
+
var html = '';
|
|
23709
|
+
// Header strip
|
|
23710
|
+
html += '<div style="margin-bottom:18px"><h2 style="margin:0 0 4px;font-size:18px;font-weight:600;color:var(--text-primary)">Tools & MCP catalog</h2>'
|
|
23711
|
+
+ '<div style="font-size:12px;color:var(--text-muted)">'+ esc(servers.length) +' MCP server' + (servers.length === 1 ? '' : 's') + ' configured. Click any task in the Tasks tab to bind specific tools to that task.</div></div>';
|
|
23712
|
+
// Four-card taxonomy. Each section is a labeled bucket of cards.
|
|
23713
|
+
var sections = [
|
|
23714
|
+
{ key: 'builtin', label: 'Built-in', desc: 'Claude SDK native tools — always available to every task at the agent profile\\x27s permission tier.' },
|
|
23715
|
+
{ key: 'custom', label: 'Custom in-process MCP', desc: 'MCP servers defined in clementine\\x27s code, loaded inside the daemon process.' },
|
|
23716
|
+
{ key: 'shell', label: 'Shell commands', desc: 'Local CLI binaries (sf, gh, gcloud…) wrapped as MCP servers.' },
|
|
23717
|
+
{ key: 'external', label: 'External MCP servers', desc: 'Third-party MCP servers reached over stdio, SSE, or HTTP.' },
|
|
23718
|
+
];
|
|
23719
|
+
for (var k = 0; k < sections.length; k++) {
|
|
23720
|
+
var sec = sections[k];
|
|
23721
|
+
var bucket = buckets[sec.key] || [];
|
|
23722
|
+
html += '<div style="margin-bottom:24px">';
|
|
23723
|
+
html += '<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:10px">'
|
|
23724
|
+
+ '<h3 style="margin:0;font-size:14px;font-weight:600;color:var(--text-primary)">' + esc(sec.label) + '</h3>'
|
|
23725
|
+
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:500">' + bucket.length + '</span>'
|
|
23726
|
+
+ '</div>';
|
|
23727
|
+
html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:12px">' + esc(sec.desc) + '</div>';
|
|
23728
|
+
if (bucket.length === 0) {
|
|
23729
|
+
html += '<div class="empty-state" style="padding:14px;color:var(--text-muted);font-size:12px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:6px">No servers in this bucket.</div>';
|
|
23730
|
+
} else {
|
|
23731
|
+
html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">';
|
|
23732
|
+
for (var b = 0; b < bucket.length; b++) html += renderMcpCatalogCard(bucket[b], statusMap);
|
|
23733
|
+
html += '</div>';
|
|
23734
|
+
}
|
|
23735
|
+
html += '</div>';
|
|
23736
|
+
}
|
|
23737
|
+
panel.innerHTML = html;
|
|
23738
|
+
}
|
|
23739
|
+
|
|
23740
|
+
// Render one MCP server card. Status pill colors mirror the PRD's five
|
|
23741
|
+
// states (connected / failed / needs-auth / pending / disabled). The
|
|
23742
|
+
// statusMap shape comes from gw.getMcpStatus() — varies a bit between
|
|
23743
|
+
// SDK versions; we defensively probe for connected/healthy fields.
|
|
23744
|
+
function renderMcpCatalogCard(server, statusMap) {
|
|
23745
|
+
var name = server.name || '(unnamed)';
|
|
23746
|
+
var transport = server.type || 'stdio';
|
|
23747
|
+
var enabled = server.enabled !== false;
|
|
23748
|
+
var status = statusMap && statusMap[name];
|
|
23749
|
+
var statusKind, statusLabel, statusColor;
|
|
23750
|
+
if (!enabled) { statusKind = 'disabled'; statusLabel = 'disabled'; statusColor = 'var(--text-muted)'; }
|
|
23751
|
+
else if (status && (status.connected === true || status.status === 'connected' || status.healthy === true)) { statusKind = 'connected'; statusLabel = 'connected'; statusColor = 'var(--green)'; }
|
|
23752
|
+
else if (status && (status.needsAuth === true || status.status === 'needs-auth')) { statusKind = 'needsauth'; statusLabel = 'needs auth'; statusColor = 'var(--yellow)'; }
|
|
23753
|
+
else if (status && (status.connected === false || status.status === 'failed' || status.error)) { statusKind = 'failed'; statusLabel = 'failed'; statusColor = 'var(--red)'; }
|
|
23754
|
+
else { statusKind = 'pending'; statusLabel = 'pending'; statusColor = 'var(--text-muted)'; }
|
|
23755
|
+
var toolCount = (status && (status.toolCount != null ? status.toolCount : (Array.isArray(status.tools) ? status.tools.length : null))) || (Array.isArray(server.exposedTools) ? server.exposedTools.length : null);
|
|
23756
|
+
var lastChecked = status && (status.lastCheckedAt || status.checkedAt || status.updatedAt);
|
|
23757
|
+
var src = server.source === 'auto-detected' ? 'auto-detected' : 'user-configured';
|
|
23758
|
+
var lastError = status && status.error;
|
|
23759
|
+
var html = ''
|
|
23760
|
+
+ '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:8px">'
|
|
23761
|
+
+ '<div style="display:flex;align-items:center;gap:8px">'
|
|
23762
|
+
+ '<div style="font-size:13px;font-weight:600;color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(name) + '">' + esc(name) + '</div>'
|
|
23763
|
+
+ '<span style="display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:500;color:' + statusColor + ';background:' + statusColor + '20;padding:2px 8px;border-radius:999px;text-transform:uppercase;letter-spacing:0.04em">'
|
|
23764
|
+
+ '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:' + statusColor + '"></span>' + esc(statusLabel)
|
|
23765
|
+
+ '</span>'
|
|
23766
|
+
+ '</div>'
|
|
23767
|
+
+ '<div style="display:flex;align-items:center;gap:10px;font-size:11px;color:var(--text-muted)">'
|
|
23768
|
+
+ '<span style="text-transform:uppercase;letter-spacing:0.04em">' + esc(transport) + '</span>'
|
|
23769
|
+
+ '<span>·</span>'
|
|
23770
|
+
+ '<span>' + esc(src) + '</span>'
|
|
23771
|
+
+ (toolCount != null ? '<span>·</span><span>' + esc(toolCount) + ' tool' + (toolCount === 1 ? '' : 's') + '</span>' : '')
|
|
23772
|
+
+ '</div>'
|
|
23773
|
+
+ (server.description ? '<div style="font-size:12px;color:var(--text-secondary);line-height:1.45">' + esc(String(server.description).slice(0, 240)) + '</div>' : '')
|
|
23774
|
+
+ (lastError ? '<div style="font-size:11px;color:var(--red);background:rgba(239,68,68,0.06);padding:6px 8px;border-radius:4px;word-break:break-word">' + esc(String(lastError).slice(0, 200)) + '</div>' : '')
|
|
23775
|
+
+ (lastChecked ? '<div style="font-size:11px;color:var(--text-muted)">Checked ' + esc(timeAgo(lastChecked)) + '</div>' : '')
|
|
23776
|
+
+ '<div style="display:flex;gap:6px;margin-top:4px;flex-wrap:wrap">'
|
|
23777
|
+
+ '<button class="btn-sm" onclick="toggleMcpServerEnabled(\\x27' + jsStr(name) + '\\x27,' + (enabled ? 'false' : 'true') + ')" title="' + (enabled ? 'Disable this MCP server for all tasks' : 'Enable this MCP server') + '">' + (enabled ? 'Disable' : 'Enable') + '</button>'
|
|
23778
|
+
+ '<button class="btn-sm" onclick="reconnectMcpServer(\\x27' + jsStr(name) + '\\x27)" title="Clear cached status — next query will reconnect">Reconnect</button>'
|
|
23779
|
+
+ '<button class="btn-sm" onclick="openMcpServerEditModal(\\x27' + jsStr(name) + '\\x27)" title="View or edit this server\\x27s config">Edit</button>'
|
|
23780
|
+
+ '</div>'
|
|
23781
|
+
+ '</div>';
|
|
23782
|
+
return html;
|
|
23783
|
+
}
|
|
23784
|
+
|
|
23785
|
+
// PRD Phase 2.1: Reconnect — invalidate cached status server-side, refresh
|
|
23786
|
+
// the catalog so the user sees the pending pill until the next query
|
|
23787
|
+
// handshake repopulates it.
|
|
23788
|
+
async function reconnectMcpServer(name) {
|
|
23789
|
+
try {
|
|
23790
|
+
var r = await apiFetch('/api/mcp-servers/' + encodeURIComponent(name) + '/reconnect', { method: 'POST' });
|
|
23791
|
+
var d = await r.json();
|
|
23792
|
+
if (!r.ok || d.ok === false) {
|
|
23793
|
+
toast('Reconnect failed: ' + (d.error || 'unknown'), 'error');
|
|
23794
|
+
return;
|
|
23795
|
+
}
|
|
23796
|
+
toast(d.message || 'Reconnect queued.', 'info');
|
|
23797
|
+
refreshToolsMcpCatalog();
|
|
23798
|
+
} catch (e) {
|
|
23799
|
+
toast('Reconnect failed: ' + String(e), 'error');
|
|
23800
|
+
}
|
|
23801
|
+
}
|
|
23802
|
+
|
|
23803
|
+
// PRD Phase 2.1: Edit modal. User-managed servers get an editable config
|
|
23804
|
+
// form; auto-detected servers render the same fields read-only with a note
|
|
23805
|
+
// pointing the user at the underlying config file.
|
|
23806
|
+
async function openMcpServerEditModal(name) {
|
|
23807
|
+
// Pull the latest server config — don't trust whatever was on the rendered card.
|
|
23808
|
+
var server;
|
|
23809
|
+
try {
|
|
23810
|
+
var r = await apiFetch('/api/mcp-servers');
|
|
23811
|
+
var d = await r.json();
|
|
23812
|
+
server = (d && d.servers || []).find(function(s) { return s.name === name; });
|
|
23813
|
+
} catch (e) {
|
|
23814
|
+
toast('Failed to load server: ' + String(e), 'error');
|
|
23815
|
+
return;
|
|
23816
|
+
}
|
|
23817
|
+
if (!server) { toast('Server "' + name + '" not found', 'error'); return; }
|
|
23818
|
+
var modal = document.getElementById('mcp-edit-modal');
|
|
23819
|
+
if (!modal) { toast('Edit modal missing from DOM', 'error'); return; }
|
|
23820
|
+
document.getElementById('mcp-edit-title').textContent = 'Edit: ' + name;
|
|
23821
|
+
var roNote = document.getElementById('mcp-edit-readonly-note');
|
|
23822
|
+
var isReadOnly = server.source === 'auto-detected';
|
|
23823
|
+
if (roNote) roNote.style.display = isReadOnly ? '' : 'none';
|
|
23824
|
+
// Set fields
|
|
23825
|
+
document.getElementById('mcp-edit-name').value = server.name || '';
|
|
23826
|
+
document.getElementById('mcp-edit-name').disabled = true; // never rename via this path
|
|
23827
|
+
document.getElementById('mcp-edit-type').value = server.type || 'stdio';
|
|
23828
|
+
document.getElementById('mcp-edit-type').disabled = isReadOnly;
|
|
23829
|
+
document.getElementById('mcp-edit-description').value = server.description || '';
|
|
23830
|
+
document.getElementById('mcp-edit-description').disabled = isReadOnly;
|
|
23831
|
+
document.getElementById('mcp-edit-enabled').checked = server.enabled !== false;
|
|
23832
|
+
document.getElementById('mcp-edit-enabled').disabled = isReadOnly;
|
|
23833
|
+
document.getElementById('mcp-edit-command').value = server.command || '';
|
|
23834
|
+
document.getElementById('mcp-edit-command').disabled = isReadOnly;
|
|
23835
|
+
document.getElementById('mcp-edit-args').value = Array.isArray(server.args) ? server.args.join('\\n') : '';
|
|
23836
|
+
document.getElementById('mcp-edit-args').disabled = isReadOnly;
|
|
23837
|
+
document.getElementById('mcp-edit-url').value = server.url || '';
|
|
23838
|
+
document.getElementById('mcp-edit-url').disabled = isReadOnly;
|
|
23839
|
+
document.getElementById('mcp-edit-headers').value = server.headers && Object.keys(server.headers).length ? JSON.stringify(server.headers, null, 2) : '';
|
|
23840
|
+
document.getElementById('mcp-edit-headers').disabled = isReadOnly;
|
|
23841
|
+
document.getElementById('mcp-edit-env').value = server.env && Object.keys(server.env).length ? JSON.stringify(server.env, null, 2) : '';
|
|
23842
|
+
document.getElementById('mcp-edit-env').disabled = isReadOnly;
|
|
23843
|
+
// Show the right transport fields
|
|
23844
|
+
syncMcpEditTransportRows();
|
|
23845
|
+
// Save button hidden for read-only auto-detected servers.
|
|
23846
|
+
var saveBtn = document.getElementById('mcp-edit-save');
|
|
23847
|
+
if (saveBtn) saveBtn.style.display = isReadOnly ? 'none' : '';
|
|
23848
|
+
modal.classList.add('show');
|
|
23849
|
+
}
|
|
23850
|
+
|
|
23851
|
+
function closeMcpServerEditModal() {
|
|
23852
|
+
var modal = document.getElementById('mcp-edit-modal');
|
|
23853
|
+
if (modal) modal.classList.remove('show');
|
|
23854
|
+
}
|
|
23855
|
+
|
|
23856
|
+
// Show only the row matching the selected transport (stdio vs http/sse).
|
|
23857
|
+
function syncMcpEditTransportRows() {
|
|
23858
|
+
var t = (document.getElementById('mcp-edit-type') || {}).value || 'stdio';
|
|
23859
|
+
var stdioRow = document.getElementById('mcp-edit-stdio-rows');
|
|
23860
|
+
var httpRow = document.getElementById('mcp-edit-http-rows');
|
|
23861
|
+
if (stdioRow) stdioRow.style.display = (t === 'stdio') ? '' : 'none';
|
|
23862
|
+
if (httpRow) httpRow.style.display = (t === 'http' || t === 'sse') ? '' : 'none';
|
|
23863
|
+
}
|
|
23864
|
+
|
|
23865
|
+
async function saveMcpServerEdit() {
|
|
23866
|
+
var name = (document.getElementById('mcp-edit-name') || {}).value;
|
|
23867
|
+
if (!name) return;
|
|
23868
|
+
var type = document.getElementById('mcp-edit-type').value;
|
|
23869
|
+
var description = document.getElementById('mcp-edit-description').value.trim();
|
|
23870
|
+
var enabled = !!document.getElementById('mcp-edit-enabled').checked;
|
|
23871
|
+
var commandRaw = document.getElementById('mcp-edit-command').value.trim();
|
|
23872
|
+
var argsRaw = document.getElementById('mcp-edit-args').value;
|
|
23873
|
+
var url = document.getElementById('mcp-edit-url').value.trim();
|
|
23874
|
+
var headersRaw = document.getElementById('mcp-edit-headers').value.trim();
|
|
23875
|
+
var envRaw = document.getElementById('mcp-edit-env').value.trim();
|
|
23876
|
+
|
|
23877
|
+
var headers, env;
|
|
23878
|
+
if (headersRaw) {
|
|
23879
|
+
try {
|
|
23880
|
+
headers = JSON.parse(headersRaw);
|
|
23881
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) throw new Error('Headers must be a JSON object');
|
|
23882
|
+
} catch (e) {
|
|
23883
|
+
toast('Headers JSON invalid: ' + (e.message || String(e)), 'error');
|
|
23884
|
+
document.getElementById('mcp-edit-headers').focus();
|
|
23885
|
+
return;
|
|
23886
|
+
}
|
|
23887
|
+
}
|
|
23888
|
+
if (envRaw) {
|
|
23889
|
+
try {
|
|
23890
|
+
env = JSON.parse(envRaw);
|
|
23891
|
+
if (!env || typeof env !== 'object' || Array.isArray(env)) throw new Error('Env must be a JSON object');
|
|
23892
|
+
} catch (e) {
|
|
23893
|
+
toast('Env JSON invalid: ' + (e.message || String(e)), 'error');
|
|
23894
|
+
document.getElementById('mcp-edit-env').focus();
|
|
23895
|
+
return;
|
|
23896
|
+
}
|
|
23897
|
+
}
|
|
23898
|
+
var args = argsRaw.split(/\\r?\\n/).map(function(s){ return s.trim(); }).filter(Boolean);
|
|
23899
|
+
var body = { type: type, description: description, enabled: enabled };
|
|
23900
|
+
if (type === 'stdio') {
|
|
23901
|
+
if (!commandRaw) { toast('Command is required for stdio transport', 'error'); document.getElementById('mcp-edit-command').focus(); return; }
|
|
23902
|
+
body.command = commandRaw;
|
|
23903
|
+
body.args = args;
|
|
23904
|
+
if (env) body.env = env;
|
|
23905
|
+
} else {
|
|
23906
|
+
if (!url) { toast('URL is required for ' + type + ' transport', 'error'); document.getElementById('mcp-edit-url').focus(); return; }
|
|
23907
|
+
body.url = url;
|
|
23908
|
+
if (headers) body.headers = headers;
|
|
23909
|
+
}
|
|
23910
|
+
try {
|
|
23911
|
+
var r = await apiFetch('/api/mcp-servers/' + encodeURIComponent(name), {
|
|
23912
|
+
method: 'PUT',
|
|
23913
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23914
|
+
body: JSON.stringify(body),
|
|
23915
|
+
});
|
|
23916
|
+
var d = await r.json();
|
|
23917
|
+
if (!r.ok || d.error) {
|
|
23918
|
+
toast('Save failed: ' + (d.error || 'unknown'), 'error');
|
|
23919
|
+
return;
|
|
23920
|
+
}
|
|
23921
|
+
toast('Saved.', 'success');
|
|
23922
|
+
closeMcpServerEditModal();
|
|
23923
|
+
refreshToolsMcpCatalog();
|
|
23924
|
+
} catch (e) {
|
|
23925
|
+
toast('Save failed: ' + String(e), 'error');
|
|
23926
|
+
}
|
|
23927
|
+
}
|
|
23928
|
+
|
|
23929
|
+
// PUT helper for the Toggle button. Lazy: re-fetches the catalog after
|
|
23930
|
+
// the round-trip so the new state is reflected. Future slice will swap
|
|
23931
|
+
// to optimistic update + rollback on error.
|
|
23932
|
+
async function toggleMcpServerEnabled(name, nextEnabled) {
|
|
23933
|
+
try {
|
|
23934
|
+
var r = await apiFetch('/api/mcp-servers/' + encodeURIComponent(name), {
|
|
23935
|
+
method: 'PUT',
|
|
23936
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23937
|
+
body: JSON.stringify({ enabled: nextEnabled }),
|
|
23938
|
+
});
|
|
23939
|
+
var d = await r.json();
|
|
23940
|
+
if (!r.ok || d.error) {
|
|
23941
|
+
toast('Toggle failed: ' + (d.error || 'unknown'), 'error');
|
|
23942
|
+
return;
|
|
23943
|
+
}
|
|
23944
|
+
toast(name + ' is now ' + (nextEnabled ? 'enabled' : 'disabled'), 'success');
|
|
23945
|
+
refreshToolsMcpCatalog();
|
|
23946
|
+
} catch (e) {
|
|
23947
|
+
toast('Toggle failed: ' + String(e), 'error');
|
|
23948
|
+
}
|
|
23949
|
+
}
|
|
23950
|
+
|
|
23535
23951
|
async function refreshCron() {
|
|
23536
23952
|
try {
|
|
23537
23953
|
// Fetch operations + cross-job recent runs in parallel for the three-zone
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -255,6 +255,15 @@ export declare class Gateway {
|
|
|
255
255
|
}>;
|
|
256
256
|
updatedAt: string;
|
|
257
257
|
};
|
|
258
|
+
/** PRD Phase 2.1: thin pass-through for the dashboard's Reconnect button. */
|
|
259
|
+
invalidateMcpStatus(serverName: string): {
|
|
260
|
+
servers: Array<{
|
|
261
|
+
name: string;
|
|
262
|
+
status: string;
|
|
263
|
+
}>;
|
|
264
|
+
updatedAt: string;
|
|
265
|
+
cleared: boolean;
|
|
266
|
+
};
|
|
258
267
|
getPresenceInfo(sessionKey: string): {
|
|
259
268
|
model: string;
|
|
260
269
|
project: string | null;
|
package/dist/gateway/router.js
CHANGED
|
@@ -2241,6 +2241,10 @@ export class Gateway {
|
|
|
2241
2241
|
getMcpStatus() {
|
|
2242
2242
|
return this.assistant.getMcpStatus();
|
|
2243
2243
|
}
|
|
2244
|
+
/** PRD Phase 2.1: thin pass-through for the dashboard's Reconnect button. */
|
|
2245
|
+
invalidateMcpStatus(serverName) {
|
|
2246
|
+
return this.assistant.invalidateMcpStatus(serverName);
|
|
2247
|
+
}
|
|
2244
2248
|
getPresenceInfo(sessionKey) {
|
|
2245
2249
|
const sess = this.sessions.get(sessionKey);
|
|
2246
2250
|
const modelName = sess?.model
|